mirror of
https://github.com/FreeTubeApp/FreeTube.git
synced 2025-12-05 01:10:31 +00:00
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>
This commit is contained in:
@@ -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.
|
* 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}`.
|
* 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.
|
* 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 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 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.
|
* 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.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const path = require('path')
|
|||||||
const { readFileSync, readdirSync } = require('fs')
|
const { readFileSync, readdirSync } = require('fs')
|
||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
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 MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
|
||||||
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
||||||
@@ -56,7 +56,7 @@ const config = {
|
|||||||
loader: 'vue-loader',
|
loader: 'vue-loader',
|
||||||
options: {
|
options: {
|
||||||
compilerOptions: {
|
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': true,
|
||||||
'process.env.IS_ELECTRON_MAIN': false,
|
'process.env.IS_ELECTRON_MAIN': false,
|
||||||
'process.env.SUPPORTS_LOCAL_API': true,
|
'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.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
|
||||||
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
||||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
||||||
@@ -180,9 +186,6 @@ const config = {
|
|||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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'),
|
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/electron.js'),
|
||||||
|
|
||||||
'youtubei.js$': 'youtubei.js/web',
|
'youtubei.js$': 'youtubei.js/web',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const path = require('path')
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
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 CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
|
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
|
||||||
@@ -45,7 +45,7 @@ const config = {
|
|||||||
loader: 'vue-loader',
|
loader: 'vue-loader',
|
||||||
options: {
|
options: {
|
||||||
compilerOptions: {
|
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': false,
|
||||||
'process.env.IS_ELECTRON_MAIN': false,
|
'process.env.IS_ELECTRON_MAIN': false,
|
||||||
'process.env.SUPPORTS_LOCAL_API': 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}'`
|
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
|
||||||
}),
|
}),
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
@@ -158,9 +164,6 @@ const config = {
|
|||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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'),
|
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)
|
// change to "shaka-player.ui.debug.js" to get debug logs (update jsconfig to get updated types)
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export default [
|
|||||||
ts: false,
|
ts: false,
|
||||||
}),
|
}),
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
...eslintPluginVue.configs['flat/vue2-recommended'],
|
...eslintPluginVue.configs['flat/recommended'],
|
||||||
...vuejsAccessibility.configs["flat/recommended"],
|
...vuejsAccessibility.configs["flat/recommended"],
|
||||||
...intlifyVueI18N.configs['flat/recommended'],
|
...intlifyVueI18N.configs['recommended'],
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
'**/*.{js,vue}',
|
'**/*.{js,vue}',
|
||||||
@@ -63,7 +63,7 @@ export default [
|
|||||||
settings: {
|
settings: {
|
||||||
'vue-i18n': {
|
'vue-i18n': {
|
||||||
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
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-unused-vars': 'warn',
|
||||||
'no-undef': 'warn',
|
'no-undef': 'warn',
|
||||||
'object-shorthand': 'off',
|
'object-shorthand': 'off',
|
||||||
'vue/no-template-key': 'warn',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
|
|
||||||
'vuejs-accessibility/label-has-for': ['error', {
|
'vuejs-accessibility/label-has-for': ['error', {
|
||||||
@@ -119,8 +118,6 @@ export default [
|
|||||||
ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube'],
|
ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube'],
|
||||||
}],
|
}],
|
||||||
|
|
||||||
'@intlify/vue-i18n/no-deprecated-tc': 'off',
|
|
||||||
'vue/require-explicit-emits': 'error',
|
|
||||||
'vue/no-unused-emit-declarations': 'error',
|
'vue/no-unused-emit-declarations': 'error',
|
||||||
|
|
||||||
'jsdoc/check-alignment': 'error',
|
'jsdoc/check-alignment': 'error',
|
||||||
@@ -166,7 +163,7 @@ export default [
|
|||||||
settings: {
|
settings: {
|
||||||
'vue-i18n': {
|
'vue-i18n': {
|
||||||
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
||||||
messageSyntaxVersion: '^8.0.0',
|
messageSyntaxVersion: '^11.0.0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -191,7 +188,7 @@ export default [
|
|||||||
settings: {
|
settings: {
|
||||||
'vue-i18n': {
|
'vue-i18n': {
|
||||||
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
||||||
messageSyntaxVersion: '^8.0.0',
|
messageSyntaxVersion: '^11.0.0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -210,7 +207,7 @@ export default [
|
|||||||
settings: {
|
settings: {
|
||||||
'vue-i18n': {
|
'vue-i18n': {
|
||||||
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
||||||
messageSyntaxVersion: '^8.0.0',
|
messageSyntaxVersion: '^11.0.0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"vueCompilerOptions": {
|
"vueCompilerOptions": {
|
||||||
"target": 2.7
|
"target": 3.5
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -58,21 +58,21 @@
|
|||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-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",
|
"@seald-io/nedb": "^4.1.2",
|
||||||
"autolinker": "^4.1.5",
|
"autolinker": "^4.1.5",
|
||||||
"bgutils-js": "^3.2.0",
|
"bgutils-js": "^3.2.0",
|
||||||
"electron-context-menu": "^4.1.1",
|
"electron-context-menu": "^4.1.1",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"portal-vue": "^2.1.7",
|
"portal-vue": "^3.0.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"shaka-player": "^4.16.6",
|
"shaka-player": "^4.16.6",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"vue": "^2.7.16",
|
"vue": "^3.5.22",
|
||||||
"vue-i18n": "^8.28.2",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-observe-visibility": "^1.0.0",
|
"vue-observe-visibility": "^2.0.0-alpha.1",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^4.6.3",
|
||||||
"vuex": "^3.6.2",
|
"vuex": "^4.1.0",
|
||||||
"youtubei.js": "^16.0.1"
|
"youtubei.js": "^16.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"@babel/preset-env": "^7.28.5",
|
"@babel/preset-env": "^7.28.5",
|
||||||
"@double-great/stylelint-a11y": "^3.4.0",
|
"@double-great/stylelint-a11y": "^3.4.0",
|
||||||
"@eslint/js": "^9.38.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",
|
"babel-loader": "^10.0.0",
|
||||||
"copy-webpack-plugin": "^13.0.1",
|
"copy-webpack-plugin": "^13.0.1",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
@@ -112,9 +112,8 @@
|
|||||||
"stylelint-high-performance-animation": "^1.11.0",
|
"stylelint-high-performance-animation": "^1.11.0",
|
||||||
"stylelint-use-logical-spec": "^5.0.1",
|
"stylelint-use-logical-spec": "^5.0.1",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"vue-devtools": "^5.1.4",
|
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-loader": "^15.10.0",
|
"vue-loader": "^17.4.2",
|
||||||
"webpack": "^5.102.1",
|
"webpack": "^5.102.1",
|
||||||
"webpack-cli": "^6.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-dev-server": "^5.2.2",
|
"webpack-dev-server": "^5.2.2",
|
||||||
|
|||||||
@@ -710,14 +710,6 @@ function runApp() {
|
|||||||
|
|
||||||
await createWindow()
|
await createWindow()
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
try {
|
|
||||||
require('vue-devtools').install()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
mainWindow.webContents.openDevTools()
|
mainWindow.webContents.openDevTools()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,8 @@
|
|||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter,
|
.fade-enter-from,
|
||||||
.fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,18 +209,16 @@ export default defineComponent({
|
|||||||
}, 500)
|
}, 500)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$router.onReady(() => {
|
if (this.$route.path === '/') {
|
||||||
if (this.$router.currentRoute.path === '/') {
|
this.$router.replace({ path: this.landingPage })
|
||||||
this.$router.replace({ path: this.landingPage })
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.setWindowTitle()
|
this.setWindowTitle()
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('dragstart', this.handleDragStart)
|
document.addEventListener('dragstart', this.handleDragStart)
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeUnmount: function () {
|
||||||
document.removeEventListener('dragstart', this.handleDragStart)
|
document.removeEventListener('dragstart', this.handleDragStart)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -332,8 +330,8 @@ export default defineComponent({
|
|||||||
this.showBlogBanner = false
|
this.showBlogBanner = false
|
||||||
},
|
},
|
||||||
|
|
||||||
handlePromptPortalUpdate: function(newVal) {
|
handlePromptPortalUpdate: function(data) {
|
||||||
this.isPromptOpen = newVal
|
this.isPromptOpen = data.hasContent
|
||||||
},
|
},
|
||||||
|
|
||||||
openDownloadsPage: function () {
|
openDownloadsPage: function () {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="dataReady"
|
v-if="dataReady"
|
||||||
id="app"
|
|
||||||
class="app"
|
class="app"
|
||||||
:class="{
|
:class="{
|
||||||
hideOutlines: outlinesHidden,
|
hideOutlines: outlinesHidden,
|
||||||
@@ -101,17 +100,18 @@
|
|||||||
@click="handleNewBlogBannerClick"
|
@click="handleNewBlogBannerClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<transition
|
<RouterView
|
||||||
v-if="dataReady"
|
v-if="dataReady"
|
||||||
mode="out-in"
|
v-slot="{ Component }"
|
||||||
name="fade"
|
class="routerView"
|
||||||
>
|
>
|
||||||
<!-- <keep-alive> -->
|
<Transition
|
||||||
<RouterView
|
mode="out-in"
|
||||||
class="routerView"
|
name="fade"
|
||||||
/>
|
>
|
||||||
<!-- </keep-alive> -->
|
<component :is="Component" />
|
||||||
</transition>
|
</Transition>
|
||||||
|
</RouterView>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
v-if="subCount !== null && !hideChannelSubscriptions"
|
v-if="subCount !== null && !hideChannelSubscriptions"
|
||||||
class="subCount"
|
class="subCount"
|
||||||
>
|
>
|
||||||
{{ $tc('Global.Counts.Subscriber Count', subCount, { count: formattedSubCount }) }}
|
{{ $t('Global.Counts.Subscriber Count', { count: formattedSubCount }, subCount) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
class="tab"
|
class="tab"
|
||||||
:class="{ selectedTab: currentTab === 'home' }"
|
:class="{ selectedTab: currentTab === 'home' }"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'home')"
|
:aria-selected="currentTab === 'home'"
|
||||||
aria-controls="homePanel"
|
aria-controls="homePanel"
|
||||||
:tabindex="(currentTab === 'home' || currentTab === 'search') ? 0 : -1"
|
:tabindex="(currentTab === 'home' || currentTab === 'search') ? 0 : -1"
|
||||||
@click="changeTab('home')"
|
@click="changeTab('home')"
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
class="tab"
|
class="tab"
|
||||||
:class="{ selectedTab: currentTab === 'videos' }"
|
:class="{ selectedTab: currentTab === 'videos' }"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'videos')"
|
:aria-selected="currentTab === 'videos'"
|
||||||
aria-controls="videoPanel"
|
aria-controls="videoPanel"
|
||||||
:tabindex="(currentTab === 'videos' || currentTab === 'search') ? 0 : -1"
|
:tabindex="(currentTab === 'videos' || currentTab === 'search') ? 0 : -1"
|
||||||
@click="changeTab('videos')"
|
@click="changeTab('videos')"
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
class="tab"
|
class="tab"
|
||||||
:class="{ selectedTab: currentTab === 'shorts' }"
|
:class="{ selectedTab: currentTab === 'shorts' }"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'shorts')"
|
:aria-selected="currentTab === 'shorts'"
|
||||||
aria-controls="shortPanel"
|
aria-controls="shortPanel"
|
||||||
:tabindex="currentTab === 'shorts' ? 0 : -1"
|
:tabindex="currentTab === 'shorts' ? 0 : -1"
|
||||||
@click="changeTab('shorts')"
|
@click="changeTab('shorts')"
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
class="tab"
|
class="tab"
|
||||||
:class="{ selectedTab: currentTab === 'live' }"
|
:class="{ selectedTab: currentTab === 'live' }"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'live')"
|
:aria-selected="currentTab === 'live'"
|
||||||
aria-controls="livePanel"
|
aria-controls="livePanel"
|
||||||
:tabindex="currentTab === 'live' ? 0 : -1"
|
:tabindex="currentTab === 'live' ? 0 : -1"
|
||||||
@click="changeTab('live')"
|
@click="changeTab('live')"
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
id="releasesTab"
|
id="releasesTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'releases')"
|
:aria-selected="currentTab === 'releases'"
|
||||||
aria-controls="releasePanel"
|
aria-controls="releasePanel"
|
||||||
:tabindex="currentTab === 'releases' ? 0 : -1"
|
:tabindex="currentTab === 'releases' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'releases' }"
|
:class="{ selectedTab: currentTab === 'releases' }"
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
id="podcastsTab"
|
id="podcastsTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'podcasts')"
|
:aria-selected="currentTab === 'podcasts'"
|
||||||
aria-controls="podcastPanel"
|
aria-controls="podcastPanel"
|
||||||
:tabindex="currentTab === 'podcasts' ? 0 : -1"
|
:tabindex="currentTab === 'podcasts' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'podcasts' }"
|
:class="{ selectedTab: currentTab === 'podcasts' }"
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
id="coursesTab"
|
id="coursesTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'courses')"
|
:aria-selected="currentTab === 'courses'"
|
||||||
aria-controls="coursesPanel"
|
aria-controls="coursesPanel"
|
||||||
:tabindex="currentTab === 'courses' ? 0 : -1"
|
:tabindex="currentTab === 'courses' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'courses' }"
|
:class="{ selectedTab: currentTab === 'courses' }"
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
id="playlistsTab"
|
id="playlistsTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'playlists')"
|
:aria-selected="currentTab === 'playlists'"
|
||||||
aria-controls="playlistPanel"
|
aria-controls="playlistPanel"
|
||||||
:tabindex="currentTab === 'playlists' ? 0 : -1"
|
:tabindex="currentTab === 'playlists' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'playlists' }"
|
:class="{ selectedTab: currentTab === 'playlists' }"
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
id="communityTab"
|
id="communityTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'community')"
|
:aria-selected="currentTab === 'community'"
|
||||||
aria-controls="communityPanel"
|
aria-controls="communityPanel"
|
||||||
:tabindex="currentTab === 'community' ? 0 : -1"
|
:tabindex="currentTab === 'community' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'community' }"
|
:class="{ selectedTab: currentTab === 'community' }"
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
id="aboutTab"
|
id="aboutTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'about')"
|
:aria-selected="currentTab === 'about'"
|
||||||
aria-controls="aboutPanel"
|
aria-controls="aboutPanel"
|
||||||
:tabindex="currentTab === 'about' ? 0 : -1"
|
:tabindex="currentTab === 'about' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'about' }"
|
:class="{ selectedTab: currentTab === 'about' }"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from '../composables/use-i18n-polyfill'
|
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 FtButton from './FtButton/FtButton.vue'
|
||||||
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { useId } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
channelId: {
|
channelId: {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<template
|
<template
|
||||||
v-for="(label, index) in labels"
|
v-for="(label, index) in labels"
|
||||||
|
:key="values[index]"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="id + values[index]"
|
:id="id + values[index]"
|
||||||
:key="'value' + values[index]"
|
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:name="id"
|
:name="id"
|
||||||
:value="values[index]"
|
:value="values[index]"
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:key="'label' + values[index]"
|
|
||||||
:for="id + values[index]"
|
:for="id + values[index]"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -27,12 +26,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
|
|
||||||
const id = useId()
|
const id = useId()
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -49,39 +47,9 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
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)
|
const modelValue = defineModel({ type: Array, required: true })
|
||||||
// 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 }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped src="./FtCheckboxList.css" />
|
<style scoped src="./FtCheckboxList.css" />
|
||||||
|
|||||||
@@ -117,8 +117,8 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="likeCount"
|
class="likeCount"
|
||||||
:title="$tc('Global.Counts.Like Count', voteCount, {count: formattedVoteCount})"
|
:title="$t('Global.Counts.Like Count', {count: formattedVoteCount}, voteCount)"
|
||||||
:aria-label="$tc('Global.Counts.Like Count', voteCount, {count: formattedVoteCount})"
|
:aria-label="$t('Global.Counts.Like Count', {count: formattedVoteCount}, voteCount)"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
class="thumbs-up-icon"
|
class="thumbs-up-icon"
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="commentCount"
|
class="commentCount"
|
||||||
:title="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
|
:title="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
|
||||||
:aria-label="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
|
:aria-label="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
class="comment-count-icon"
|
class="comment-count-icon"
|
||||||
@@ -148,8 +148,8 @@
|
|||||||
<span
|
<span
|
||||||
v-else-if="commentCount != null"
|
v-else-if="commentCount != null"
|
||||||
class="commentCount"
|
class="commentCount"
|
||||||
:title="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
|
:title="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
|
||||||
:aria-label="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
|
:aria-label="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
class="comment-count-icon"
|
class="comment-count-icon"
|
||||||
|
|||||||
@@ -79,8 +79,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { ref } from 'vue'
|
import { ref, useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
|
|
||||||
import FtInput from '../ft-input/ft-input.vue'
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
|
|||||||
@@ -44,14 +44,14 @@
|
|||||||
class="subscriberCount"
|
class="subscriberCount"
|
||||||
>
|
>
|
||||||
<template v-if="handle !== null"> • </template>
|
<template v-if="handle !== null"> • </template>
|
||||||
{{ $tc('Global.Counts.Subscriber Count', subscriberCount, {count: formattedSubscriberCount}) }}
|
{{ $t('Global.Counts.Subscriber Count', {count: formattedSubscriberCount}, subscriberCount) }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="handle == null && videoCount != null"
|
v-if="handle == null && videoCount != null"
|
||||||
class="videoCount"
|
class="videoCount"
|
||||||
>
|
>
|
||||||
<template v-if="subscriberCount !== null && !hideChannelSubscriptions"> • </template>
|
<template v-if="subscriberCount !== null && !hideChannelSubscriptions"> • </template>
|
||||||
{{ $tc('Global.Counts.Video Count', videoCount, {count: formattedVideoCount}) }}
|
{{ $t('Global.Counts.Video Count', {count: formattedVideoCount}, videoCount) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -34,14 +34,14 @@
|
|||||||
v-if="channelCount"
|
v-if="channelCount"
|
||||||
class="channelCount"
|
class="channelCount"
|
||||||
>
|
>
|
||||||
{{ $tc('Global.Counts.Channel Count', channelCount, {count: formattedChannelCount}) }}
|
{{ $t('Global.Counts.Channel Count', {count: formattedChannelCount}, channelCount) }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="videoCount"
|
v-if="videoCount"
|
||||||
class="videoCount"
|
class="videoCount"
|
||||||
>
|
>
|
||||||
<template v-if="channelCount"> • </template>
|
<template v-if="channelCount"> • </template>
|
||||||
{{ $tc('Global.Counts.Video Count', videoCount, {count: formattedVideosCount}) }}
|
{{ $t('Global.Counts.Video Count', {count: formattedVideosCount}, videoCount) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
import { useId } from 'vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
|
|
||||||
import { getFirstCharacter } from '../../helpers/strings'
|
import { getFirstCharacter } from '../../helpers/strings'
|
||||||
|
|||||||
@@ -222,14 +222,15 @@ function handleDeletePromptClick(value) {
|
|||||||
showToast(t('Profile.Profile has been updated'))
|
showToast(t('Profile.Profile has been updated'))
|
||||||
selectNone()
|
selectNone()
|
||||||
} else {
|
} else {
|
||||||
/** @type {Profile} */
|
|
||||||
const profile = deepCopy(props.profile)
|
|
||||||
|
|
||||||
subscriptions.value = subscriptions.value.filter((channel) => {
|
subscriptions.value = subscriptions.value.filter((channel) => {
|
||||||
return !selected_.includes(channel.id)
|
return !selected_.includes(channel.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
profile.subscriptions = subscriptions.value
|
/** @type {Profile} */
|
||||||
|
const profile = {
|
||||||
|
...props.profile,
|
||||||
|
subscriptions: deepCopy(subscriptions.value)
|
||||||
|
}
|
||||||
|
|
||||||
store.dispatch('updateProfile', profile)
|
store.dispatch('updateProfile', profile)
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
:show-action-button="false"
|
:show-action-button="false"
|
||||||
:maxlength="100"
|
:maxlength="100"
|
||||||
@input="profileName = $event"
|
@input="profileName = $event"
|
||||||
@keydown.enter.native="saveProfile"
|
@keydown.enter="saveProfile"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -121,7 +121,7 @@ import store from '../../store/index'
|
|||||||
|
|
||||||
import { MAIN_PROFILE_ID } from '../../../constants'
|
import { MAIN_PROFILE_ID } from '../../../constants'
|
||||||
import { calculateColorLuminance, colors } from '../../helpers/colors'
|
import { calculateColorLuminance, colors } from '../../helpers/colors'
|
||||||
import { showToast } from '../../helpers/utils'
|
import { deepCopy, showToast } from '../../helpers/utils'
|
||||||
import { getFirstCharacter } from '../../helpers/strings'
|
import { getFirstCharacter } from '../../helpers/strings'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,7 +204,7 @@ function saveProfile() {
|
|||||||
name: profileName.value,
|
name: profileName.value,
|
||||||
bgColor: profileBgColor.value,
|
bgColor: profileBgColor.value,
|
||||||
textColor: profileTextColor.value,
|
textColor: profileTextColor.value,
|
||||||
subscriptions: props.profile.subscriptions
|
subscriptions: deepCopy(props.profile.subscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.isNew) {
|
if (!props.isNew) {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
ref="profileListRef"
|
ref="profileListRef"
|
||||||
class="profileList"
|
class="profileList"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@focusout.native="handleProfileListFocusOut"
|
@focusout="handleProfileListFocusOut"
|
||||||
@keydown.native.esc.stop="handleProfileListEscape"
|
@keydown.esc.stop="handleProfileListEscape"
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
:id="id + 'title'"
|
:id="id + 'title'"
|
||||||
@@ -80,10 +80,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref, useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
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 FtCard from '../ft-card/ft-card.vue'
|
||||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||||
|
|||||||
@@ -56,8 +56,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref, useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
|
|
||||||
import store from '../../store/index'
|
import store from '../../store/index'
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,18 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<template
|
<template
|
||||||
v-for="(label, index) in labels"
|
v-for="(label, index) in labels"
|
||||||
|
:key="values[index]"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="id + values[index]"
|
:id="id + values[index]"
|
||||||
:key="'value' + values[index]"
|
v-model="modelValue"
|
||||||
:name="id"
|
:name="id"
|
||||||
:value="values[index]"
|
:value="values[index]"
|
||||||
:checked="value === values[index]"
|
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="radio"
|
class="radio"
|
||||||
type="radio"
|
type="radio"
|
||||||
@change="handleChange(values[index])"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:key="'label' + values[index]"
|
|
||||||
:for="id + values[index]"
|
:for="id + values[index]"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -28,7 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
import { useId } from 'vue'
|
||||||
|
|
||||||
const id = useId()
|
const id = useId()
|
||||||
|
|
||||||
@@ -49,27 +47,9 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
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)
|
const modelValue = defineModel({ type: String, required: true })
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped src="./FtRadioButton.css" />
|
<style scoped src="./FtRadioButton.css" />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
import { useId } from 'vue'
|
||||||
|
|
||||||
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, useId, watch } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
|
|||||||
@@ -30,8 +30,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
|
|
||||||
import FtSelect from '../FtSelect/FtSelect.vue'
|
import FtSelect from '../FtSelect/FtSelect.vue'
|
||||||
|
|||||||
@@ -90,8 +90,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, ref, shallowRef } from 'vue'
|
import { computed, ref, shallowRef, useId } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
|
|
||||||
import FtButton from '../FtButton/FtButton.vue'
|
import FtButton from '../FtButton/FtButton.vue'
|
||||||
@@ -246,7 +245,7 @@ function handleSubscription(profile) {
|
|||||||
const subscribeButton = ref(null)
|
const subscribeButton = ref(null)
|
||||||
|
|
||||||
function handleProfileDropdownFocusOut() {
|
function handleProfileDropdownFocusOut() {
|
||||||
if (!subscribeButton.value.matches(':focus-within')) {
|
if (subscribeButton.value && !subscribeButton.value.matches(':focus-within')) {
|
||||||
isProfileDropdownOpen.value = false
|
isProfileDropdownOpen.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router/composables'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
inputHtml: {
|
inputHtml: {
|
||||||
@@ -21,7 +21,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const videoId = router.currentRoute.params.id
|
const videoId = router.currentRoute.value.params.id
|
||||||
|
|
||||||
/** @type {import('vue').ComputedRef<string>} */
|
/** @type {import('vue').ComputedRef<string>} */
|
||||||
const displayText = computed(() => props.inputHtml.replaceAll(/(?:(\d+):)?(\d+):(\d+)/g, (timestamp, hours, minutes, seconds) => {
|
const displayText = computed(() => props.inputHtml.replaceAll(/(?:(\d+):)?(\d+):(\d+)/g, (timestamp, hours, minutes, seconds) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onBeforeUnmount, onMounted, shallowReactive } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, reactive } from 'vue'
|
||||||
import { ToastEventBus } from '../../helpers/utils'
|
import { ToastEventBus } from '../../helpers/utils'
|
||||||
|
|
||||||
let idCounter = 0
|
let idCounter = 0
|
||||||
@@ -34,21 +34,23 @@ let idCounter = 0
|
|||||||
* @property {number} id
|
* @property {number} id
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {import('vue').ShallowReactive<Toast[]>} */
|
/** @type {import('vue').Reactive<Toast[]>} */
|
||||||
const toasts = shallowReactive([])
|
const toasts = reactive([])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CustomEvent<{ message: string | (({elapsedMs: number, remainingMs: number}) => string), time: number | null, action: Function | null, abortSignal: AbortSignal | null }>} event
|
* @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 } }) {
|
function open({ detail: { message, time, action, abortSignal } }) {
|
||||||
|
const id = idCounter++
|
||||||
|
|
||||||
/** @type {Toast} */
|
/** @type {Toast} */
|
||||||
const toast = {
|
const toast = {
|
||||||
|
id,
|
||||||
message,
|
message,
|
||||||
action,
|
action,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
interval: 0,
|
interval: 0
|
||||||
id: idCounter++
|
|
||||||
}
|
}
|
||||||
time ||= 3000
|
time ||= 3000
|
||||||
let elapsed = 0
|
let elapsed = 0
|
||||||
@@ -60,7 +62,14 @@ function open({ detail: { message, time, action, abortSignal } }) {
|
|||||||
elapsed += updateDelay
|
elapsed += updateDelay
|
||||||
// Skip last update
|
// Skip last update
|
||||||
if (elapsed >= time) { return }
|
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)
|
}, updateDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +81,13 @@ function open({ detail: { message, time, action, abortSignal } }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
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) {
|
if (toasts.length > 4) {
|
||||||
|
|||||||
@@ -36,8 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, useId, watch } from 'vue'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
|
||||||
|
|
||||||
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { useId } from '../../composables/use-id-polyfill'
|
import { useId } from 'vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
position: {
|
position: {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
input-type="password"
|
input-type="password"
|
||||||
:value="password"
|
:value="password"
|
||||||
@input="e => password = e"
|
@input="e => password = e"
|
||||||
@keydown.enter.native="handleSetPassword"
|
@keydown.enter="handleSetPassword"
|
||||||
/>
|
/>
|
||||||
<FtButton
|
<FtButton
|
||||||
class="centerButton"
|
class="centerButton"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
:value="newTitle"
|
:value="newTitle"
|
||||||
:maxlength="255"
|
:maxlength="255"
|
||||||
@input="handlePlaylistNameInput"
|
@input="handlePlaylistNameInput"
|
||||||
@keydown.enter.native="savePlaylistInfo"
|
@keydown.enter="savePlaylistInfo"
|
||||||
/>
|
/>
|
||||||
<FtFlexBox v-if="inputPlaylistNameBlank">
|
<FtFlexBox v-if="inputPlaylistNameBlank">
|
||||||
<p>
|
<p>
|
||||||
@@ -67,9 +67,9 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ $tc('Global.Counts.Video Count', videoCount, { count: parsedVideoCount }) }}
|
{{ t('Global.Counts.Video Count', { count: parsedVideoCount }, videoCount) }}
|
||||||
<span v-if="!hideViews && !isUserPlaylist">
|
<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>
|
<span>- </span>
|
||||||
<span v-if="infoSource !== 'local'">
|
<span v-if="infoSource !== 'local'">
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
:show-label="false"
|
:show-label="false"
|
||||||
:value="newDescription"
|
:value="newDescription"
|
||||||
@input="(input) => newDescription = input"
|
@input="(input) => newDescription = input"
|
||||||
@keydown.enter.native="savePlaylistInfo"
|
@keydown.enter="savePlaylistInfo"
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
v-else
|
v-else
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
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 FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||||
@@ -282,6 +282,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
getTodayDateStrLocalTimezone,
|
getTodayDateStrLocalTimezone,
|
||||||
writeFileWithPicker,
|
writeFileWithPicker,
|
||||||
|
deepCopy,
|
||||||
} from '../../helpers/utils'
|
} from '../../helpers/utils'
|
||||||
import thumbnailPlaceholder from '../../assets/img/thumbnail_placeholder.svg'
|
import thumbnailPlaceholder from '../../assets/img/thumbnail_placeholder.svg'
|
||||||
|
|
||||||
@@ -481,7 +482,7 @@ const userPlaylistAnyVideoWatched = computed(() => {
|
|||||||
|
|
||||||
const historyCacheById_ = historyCacheById.value
|
const historyCacheById_ = historyCacheById.value
|
||||||
return selectedUserPlaylist.value.videos.some((video) => {
|
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,
|
playlistName: newTitle.value,
|
||||||
protected: selectedUserPlaylist.value.protected,
|
protected: selectedUserPlaylist.value.protected,
|
||||||
description: newDescription.value,
|
description: newDescription.value,
|
||||||
videos: selectedUserPlaylist.value.videos,
|
videos: deepCopy(selectedUserPlaylist.value.videos),
|
||||||
_id: props.id,
|
_id: props.id,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -740,7 +741,7 @@ const userPlaylistWatchedVideoCount = computed(() => {
|
|||||||
|
|
||||||
const historyCacheById_ = historyCacheById.value
|
const historyCacheById_ = historyCacheById.value
|
||||||
return selectedUserPlaylist.value.videos.reduce((count, video) => {
|
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)
|
}, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -787,7 +788,7 @@ async function handleRemoveDuplicateVideosPromptAnswer(option) {
|
|||||||
playlistName: props.title,
|
playlistName: props.title,
|
||||||
protected: selectedUserPlaylist.value.protected,
|
protected: selectedUserPlaylist.value.protected,
|
||||||
description: props.description,
|
description: props.description,
|
||||||
videos: newVideoItems,
|
videos: deepCopy(newVideoItems),
|
||||||
_id: props.id,
|
_id: props.id,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -824,7 +825,7 @@ async function handleRemoveVideosOnWatchPromptAnswer(option) {
|
|||||||
playlistName: props.title,
|
playlistName: props.title,
|
||||||
protected: selectedUserPlaylist.value.protected,
|
protected: selectedUserPlaylist.value.protected,
|
||||||
description: props.description,
|
description: props.description,
|
||||||
videos: videosToWatch,
|
videos: deepCopy(videosToWatch),
|
||||||
_id: props.id
|
_id: props.id
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
show-label
|
show-label
|
||||||
:value="proxyHostname"
|
:value="proxyHostname"
|
||||||
@input="handleUpdateProxyHostname"
|
@input="handleUpdateProxyHostname"
|
||||||
@keydown.enter.native="testProxy"
|
@keydown.enter="testProxy"
|
||||||
/>
|
/>
|
||||||
<FtInput
|
<FtInput
|
||||||
:placeholder="$t('Settings.Proxy Settings.Proxy Port Number')"
|
:placeholder="$t('Settings.Proxy Settings.Proxy Port Number')"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
:value="proxyPort"
|
:value="proxyPort"
|
||||||
:maxlength="5"
|
:maxlength="5"
|
||||||
@input="handleUpdateProxyPort"
|
@input="handleUpdateProxyPort"
|
||||||
@keydown.enter.native="testProxy"
|
@keydown.enter="testProxy"
|
||||||
/>
|
/>
|
||||||
</FtFlexBox>
|
</FtFlexBox>
|
||||||
<FtFlexBox>
|
<FtFlexBox>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
show-label
|
show-label
|
||||||
:value="proxyUsername"
|
:value="proxyUsername"
|
||||||
@input="handleUpdateProxyUsername"
|
@input="handleUpdateProxyUsername"
|
||||||
@keydown.enter.native="testProxy"
|
@keydown.enter="testProxy"
|
||||||
/>
|
/>
|
||||||
<FtInput
|
<FtInput
|
||||||
:placeholder="$t('Settings.Proxy Settings.Proxy Password')"
|
:placeholder="$t('Settings.Proxy Settings.Proxy Password')"
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
:value="proxyPassword"
|
:value="proxyPassword"
|
||||||
input-type="password"
|
input-type="password"
|
||||||
@input="handleUpdateProxyPassword"
|
@input="handleUpdateProxyPassword"
|
||||||
@keydown.enter.native="testProxy"
|
@keydown.enter="testProxy"
|
||||||
/>
|
/>
|
||||||
</FtFlexBox>
|
</FtFlexBox>
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import router from '../../router/index.js'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import store from '../../store/index'
|
import store from '../../store/index'
|
||||||
|
|
||||||
@@ -238,6 +238,8 @@ function handleClickOutside(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ const hideWatchedSubs = computed(() => {
|
|||||||
const filteredVideoList = computed(() => {
|
const filteredVideoList = computed(() => {
|
||||||
if (hideWatchedSubs.value && !props.isCommunity) {
|
if (hideWatchedSubs.value && !props.isCommunity) {
|
||||||
return props.videoList.filter((video) => {
|
return props.videoList.filter((video) => {
|
||||||
return !Object.hasOwn(historyCacheById.value, video.videoId)
|
return historyCacheById.value[video.videoId] === undefined
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return props.videoList
|
return props.videoList
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
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 FtInput from '../ft-input/ft-input.vue'
|
||||||
import FtProfileSelector from '../FtProfileSelector/FtProfileSelector.vue'
|
import FtProfileSelector from '../FtProfileSelector/FtProfileSelector.vue'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
:input-html="processedShownDescription"
|
:input-html="processedShownDescription"
|
||||||
:link-tab-index="linkTabIndex"
|
:link-tab-index="linkTabIndex"
|
||||||
@timestamp-event="onTimestamp"
|
@timestamp-event="onTimestamp"
|
||||||
@click.native="expandDescriptionWithClick"
|
@click="expandDescriptionWithClick"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="license && showFullDescription"
|
v-if="license && showFullDescription"
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default defineComponent({
|
|||||||
window.addEventListener('resize', this.handleResize)
|
window.addEventListener('resize', this.handleResize)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeUnmount: function () {
|
||||||
if (this.dropdownModalOnMobile) {
|
if (this.dropdownModalOnMobile) {
|
||||||
window.removeEventListener('resize', this.handleResize)
|
window.removeEventListener('resize', this.handleResize)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineComponent } from 'vue'
|
import { defineComponent, useId } from 'vue'
|
||||||
import { mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
|
|
||||||
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
import FtTooltip from '../FtTooltip/FtTooltip.vue'
|
||||||
@@ -77,13 +77,19 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['clear', 'click', 'input', 'remove'],
|
emits: ['clear', 'click', 'input', 'remove'],
|
||||||
|
setup() {
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
let actionIcon = ['fas', 'search']
|
let actionIcon = ['fas', 'search']
|
||||||
if (this.forceActionButtonIconName !== null) {
|
if (this.forceActionButtonIconName !== null) {
|
||||||
actionIcon = this.forceActionButtonIconName
|
actionIcon = this.forceActionButtonIconName
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: '',
|
|
||||||
inputData: '',
|
inputData: '',
|
||||||
searchState: {
|
searchState: {
|
||||||
showOptions: false,
|
showOptions: false,
|
||||||
@@ -152,7 +158,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function () {
|
||||||
this.id = this._uid
|
|
||||||
this.inputData = this.value
|
this.inputData = this.value
|
||||||
this.updateVisibleDataList()
|
this.updateVisibleDataList()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
class="thumbnailLink"
|
class="thumbnailLink"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:to="watchVideoRouterLink"
|
:to="watchVideoRouterLink"
|
||||||
@click.native="handleWatchPageLinkClick"
|
@click="handleWatchPageLinkClick"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="thumbnail"
|
:src="thumbnail"
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
class="title"
|
class="title"
|
||||||
:to="watchVideoRouterLink"
|
:to="watchVideoRouterLink"
|
||||||
@click.native="handleWatchPageLinkClick"
|
@click="handleWatchPageLinkClick"
|
||||||
>
|
>
|
||||||
<h3 class="h3Title">
|
<h3 class="h3Title">
|
||||||
{{ displayTitle }}
|
{{ displayTitle }}
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
class="viewCount"
|
class="viewCount"
|
||||||
>
|
>
|
||||||
<template v-if="channelId !== null || channelName !== null"> • </template>
|
<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>
|
||||||
<span
|
<span
|
||||||
v-if="uploadedTime !== '' && !isLive"
|
v-if="uploadedTime !== '' && !isLive"
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
<span
|
<span
|
||||||
v-if="isLive && !hideViews"
|
v-if="isLive && !hideViews"
|
||||||
class="viewCount"
|
class="viewCount"
|
||||||
> • {{ $tc('Global.Counts.Watching Count', viewCount, {count: parsedViewCount}) }}</span>
|
> • {{ $t('Global.Counts.Watching Count', {count: parsedViewCount}, viewCount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="is4k || hasCaptions || is8k || isNew || isVr180 || isVr360 || is3D"
|
v-if="is4k || hasCaptions || is8k || isNew || isVr180 || isVr360 || is3D"
|
||||||
|
|||||||
@@ -1192,16 +1192,18 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTimeupdate() {
|
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) {
|
if (showStats.value && hasLoaded.value) {
|
||||||
updateStats()
|
updateStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useSponsorBlock.value && sponsorBlockSegments.length > 0 && canSeek()) {
|
if (useSponsorBlock.value && sponsorBlockSegments.length > 0 && canSeek()) {
|
||||||
skipSponsorBlockSegments(currentTime)
|
skipSponsorBlockSegments(currentTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export class AudioTrackSelection extends shaka.ui.SettingsMenu {
|
|||||||
updateLocalisedStrings_() {
|
updateLocalisedStrings_() {
|
||||||
this.backButton.ariaLabel = this.localization.resolve('BACK')
|
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.button.ariaLabel = audioTracksText
|
||||||
this.nameSpan.textContent = audioTracksText
|
this.nameSpan.textContent = audioTracksText
|
||||||
|
|||||||
@@ -67,12 +67,12 @@ export class AutoplayToggle extends shaka.ui.Element {
|
|||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
updateLocalisedStrings_() {
|
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.icon_.use(this.autoplayEnabled_ ? PlayerIcons.PLAY_CIRCLE_FILLED : PlayerIcons.PAUSE_CIRCLE_FILLED)
|
||||||
|
|
||||||
this.currentState_.textContent = this.localization.resolve(this.autoplayEnabled_ ? 'ON' : 'OFF')
|
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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.icon_.use(this.fullWindowEnabled_ ? PlayerIcons.CLOSE_FULLSCREEN_FILLED : PlayerIcons.OPEN_IN_FULL_FILLED)
|
||||||
this.currentState_.textContent = this.localization.resolve(this.fullWindowEnabled_ ? 'ON' : 'OFF')
|
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(
|
const newLabel = addKeyboardShortcutToActionTitle(
|
||||||
baseAriaLabel,
|
baseAriaLabel,
|
||||||
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLWINDOW
|
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLWINDOW
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class ScreenshotButton extends shaka.ui.Element {
|
|||||||
/** @private */
|
/** @private */
|
||||||
updateLocalisedStrings_() {
|
updateLocalisedStrings_() {
|
||||||
const label = addKeyboardShortcutToActionTitle(
|
const label = addKeyboardShortcutToActionTitle(
|
||||||
i18n.t('Video.Player.Take Screenshot'),
|
i18n.global.t('Video.Player.Take Screenshot'),
|
||||||
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.TAKE_SCREENSHOT
|
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.TAKE_SCREENSHOT
|
||||||
)
|
)
|
||||||
this.nameSpan_.textContent = this.button_.ariaLabel = label
|
this.nameSpan_.textContent = this.button_.ariaLabel = label
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class StatsButton extends shaka.ui.Element {
|
|||||||
updateLocalisedStrings_() {
|
updateLocalisedStrings_() {
|
||||||
this.icon_.use(this.showStats_ ? PlayerIcons.INSERT_CHART_FILLED : PlayerIcons.INSERT_CHART_DEFAULT)
|
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(
|
const label = addKeyboardShortcutToActionTitle(
|
||||||
baseLabel,
|
baseLabel,
|
||||||
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.STATS
|
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.STATS
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class TheatreModeButton extends shaka.ui.Element {
|
|||||||
|
|
||||||
this.currentState_.textContent = this.localization.resolve(this.theatreModeEnabled_ ? 'ON' : 'OFF')
|
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(
|
this.nameSpan_.textContent = this.button_.ariaLabel = addKeyboardShortcutToActionTitle(
|
||||||
baseAriaLabel,
|
baseAriaLabel,
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export default defineComponent({
|
|||||||
this.setCurrentInvidiousInstanceBounce =
|
this.setCurrentInvidiousInstanceBounce =
|
||||||
debounce(this.setCurrentInvidiousInstance, 500)
|
debounce(this.setCurrentInvidiousInstance, 500)
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeUnmount: function () {
|
||||||
if (this.currentInvidiousInstance === '') {
|
if (this.currentInvidiousInstance === '') {
|
||||||
// FIXME: If we call an action from here, there's no guarantee it will finish
|
// 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
|
// before the component is destroyed, which could bring up some problems
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export default defineComponent({
|
|||||||
return null
|
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 () {
|
dateString: function () {
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export default defineComponent({
|
|||||||
navigator.mediaSession.setActionHandler('nexttrack', this.playNextVideo)
|
navigator.mediaSession.setActionHandler('nexttrack', this.playNextVideo)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeUnmount: function () {
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', null)
|
navigator.mediaSession.setActionHandler('previoustrack', null)
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', null)
|
navigator.mediaSession.setActionHandler('nexttrack', null)
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import i18n from '../i18n/index'
|
|||||||
export function useI18n() {
|
export function useI18n() {
|
||||||
const locale = computed({
|
const locale = computed({
|
||||||
get() {
|
get() {
|
||||||
return i18n.locale
|
return i18n.global.locale
|
||||||
},
|
},
|
||||||
set(locale) {
|
set(locale) {
|
||||||
i18n.locale = locale
|
i18n.global.locale = locale
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -74,22 +74,9 @@ export function useI18n() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} key
|
* @param {...any} args
|
||||||
* @param {number | unknown[] | Record<string, unknown> | undefined} arg1
|
|
||||||
* @param {number | undefined} arg2
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function t(key, arg1, arg2) {
|
function t(...args) {
|
||||||
// Remove these lines in the Vue 3 migration and pass all args to the `.t()` call
|
return i18n.global.t(...args)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++}`
|
|
||||||
}
|
|
||||||
@@ -98,21 +98,21 @@ export async function getSponsorBlockSegments(videoId, categories) {
|
|||||||
export function translateSponsorBlockCategory(category) {
|
export function translateSponsorBlockCategory(category) {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case 'sponsor':
|
case 'sponsor':
|
||||||
return i18n.t('Video.Sponsor Block category.sponsor')
|
return i18n.global.t('Video.Sponsor Block category.sponsor')
|
||||||
case 'intro':
|
case 'intro':
|
||||||
return i18n.t('Video.Sponsor Block category.intro')
|
return i18n.global.t('Video.Sponsor Block category.intro')
|
||||||
case 'outro':
|
case 'outro':
|
||||||
return i18n.t('Video.Sponsor Block category.outro')
|
return i18n.global.t('Video.Sponsor Block category.outro')
|
||||||
case 'recap':
|
case 'recap':
|
||||||
return i18n.t('Video.Sponsor Block category.recap')
|
return i18n.global.t('Video.Sponsor Block category.recap')
|
||||||
case 'selfpromo':
|
case 'selfpromo':
|
||||||
return i18n.t('Video.Sponsor Block category.self-promotion')
|
return i18n.global.t('Video.Sponsor Block category.self-promotion')
|
||||||
case 'interaction':
|
case 'interaction':
|
||||||
return i18n.t('Video.Sponsor Block category.interaction')
|
return i18n.global.t('Video.Sponsor Block category.interaction')
|
||||||
case 'music_offtopic':
|
case 'music_offtopic':
|
||||||
return i18n.t('Video.Sponsor Block category.music offtopic')
|
return i18n.global.t('Video.Sponsor Block category.music offtopic')
|
||||||
case 'filler':
|
case 'filler':
|
||||||
return i18n.t('Video.Sponsor Block category.filler')
|
return i18n.global.t('Video.Sponsor Block category.filler')
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown translation for SponsorBlock category ${category}`)
|
console.error(`Unknown translation for SponsorBlock category ${category}`)
|
||||||
return category
|
return category
|
||||||
@@ -131,7 +131,7 @@ export function translateSponsorBlockCategory(category) {
|
|||||||
* }[]} captions
|
* }[]} captions
|
||||||
*/
|
*/
|
||||||
export function sortCaptions(captions) {
|
export function sortCaptions(captions) {
|
||||||
const currentLocale = i18n.locale
|
const currentLocale = i18n.global.locale
|
||||||
const userLocale = currentLocale.split('-') // ex. [en,US]
|
const userLocale = currentLocale.split('-') // ex. [en,US]
|
||||||
|
|
||||||
const collator = new Intl.Collator([currentLocale, 'en'])
|
const collator = new Intl.Collator([currentLocale, 'en'])
|
||||||
|
|||||||
@@ -32,25 +32,25 @@ export function isKeyboardEventKeyPrintableChar(eventKey) {
|
|||||||
export function translateWindowTitle(title) {
|
export function translateWindowTitle(title) {
|
||||||
switch (title) {
|
switch (title) {
|
||||||
case 'Subscriptions':
|
case 'Subscriptions':
|
||||||
return i18n.t('Subscriptions.Subscriptions')
|
return i18n.global.t('Subscriptions.Subscriptions')
|
||||||
case 'Channels':
|
case 'Channels':
|
||||||
return i18n.t('Channels.Title')
|
return i18n.global.t('Channels.Title')
|
||||||
case 'Trending':
|
case 'Trending':
|
||||||
return i18n.t('Trending.Trending')
|
return i18n.global.t('Trending.Trending')
|
||||||
case 'Most Popular':
|
case 'Most Popular':
|
||||||
return i18n.t('Most Popular')
|
return i18n.global.t('Most Popular')
|
||||||
case 'Your Playlists':
|
case 'Your Playlists':
|
||||||
return i18n.t('User Playlists.Your Playlists')
|
return i18n.global.t('User Playlists.Your Playlists')
|
||||||
case 'History':
|
case 'History':
|
||||||
return i18n.t('History.History')
|
return i18n.global.t('History.History')
|
||||||
case 'Settings':
|
case 'Settings':
|
||||||
return i18n.t('Settings.Settings')
|
return i18n.global.t('Settings.Settings')
|
||||||
case 'About':
|
case 'About':
|
||||||
return i18n.t('About.About')
|
return i18n.global.t('About.About')
|
||||||
case 'Profile Settings':
|
case 'Profile Settings':
|
||||||
return i18n.t('Profile.Profile Settings')
|
return i18n.global.t('Profile.Profile Settings')
|
||||||
case 'Playlist':
|
case 'Playlist':
|
||||||
return i18n.t('Playlist.Playlist')
|
return i18n.global.t('Playlist.Playlist')
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,11 +206,11 @@ export async function copyToClipboard(content, { messageOnSuccess = null, messag
|
|||||||
if (messageOnError !== null) {
|
if (messageOnError !== null) {
|
||||||
showToast(`${messageOnError}: ${error}`, 5000)
|
showToast(`${messageOnError}: ${error}`, 5000)
|
||||||
} else {
|
} else {
|
||||||
showToast(`${i18n.t('Clipboard.Copy failed')}: ${error}`, 5000)
|
showToast(`${i18n.global.t('Clipboard.Copy failed')}: ${error}`, 5000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,7 +556,7 @@ export function extractNumberFromString(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function showExternalPlayerUnsupportedActionToast(externalPlayer, action) {
|
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)
|
showToast(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +682,7 @@ export function toDistractionFreeTitle(title, minUpperCase = 3) {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function formatNumber(number, options = undefined) {
|
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() {
|
export function getTodayDateStrLocalTimezone() {
|
||||||
@@ -715,7 +715,7 @@ export function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDayM
|
|||||||
let timeUnit = 'second'
|
let timeUnit = 'second'
|
||||||
|
|
||||||
if (timeDiffFromNow < 60 && hideSeconds) {
|
if (timeDiffFromNow < 60 && hideSeconds) {
|
||||||
return i18n.t('Moments Ago')
|
return i18n.global.t('Moments Ago')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeDiffFromNow >= 60) {
|
if (timeDiffFromNow >= 60) {
|
||||||
@@ -755,7 +755,7 @@ export function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDayM
|
|||||||
|
|
||||||
// Using `Math.ceil` so that -1.x days ago displayed as 1 day ago
|
// 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"
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -906,23 +906,23 @@ export function getChannelPlaylistId(channelId, type, sortBy) {
|
|||||||
function getIndividualLocalizedShortcut(shortcut) {
|
function getIndividualLocalizedShortcut(shortcut) {
|
||||||
switch (shortcut) {
|
switch (shortcut) {
|
||||||
case 'alt':
|
case 'alt':
|
||||||
return i18n.t('Keys.alt')
|
return i18n.global.t('Keys.alt')
|
||||||
case 'ctrl':
|
case 'ctrl':
|
||||||
return i18n.t('Keys.ctrl')
|
return i18n.global.t('Keys.ctrl')
|
||||||
case 'shift':
|
case 'shift':
|
||||||
return i18n.t('Keys.shift')
|
return i18n.global.t('Keys.shift')
|
||||||
case 'enter':
|
case 'enter':
|
||||||
return i18n.t('Keys.enter')
|
return i18n.global.t('Keys.enter')
|
||||||
case 'plus':
|
case 'plus':
|
||||||
return i18n.t('Keys.plus')
|
return i18n.global.t('Keys.plus')
|
||||||
case 'arrowleft':
|
case 'arrowleft':
|
||||||
return i18n.t('Keys.arrowleft')
|
return i18n.global.t('Keys.arrowleft')
|
||||||
case 'arrowright':
|
case 'arrowright':
|
||||||
return i18n.t('Keys.arrowright')
|
return i18n.global.t('Keys.arrowright')
|
||||||
case 'arrowup':
|
case 'arrowup':
|
||||||
return i18n.t('Keys.arrowup')
|
return i18n.global.t('Keys.arrowup')
|
||||||
case 'arrowdown':
|
case 'arrowdown':
|
||||||
return i18n.t('Keys.arrowdown')
|
return i18n.global.t('Keys.arrowdown')
|
||||||
default:
|
default:
|
||||||
return shortcut
|
return shortcut
|
||||||
}
|
}
|
||||||
@@ -967,7 +967,7 @@ export function getLocalizedShortcut(shortcut) {
|
|||||||
return shortcutsAsIcons.join('')
|
return shortcutsAsIcons.join('')
|
||||||
} else {
|
} else {
|
||||||
const localizedShortcuts = shortcuts.map((shortcut) => getIndividualLocalizedShortcut(shortcut))
|
const localizedShortcuts = shortcuts.map((shortcut) => getIndividualLocalizedShortcut(shortcut))
|
||||||
const shortcutJoinOperator = i18n.t('shortcutJoinOperator')
|
const shortcutJoinOperator = i18n.global.t('shortcutJoinOperator')
|
||||||
return localizedShortcuts.join(shortcutJoinOperator)
|
return localizedShortcuts.join(shortcutJoinOperator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -978,7 +978,7 @@ export function getLocalizedShortcut(shortcut) {
|
|||||||
* @returns {string} the localized action title with keyboard shortcut
|
* @returns {string} the localized action title with keyboard shortcut
|
||||||
*/
|
*/
|
||||||
export function addKeyboardShortcutToActionTitle(actionTitle, shortcut) {
|
export function addKeyboardShortcutToActionTitle(actionTitle, shortcut) {
|
||||||
return i18n.t('KeyboardShortcutTemplate', {
|
return i18n.global.t('KeyboardShortcutTemplate', {
|
||||||
label: actionTitle,
|
label: actionTitle,
|
||||||
shortcut
|
shortcut
|
||||||
})
|
})
|
||||||
@@ -995,7 +995,7 @@ export function localizeAndAddKeyboardShortcutToActionTitle(localizedActionTitle
|
|||||||
unlocalizedShortcuts = [unlocalizedShortcuts]
|
unlocalizedShortcuts = [unlocalizedShortcuts]
|
||||||
}
|
}
|
||||||
const localizedShortcuts = unlocalizedShortcuts.map((s) => getLocalizedShortcut(s))
|
const localizedShortcuts = unlocalizedShortcuts.map((s) => getLocalizedShortcut(s))
|
||||||
const shortcutLabelSeparator = i18n.t('shortcutLabelSeparator')
|
const shortcutLabelSeparator = i18n.global.t('shortcutLabelSeparator')
|
||||||
return addKeyboardShortcutToActionTitle(localizedActionTitle, localizedShortcuts.join(shortcutLabelSeparator))
|
return addKeyboardShortcutToActionTitle(localizedActionTitle, localizedShortcuts.join(shortcutLabelSeparator))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import Vue from 'vue'
|
import { createI18n } from 'vue-i18n'
|
||||||
import VueI18n from 'vue-i18n'
|
|
||||||
import { createWebURL } from '../helpers/utils'
|
import { createWebURL } from '../helpers/utils'
|
||||||
// List of locales approved for use
|
// List of locales approved for use
|
||||||
import activeLocales from '../../../static/locales/activeLocales.json'
|
import activeLocales from '../../../static/locales/activeLocales.json'
|
||||||
|
|
||||||
Vue.use(VueI18n)
|
const i18n = createI18n({
|
||||||
|
|
||||||
const i18n = new VueI18n({
|
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
|
legacy: true,
|
||||||
fallbackLocale: {
|
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 -> en-US
|
||||||
'es-AR': ['es'],
|
'es-AR': ['es'],
|
||||||
@@ -26,7 +24,8 @@ const i18n = new VueI18n({
|
|||||||
|
|
||||||
export async function loadLocale(locale) {
|
export async function loadLocale(locale) {
|
||||||
// don't need to load it if it's already loaded
|
// 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
|
return
|
||||||
}
|
}
|
||||||
if (!activeLocales.includes(locale)) {
|
if (!activeLocales.includes(locale)) {
|
||||||
@@ -47,7 +46,7 @@ export async function loadLocale(locale) {
|
|||||||
|
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
i18n.setLocaleMessage(locale, data)
|
i18n.global.setLocaleMessage(locale, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set by _scripts/ProcessLocalesPlugin.js
|
// Set by _scripts/ProcessLocalesPlugin.js
|
||||||
@@ -60,10 +59,11 @@ if (process.env.HOT_RELOAD_LOCALES) {
|
|||||||
if (message.type === 'freetube-locale-update') {
|
if (message.type === 'freetube-locale-update') {
|
||||||
for (const [locale, data] of message.data) {
|
for (const [locale, data] of message.data) {
|
||||||
// Only update locale data if it was already loaded
|
// 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)
|
const localeData = JSON.parse(data)
|
||||||
|
|
||||||
i18n.setLocaleMessage(locale, localeData)
|
i18n.global.setLocaleMessage(locale, localeData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// import the styles
|
// import the styles
|
||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
import i18n from './i18n/index'
|
import i18n from './i18n/index'
|
||||||
import router from './router/index'
|
import router from './router/index'
|
||||||
import store from './store/index'
|
import store from './store/index'
|
||||||
@@ -133,10 +133,6 @@ import {
|
|||||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||||
import PortalVue from 'portal-vue'
|
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
|
// Please keep the list of constants sorted by name
|
||||||
// to avoid code conflict and duplicate entries
|
// to avoid code conflict and duplicate entries
|
||||||
library.add(
|
library.add(
|
||||||
@@ -261,19 +257,23 @@ library.add(
|
|||||||
|
|
||||||
registerSwiper()
|
registerSwiper()
|
||||||
|
|
||||||
Vue.component('FontAwesomeIcon', FontAwesomeIcon)
|
const app = createApp(App)
|
||||||
Vue.component('FontAwesomeLayers', FontAwesomeLayers)
|
|
||||||
Vue.directive('observe-visibility', ObserveVisibility)
|
|
||||||
|
|
||||||
/* eslint-disable-next-line no-new */
|
app.config.performance = process.env.NODE_ENV === 'development'
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
app
|
||||||
router,
|
.component('FontAwesomeIcon', FontAwesomeIcon)
|
||||||
store,
|
.component('FontAwesomeLayers', FontAwesomeLayers)
|
||||||
i18n,
|
.directive('observe-visibility', ObserveVisibility)
|
||||||
render: h => h(App)
|
|
||||||
|
.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
|
// to avoid accessing electron api from web app build
|
||||||
if (process.env.IS_ELECTRON) {
|
if (process.env.IS_ELECTRON) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Vue from 'vue'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import Router from 'vue-router'
|
|
||||||
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
|
||||||
import SubscribedChannels from '../views/SubscribedChannels/SubscribedChannels.vue'
|
import SubscribedChannels from '../views/SubscribedChannels/SubscribedChannels.vue'
|
||||||
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.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 Hashtag from '../views/Hashtag/Hashtag.vue'
|
||||||
import Post from '../views/Post.vue'
|
import Post from '../views/Post.vue'
|
||||||
|
|
||||||
Vue.use(Router)
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
const router = new Router({
|
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -149,43 +147,11 @@ const router = new Router({
|
|||||||
if (savedPosition !== null) {
|
if (savedPosition !== null) {
|
||||||
resolve(savedPosition)
|
resolve(savedPosition)
|
||||||
} else {
|
} else {
|
||||||
resolve({ x: 0, y: 0 })
|
resolve({ left: 0, top: 0 })
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 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
|
export default router
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Vue from 'vue'
|
import { createStore } from 'vuex'
|
||||||
import Vuex from 'vuex'
|
|
||||||
// import createPersistedState from 'vuex-persistedstate'
|
// import createPersistedState from 'vuex-persistedstate'
|
||||||
|
|
||||||
import history from './modules/history'
|
import history from './modules/history'
|
||||||
@@ -12,9 +11,7 @@ import subscriptionCache from './modules/subscription-cache'
|
|||||||
import utils from './modules/utils'
|
import utils from './modules/utils'
|
||||||
import player from './modules/player'
|
import player from './modules/player'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
export default createStore({
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
modules: {
|
modules: {
|
||||||
history,
|
history,
|
||||||
invidious,
|
invidious,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { set as vueSet, del as vueDel } from 'vue'
|
|
||||||
import { DBHistoryHandlers } from '../../../datastores/handlers/index'
|
import { DBHistoryHandlers } from '../../../datastores/handlers/index'
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -125,7 +124,7 @@ const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.historyCacheSorted.unshift(record)
|
state.historyCacheSorted.unshift(record)
|
||||||
vueSet(state.historyCacheById, record.videoId, record)
|
state.historyCacheById[record.videoId] = record
|
||||||
},
|
},
|
||||||
|
|
||||||
updateRecordWatchProgressInHistoryCache(state, { videoId, watchProgress }) {
|
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
|
// Don't set, if the item was removed from the watch history, as we don't have any video details
|
||||||
if (record) {
|
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
|
// Don't set, if the item was removed from the watch history, as we don't have any video details
|
||||||
if (record) {
|
if (record) {
|
||||||
vueSet(record, 'lastViewedPlaylistId', lastViewedPlaylistId)
|
record.lastViewedPlaylistId = lastViewedPlaylistId
|
||||||
vueSet(record, 'lastViewedPlaylistType', lastViewedPlaylistType)
|
record.lastViewedPlaylistType = lastViewedPlaylistType
|
||||||
vueSet(record, 'lastViewedPlaylistItemId', lastViewedPlaylistItemId)
|
record.lastViewedPlaylistItemId = lastViewedPlaylistItemId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -162,7 +161,7 @@ const mutations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vueDel(state.historyCacheById, videoId)
|
delete state.historyCacheById[videoId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { set as vueSet } from 'vue'
|
|
||||||
import { createWebURL } from '../../helpers/utils'
|
import { createWebURL } from '../../helpers/utils'
|
||||||
|
|
||||||
// replace with a Map after the Vue 3 and Pinia migrations
|
// replace with a Map after the Vue 3 and Pinia migrations
|
||||||
@@ -23,7 +22,7 @@ const actions = {
|
|||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
addPlayerLocaleToCache(state, { locale, data }) {
|
addPlayerLocaleToCache(state, { locale, data }) {
|
||||||
vueSet(state.cachedPlayerLocales, locale, data)
|
state.cachedPlayerLocales[locale] = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ const sideEffectHandlers = {
|
|||||||
|
|
||||||
await Promise.allSettled(loadPromises)
|
await Promise.allSettled(loadPromises)
|
||||||
|
|
||||||
i18n.locale = targetLocale
|
i18n.global.locale = targetLocale
|
||||||
await dispatch('getRegionData', targetLocale)
|
await dispatch('getRegionData', targetLocale)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import i18n from '../../i18n/index'
|
import i18n from '../../i18n/index'
|
||||||
import { set as vueSet } from 'vue'
|
|
||||||
|
|
||||||
import { DefaultFolderKind } from '../../../constants'
|
import { DefaultFolderKind } from '../../../constants'
|
||||||
import {
|
import {
|
||||||
@@ -223,11 +222,11 @@ const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showToast(i18n.t('Downloading failed', { videoTitle: title }))
|
showToast(i18n.global.t('Downloading failed', { videoTitle: title }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(i18n.t('Starting download', { videoTitle: title }))
|
showToast(i18n.global.t('Starting download', { videoTitle: title }))
|
||||||
|
|
||||||
let writeableFileStream
|
let writeableFileStream
|
||||||
|
|
||||||
@@ -238,20 +237,20 @@ const actions = {
|
|||||||
writeableFileStream = await handle.createWritable()
|
writeableFileStream = await handle.createWritable()
|
||||||
|
|
||||||
await response.body.pipeTo(writeableFileStream, { preventClose: true })
|
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 {
|
} else {
|
||||||
throw new Error(`Bad status code: ${response.status}`)
|
throw new Error(`Bad status code: ${response.status}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showToast(i18n.t('Downloading failed', { videoTitle: title }))
|
showToast(i18n.global.t('Downloading failed', { videoTitle: title }))
|
||||||
} finally {
|
} finally {
|
||||||
if (writeableFileStream) {
|
if (writeableFileStream) {
|
||||||
await writeableFileStream.close()
|
await writeableFileStream.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast(i18n.t('Starting download', { videoTitle: title }))
|
showToast(i18n.global.t('Starting download', { videoTitle: title }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
@@ -263,13 +262,13 @@ const actions = {
|
|||||||
await window.ftElectron.writeToDefaultFolder(DefaultFolderKind.DOWNLOADS, fileName, arrayBuffer)
|
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 {
|
} else {
|
||||||
throw new Error(`Bad status code: ${response.status}`)
|
throw new Error(`Bad status code: ${response.status}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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)) {
|
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) {
|
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
|
return parsedString
|
||||||
@@ -733,7 +732,7 @@ const actions = {
|
|||||||
args.push(cmdArgs.startOffset, Math.trunc(payload.watchProgress))
|
args.push(cmdArgs.startOffset, Math.trunc(payload.watchProgress))
|
||||||
}
|
}
|
||||||
} else if (!ignoreWarnings) {
|
} 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') {
|
if (typeof cmdArgs.playbackRate === 'string') {
|
||||||
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
|
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
|
||||||
} else if (!ignoreWarnings) {
|
} 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') {
|
if (typeof cmdArgs.playlistIndex === 'string') {
|
||||||
args.push(`${cmdArgs.playlistIndex}${payload.playlistIndex}`)
|
args.push(`${cmdArgs.playlistIndex}${payload.playlistIndex}`)
|
||||||
} else if (!ignoreWarnings) {
|
} 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') {
|
if (typeof cmdArgs.playlistReverse === 'string') {
|
||||||
args.push(cmdArgs.playlistReverse)
|
args.push(cmdArgs.playlistReverse)
|
||||||
} else if (!ignoreWarnings) {
|
} 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') {
|
if (typeof cmdArgs.playlistShuffle === 'string') {
|
||||||
args.push(cmdArgs.playlistShuffle)
|
args.push(cmdArgs.playlistShuffle)
|
||||||
} else if (!ignoreWarnings) {
|
} 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') {
|
if (typeof cmdArgs.playlistLoop === 'string') {
|
||||||
args.push(cmdArgs.playlistLoop)
|
args.push(cmdArgs.playlistLoop)
|
||||||
} else if (!ignoreWarnings) {
|
} 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 {
|
} else {
|
||||||
if (payload.playlistId != null && payload.playlistId !== '' && !ignoreWarnings) {
|
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) {
|
if (payload.videoId != null) {
|
||||||
args.push(`${cmdArgs.videoUrl}https://www.youtube.com/watch?v=${payload.videoId}`)
|
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 !== ''
|
const videoOrPlaylist = payload.playlistId != null && payload.playlistId !== ''
|
||||||
? i18n.t('Video.External Player.playlist')
|
? i18n.global.t('Video.External Player.playlist')
|
||||||
: i18n.t('Video.External Player.video')
|
: 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) {
|
if (process.env.IS_ELECTRON) {
|
||||||
window.ftElectron.openInExternalPlayer(executable, args)
|
window.ftElectron.openInExternalPlayer(executable, args)
|
||||||
@@ -836,14 +835,12 @@ const mutations = {
|
|||||||
const sameVideo = state.deArrowCache[payload.videoId]
|
const sameVideo = state.deArrowCache[payload.videoId]
|
||||||
|
|
||||||
if (!sameVideo) {
|
if (!sameVideo) {
|
||||||
// setting properties directly doesn't trigger watchers in Vue 2,
|
state.deArrowCache[payload.videoId] = payload
|
||||||
// so we need to use Vue's set function
|
|
||||||
vueSet(state.deArrowCache, payload.videoId, payload)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addThumbnailToDeArrowCache (state, payload) {
|
addThumbnailToDeArrowCache (state, payload) {
|
||||||
vueSet(state.deArrowCache, payload.videoId, payload)
|
state.deArrowCache[payload.videoId] = payload
|
||||||
},
|
},
|
||||||
|
|
||||||
removeFromSessionSearchHistory (state, query) {
|
removeFromSessionSearchHistory (state, query) {
|
||||||
|
|||||||
@@ -267,8 +267,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|||||||
import autolinker from 'autolinker'
|
import autolinker from 'autolinker'
|
||||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
|
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router/composables'
|
|
||||||
import { YTNodes } from 'youtubei.js'
|
import { YTNodes } from 'youtubei.js'
|
||||||
|
|
||||||
import ChannelAbout from '../../components/ChannelAbout/ChannelAbout.vue'
|
import ChannelAbout from '../../components/ChannelAbout/ChannelAbout.vue'
|
||||||
@@ -2333,7 +2332,7 @@ function handleSubscription() {
|
|||||||
|
|
||||||
function filterWatchedArray(videos) {
|
function filterWatchedArray(videos) {
|
||||||
const historyCache = store.getters.getHistoryCacheById
|
const historyCache = store.getters.getHistoryCacheById
|
||||||
return videos.filter(video => !Object.hasOwn(historyCache, video.videoId))
|
return videos.filter(video => historyCache[video.videoId] === undefined)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
|||||||
import FtLoader from '../../components/FtLoader/FtLoader.vue'
|
import FtLoader from '../../components/FtLoader/FtLoader.vue'
|
||||||
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
|
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
|
||||||
import store from '../../store/index'
|
import store from '../../store/index'
|
||||||
import { useRoute } from 'vue-router/composables'
|
import { useRoute } from 'vue-router'
|
||||||
import packageDetails from '../../../../package.json'
|
import packageDetails from '../../../../package.json'
|
||||||
import { getHashtagLocal, parseLocalListVideo } from '../../helpers/api/local'
|
import { getHashtagLocal, parseLocalListVideo } from '../../helpers/api/local'
|
||||||
import { copyToClipboard, showToast } from '../../helpers/utils'
|
import { copyToClipboard, showToast } from '../../helpers/utils'
|
||||||
|
|||||||
@@ -84,8 +84,7 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
|
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router/composables'
|
|
||||||
|
|
||||||
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
|
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
|
||||||
import FtButton from '../../components/FtButton/FtButton.vue'
|
import FtButton from '../../components/FtButton/FtButton.vue'
|
||||||
|
|||||||
@@ -159,8 +159,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
|
import { isNavigationFailure, NavigationFailureType, onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router/composables'
|
|
||||||
|
|
||||||
import FtLoader from '../../components/FtLoader/FtLoader.vue'
|
import FtLoader from '../../components/FtLoader/FtLoader.vue'
|
||||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
@@ -703,7 +702,7 @@ function moveVideoUp(videoId, playlistItemId) {
|
|||||||
playlistName: playlistTitle.value,
|
playlistName: playlistTitle.value,
|
||||||
protected: selectedUserPlaylist.value.protected,
|
protected: selectedUserPlaylist.value.protected,
|
||||||
description: playlistDescription.value,
|
description: playlistDescription.value,
|
||||||
videos: playlistItems_,
|
videos: deepCopy(playlistItems_),
|
||||||
_id: playlistId.value
|
_id: playlistId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,7 +737,7 @@ function moveVideoDown(videoId, playlistItemId) {
|
|||||||
playlistName: playlistTitle.value,
|
playlistName: playlistTitle.value,
|
||||||
protected: selectedUserPlaylist.value.protected,
|
protected: selectedUserPlaylist.value.protected,
|
||||||
description: playlistDescription.value,
|
description: playlistDescription.value,
|
||||||
videos: playlistItems_,
|
videos: deepCopy(playlistItems_),
|
||||||
_id: playlistId.value
|
_id: playlistId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +796,8 @@ async function removeToBeDeletedVideosSometimes() {
|
|||||||
if (toBeDeletedPlaylistItemIds.value.length > 0) {
|
if (toBeDeletedPlaylistItemIds.value.length > 0) {
|
||||||
await store.dispatch('removeVideos', {
|
await store.dispatch('removeVideos', {
|
||||||
_id: playlistId.value,
|
_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 = []
|
toBeDeletedPlaylistItemIds.value = []
|
||||||
@@ -880,7 +880,7 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeRouteLeave((to, from, next) => {
|
onBeforeRouteLeave((to) => {
|
||||||
if (!isLoading.value && to.path.startsWith('/watch') && to.query.playlistId === playlistId.value) {
|
if (!isLoading.value && to.path.startsWith('/watch') && to.query.playlistId === playlistId.value) {
|
||||||
store.commit('setCachedPlaylist', {
|
store.commit('setCachedPlaylist', {
|
||||||
id: playlistId.value,
|
id: playlistId.value,
|
||||||
@@ -895,7 +895,6 @@ onBeforeRouteLeave((to, from, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeToBeDeletedVideosSometimes()
|
removeToBeDeletedVideosSometimes()
|
||||||
next()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
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 packageDetails from '../../../package.json'
|
||||||
import { useI18n } from '../composables/use-i18n-polyfill'
|
import { useI18n } from '../composables/use-i18n-polyfill'
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
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 FtLoader from '../../components/FtLoader/FtLoader.vue'
|
||||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export default defineComponent({
|
|||||||
this.handleMounted()
|
this.handleMounted()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeUnmount: function () {
|
||||||
document.removeEventListener('scroll', this.markScrolledToSectionAsActive)
|
document.removeEventListener('scroll', this.markScrolledToSectionAsActive)
|
||||||
window.removeEventListener('resize', this.handleResize)
|
window.removeEventListener('resize', this.handleResize)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,11 +39,11 @@
|
|||||||
<div class="settingsSections">
|
<div class="settingsSections">
|
||||||
<template
|
<template
|
||||||
v-for="(settingsComponent) in settingsSectionComponents"
|
v-for="(settingsComponent) in settingsSectionComponents"
|
||||||
|
:key="settingsComponent.type"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="settingsComponent.type"
|
:is="settingsComponent.type"
|
||||||
:ref="settingsComponent.type"
|
:ref="settingsComponent.type"
|
||||||
:key="settingsComponent.type"
|
|
||||||
:class="{ hideOnMobile: settingsSectionTypeOpenInMobile !== settingsComponent.type }"
|
:class="{ hideOnMobile: settingsSectionTypeOpenInMobile !== settingsComponent.type }"
|
||||||
class="section"
|
class="section"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -83,8 +83,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
|
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router/composables'
|
|
||||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||||
import FtInput from '../../components/ft-input/ft-input.vue'
|
import FtInput from '../../components/ft-input/ft-input.vue'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
ref="videosTab"
|
ref="videosTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'videos')"
|
:aria-selected="currentTab === 'videos'"
|
||||||
aria-controls="subscriptionsPanel"
|
aria-controls="subscriptionsPanel"
|
||||||
:tabindex="currentTab === 'videos' ? 0 : -1"
|
:tabindex="currentTab === 'videos' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'videos' }"
|
:class="{ selectedTab: currentTab === 'videos' }"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
ref="shortsTab"
|
ref="shortsTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'shorts')"
|
:aria-selected="currentTab === 'shorts'"
|
||||||
aria-controls="subscriptionsPanel"
|
aria-controls="subscriptionsPanel"
|
||||||
:tabindex="currentTab === 'shorts' ? 0 : -1"
|
:tabindex="currentTab === 'shorts' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'shorts' }"
|
:class="{ selectedTab: currentTab === 'shorts' }"
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
ref="liveTab"
|
ref="liveTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'live')"
|
:aria-selected="currentTab === 'live'"
|
||||||
aria-controls="subscriptionsPanel"
|
aria-controls="subscriptionsPanel"
|
||||||
:tabindex="currentTab === 'live' ? 0 : -1"
|
:tabindex="currentTab === 'live' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'live' }"
|
:class="{ selectedTab: currentTab === 'live' }"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
ref="communityTab"
|
ref="communityTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'community')"
|
:aria-selected="currentTab === 'community'"
|
||||||
aria-controls="subscriptionsPanel"
|
aria-controls="subscriptionsPanel"
|
||||||
:tabindex="currentTab === 'community' ? 0 : -1"
|
:tabindex="currentTab === 'community' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'community' }"
|
:class="{ selectedTab: currentTab === 'community' }"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
ref="defaultTab"
|
ref="defaultTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'default')"
|
:aria-selected="currentTab === 'default'"
|
||||||
aria-controls="trendingPanel"
|
aria-controls="trendingPanel"
|
||||||
:tabindex="currentTab === 'default' ? 0 : -1"
|
:tabindex="currentTab === 'default' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'default' }"
|
:class="{ selectedTab: currentTab === 'default' }"
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
ref="musicTab"
|
ref="musicTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'music')"
|
:aria-selected="currentTab === 'music'"
|
||||||
aria-controls="trendingPanel"
|
aria-controls="trendingPanel"
|
||||||
:tabindex="currentTab === 'music' ? 0 : -1"
|
:tabindex="currentTab === 'music' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'music' }"
|
:class="{ selectedTab: currentTab === 'music' }"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
ref="gamingTab"
|
ref="gamingTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'gaming')"
|
:aria-selected="currentTab === 'gaming'"
|
||||||
aria-controls="trendingPanel"
|
aria-controls="trendingPanel"
|
||||||
:tabindex="currentTab === 'gaming' ? 0 : -1"
|
:tabindex="currentTab === 'gaming' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'gaming' }"
|
:class="{ selectedTab: currentTab === 'gaming' }"
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
ref="moviesTab"
|
ref="moviesTab"
|
||||||
class="tab"
|
class="tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:aria-selected="String(currentTab === 'movies')"
|
:aria-selected="currentTab === 'movies'"
|
||||||
aria-controls="trendingPanel"
|
aria-controls="trendingPanel"
|
||||||
:tabindex="currentTab === 'movies' ? 0 : -1"
|
:tabindex="currentTab === 'movies' ? 0 : -1"
|
||||||
:class="{ selectedTab: currentTab === 'movies' }"
|
:class="{ selectedTab: currentTab === 'movies' }"
|
||||||
|
|||||||
@@ -99,8 +99,7 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from '../../composables/use-i18n-polyfill'
|
import { useI18n } from '../../composables/use-i18n-polyfill'
|
||||||
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
|
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router/composables'
|
|
||||||
|
|
||||||
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
|
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
|
||||||
import FtButton from '../../components/FtButton/FtButton.vue'
|
import FtButton from '../../components/FtButton/FtButton.vue'
|
||||||
|
|||||||
@@ -1372,7 +1372,7 @@ export default defineComponent({
|
|||||||
showToast(
|
showToast(
|
||||||
({ remainingMs }) => {
|
({ remainingMs }) => {
|
||||||
const countDownTimeLeftInSecond = remainingMs / 1000
|
const countDownTimeLeftInSecond = remainingMs / 1000
|
||||||
return this.$tc('Playing Next Video Interval', countDownTimeLeftInSecond, { nextVideoInterval: countDownTimeLeftInSecond })
|
return this.$t('Playing Next Video Interval', { nextVideoInterval: countDownTimeLeftInSecond }, countDownTimeLeftInSecond)
|
||||||
},
|
},
|
||||||
// So that we don't see last countdown text like 0/N
|
// So that we don't see last countdown text like 0/N
|
||||||
nextVideoInterval * 1000,
|
nextVideoInterval * 1000,
|
||||||
|
|||||||
@@ -867,7 +867,7 @@ Video:
|
|||||||
Stats: 'Statistieke'
|
Stats: 'Statistieke'
|
||||||
Video ID: 'Video ID: {videoId}'
|
Video ID: 'Video ID: {videoId}'
|
||||||
Media Formats: 'Mediaformate: {formats}'
|
Media Formats: 'Mediaformate: {formats}'
|
||||||
Resolution: 'Resolusie: {width}x{height}@{frameRate}'
|
Resolution: 'Resolusie: {width}x{height}{''@''}{frameRate}'
|
||||||
Player Dimensions: 'Spelerdimensies: {width}x{height}'
|
Player Dimensions: 'Spelerdimensies: {width}x{height}'
|
||||||
Bitrate: 'Bistempo: {bitrate} kbps'
|
Bitrate: 'Bistempo: {bitrate} kbps'
|
||||||
Volume: 'Volume: {volumePercentage}%'
|
Volume: 'Volume: {volumePercentage}%'
|
||||||
|
|||||||
@@ -833,7 +833,7 @@ Video:
|
|||||||
CodecsVideoAudio: 'برامج الترميز: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'برامج الترميز: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
Stats: الاحصائيات
|
Stats: الاحصائيات
|
||||||
Video ID: 'معرف الفيديو: {videoId}'
|
Video ID: 'معرف الفيديو: {videoId}'
|
||||||
Resolution: 'الدقة: {width}x{height}@{frameRate}'
|
Resolution: 'الدقة: {width}x{height}{''@''}{frameRate}'
|
||||||
Bandwidth: 'عرض النطاق الترددي: {bandwidth} كيلوبت في الثانية'
|
Bandwidth: 'عرض النطاق الترددي: {bandwidth} كيلوبت في الثانية'
|
||||||
Skipped segment: تم تخطي شريحة {segmentCategory}
|
Skipped segment: تم تخطي شريحة {segmentCategory}
|
||||||
Theatre Mode: وضع المسرح
|
Theatre Mode: وضع المسرح
|
||||||
|
|||||||
@@ -845,7 +845,7 @@ Video:
|
|||||||
Player Dimensions: 'Памеры прайгравальніка: {width}x{height}'
|
Player Dimensions: 'Памеры прайгравальніка: {width}x{height}'
|
||||||
Bitrate: 'Бітрэйт: {bitrate} кбіт/с'
|
Bitrate: 'Бітрэйт: {bitrate} кбіт/с'
|
||||||
Buffered: 'Буферызавана: {bufferedPercentage}%'
|
Buffered: 'Буферызавана: {bufferedPercentage}%'
|
||||||
Resolution: 'Раздзяляльнасць: {width}x{height}@{frameRate}'
|
Resolution: 'Раздзяляльнасць: {width}x{height}{''@''}{frameRate}'
|
||||||
Dropped Frames / Total Frames: 'Прапушчаныя кадры: {droppedFrames} / Усяго кадраў: {totalFrames}'
|
Dropped Frames / Total Frames: 'Прапушчаныя кадры: {droppedFrames} / Усяго кадраў: {totalFrames}'
|
||||||
CodecsVideoAudio: 'Кодэкі: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'Кодэкі: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
CodecsVideoAudioNoItags: 'Кодэкі: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Кодэкі: {videoCodec} / {audioCodec}'
|
||||||
|
|||||||
@@ -842,7 +842,7 @@ Video:
|
|||||||
CodecAudio: 'Кодек: {audioCodec} ({audioItag})'
|
CodecAudio: 'Кодек: {audioCodec} ({audioItag})'
|
||||||
CodecsVideoAudio: 'Кодеци: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'Кодеци: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
CodecsVideoAudioNoItags: 'Кодеци: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Кодеци: {videoCodec} / {audioCodec}'
|
||||||
Resolution: 'Разделителна способност: {width}x{height}@{frameRate}'
|
Resolution: 'Разделителна способност: {width}x{height}{''@''}{frameRate}'
|
||||||
Player Dimensions: 'Размери на плейъра: {width}x{height}'
|
Player Dimensions: 'Размери на плейъра: {width}x{height}'
|
||||||
Bitrate: 'Скорост на предаване: {bitrate} kbps'
|
Bitrate: 'Скорост на предаване: {bitrate} kbps'
|
||||||
Dropped Frames / Total Frames: 'Изпуснати кадри: {droppedFrames} / Общо кадри: {totalFrames}'
|
Dropped Frames / Total Frames: 'Изпуснати кадри: {droppedFrames} / Общо кадри: {totalFrames}'
|
||||||
|
|||||||
@@ -903,7 +903,7 @@ Video:
|
|||||||
Stats: 'Stadegoù'
|
Stats: 'Stadegoù'
|
||||||
Video ID: 'ID ar Video : {videoId}'
|
Video ID: 'ID ar Video : {videoId}'
|
||||||
Media Formats: 'Furmadoù ar media : {formats}'
|
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}'
|
Player Dimensions: 'Ment al lenner : {width}x{height}'
|
||||||
Bitrate: 'Fonnder bit : {bitrate} kbps'
|
Bitrate: 'Fonnder bit : {bitrate} kbps'
|
||||||
Volume: 'Live-son : {volumePercentage}%'
|
Volume: 'Live-son : {volumePercentage}%'
|
||||||
|
|||||||
@@ -835,7 +835,7 @@ Video:
|
|||||||
Stats: Statistiky
|
Stats: Statistiky
|
||||||
Video ID: 'ID videa: {videoId}'
|
Video ID: 'ID videa: {videoId}'
|
||||||
Media Formats: 'Formáty médií: {formats}'
|
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'
|
Bandwidth: 'Šířka pásma: {bandwidth} kb/s'
|
||||||
Buffered: 'Načteno: {bufferedPercentage} %'
|
Buffered: 'Načteno: {bufferedPercentage} %'
|
||||||
Dropped Frames / Total Frames: 'Vypuštěno snímků: {droppedFrames} / Celkem snímků: {totalFrames}'
|
Dropped Frames / Total Frames: 'Vypuštěno snímků: {droppedFrames} / Celkem snímků: {totalFrames}'
|
||||||
|
|||||||
@@ -844,7 +844,7 @@ Video:
|
|||||||
Player:
|
Player:
|
||||||
Take Screenshot: Tynnu Llun Sgrin
|
Take Screenshot: Tynnu Llun Sgrin
|
||||||
Stats:
|
Stats:
|
||||||
Resolution: 'Cydraniad: {width}x{height}@{frameRate}'
|
Resolution: 'Cydraniad: {width}x{height}{''@''}{frameRate}'
|
||||||
Bitrate: 'Cyfradd didau: {bitrate} kbps'
|
Bitrate: 'Cyfradd didau: {bitrate} kbps'
|
||||||
Bandwidth: 'Lled band: {bandwidth} kbps'
|
Bandwidth: 'Lled band: {bandwidth} kbps'
|
||||||
Buffered: 'Byffrwyd: {bufferedPercentage}%'
|
Buffered: 'Byffrwyd: {bufferedPercentage}%'
|
||||||
|
|||||||
@@ -817,7 +817,7 @@ Video:
|
|||||||
Stats:
|
Stats:
|
||||||
Video ID: 'Video-ID: {videoId}'
|
Video ID: 'Video-ID: {videoId}'
|
||||||
Media Formats: 'Medieformater: {formats}'
|
Media Formats: 'Medieformater: {formats}'
|
||||||
Resolution: 'Opløsning: {width}x{height}@{frameRate}'
|
Resolution: 'Opløsning: {width}x{height}{''@''}{frameRate}'
|
||||||
Bandwidth: 'Båndbredde: {bandwidth} kbps'
|
Bandwidth: 'Båndbredde: {bandwidth} kbps'
|
||||||
Stats: Statistik
|
Stats: Statistik
|
||||||
Bitrate: 'Bitrate: {bitrate} kbps'
|
Bitrate: 'Bitrate: {bitrate} kbps'
|
||||||
|
|||||||
@@ -791,7 +791,7 @@ Video:
|
|||||||
Stats: Statistiken
|
Stats: Statistiken
|
||||||
Video ID: 'Videokennung (ID): {videoId}'
|
Video ID: 'Videokennung (ID): {videoId}'
|
||||||
Media Formats: 'Medienformate: {formats}'
|
Media Formats: 'Medienformate: {formats}'
|
||||||
Resolution: 'Auflösung: {width}×{height}@{frameRate}'
|
Resolution: 'Auflösung: {width}×{height}{''@''}{frameRate}'
|
||||||
Player Dimensions: 'Player-Dimensionen: {width}×{height}'
|
Player Dimensions: 'Player-Dimensionen: {width}×{height}'
|
||||||
Bitrate: 'Bitrate: {bitrate} kb/s'
|
Bitrate: 'Bitrate: {bitrate} kb/s'
|
||||||
Volume: 'Lautstärke: {volumePercentage} %'
|
Volume: 'Lautstärke: {volumePercentage} %'
|
||||||
|
|||||||
@@ -831,7 +831,7 @@ Video:
|
|||||||
CodecsVideoAudioNoItags: 'Κωδικοποιητές: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Κωδικοποιητές: {videoCodec} / {audioCodec}'
|
||||||
CodecAudio: 'Κωδικοποιητής: {audioCodec} ({audioItag})'
|
CodecAudio: 'Κωδικοποιητής: {audioCodec} ({audioItag})'
|
||||||
Bitrate: 'Ρυθμός μετάδοσης δεδομένων: {bitrate} kbps'
|
Bitrate: 'Ρυθμός μετάδοσης δεδομένων: {bitrate} kbps'
|
||||||
Resolution: 'Διαστάσεις: {width}x{height}@{frameRate}'
|
Resolution: 'Διαστάσεις: {width}x{height}{''@''}{frameRate}'
|
||||||
Buffered: 'Αποθηκευμένη: {bufferedPercentage}%'
|
Buffered: 'Αποθηκευμένη: {bufferedPercentage}%'
|
||||||
Theatre Mode: Λειτουργία Θεάτρου
|
Theatre Mode: Λειτουργία Θεάτρου
|
||||||
Full Window: Πλήρες Παράθυρο
|
Full Window: Πλήρες Παράθυρο
|
||||||
|
|||||||
@@ -841,7 +841,7 @@ Video:
|
|||||||
Stats: Stats
|
Stats: Stats
|
||||||
Video ID: 'Video ID: {videoId}'
|
Video ID: 'Video ID: {videoId}'
|
||||||
Media Formats: 'Media formats: {formats}'
|
Media Formats: 'Media formats: {formats}'
|
||||||
Resolution: 'Resolution: {width}×{height}@{frameRate}'
|
Resolution: 'Resolution: {width}×{height}{''@''}{frameRate}'
|
||||||
Player Dimensions: 'Player dimensions: {width}×{height}'
|
Player Dimensions: 'Player dimensions: {width}×{height}'
|
||||||
Bitrate: 'Bitrate: {bitrate} kb/s'
|
Bitrate: 'Bitrate: {bitrate} kb/s'
|
||||||
Volume: 'Volume: {volumePercentage}%'
|
Volume: 'Volume: {volumePercentage}%'
|
||||||
|
|||||||
@@ -931,7 +931,7 @@ Video:
|
|||||||
Stats: Stats
|
Stats: Stats
|
||||||
Video ID: 'Video ID: {videoId}'
|
Video ID: 'Video ID: {videoId}'
|
||||||
Media Formats: 'Media Formats: {formats}'
|
Media Formats: 'Media Formats: {formats}'
|
||||||
Resolution: 'Resolution: {width}x{height}@{frameRate}'
|
Resolution: 'Resolution: {width}x{height}{''@''}{frameRate}'
|
||||||
Player Dimensions: 'Player Dimensions: {width}x{height}'
|
Player Dimensions: 'Player Dimensions: {width}x{height}'
|
||||||
Bitrate: 'Bitrate: {bitrate} kbps'
|
Bitrate: 'Bitrate: {bitrate} kbps'
|
||||||
Volume: 'Volume: {volumePercentage}%'
|
Volume: 'Volume: {volumePercentage}%'
|
||||||
|
|||||||
@@ -832,7 +832,7 @@ Video:
|
|||||||
Stats: Estadísticas
|
Stats: Estadísticas
|
||||||
Video ID: 'ID del vídeo: {videoId}'
|
Video ID: 'ID del vídeo: {videoId}'
|
||||||
Media Formats: 'Formatos del medio: {formats}'
|
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}'
|
Player Dimensions: 'Dimensiones del reproductor: {width}x{height}'
|
||||||
Bitrate: 'Tasa de bits: {bitrate} kbps'
|
Bitrate: 'Tasa de bits: {bitrate} kbps'
|
||||||
Volume: 'Volumen: {volumePercentage}%'
|
Volume: 'Volumen: {volumePercentage}%'
|
||||||
|
|||||||
@@ -843,7 +843,7 @@ Video:
|
|||||||
CodecsVideoAudio: 'Koodekid: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'Koodekid: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
CodecsVideoAudioNoItags: 'Koodekid: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Koodekid: {videoCodec} / {audioCodec}'
|
||||||
Stats: Statistika
|
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}'
|
Player Dimensions: 'Meediamängija mõõdud: {width}x{height}'
|
||||||
Dropped Frames / Total Frames: 'Vahelejäetud kaadreid: {droppedFrames} / Kaadreid kokku: {totalFrames}'
|
Dropped Frames / Total Frames: 'Vahelejäetud kaadreid: {droppedFrames} / Kaadreid kokku: {totalFrames}'
|
||||||
Skipped segment: Vahelejäetud {segmentCategory} segment
|
Skipped segment: Vahelejäetud {segmentCategory} segment
|
||||||
|
|||||||
@@ -837,7 +837,7 @@ Video:
|
|||||||
Dropped Frames / Total Frames: 'Jaregindako fotogramak: {droppedFrames} / Total Frames: {totalFrames}'
|
Dropped Frames / Total Frames: 'Jaregindako fotogramak: {droppedFrames} / Total Frames: {totalFrames}'
|
||||||
Volume: 'Bolumena: {volumePercentage}%'
|
Volume: 'Bolumena: {volumePercentage}%'
|
||||||
CodecsVideoAudio: 'Kodekak: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
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}'
|
Player Dimensions: 'Erreproduzigailuaren neurriak: {width}x{height}'
|
||||||
Bitrate: 'Bit-emaria: {bitrate} kbps'
|
Bitrate: 'Bit-emaria: {bitrate} kbps'
|
||||||
CodecsVideoAudioNoItags: 'Kodekak: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Kodekak: {videoCodec} / {audioCodec}'
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ Video:
|
|||||||
Video ID: 'شناسه ویدئو: {videoId}'
|
Video ID: 'شناسه ویدئو: {videoId}'
|
||||||
Media Formats: 'فرمت ویدئو: {formats}'
|
Media Formats: 'فرمت ویدئو: {formats}'
|
||||||
Stats: اطلاعات
|
Stats: اطلاعات
|
||||||
Resolution: 'وضوح: {width}x{height}@{frameRate}'
|
Resolution: 'وضوح: {width}x{height}{''@''}{frameRate}'
|
||||||
Bandwidth: 'پهنای باند: {bandwidth} کیلوبیتبرثانیه'
|
Bandwidth: 'پهنای باند: {bandwidth} کیلوبیتبرثانیه'
|
||||||
Player Dimensions: 'ابعاد پخش کننده: {width}x{height}'
|
Player Dimensions: 'ابعاد پخش کننده: {width}x{height}'
|
||||||
Bitrate: 'نرخ بیت: {bitrate} کیلوبیتبرثانیه'
|
Bitrate: 'نرخ بیت: {bitrate} کیلوبیتبرثانیه'
|
||||||
|
|||||||
@@ -789,7 +789,7 @@ Video:
|
|||||||
Stats: Stats
|
Stats: Stats
|
||||||
Video ID: 'Identifiant de la vidéo : {videoId}'
|
Video ID: 'Identifiant de la vidéo : {videoId}'
|
||||||
Media Formats: 'Formats du média : {formats}'
|
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}'
|
Player Dimensions: 'Dimensions du lecteur : {width}×{height}'
|
||||||
Volume: 'Volume : {volumePercentage} %'
|
Volume: 'Volume : {volumePercentage} %'
|
||||||
Bandwidth: 'Bande passante : {bandwidth} kb/s'
|
Bandwidth: 'Bande passante : {bandwidth} kb/s'
|
||||||
|
|||||||
@@ -835,7 +835,7 @@ Video:
|
|||||||
Show Stats: הצג נתונים
|
Show Stats: הצג נתונים
|
||||||
Stats:
|
Stats:
|
||||||
Video ID: 'ID סרטון: {videoId}'
|
Video ID: 'ID סרטון: {videoId}'
|
||||||
Resolution: 'רזולוציה: {height}x{width}@{frameRate}'
|
Resolution: 'רזולוציה: {height}x{width}{''@''}{frameRate}'
|
||||||
Stats: נתונים
|
Stats: נתונים
|
||||||
Media Formats: 'פורמטים מדיה: {formats}'
|
Media Formats: 'פורמטים מדיה: {formats}'
|
||||||
Player Dimensions: 'ממדי הנגן: {height}x{width}'
|
Player Dimensions: 'ממדי הנגן: {height}x{width}'
|
||||||
|
|||||||
@@ -827,7 +827,7 @@ Video:
|
|||||||
Stats: Statistika
|
Stats: Statistika
|
||||||
CodecsVideoAudio: 'Kodeci: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'Kodeci: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
Bitrate: 'Brzina: {bitrate} kbps'
|
Bitrate: 'Brzina: {bitrate} kbps'
|
||||||
Resolution: 'Rezolucija: {width}x{height}@{frameRate}'
|
Resolution: 'Rezolucija: {width}x{height}{''@''}{frameRate}'
|
||||||
CodecsVideoAudioNoItags: 'Kodeci: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Kodeci: {videoCodec} / {audioCodec}'
|
||||||
Volume: 'Glasnoća: {volumePercentage}%'
|
Volume: 'Glasnoća: {volumePercentage}%'
|
||||||
Video ID: 'ID videa: {videoId}'
|
Video ID: 'ID videa: {videoId}'
|
||||||
|
|||||||
@@ -846,7 +846,7 @@ Video:
|
|||||||
CodecAudio: 'Kodek: {audioCodec} ({audioItag})'
|
CodecAudio: 'Kodek: {audioCodec} ({audioItag})'
|
||||||
CodecsVideoAudioNoItags: 'Kodekek: {videoCodec} / {audioCodec}'
|
CodecsVideoAudioNoItags: 'Kodekek: {videoCodec} / {audioCodec}'
|
||||||
Dropped Frames / Total Frames: 'Kihagyott képkockák: {droppedFrames} / Összes képkocka: {totalFrames}'
|
Dropped Frames / Total Frames: 'Kihagyott képkockák: {droppedFrames} / Összes képkocka: {totalFrames}'
|
||||||
Resolution: 'Felbontás: {width}x{height}@{frameRate}'
|
Resolution: 'Felbontás: {width}x{height}{''@''}{frameRate}'
|
||||||
CodecsVideoAudio: 'Kodekek: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'Kodekek: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
You appear to be offline: Úgy tűnik, hogy jelenleg nem kapcsolódsz a hálózathoz.
|
You appear to be offline: Úgy tűnik, hogy jelenleg nem kapcsolódsz a hálózathoz.
|
||||||
Skipped segment: Kihagyott {segmentCategory} szakasz
|
Skipped segment: Kihagyott {segmentCategory} szakasz
|
||||||
|
|||||||
@@ -839,7 +839,7 @@ Video:
|
|||||||
Dropped Frames / Total Frames: 'Frame yang Dihilangkan: {droppedFrames} / Jumlah Frame: {totalFrames}'
|
Dropped Frames / Total Frames: 'Frame yang Dihilangkan: {droppedFrames} / Jumlah Frame: {totalFrames}'
|
||||||
Media Formats: 'Format Media: {formats}'
|
Media Formats: 'Format Media: {formats}'
|
||||||
CodecsVideoAudio: 'Kodek: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
CodecsVideoAudio: 'Kodek: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
|
||||||
Resolution: 'Resolusi: {width}x{height}@{frameRate}'
|
Resolution: 'Resolusi: {width}x{height}{''@''}{frameRate}'
|
||||||
Player Dimensions: 'Dimensi Pemain: {width}x{height}'
|
Player Dimensions: 'Dimensi Pemain: {width}x{height}'
|
||||||
Stats: Statistik
|
Stats: Statistik
|
||||||
Bandwidth: 'bandwidth: {bandwidth} kbps'
|
Bandwidth: 'bandwidth: {bandwidth} kbps'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user