mirror of https://github.com/FreeTubeApp/FreeTube
Merge branch 'development' into add-command-line-arg-to-search
This commit is contained in:
commit
11973f9597
4
.babelrc
4
.babelrc
|
@ -4,8 +4,8 @@
|
|||
"@babel/env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "106",
|
||||
"node": "16.16.0"
|
||||
"chrome": "122",
|
||||
"node": "20.9.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
46
.eslintrc.js
46
.eslintrc.js
|
@ -1,3 +1,8 @@
|
|||
const path = require('path')
|
||||
const { readFileSync } = require('fs')
|
||||
|
||||
const activeLocales = JSON.parse(readFileSync(path.join(__dirname, './static/locales/activeLocales.json')))
|
||||
|
||||
module.exports = {
|
||||
// https://eslint.org/docs/user-guide/configuring#using-configuration-files-1
|
||||
root: true,
|
||||
|
@ -47,11 +52,12 @@ module.exports = {
|
|||
'plugin:vue/recommended',
|
||||
'standard',
|
||||
'plugin:jsonc/recommended-with-json',
|
||||
'plugin:vuejs-accessibility/recommended'
|
||||
'plugin:vuejs-accessibility/recommended',
|
||||
'plugin:@intlify/vue-i18n/recommended'
|
||||
],
|
||||
|
||||
// https://eslint.org/docs/user-guide/configuring#configuring-plugins
|
||||
plugins: ['vue', 'vuejs-accessibility', 'n', 'unicorn'],
|
||||
plugins: ['vue', 'vuejs-accessibility', 'n', 'unicorn', '@intlify/vue-i18n'],
|
||||
|
||||
rules: {
|
||||
'space-before-function-paren': 'off',
|
||||
|
@ -77,6 +83,40 @@ module.exports = {
|
|||
'unicorn/no-array-push-push': 'error',
|
||||
'unicorn/prefer-keyboard-event-key': 'error',
|
||||
'unicorn/prefer-regexp-test': 'error',
|
||||
'unicorn/prefer-string-replace-all': 'error'
|
||||
'unicorn/prefer-string-replace-all': 'error',
|
||||
'@intlify/vue-i18n/no-dynamic-keys': 'error',
|
||||
// TODO: enable at a later date. currently disabled to prevent massive conflicts for initial PR
|
||||
// '@intlify/vue-i18n/no-unused-keys': [
|
||||
// 'error',
|
||||
// {
|
||||
// extensions: ['.js', '.vue', 'yaml']
|
||||
// }
|
||||
// ],
|
||||
'@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
|
||||
'@intlify/vue-i18n/no-raw-text': [
|
||||
'error',
|
||||
{
|
||||
attributes: {
|
||||
'/.+/': [
|
||||
'title',
|
||||
'aria-label',
|
||||
'aria-placeholder',
|
||||
'aria-roledescription',
|
||||
'aria-valuetext',
|
||||
'tooltip',
|
||||
'message'
|
||||
],
|
||||
input: ['placeholder', 'value'],
|
||||
img: ['alt']
|
||||
},
|
||||
ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube']
|
||||
}
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
'vue-i18n': {
|
||||
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
|
||||
messageSyntaxVersion: '^8.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ body:
|
|||
- Portable
|
||||
- .rpm
|
||||
- .zip
|
||||
- .apk (Android, FreeTubeCordova Unofficial)
|
||||
- .apk (FreeTubeAndroid Unofficial)
|
||||
- AUR (Unofficial)
|
||||
- Chocolatey (Unofficial)
|
||||
- Homebrew (Unofficial)
|
||||
|
|
|
@ -8,7 +8,7 @@ contact_links:
|
|||
about: Ask and answer questions
|
||||
- name: Matrix Community
|
||||
url: https://matrix.to/#/+freetube:matrix.org
|
||||
about: Join our Matrix chatroom - "Note: Bugs and Feature requests should be made on GitHub and not in the Matrix room"
|
||||
about: 'Join our Matrix chatroom - "Note: Bugs and Feature requests should be made on GitHub and not in the Matrix room"'
|
||||
- name: Translate FreeTube
|
||||
url: https://hosted.weblate.org/engage/free-tube/
|
||||
about: Help translate FreeTube on Weblate
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- '(visual bug)'
|
||||
|
||||
'B: Unofficial Download':
|
||||
- '(AUR \(Unofficial\)|Chocolatey \(Unofficial\)|\.apk \(Android, FreeTubeCordova Unofficial\)|Homebrew \(Unofficial\)|PortableApps \(Unofficial\)|WAPT \(Unofficial\)|winget \(Unofficial\)|Scoop \(Unofficial\)|Snapcraft \(Unofficial\)|MPR \(Unofficial\)|Nix \(Unofficial\))'
|
||||
- '(AUR \(Unofficial\)|Chocolatey \(Unofficial\)|\.apk \(FreeTubeAndroid Unofficial\)|Homebrew \(Unofficial\)|PortableApps \(Unofficial\)|WAPT \(Unofficial\)|winget \(Unofficial\)|Scoop \(Unofficial\)|Snapcraft \(Unofficial\)|MPR \(Unofficial\)|Nix \(Unofficial\))'
|
||||
|
||||
'B: keyboard control':
|
||||
- '(keyboard control not working)'
|
||||
|
|
|
@ -5,10 +5,10 @@ on:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Comment Author
|
||||
if: github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER'
|
||||
uses: Amwam/issue-comment-action@v1.3.1
|
||||
with:
|
||||
keywords: '["duplicate of #", "duplicate of https://github.com/FreeTubeApp/FreeTube/issues/", "duplicate of https://github.com/FreeTubeApp/FreeTube/pulls/"]'
|
||||
|
|
|
@ -5,11 +5,11 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ !github.event.pull_request.draft && (contains(github.event.pull_request.base.ref, 'development') || contains(github.event.pull_request.base.ref, 'RC')) }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Auto Merge PR
|
||||
if: ${{ !github.event.pull_request.draft && (contains(github.event.pull_request.base.ref, 'development') || contains(github.event.pull_request.base.ref, 'RC')) }}
|
||||
run: |
|
||||
echo ${{ secrets.PUSH_TOKEN }} >> auth.txt
|
||||
gh auth login --with-token < auth.txt
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
runtime:
|
||||
- linux-x64
|
||||
- linux-armv7l
|
||||
|
|
|
@ -18,10 +18,12 @@ jobs:
|
|||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js 18.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: "yarn"
|
||||
- run: yarn run ci
|
||||
- run: yarn run lint
|
||||
# let's verify that webpack is able to package the project
|
||||
- run: yarn run pack
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
runtime:
|
||||
- linux-x64
|
||||
- linux-armv7l
|
||||
|
|
64
README.md
64
README.md
|
@ -4,7 +4,7 @@
|
|||
|
||||
FreeTube is an open source desktop YouTube player built with privacy in mind.
|
||||
Use YouTube without advertisements and prevent Google from tracking you with their cookies and JavaScript.
|
||||
Available for Windows, Mac & Linux thanks to Electron.
|
||||
Available for Windows (10 and later), Mac (macOS 10.15 and later) & Linux thanks to Electron.
|
||||
|
||||
<p align="center"><a href="https://github.com/FreeTubeApp/FreeTube/releases">Download FreeTube</a></p>
|
||||
<p align="center">
|
||||
|
@ -21,17 +21,20 @@ Available for Windows, Mac & Linux thanks to Electron.
|
|||
<p align="center"><a href="https://freetubeapp.io/">Website</a> • <a href="https://blog.freetubeapp.io/">Blog</a> • <a href="https://docs.freetubeapp.io/">Documentation</a> • <a href="https://docs.freetubeapp.io/faq/">FAQ</a> • <a href="https://github.com/FreeTubeApp/FreeTube/discussions">Discussions</a></p>
|
||||
<hr>
|
||||
|
||||
<b>Please note that FreeTube is currently in Beta. While it should work well for most users, there are still bugs and missing features that need to be addressed. If you have an idea or if you found a bug, please submit a [GitHub issue](https://github.com/FreeTubeApp/FreeTube/issues/new/choose) so that
|
||||
we can track it. Please search [the existing issues](https://github.com/FreeTubeApp/FreeTube/issues) before submitting to
|
||||
prevent duplicates!</b>
|
||||
> [!NOTE]
|
||||
> FreeTube is currently in Beta. While it should work well for most users, there are still bugs and missing features that need to be addressed.
|
||||
>
|
||||
> If you have an idea or if you found a bug, please submit a [GitHub issue](https://github.com/FreeTubeApp/FreeTube/issues/new/choose) so that we can track it. Please search [the existing issues](https://github.com/FreeTubeApp/FreeTube/issues) before submitting to prevent duplicates!
|
||||
|
||||
## Screenshots
|
||||
<img src="https://i.imgur.com/zFgZUUV.png" width=300> <img src="https://i.imgur.com/9evYHgN.png" width=300> <img src="https://i.imgur.com/yT2UzPa.png" width=300> <img src="https://i.imgur.com/47zIEt4.png" width=300> <img src="https://i.imgur.com/hFB2fKC.png" width=300>
|
||||
|
||||
## How does it work?
|
||||
FreeTube uses a built in extractor to grab and serve data / videos. The [Invidious API](https://github.com/iv-org/invidious) can also optionally be used. FreeTube does not use any official APIs to obtain data. While YouTube can still see your video requests, it can no
|
||||
longer track you using cookies or JavaScript. Your subscriptions and history are stored locally on your computer and never sent out. Using a VPN or Tor is highly recommended
|
||||
to hide your IP while using FreeTube.
|
||||
longer track you using cookies or JavaScript. Your subscriptions and history are stored locally on your computer and never sent out.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Using a VPN or Tor is highly recommended to hide your IP while using FreeTube.
|
||||
|
||||
## Features
|
||||
* Watch videos without ads
|
||||
|
@ -59,16 +62,26 @@ to hide your IP while using FreeTube.
|
|||
* View most age restricted videos
|
||||
|
||||
### Browser Extension
|
||||
FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) and [LibRedirect](https://github.com/libredirect/libredirect) extensions, which will allow you to open YouTube links into FreeTube. You must enable the option within the advanced settings of the extension for it to work.
|
||||
FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) and [LibRedirect](https://github.com/libredirect/libredirect) extensions, which will allow you to open YouTube links into FreeTube.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You must enable the option within the advanced settings of the extension for it to work.
|
||||
|
||||
* Download Privacy Redirect for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-redirect/) or [Google Chrome](https://chrome.google.com/webstore/detail/privacy-redirect/pmcmeagblkinmogikoikkdjiligflglb).
|
||||
|
||||
* Download LibRedirect for [Firefox](https://addons.mozilla.org/firefox/addon/libredirect/) or [Google Chrome](https://libredirect.github.io/download_chromium.html).
|
||||
|
||||
If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository. This extension does not work on Linux portable builds!
|
||||
> [!NOTE]
|
||||
> This extension does not work on Linux portable builds!
|
||||
>
|
||||
> If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository.
|
||||
|
||||
## Download Links
|
||||
### Official Downloads
|
||||
|
||||
> [!CAUTION]
|
||||
> FreeTube is only supported on Windows 10 and later, macOS 10.15 and above, and various Linux distributions. Installing it on unsupported systems may result in unexpected issues.
|
||||
|
||||
* [GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases)
|
||||
|
||||
* [FreeTube Website](https://freetubeapp.io/#download)
|
||||
|
@ -76,18 +89,25 @@ If you have issues with the extension working with FreeTube, please create an is
|
|||
* Flatpak on Flathub: [Download](https://flathub.org/apps/details/io.freetubeapp.FreeTube) and [Source Code](https://github.com/flathub/io.freetubeapp.FreeTube)
|
||||
|
||||
#### Automated Builds (Nightly / Weekly)
|
||||
> [!WARNING]
|
||||
> Use these builds at your own risk. These are pre-release versions and are only intended for people that want to test changes early and are willing to accept that things could break from one build to another.
|
||||
|
||||
Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild).
|
||||
|
||||
The first build with a green check mark is the latest build. You will need to have a GitHub account to download these builds.
|
||||
The first build with a green check mark is the latest build.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You will need to have a GitHub account to download these builds.
|
||||
|
||||
### Unofficial Downloads
|
||||
These builds are maintained by the community. While they should be safe, download at your own risk. There may be issues with using these versus the official builds. Any issues specific with these builds should be sent to their respective maintainer. <b>Make sure u always try an [official download](https://github.com/freetubeapp/freetube/#official-downloads) before reporting your issue to us!</b>
|
||||
> [!WARNING]
|
||||
> These builds are maintained by the community. While they should be safe, download at your own risk. There may be issues with using these versus the official builds. Any issues specific with these builds should be sent to their respective maintainer. Make sure u always try an [official download](https://github.com/freetubeapp/freetube/#official-downloads) before reporting your issue to us!
|
||||
|
||||
* Arch User Repository (AUR): [Download](https://aur.archlinux.org/packages/freetube-bin/)
|
||||
|
||||
* Chocolatey: [Download](https://chocolatey.org/packages/freetube/)
|
||||
|
||||
* FreeTubeCordova (FreeTube port for Android and PWA): [Download](https://github.com/MarmadileManteater/FreeTubeCordova/releases) and [Source Code](https://github.com/MarmadileManteater/FreeTubeCordova)
|
||||
* FreeTubeAndroid (FreeTube port for Android and PWA): [Download](https://github.com/MarmadileManteater/FreeTubeAndroid/releases) and [Source Code](https://github.com/MarmadileManteater/FreeTubeAndroid)
|
||||
|
||||
* Homebrew Formulae (Mac only): [Download](https://formulae.brew.sh/cask/freetube)
|
||||
|
||||
|
@ -106,14 +126,14 @@ These builds are maintained by the community. While they should be safe, downloa
|
|||
* Windows Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
|
||||
|
||||
## Contributing
|
||||
If you like to get your hands dirty and want to contribute, we would love to
|
||||
have your help. Send a pull request and someone will review your code. Please
|
||||
follow the [Contribution
|
||||
Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/development/CONTRIBUTING.md)
|
||||
before sending your pull request.
|
||||
|
||||
Thank you very much to the [People and Projects](https://docs.freetubeapp.io/credits/) that make FreeTube possible!
|
||||
|
||||
If you like to get your hands dirty and want to contribute, we would love to
|
||||
have your help. Send a pull request and someone will review your code.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Please follow the [Contribution Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/development/CONTRIBUTING.md) before sending your pull request.
|
||||
|
||||
## Localization
|
||||
<a href="https://hosted.weblate.org/engage/free-tube/">
|
||||
<img src="https://hosted.weblate.org/widgets/free-tube/-/287x66-grey.png" alt="Translation status" />
|
||||
|
@ -124,7 +144,10 @@ We are actively looking for translations! We use [Weblate](https://hosted.webla
|
|||
For the Linux Flatpak, the desktop entry comment string can be translated at our [Flatpak repository](https://github.com/flathub/io.freetubeapp.FreeTube/blob/master/io.freetubeapp.FreeTube.desktop).
|
||||
|
||||
## Contact
|
||||
If you ever have any questions, feel free to ask it on our [Discussions](https://github.com/FreeTubeApp/FreeTube/discussions) page. Alternatively, you can email us at FreeTubeApp@protonmail.com or you can join our [Matrix Community](https://matrix.to/#/+freetube:matrix.org). Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining.
|
||||
If you ever have any questions, feel free to ask it on our [Discussions](https://github.com/FreeTubeApp/FreeTube/discussions) page. Alternatively, you can email us at FreeTubeApp@protonmail.com or you can join our [Matrix Community](https://matrix.to/#/+freetube:matrix.org).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining.
|
||||
|
||||
## Donate
|
||||
If you enjoy using FreeTube, you're welcome to leave a donation using the following methods.
|
||||
|
@ -135,7 +158,10 @@ If you enjoy using FreeTube, you're welcome to leave a donation using the follow
|
|||
|
||||
* Monero Address: `48WyAPdjwc6VokeXACxSZCFeKEXBiYPV6GjfvBsfg4CrUJ95LLCQSfpM9pvNKy5GE5H4hNaw99P8RZyzmaU9kb1pD7kzhCB`
|
||||
|
||||
While your donations are much appreciated, only donate if you really want to. Donations are used for keeping the website up and running and eventual code signing costs. If you are using the Invidious API then we recommend that you donate to the instance that you use. You can also donate to the [Invidious team](https://invidious.io/donate/) or the [Local API developer](https://github.com/sponsors/LuanRT).
|
||||
While your donations are much appreciated, only donate if you really want to. Donations are used for keeping the website up and running and eventual code signing costs.
|
||||
|
||||
> [!TIP]
|
||||
> If you are using the Invidious API then we recommend that you donate to the instance that you use. You can also donate to the [Invidious team](https://invidious.io/donate/) or the [Local API developer](https://github.com/sponsors/LuanRT).
|
||||
|
||||
## License
|
||||
[![GNU AGPLv3 Image](https://www.gnu.org/graphics/agplv3-155x51.png)](https://www.gnu.org/licenses/agpl-3.0.html)
|
||||
|
|
|
@ -56,7 +56,7 @@ if (!isDevMode) {
|
|||
to: path.join(__dirname, '../dist/static'),
|
||||
globOptions: {
|
||||
dot: true,
|
||||
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
|
||||
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/manifest.json', '**/dashFiles/**', '**/storyboards/**'],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
@ -119,6 +119,7 @@ const config = {
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': true,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.SUPPORTS_LOCAL_API': true,
|
||||
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
|
||||
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
|
||||
|
@ -126,10 +127,7 @@ const config = {
|
|||
new HtmlWebpackPlugin({
|
||||
excludeChunks: ['processTaskWorker'],
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
nodeModules: isDevMode
|
||||
? path.resolve(__dirname, '../node_modules')
|
||||
: false,
|
||||
template: path.resolve(__dirname, '../src/index.ejs')
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
|
|
|
@ -116,6 +116,7 @@ const config = {
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': false,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.SUPPORTS_LOCAL_API': false,
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
||||
|
||||
// video.js' vhs-utils supports both atob() in web browsers and Buffer in node
|
||||
|
@ -136,8 +137,7 @@ const config = {
|
|||
new HtmlWebpackPlugin({
|
||||
excludeChunks: ['processTaskWorker'],
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
nodeModules: false,
|
||||
template: path.resolve(__dirname, '../src/index.ejs')
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
|
|
31
package.json
31
package.json
|
@ -2,7 +2,7 @@
|
|||
"name": "freetube",
|
||||
"productName": "FreeTube",
|
||||
"description": "A private YouTube client",
|
||||
"version": "0.19.1",
|
||||
"version": "0.20.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "./dist/main.js",
|
||||
"private": true,
|
||||
|
@ -25,7 +25,7 @@
|
|||
"build-release": "node _scripts/build.js",
|
||||
"build-release:arm64": "node _scripts/build.js arm64",
|
||||
"build-release:arm32": "node _scripts/build.js arm32",
|
||||
"clean": "rimraf build/ static/dashFiles/ dist/ static/storyboards/",
|
||||
"clean": "rimraf build/ dist/",
|
||||
"debug": "run-s rebuild:electron debug-runner",
|
||||
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
|
||||
"dev": "run-s rebuild:electron dev-runner",
|
||||
|
@ -53,9 +53,9 @@
|
|||
"ci": "yarn install --silent --frozen-lockfile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.10",
|
||||
"@seald-io/nedb": "^4.0.4",
|
||||
"@silvermine/videojs-quality-selector": "^1.3.1",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"marked": "^12.0.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
"swiper": "^11.0.7",
|
||||
"swiper": "^11.1.0",
|
||||
"video.js": "7.21.5",
|
||||
"videojs-contrib-quality-levels": "^3.0.0",
|
||||
"videojs-http-source-selector": "^1.1.6",
|
||||
|
@ -77,19 +77,20 @@
|
|||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2",
|
||||
"youtubei.js": "^9.1.0"
|
||||
"youtubei.js": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.3",
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/eslint-parser": "^7.24.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/preset-env": "^7.24.3",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@double-great/stylelint-a11y": "^3.0.2",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^2.0.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^6.10.0",
|
||||
"css-loader": "^7.0.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"electron": "^29.1.5",
|
||||
"electron": "^29.2.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
@ -100,22 +101,22 @@
|
|||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-unicorn": "^51.0.1",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"eslint-plugin-vue": "^9.24.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||
"eslint-plugin-yml": "^1.13.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-minimizer-webpack-plugin": "^5.0.0",
|
||||
"lefthook": "^1.6.7",
|
||||
"lefthook": "^1.6.8",
|
||||
"mini-css-extract-plugin": "^2.8.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
"sass": "^1.72.0",
|
||||
"sass": "^1.74.1",
|
||||
"sass-loader": "^14.1.1",
|
||||
"stylelint": "^16.3.0",
|
||||
"stylelint": "^16.3.1",
|
||||
"stylelint-config-sass-guidelines": "^11.1.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-high-performance-animation": "^1.10.0",
|
||||
|
|
|
@ -23,7 +23,10 @@ const IpcChannels = {
|
|||
SYNC_SETTINGS: 'sync-settings',
|
||||
SYNC_HISTORY: 'sync-history',
|
||||
SYNC_PROFILES: 'sync-profiles',
|
||||
SYNC_PLAYLISTS: 'sync-playlists'
|
||||
SYNC_PLAYLISTS: 'sync-playlists',
|
||||
|
||||
GET_REPLACE_HTTP_CACHE: 'get-replace-http-cache',
|
||||
TOGGLE_REPLACE_HTTP_CACHE: 'toggle-replace-http-cache'
|
||||
}
|
||||
|
||||
const DBActions = {
|
||||
|
|
|
@ -9,13 +9,6 @@
|
|||
<link rel="manifest" href="static/manifest.json" />
|
||||
<% } %>
|
||||
<title></title>
|
||||
<% if (htmlWebpackPlugin.options.nodeModules) { %>
|
||||
<script>
|
||||
require('module').globalPaths.push(
|
||||
`<%= htmlWebpackPlugin.options.nodeModules.replace(/\\/g, '\\\\') %>`
|
||||
)
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { IpcChannels, DBActions, SyncEvents } from '../constants'
|
|||
import baseHandlers from '../datastores/handlers/base'
|
||||
import { extractExpiryTimestamp, ImageCache } from './ImageCache'
|
||||
import { existsSync } from 'fs'
|
||||
import asyncFs from 'fs/promises'
|
||||
|
||||
import packageDetails from '../../package.json'
|
||||
|
||||
|
@ -177,7 +178,8 @@ function runApp() {
|
|||
// command line switches need to be added before the app ready event first
|
||||
// that means we can't use the normal settings system as that is asynchronous,
|
||||
// doing it synchronously ensures that we add it before the event fires
|
||||
const replaceHttpCache = existsSync(`${app.getPath('userData')}/experiment-replace-http-cache`)
|
||||
const REPLACE_HTTP_CACHE_PATH = `${app.getPath('userData')}/experiment-replace-http-cache`
|
||||
const replaceHttpCache = existsSync(REPLACE_HTTP_CACHE_PATH)
|
||||
if (replaceHttpCache) {
|
||||
// the http cache causes excessive disk usage during video playback
|
||||
// we've got a custom image cache to make up for disabling the http cache
|
||||
|
@ -662,7 +664,7 @@ function runApp() {
|
|||
}
|
||||
})
|
||||
|
||||
ipcMain.once('relaunchRequest', () => {
|
||||
function relaunch() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.exit(parseInt(process.env.FREETUBE_RELAUNCH_EXIT_CODE))
|
||||
return
|
||||
|
@ -693,6 +695,10 @@ function runApp() {
|
|||
}
|
||||
|
||||
app.quit()
|
||||
}
|
||||
|
||||
ipcMain.once('relaunchRequest', () => {
|
||||
relaunch()
|
||||
})
|
||||
|
||||
nativeTheme.on('updated', () => {
|
||||
|
@ -780,6 +786,22 @@ function runApp() {
|
|||
child.unref()
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannels.GET_REPLACE_HTTP_CACHE, () => {
|
||||
return replaceHttpCache
|
||||
})
|
||||
|
||||
ipcMain.once(IpcChannels.TOGGLE_REPLACE_HTTP_CACHE, async () => {
|
||||
if (replaceHttpCache) {
|
||||
await asyncFs.rm(REPLACE_HTTP_CACHE_PATH)
|
||||
} else {
|
||||
// create an empty file
|
||||
const handle = await asyncFs.open(REPLACE_HTTP_CACHE_PATH, 'w')
|
||||
await handle.close()
|
||||
}
|
||||
|
||||
relaunch()
|
||||
})
|
||||
|
||||
// ************************************************* //
|
||||
// DB related IPC calls
|
||||
// *********** //
|
||||
|
|
|
@ -15,6 +15,7 @@ import { marked } from 'marked'
|
|||
import { IpcChannels } from '../constants'
|
||||
import packageDetails from '../../package.json'
|
||||
import { openExternalLink, openInternalPath, showToast } from './helpers/utils'
|
||||
import { translateWindowTitle } from './helpers/strings'
|
||||
|
||||
let ipcRenderer = null
|
||||
|
||||
|
@ -77,14 +78,13 @@ export default defineComponent({
|
|||
return this.$store.getters.getShowCreatePlaylistPrompt
|
||||
},
|
||||
windowTitle: function () {
|
||||
const routeTitle = this.$route.meta.title
|
||||
if (routeTitle !== 'Channel' && routeTitle !== 'Watch' && routeTitle !== 'Hashtag') {
|
||||
let title =
|
||||
this.$route.meta.path === '/home'
|
||||
? packageDetails.productName
|
||||
: `${this.$t(this.$route.meta.title)} - ${packageDetails.productName}`
|
||||
const routePath = this.$route.path
|
||||
if (!routePath.startsWith('/channel/') && !routePath.startsWith('/watch/') && !routePath.startsWith('/hashtag/')) {
|
||||
let title = translateWindowTitle(this.$route.meta.title, this.$i18n)
|
||||
if (!title) {
|
||||
title = packageDetails.productName
|
||||
} else {
|
||||
title = `${title} - ${packageDetails.productName}`
|
||||
}
|
||||
return title
|
||||
} else {
|
||||
|
@ -466,12 +466,7 @@ export default defineComponent({
|
|||
|
||||
default: {
|
||||
// Unknown URL type
|
||||
let message = 'Unknown YouTube url type, cannot be opened in app'
|
||||
if (this.$te(message) && this.$t(message) !== '') {
|
||||
message = this.$t(message)
|
||||
}
|
||||
|
||||
showToast(message)
|
||||
showToast(this.$t('Unknown YouTube url type, cannot be opened in app'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -520,7 +520,7 @@ export default defineComponent({
|
|||
]
|
||||
}
|
||||
|
||||
await this.promptAndWriteToFile(options, subscriptionsDb, 'Subscriptions have been successfully exported')
|
||||
await this.promptAndWriteToFile(options, subscriptionsDb, this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportYouTubeSubscriptions: async function () {
|
||||
|
@ -573,7 +573,7 @@ export default defineComponent({
|
|||
return object
|
||||
})
|
||||
|
||||
await this.promptAndWriteToFile(options, JSON.stringify(subscriptionsObject), 'Subscriptions have been successfully exported')
|
||||
await this.promptAndWriteToFile(options, JSON.stringify(subscriptionsObject), this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportOpmlYouTubeSubscriptions: async function () {
|
||||
|
@ -601,7 +601,7 @@ export default defineComponent({
|
|||
|
||||
opmlData += '</outline></body></opml>'
|
||||
|
||||
await this.promptAndWriteToFile(options, opmlData, 'Subscriptions have been successfully exported')
|
||||
await this.promptAndWriteToFile(options, opmlData, this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportCsvYouTubeSubscriptions: async function () {
|
||||
|
@ -628,7 +628,7 @@ export default defineComponent({
|
|||
})
|
||||
exportText += '\n'
|
||||
|
||||
await this.promptAndWriteToFile(options, exportText, 'Subscriptions have been successfully exported')
|
||||
await this.promptAndWriteToFile(options, exportText, this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
exportNewPipeSubscriptions: async function () {
|
||||
|
@ -662,7 +662,7 @@ export default defineComponent({
|
|||
newPipeObject.subscriptions.push(subscription)
|
||||
})
|
||||
|
||||
await this.promptAndWriteToFile(options, JSON.stringify(newPipeObject), 'Subscriptions have been successfully exported')
|
||||
await this.promptAndWriteToFile(options, JSON.stringify(newPipeObject), this.$t('Settings.Data Settings.Subscriptions have been successfully exported'))
|
||||
},
|
||||
|
||||
importHistory: async function () {
|
||||
|
@ -856,7 +856,7 @@ export default defineComponent({
|
|||
]
|
||||
}
|
||||
|
||||
await this.promptAndWriteToFile(options, historyDb, 'All watched history has been successfully exported')
|
||||
await this.promptAndWriteToFile(options, historyDb, this.$t('Settings.Data Settings.All watched history has been successfully exported'))
|
||||
},
|
||||
|
||||
importPlaylists: async function () {
|
||||
|
@ -1035,7 +1035,7 @@ export default defineComponent({
|
|||
return JSON.stringify(playlist)
|
||||
}).join('\n') + '\n'// a trailing line is expected
|
||||
|
||||
await this.promptAndWriteToFile(options, playlistsDb, 'All playlists has been successfully exported')
|
||||
await this.promptAndWriteToFile(options, playlistsDb, this.$t('Settings.Data Settings.All playlists has been successfully exported'))
|
||||
},
|
||||
|
||||
exportPlaylistsForOlderVersionsSometimes: function () {
|
||||
|
@ -1084,7 +1084,7 @@ export default defineComponent({
|
|||
})
|
||||
})
|
||||
|
||||
await this.promptAndWriteToFile(options, JSON.stringify([favoritesPlaylistData]), 'All playlists has been successfully exported')
|
||||
await this.promptAndWriteToFile(options, JSON.stringify([favoritesPlaylistData]), this.$t('Settings.Data Settings.All playlists has been successfully exported'))
|
||||
},
|
||||
|
||||
convertOldFreeTubeFormatToNew(oldData) {
|
||||
|
@ -1094,7 +1094,7 @@ export default defineComponent({
|
|||
for (const profile of channel.profile) {
|
||||
let index = convertedData.findIndex(p => p.name === profile.value)
|
||||
if (index === -1) { // profile doesn't exist yet
|
||||
const randomBgColor = getRandomColor()
|
||||
const randomBgColor = getRandomColor().value
|
||||
const contrastyTextColor = calculateColorLuminance(randomBgColor)
|
||||
convertedData.push({
|
||||
name: profile.value,
|
||||
|
@ -1118,7 +1118,7 @@ export default defineComponent({
|
|||
return convertedData
|
||||
},
|
||||
|
||||
promptAndWriteToFile: async function (saveOptions, content, successMessageKeySuffix) {
|
||||
promptAndWriteToFile: async function (saveOptions, content, successMessage) {
|
||||
const response = await showSaveDialog(saveOptions)
|
||||
if (response.canceled || response.filePath === '') {
|
||||
// User canceled the save dialog
|
||||
|
@ -1133,7 +1133,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
showToast(this.$t(`Settings.Data Settings.${successMessageKeySuffix}`))
|
||||
showToast(successMessage)
|
||||
},
|
||||
|
||||
getChannelInfoInvidious: function (channelId) {
|
||||
|
@ -1152,7 +1152,7 @@ export default defineComponent({
|
|||
copyToClipboard(err)
|
||||
})
|
||||
|
||||
if (process.env.IS_ELECTRON && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelInfoLocal(channelId))
|
||||
} else {
|
||||
|
@ -1182,7 +1182,7 @@ export default defineComponent({
|
|||
})
|
||||
|
||||
if (this.backendFallback && this.backendPreference === 'local') {
|
||||
showToast(this.$t('Falling back to the Invidious API'))
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
return await this.getChannelInfoInvidious(channelId)
|
||||
} else {
|
||||
return []
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import fs from 'fs/promises'
|
||||
import { defineComponent } from 'vue'
|
||||
import FtSettingsSection from '../ft-settings-section/ft-settings-section.vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||
import FtPrompt from '../ft-prompt/ft-prompt.vue'
|
||||
import { pathExists } from '../../helpers/filesystem'
|
||||
import { getUserDataPath } from '../../helpers/utils'
|
||||
import { IpcChannels } from '../../../constants'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ExperimentalSettings',
|
||||
|
@ -19,19 +17,16 @@ export default defineComponent({
|
|||
return {
|
||||
replaceHttpCacheLoading: true,
|
||||
replaceHttpCache: false,
|
||||
replaceHttpCachePath: '',
|
||||
showRestartPrompt: false
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
getUserDataPath().then((userData) => {
|
||||
this.replaceHttpCachePath = `${userData}/experiment-replace-http-cache`
|
||||
mounted: async function () {
|
||||
if (process.env.IS_ELECTRON) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
this.replaceHttpCache = await ipcRenderer.invoke(IpcChannels.GET_REPLACE_HTTP_CACHE)
|
||||
}
|
||||
|
||||
pathExists(this.replaceHttpCachePath).then((exists) => {
|
||||
this.replaceHttpCache = exists
|
||||
this.replaceHttpCacheLoading = false
|
||||
})
|
||||
})
|
||||
this.replaceHttpCacheLoading = false
|
||||
},
|
||||
methods: {
|
||||
handleRestartPrompt: function (value) {
|
||||
|
@ -39,7 +34,7 @@ export default defineComponent({
|
|||
this.showRestartPrompt = true
|
||||
},
|
||||
|
||||
handleReplaceHttpCache: async function (value) {
|
||||
handleReplaceHttpCache: function (value) {
|
||||
this.showRestartPrompt = false
|
||||
|
||||
if (value === null || value === 'no') {
|
||||
|
@ -47,16 +42,10 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
if (this.replaceHttpCache) {
|
||||
// create an empty file
|
||||
const handle = await fs.open(this.replaceHttpCachePath, 'w')
|
||||
await handle.close()
|
||||
} else {
|
||||
await fs.rm(this.replaceHttpCachePath)
|
||||
if (process.env.IS_ELECTRON) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send(IpcChannels.TOGGLE_REPLACE_HTTP_CACHE)
|
||||
}
|
||||
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('relaunchRequest')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -21,9 +21,15 @@ export default defineComponent({
|
|||
computed: {
|
||||
externalPlayerNames: function () {
|
||||
const fallbackNames = this.$store.getters.getExternalPlayerNames
|
||||
const nameTranslationKeys = this.$store.getters.getExternalPlayerNameTranslationKeys
|
||||
const translations = [{
|
||||
name: 'None',
|
||||
translatedValue: this.$t('Settings.External Player Settings.Players.None.Name')
|
||||
}]
|
||||
|
||||
return nameTranslationKeys.map((translationKey, idx) => this.$te(translationKey) ? this.$t(translationKey) : fallbackNames[idx])
|
||||
return fallbackNames.map((name) => {
|
||||
const translation = translations.find(e => e.name === name)
|
||||
return translation ? translation.translatedValue : name
|
||||
})
|
||||
},
|
||||
externalPlayerValues: function () {
|
||||
return this.$store.getters.getExternalPlayerValues
|
||||
|
|
|
@ -23,7 +23,7 @@ export default defineComponent({
|
|||
return this.$t('Age Restricted.This channel is age restricted')
|
||||
}
|
||||
|
||||
return this.$t('Age Restricted.This video is age restricted:')
|
||||
return this.$t('Age Restricted.This video is age restricted')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -95,7 +95,7 @@ export default defineComponent({
|
|||
this.postText = 'Shared post'
|
||||
this.type = 'text'
|
||||
let authorThumbnails = ['', 'https://yt3.ggpht.com/ytc/AAUvwnjm-0qglHJkAHqLFsCQQO97G7cCNDuDLldsrn25Lg=s88-c-k-c0x00ffffff-no-rj']
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
authorThumbnails = authorThumbnails.map(thumbnail => {
|
||||
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
|
||||
return thumbnail
|
||||
|
@ -106,7 +106,7 @@ export default defineComponent({
|
|||
}
|
||||
this.postText = autolinker.link(this.data.postText)
|
||||
const authorThumbnails = deepCopy(this.data.authorThumbnails)
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
authorThumbnails.forEach(thumbnail => {
|
||||
thumbnail.url = youtubeImageUrlToInvidious(thumbnail.url)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,44 @@
|
|||
@use '../../scss-partials/_ft-list-item';
|
||||
|
||||
.ft-list-channel {
|
||||
&.grid {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.infoAndSubscribe {
|
||||
flex-flow: column wrap;
|
||||
align-items: center;
|
||||
|
||||
.info {
|
||||
margin-block-end: 12px;
|
||||
|
||||
.infoLine {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.list {
|
||||
.infoAndSubscribe {
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
|
||||
.channelSubscribeButton {
|
||||
margin-block: auto;
|
||||
margin-inline: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infoAndSubscribe {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.handle {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -83,7 +83,8 @@ export default defineComponent({
|
|||
data: function () {
|
||||
return {
|
||||
visible: false,
|
||||
show: true
|
||||
show: true,
|
||||
stopWatchingInitialVisibleState: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -111,13 +112,29 @@ export default defineComponent({
|
|||
},
|
||||
created() {
|
||||
this.visible = this.initialVisibleState
|
||||
|
||||
if (!this.initialVisibleState) {
|
||||
this.stopWatchingInitialVisibleState = this.$watch('initialVisibleState', (newValue) => {
|
||||
this.visible = newValue
|
||||
this.stopWatchingInitialVisibleState()
|
||||
this.stopWatchingInitialVisibleState = null
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onVisibilityChanged: function (visible) {
|
||||
if (visible && this.shouldBeVisible) {
|
||||
this.visible = visible
|
||||
if (this.stopWatchingInitialVisibleState) {
|
||||
this.stopWatchingInitialVisibleState()
|
||||
this.stopWatchingInitialVisibleState = null
|
||||
}
|
||||
} else if (visible) {
|
||||
this.show = false
|
||||
if (this.stopWatchingInitialVisibleState) {
|
||||
this.stopWatchingInitialVisibleState()
|
||||
this.stopWatchingInitialVisibleState = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="show"
|
||||
v-observe-visibility="!initialVisibleState ? {
|
||||
v-observe-visibility="initialVisibleState || visible ? false : {
|
||||
callback: onVisibilityChanged,
|
||||
once: true,
|
||||
} : null"
|
||||
}"
|
||||
:class="{ placeholder: !visible }"
|
||||
>
|
||||
<template
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<ft-icon-button
|
||||
v-if="isQuickBookmarkEnabled && quickBookmarkButtonEnabled"
|
||||
:title="quickBookmarkIconText"
|
||||
:icon="['fas', 'star']"
|
||||
:icon="isInQuickBookmarkPlaylist ? ['fas', 'check'] : ['fas', 'bookmark']"
|
||||
class="quickBookmarkVideoIcon"
|
||||
:class="{
|
||||
bookmarked: isInQuickBookmarkPlaylist,
|
||||
|
|
|
@ -119,7 +119,7 @@ export default defineComponent({
|
|||
if (typeof (playlist.playlistName) !== 'string') { return false }
|
||||
|
||||
if (this.doSearchPlaylistsWithMatchingVideos) {
|
||||
if (playlist.videos.some((v) => v.title.toLowerCase().includes(this.processedQuery))) {
|
||||
if (playlist.videos.some((v) => v.author.toLowerCase().includes(this.processedQuery) || v.title.toLowerCase().includes(this.processedQuery))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -240,13 +240,20 @@ export default defineComponent({
|
|||
this.updatePlaylist({ _id: playlist._id })
|
||||
})
|
||||
|
||||
const translationEntryKey = addedPlaylistIds.size === 1
|
||||
? 'User Playlists.AddVideoPrompt.Toast.{videoCount} video(s) added to 1 playlist'
|
||||
: 'User Playlists.AddVideoPrompt.Toast.{videoCount} video(s) added to {playlistCount} playlists'
|
||||
showToast(this.$tc(translationEntryKey, this.toBeAddedToPlaylistVideoCount, {
|
||||
videoCount: this.toBeAddedToPlaylistVideoCount,
|
||||
playlistCount: addedPlaylistIds.size,
|
||||
}))
|
||||
let message
|
||||
if (addedPlaylistIds.size === 1) {
|
||||
message = this.$tc('User Playlists.AddVideoPrompt.Toast.{videoCount} video(s) added to 1 playlist', this.toBeAddedToPlaylistVideoCount, {
|
||||
videoCount: this.toBeAddedToPlaylistVideoCount,
|
||||
playlistCount: addedPlaylistIds.size,
|
||||
})
|
||||
} else {
|
||||
message = this.$tc('User Playlists.AddVideoPrompt.Toast.{videoCount} video(s) added to {playlistCount} playlists', this.toBeAddedToPlaylistVideoCount, {
|
||||
videoCount: this.toBeAddedToPlaylistVideoCount,
|
||||
playlistCount: addedPlaylistIds.size,
|
||||
})
|
||||
}
|
||||
|
||||
showToast(message)
|
||||
this.hide()
|
||||
},
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ export default defineComponent({
|
|||
focusItem: function (value) {
|
||||
let index = value
|
||||
if (index < 0) {
|
||||
index = this.promptButtons.length
|
||||
index = this.promptButtons.length - 1
|
||||
} else if (index >= this.promptButtons.length) {
|
||||
index = 0
|
||||
}
|
||||
|
|
|
@ -58,6 +58,16 @@ export default defineComponent({
|
|||
return this.shareTargetType === 'Video'
|
||||
},
|
||||
|
||||
shareTitle: function() {
|
||||
if (this.isChannel) {
|
||||
return this.$t('Share.Share Channel')
|
||||
}
|
||||
if (this.isPlaylist) {
|
||||
return this.$t('Share.Share Playlist')
|
||||
}
|
||||
return this.$t('Share.Share Video')
|
||||
},
|
||||
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<ft-icon-button
|
||||
ref="iconButton"
|
||||
:title="$t(`Share.Share ${shareTargetType}`)"
|
||||
:title="shareTitle"
|
||||
theme="secondary"
|
||||
:icon="['fas', 'share-alt']"
|
||||
:dropdown-modal-on-mobile="true"
|
||||
|
@ -75,7 +75,7 @@
|
|||
id="invidiousShare"
|
||||
class="header invidious"
|
||||
>
|
||||
<span class="invidiousLogo" />Invidious
|
||||
<span class="invidiousLogo" /> Invidious
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
|
|
|
@ -13,10 +13,7 @@
|
|||
@change="$emit('change', currentValue)"
|
||||
>
|
||||
<span>
|
||||
{{ label }}:
|
||||
<span>
|
||||
{{ displayLabel }}
|
||||
</span>
|
||||
{{ $t('Display Label', {label: label, value: displayLabel}) }}
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import { colors } from '../../helpers/colors'
|
||||
import { colors, getColorTranslations } from '../../helpers/colors'
|
||||
import FtSelect from '../ft-select/ft-select.vue'
|
||||
import { sanitizeForHtmlId } from '../../helpers/accessibility'
|
||||
|
||||
|
@ -31,11 +31,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
colorNames: function () {
|
||||
return this.colorValues.map(colorVal => {
|
||||
// add spaces before capital letters
|
||||
const colorName = colorVal.replaceAll(/([A-Z])/g, ' $1').trim()
|
||||
return this.$t(`Settings.Theme Settings.Main Color Theme.${colorName}`)
|
||||
})
|
||||
return getColorTranslations()
|
||||
},
|
||||
|
||||
sponsorBlockValues: function() {
|
||||
|
@ -80,6 +76,29 @@ export default defineComponent({
|
|||
this.$t('Settings.SponsorBlock Settings.Skip Options.Show In Seek Bar'),
|
||||
this.$t('Settings.SponsorBlock Settings.Skip Options.Do Nothing')
|
||||
]
|
||||
},
|
||||
|
||||
translatedCategoryName: function() {
|
||||
switch (this.categoryName.toLowerCase()) {
|
||||
case 'sponsor':
|
||||
return this.$t('Video.Sponsor Block category.sponsor')
|
||||
case 'self-promotion':
|
||||
return this.$t('Video.Sponsor Block category.self-promotion')
|
||||
case 'interaction':
|
||||
return this.$t('Video.Sponsor Block category.interaction')
|
||||
case 'intro':
|
||||
return this.$t('Video.Sponsor Block category.intro')
|
||||
case 'outro':
|
||||
return this.$t('Video.Sponsor Block category.outro')
|
||||
case 'recap':
|
||||
return this.$t('Video.Sponsor Block category.recap')
|
||||
case 'music offtopic':
|
||||
return this.$t('Video.Sponsor Block category.music offtopic')
|
||||
case 'filler':
|
||||
return this.$t('Video.Sponsor Block category.filler')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:id="sanitizedId"
|
||||
class="sponsorTitle"
|
||||
>
|
||||
{{ $t("Video.Sponsor Block category." + categoryName) }}
|
||||
{{ translatedCategoryName }}
|
||||
</div>
|
||||
<ft-select
|
||||
:sanitized-id="sanitizedId + 'categoryColor'"
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<div
|
||||
class="initial"
|
||||
>
|
||||
{{ isProfileSubscribed(profile) ? '✓' : profileInitials[index] }}
|
||||
{{ isProfileSubscribed(profile) ? $t('checkmark') : profileInitials[index] }}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
|
|
|
@ -1314,7 +1314,7 @@ export default defineComponent({
|
|||
|
||||
this.useDash = false
|
||||
this.useHls = false
|
||||
this.activeSourceList = (this.proxyVideos || !process.env.IS_ELECTRON)
|
||||
this.activeSourceList = (this.proxyVideos || !process.env.SUPPORTS_LOCAL_API)
|
||||
// use map here to return slightly different list without modifying original
|
||||
? this.sourceList.map((source) => {
|
||||
return {
|
||||
|
@ -2111,6 +2111,8 @@ export default defineComponent({
|
|||
|
||||
// Unexpected errors should be reported
|
||||
console.error(err)
|
||||
// ignore as this will most likely be removed by shaka player changes
|
||||
// eslint-disable-next-line @intlify/vue-i18n/no-missing-keys
|
||||
const errorMessage = this.$t('play() request Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
|
|
|
@ -10,6 +10,7 @@ import FtButton from '../ft-button/ft-button.vue'
|
|||
import debounce from 'lodash.debounce'
|
||||
import allLocales from '../../../../static/locales/activeLocales.json'
|
||||
import { showToast } from '../../helpers/utils'
|
||||
import { translateWindowTitle } from '../../helpers/strings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'GeneralSettings',
|
||||
|
@ -23,10 +24,14 @@ export default defineComponent({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
backendValues: [
|
||||
'invidious',
|
||||
'local'
|
||||
],
|
||||
backendValues: process.env.SUPPORTS_LOCAL_API
|
||||
? [
|
||||
'invidious',
|
||||
'local'
|
||||
]
|
||||
: [
|
||||
'invidious'
|
||||
],
|
||||
viewTypeValues: [
|
||||
'grid',
|
||||
'list'
|
||||
|
@ -91,13 +96,17 @@ export default defineComponent({
|
|||
return this.$router.getRoutes().filter((route) => includedPageNames.includes(route.name))
|
||||
},
|
||||
defaultPageNames: function () {
|
||||
return this.defaultPages.map((route) => this.$t(route.meta.title))
|
||||
return this.defaultPages.map((route) => translateWindowTitle(route.meta.title, this.$i18n))
|
||||
},
|
||||
defaultPageValues: function () {
|
||||
// avoid Vue parsing issues by excluding '/' from path values
|
||||
return this.defaultPages.map((route) => route.path.substring(1))
|
||||
},
|
||||
backendPreference: function () {
|
||||
if (!process.env.SUPPORTS_LOCAL_API && this.$store.getters.getBackendPreference === 'local') {
|
||||
this.handlePreferredApiBackend('invidious')
|
||||
}
|
||||
|
||||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
landingPage: function () {
|
||||
|
@ -148,10 +157,16 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
backendNames: function () {
|
||||
return [
|
||||
this.$t('Settings.General Settings.Preferred API Backend.Invidious API'),
|
||||
this.$t('Settings.General Settings.Preferred API Backend.Local API')
|
||||
]
|
||||
if (process.env.SUPPORTS_LOCAL_API) {
|
||||
return [
|
||||
this.$t('Settings.General Settings.Preferred API Backend.Invidious API'),
|
||||
this.$t('Settings.General Settings.Preferred API Backend.Local API')
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
this.$t('Settings.General Settings.Preferred API Backend.Invidious API')
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
viewTypeNames: function () {
|
||||
|
|
|
@ -26,6 +26,7 @@ export default defineComponent({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
usingElectron: process.env.IS_ELECTRON,
|
||||
formatValues: [
|
||||
'dash',
|
||||
'legacy',
|
||||
|
@ -60,10 +61,6 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
usingElectron: function () {
|
||||
return process.env.IS_ELECTRON
|
||||
},
|
||||
|
||||
backendPreference: function () {
|
||||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
|
@ -286,7 +283,7 @@ export default defineComponent({
|
|||
this.screenshotFilenameExample = `${res}.${this.screenshotFormat}`
|
||||
return true
|
||||
}).catch(err => {
|
||||
this.screenshotFilenameExample = `❗ ${this.$t(`Settings.Player Settings.Screenshot.Error.${err.message}`)}`
|
||||
this.screenshotFilenameExample = `❗ ${err.message}`
|
||||
return false
|
||||
})
|
||||
},
|
||||
|
|
|
@ -39,9 +39,6 @@ export default defineComponent({
|
|||
saveVideoHistoryWithLastViewedPlaylist: function () {
|
||||
return this.$store.getters.getSaveVideoHistoryWithLastViewedPlaylist
|
||||
},
|
||||
removeVideoMetaFiles: function () {
|
||||
return this.$store.getters.getRemoveVideoMetaFiles
|
||||
},
|
||||
|
||||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
|
@ -74,13 +71,6 @@ export default defineComponent({
|
|||
this.updateRememberHistory(value)
|
||||
},
|
||||
|
||||
handleVideoMetaFiles: function (value) {
|
||||
if (!value) {
|
||||
this.updateRemoveVideoMetaFiles(false)
|
||||
}
|
||||
this.updateRemoveVideoMetaFiles(value)
|
||||
},
|
||||
|
||||
handleRemoveHistory: function (option) {
|
||||
this.showRemoveHistoryPrompt = false
|
||||
|
||||
|
@ -126,7 +116,6 @@ export default defineComponent({
|
|||
|
||||
...mapActions([
|
||||
'updateRememberHistory',
|
||||
'updateRemoveVideoMetaFiles',
|
||||
'removeAllHistory',
|
||||
'updateSaveWatchedProgress',
|
||||
'updateSaveVideoHistoryWithLastViewedPlaylist',
|
||||
|
|
|
@ -29,15 +29,6 @@
|
|||
@change="updateSaveVideoHistoryWithLastViewedPlaylist"
|
||||
/>
|
||||
</div>
|
||||
<div class="switchColumn">
|
||||
<ft-toggle-switch
|
||||
:label="$t('Settings.Privacy Settings.Automatically Remove Video Meta Files')"
|
||||
:compact="true"
|
||||
:default-value="removeVideoMetaFiles"
|
||||
:tooltip="$t('Tooltips.Privacy Settings.Remove Video Meta Files')"
|
||||
@change="handleVideoMetaFiles"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<ft-flex-box>
|
||||
|
|
|
@ -61,16 +61,16 @@
|
|||
{{ $t('Settings.Proxy Settings.Your Info') }}
|
||||
</h3>
|
||||
<p>
|
||||
{{ $t('Settings.Proxy Settings.Ip') }}: {{ proxyIp }}
|
||||
{{ $t('Display Label', { label: $t('Settings.Proxy Settings.Ip'), value: proxyIp }) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('Settings.Proxy Settings.Country') }}: {{ proxyCountry }}
|
||||
{{ $t('Display Label', { label: $t('Settings.Proxy Settings.Country'), value: proxyCountry }) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('Settings.Proxy Settings.Region') }}: {{ proxyRegion }}
|
||||
{{ $t('Display Label', { label: $t('Settings.Proxy Settings.Region'), value: proxyRegion }) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('Settings.Proxy Settings.City') }}: {{ proxyCity }}
|
||||
{{ $t('Display Label', { label: $t('Settings.Proxy Settings.City'), value: proxyCity }) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -125,7 +125,7 @@ export default defineComponent({
|
|||
|
||||
const postListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
|
||||
let posts = []
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
posts = await this.getChannelPostsInvidious(channel)
|
||||
} else {
|
||||
posts = await this.getChannelPostsLocal(channel)
|
||||
|
@ -229,7 +229,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelPostsLocal(channel))
|
||||
} else {
|
||||
|
|
|
@ -135,7 +135,7 @@ export default defineComponent({
|
|||
let videos = []
|
||||
let name, thumbnailUrl
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
if (useRss) {
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidiousRSS(channel))
|
||||
} else {
|
||||
|
@ -315,7 +315,7 @@ export default defineComponent({
|
|||
resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
|
||||
break
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelLiveLocal(channel, failedAttempts + 1))
|
||||
} else {
|
||||
|
@ -360,7 +360,7 @@ export default defineComponent({
|
|||
case 0:
|
||||
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
return this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
|
||||
} else {
|
||||
|
|
|
@ -120,7 +120,7 @@ export default defineComponent({
|
|||
let videos = []
|
||||
let name
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
({ videos, name } = await this.getChannelShortsInvidious(channel))
|
||||
} else {
|
||||
({ videos, name } = await this.getChannelShortsLocal(channel))
|
||||
|
@ -234,7 +234,7 @@ export default defineComponent({
|
|||
})
|
||||
switch (failedAttempts) {
|
||||
case 0:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
return this.getChannelShortsLocal(channel, failedAttempts + 1)
|
||||
} else {
|
||||
|
|
|
@ -135,7 +135,7 @@ export default defineComponent({
|
|||
let videos = []
|
||||
let name, thumbnailUrl
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
if (useRss) {
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousRSS(channel))
|
||||
} else {
|
||||
|
@ -312,7 +312,7 @@ export default defineComponent({
|
|||
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
|
||||
break
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
||||
} else {
|
||||
|
@ -358,7 +358,7 @@ export default defineComponent({
|
|||
case 0:
|
||||
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
return this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,7 @@ import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
|||
import FtSlider from '../ft-slider/ft-slider.vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
import FtPrompt from '../ft-prompt/ft-prompt.vue'
|
||||
import { colors } from '../../helpers/colors'
|
||||
import { colors, getColorTranslations } from '../../helpers/colors'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ThemeSettings',
|
||||
|
@ -20,6 +20,7 @@ export default defineComponent({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
usingElectron: process.env.IS_ELECTRON,
|
||||
minUiScale: 50,
|
||||
maxUiScale: 300,
|
||||
uiScaleStep: 5,
|
||||
|
@ -113,19 +114,11 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
colorNames: function () {
|
||||
return this.colorValues.map(colorVal => {
|
||||
// add spaces before capital letters
|
||||
const colorName = colorVal.replaceAll(/([A-Z])/g, ' $1').trim()
|
||||
return this.$t(`Settings.Theme Settings.Main Color Theme.${colorName}`)
|
||||
})
|
||||
return getColorTranslations()
|
||||
},
|
||||
|
||||
areColorThemesEnabled: function() {
|
||||
return this.baseTheme !== 'hotPink'
|
||||
},
|
||||
|
||||
usingElectron: function () {
|
||||
return process.env.IS_ELECTRON
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
|
|
@ -8,6 +8,7 @@ import debounce from 'lodash.debounce'
|
|||
|
||||
import { IpcChannels } from '../../../constants'
|
||||
import { openInternalPath } from '../../helpers/utils'
|
||||
import { translateWindowTitle } from '../../helpers/strings'
|
||||
import { clearLocalSearchSuggestionsSession, getLocalSearchSuggestions } from '../../helpers/api/local'
|
||||
import { invidiousAPICall } from '../../helpers/api/invidious'
|
||||
|
||||
|
@ -48,9 +49,10 @@ export default defineComponent({
|
|||
headerLogoTitle: function () {
|
||||
return this.$t('Go to page',
|
||||
{
|
||||
page: this.$t(this.$router.getRoutes()
|
||||
page: translateWindowTitle(this.$router.getRoutes()
|
||||
.find((route) => route.path === '/' + this.landingPage)
|
||||
.meta.title
|
||||
.meta.title,
|
||||
this.$i18n
|
||||
)
|
||||
})
|
||||
},
|
||||
|
@ -288,7 +290,7 @@ export default defineComponent({
|
|||
this.searchSuggestionsDataList = results.suggestions
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
|
||||
console.error(
|
||||
'Error gettings search suggestions. Falling back to Local API'
|
||||
)
|
||||
|
|
|
@ -33,7 +33,27 @@ export default defineComponent({
|
|||
|
||||
compact: function () {
|
||||
return !this.chapters[0].thumbnail
|
||||
}
|
||||
},
|
||||
|
||||
observeVisibilityOptions() {
|
||||
return {
|
||||
callback: (isVisible, _entry) => {
|
||||
// This is also fired when **hidden**
|
||||
// No point doing anything if not visible
|
||||
if (!isVisible) { return }
|
||||
// Only auto scroll when expanded
|
||||
if (!this.showChapters) { return }
|
||||
|
||||
this.scrollToCurrentChapter()
|
||||
},
|
||||
intersection: {
|
||||
// Only when it intersects with N% above bottom
|
||||
rootMargin: '0% 0% 0% 0%',
|
||||
},
|
||||
// Callback responsible for scolling to current chapter multiple times
|
||||
once: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChapterIndex: function (value) {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<div
|
||||
v-show="showChapters"
|
||||
ref="chaptersWrapper"
|
||||
v-observe-visibility="observeVisibilityOptions"
|
||||
class="chaptersWrapper"
|
||||
:class="{ compact }"
|
||||
@keydown.arrow-up.stop.prevent="navigateChapters('up')"
|
||||
|
|
|
@ -151,7 +151,7 @@ export default defineComponent({
|
|||
|
||||
getCommentData: function () {
|
||||
this.isLoading = true
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getCommentDataInvidious()
|
||||
} else {
|
||||
this.getCommentDataLocal()
|
||||
|
@ -162,7 +162,7 @@ export default defineComponent({
|
|||
if (this.commentData.length === 0 || this.nextPageToken === null || typeof this.nextPageToken === 'undefined') {
|
||||
showToast(this.$t('Comments.There are no more comments for this video'))
|
||||
} else {
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getCommentDataInvidious()
|
||||
} else {
|
||||
this.getCommentDataLocal(true)
|
||||
|
@ -179,7 +179,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
getCommentReplies: function (index) {
|
||||
if (process.env.IS_ELECTRON) {
|
||||
if (process.env.SUPPORTS_LOCAL_API) {
|
||||
switch (this.commentData[index].dataType) {
|
||||
case 'local':
|
||||
this.getCommentRepliesLocal(index)
|
||||
|
@ -292,7 +292,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getCommentDataLocal()
|
||||
} else {
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
<ft-icon-button
|
||||
v-if="isQuickBookmarkEnabled"
|
||||
:title="quickBookmarkIconText"
|
||||
:icon="['fas', 'star']"
|
||||
:icon="isInQuickBookmarkPlaylist ? ['fas', 'check'] : ['fas', 'bookmark']"
|
||||
class="quickBookmarkVideoIcon"
|
||||
:class="{
|
||||
bookmarked: isInQuickBookmarkPlaylist,
|
||||
|
|
|
@ -94,7 +94,7 @@ export default defineComponent({
|
|||
this.liveChatInstance = null
|
||||
},
|
||||
created: function () {
|
||||
if (!process.env.IS_ELECTRON) {
|
||||
if (!process.env.SUPPORTS_LOCAL_API) {
|
||||
this.hasError = true
|
||||
this.errorMessage = this.$t('Video["Live Chat is currently not supported in this build."]')
|
||||
this.isLoading = false
|
||||
|
|
|
@ -210,7 +210,7 @@ export default defineComponent({
|
|||
},
|
||||
playlistId: function (newVal, oldVal) {
|
||||
if (oldVal !== newVal) {
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getPlaylistInformationInvidious()
|
||||
} else {
|
||||
this.getPlaylistInformationLocal()
|
||||
|
@ -249,7 +249,7 @@ export default defineComponent({
|
|||
|
||||
if (this.selectedUserPlaylist != null) {
|
||||
this.parseUserPlaylist(this.selectedUserPlaylist)
|
||||
} else if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
} else if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getPlaylistInformationInvidious()
|
||||
} else {
|
||||
this.getPlaylistInformationLocal()
|
||||
|
@ -386,7 +386,7 @@ export default defineComponent({
|
|||
this.channelName = cachedPlaylist.channelName
|
||||
this.channelId = cachedPlaylist.channelId
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious' || cachedPlaylist.continuationData === null) {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious' || cachedPlaylist.continuationData === null) {
|
||||
this.playlistItems = cachedPlaylist.items
|
||||
} else {
|
||||
const videos = cachedPlaylist.items
|
||||
|
@ -462,7 +462,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getPlaylistInformationLocal()
|
||||
} else {
|
||||
|
|
|
@ -321,10 +321,10 @@ export function filterInvidiousFormats(formats, allowAv1 = false) {
|
|||
// Which is caused by Invidious API limitation on AV1 formats (see related issues)
|
||||
// Commented code to be restored after Invidious issue fixed
|
||||
//
|
||||
// As we generate our own DASH manifest (using YouTube.js) for multiple audio track support in Electron,
|
||||
// we can allow AV1 in that situation. If we aren't in electron,
|
||||
// As we generate our own DASH manifest (using YouTube.js) for multiple audio track support when the local API is supported,
|
||||
// we can allow AV1 in that situation. When the local API isn't supported,
|
||||
// we still can't use them until Invidious fixes the issue on their side
|
||||
if (process.env.IS_ELECTRON && allowAv1 && av1Formats.length > 0) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && allowAv1 && av1Formats.length > 0) {
|
||||
return [...audioFormats, ...av1Formats]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ClientType, Endpoints, Innertube, Misc, Utils, YT } from 'youtubei.js'
|
||||
import { ClientType, Endpoints, Innertube, Misc, UniversalCache, Utils, YT } from 'youtubei.js'
|
||||
import Autolinker from 'autolinker'
|
||||
import { join } from 'path'
|
||||
|
||||
|
@ -39,8 +39,12 @@ const TRACKING_PARAM_NAMES = [
|
|||
async function createInnertube({ withPlayer = false, location = undefined, safetyMode = false, clientType = undefined, generateSessionLocally = true } = {}) {
|
||||
let cache
|
||||
if (withPlayer) {
|
||||
const userData = await getUserDataPath()
|
||||
cache = new PlayerCache(join(userData, 'player_cache'))
|
||||
if (process.env.IS_ELECTRON) {
|
||||
const userData = await getUserDataPath()
|
||||
cache = new PlayerCache(join(userData, 'player_cache'))
|
||||
} else {
|
||||
cache = new UniversalCache(false)
|
||||
}
|
||||
}
|
||||
|
||||
return await Innertube.create({
|
||||
|
@ -77,8 +81,8 @@ export async function getLocalPlaylist(id) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {Playlist} playlist
|
||||
* @returns {Playlist|null} null when no valid playlist can be found (e.g. `empty continuation response`)
|
||||
* @param {import('youtubei.js').YT.Playlist} playlist
|
||||
* @returns {import('youtubei.js').YT.Playlist|null} null when no valid playlist can be found (e.g. `empty continuation response`)
|
||||
*/
|
||||
export async function getLocalPlaylistContinuation(playlist) {
|
||||
try {
|
||||
|
@ -98,11 +102,11 @@ export async function getLocalPlaylistContinuation(playlist) {
|
|||
* Callback for adding two numbers.
|
||||
*
|
||||
* @callback untilEndOfLocalPlayListCallback
|
||||
* @param {Playlist} playlist
|
||||
* @param {import('youtubei.js').YT.Playlist} playlist
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Playlist} playlist
|
||||
* @param {import('youtubei.js').YT.Playlist} playlist
|
||||
* @param {untilEndOfLocalPlayListCallback} callback
|
||||
* @param {object} options
|
||||
* @param {boolean} options.runCallbackOnceFirst
|
||||
|
@ -331,7 +335,7 @@ export async function getLocalChannelLiveStreams(id) {
|
|||
// it has some empty fields in the protobuf but it doesn't work if you remove them)
|
||||
}))
|
||||
|
||||
const liveStreamsTab = new YT.Channel(null, response)
|
||||
let liveStreamsTab = new YT.Channel(innertube.actions, response)
|
||||
const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(liveStreamsTab)
|
||||
|
||||
let videos
|
||||
|
@ -339,7 +343,16 @@ export async function getLocalChannelLiveStreams(id) {
|
|||
// if the channel doesn't have a live tab, YouTube returns the home tab instead
|
||||
// so we need to check that we got the right tab
|
||||
if (liveStreamsTab.current_tab?.endpoint.metadata.url?.endsWith('/streams')) {
|
||||
videos = parseLocalChannelVideos(liveStreamsTab.videos, channelId, name)
|
||||
// work around YouTube bug where it will return a bunch of responses with only continuations in them
|
||||
// e.g. https://www.youtube.com/@TWLIVES/streams
|
||||
|
||||
let tempVideos = liveStreamsTab.videos
|
||||
while (tempVideos.length === 0 && liveStreamsTab.has_continuation) {
|
||||
liveStreamsTab = await liveStreamsTab.getContinuation()
|
||||
tempVideos = liveStreamsTab.videos
|
||||
}
|
||||
|
||||
videos = parseLocalChannelVideos(tempVideos, channelId, name)
|
||||
} else {
|
||||
videos = []
|
||||
}
|
||||
|
@ -513,7 +526,13 @@ export function parseLocalChannelHeader(channel) {
|
|||
}
|
||||
|
||||
if (header.content.metadata) {
|
||||
subscriberText = header.content.metadata.metadata_rows[0].metadata_parts[1].text.text
|
||||
// YouTube has already changed the indexes for where the information is stored once,
|
||||
// so we should search for it instead of using hardcoded indexes, just to be safe for the future
|
||||
|
||||
subscriberText = header.content.metadata.metadata_rows
|
||||
.flatMap(row => row.metadata_parts ? row.metadata_parts : [])
|
||||
.find(part => part.text.text?.includes('subscriber'))
|
||||
?.text.text
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -567,51 +586,66 @@ export function parseLocalChannelShorts(shorts, channelId, channelName) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('youtubei.js').YTNodes.Playlist} Playlist
|
||||
* @typedef {import('youtubei.js').YTNodes.GridPlaylist} GridPlaylist
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Playlist|GridPlaylist} playlist
|
||||
* @param {import('youtubei.js').YTNodes.Playlist|import('youtubei.js').YTNodes.GridPlaylist|import('youtubei.js').YTNodes.LockupView} playlist
|
||||
* @param {string} channelId
|
||||
* @param {string} chanelName
|
||||
*/
|
||||
export function parseLocalListPlaylist(playlist, channelId = undefined, channelName = undefined) {
|
||||
let internalChannelName
|
||||
let internalChannelId = null
|
||||
if (playlist.type === 'LockupView') {
|
||||
/** @type {import('youtubei.js').YTNodes.LockupView} */
|
||||
const lockupView = playlist
|
||||
|
||||
if (playlist.author && playlist.author.id !== 'N/A') {
|
||||
if (playlist.author instanceof Misc.Text) {
|
||||
internalChannelName = playlist.author.text
|
||||
/** @type {import('youtubei.js').YTNodes.ThumbnailOverlayBadgeView} */
|
||||
const thumbnailOverlayBadgeView = lockupView.content_image.primary_thumbnail.overlays
|
||||
.find(overlay => overlay.type === 'ThumbnailOverlayBadgeView')
|
||||
|
||||
if (channelId) {
|
||||
internalChannelId = channelId
|
||||
}
|
||||
} else {
|
||||
internalChannelName = playlist.author.name
|
||||
internalChannelId = playlist.author.id
|
||||
return {
|
||||
type: 'playlist',
|
||||
dataSource: 'local',
|
||||
title: lockupView.metadata.title.text,
|
||||
thumbnail: lockupView.content_image.primary_thumbnail.image[0].url,
|
||||
channelName,
|
||||
channelId,
|
||||
playlistId: lockupView.content_id,
|
||||
videoCount: extractNumberFromString(thumbnailOverlayBadgeView.badges[0].text)
|
||||
}
|
||||
} else if (channelId || channelName) {
|
||||
internalChannelName = channelName
|
||||
internalChannelId = channelId
|
||||
} else if (playlist.author?.name) {
|
||||
// auto-generated album playlists don't have an author
|
||||
// so in search results, the author text is "Playlist" and doesn't have a link or channel ID
|
||||
internalChannelName = playlist.author.name
|
||||
}
|
||||
} else {
|
||||
let internalChannelName
|
||||
let internalChannelId = null
|
||||
|
||||
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
|
||||
const thumbnailRenderer = playlist.thumbnail_renderer
|
||||
if (playlist.author && playlist.author.id !== 'N/A') {
|
||||
if (playlist.author instanceof Misc.Text) {
|
||||
internalChannelName = playlist.author.text
|
||||
|
||||
return {
|
||||
type: 'playlist',
|
||||
dataSource: 'local',
|
||||
title: playlist.title.text,
|
||||
thumbnail: thumbnailRenderer ? thumbnailRenderer.thumbnail[0].url : playlist.thumbnails[0].url,
|
||||
channelName: internalChannelName,
|
||||
channelId: internalChannelId,
|
||||
playlistId: playlist.id,
|
||||
videoCount: extractNumberFromString(playlist.video_count.text)
|
||||
if (channelId) {
|
||||
internalChannelId = channelId
|
||||
}
|
||||
} else {
|
||||
internalChannelName = playlist.author.name
|
||||
internalChannelId = playlist.author.id
|
||||
}
|
||||
} else if (channelId || channelName) {
|
||||
internalChannelName = channelName
|
||||
internalChannelId = channelId
|
||||
} else if (playlist.author?.name) {
|
||||
// auto-generated album playlists don't have an author
|
||||
// so in search results, the author text is "Playlist" and doesn't have a link or channel ID
|
||||
internalChannelName = playlist.author.name
|
||||
}
|
||||
|
||||
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
|
||||
const thumbnailRenderer = playlist.thumbnail_renderer
|
||||
|
||||
return {
|
||||
type: 'playlist',
|
||||
dataSource: 'local',
|
||||
title: playlist.title.text,
|
||||
thumbnail: thumbnailRenderer ? thumbnailRenderer.thumbnail[0].url : playlist.thumbnails[0].url,
|
||||
channelName: internalChannelName,
|
||||
channelId: internalChannelId,
|
||||
playlistId: playlist.id,
|
||||
videoCount: extractNumberFromString(playlist.video_count.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -738,7 +772,7 @@ export function parseLocalListVideo(item) {
|
|||
|
||||
let publishedText
|
||||
|
||||
if (!video.published?.isEmpty()) {
|
||||
if (video.published != null && !video.published.isEmpty()) {
|
||||
publishedText = video.published.text
|
||||
}
|
||||
|
||||
|
@ -834,7 +868,7 @@ function parseListItem(item) {
|
|||
export function parseLocalWatchNextVideo(video) {
|
||||
let publishedText
|
||||
|
||||
if (!video.published?.isEmpty()) {
|
||||
if (video.published != null && !video.published.isEmpty()) {
|
||||
publishedText = video.published.text
|
||||
}
|
||||
|
||||
|
@ -1023,7 +1057,7 @@ export function mapLocalFormat(format) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.Comment} comment
|
||||
* @param {import('youtubei.js').YTNodes.Comment|import('youtubei.js').YTNodes.CommentView} comment
|
||||
* @param {import('youtubei.js').YTNodes.CommentThread} commentThread
|
||||
*/
|
||||
export function parseLocalComment(comment, commentThread = undefined) {
|
||||
|
@ -1035,26 +1069,48 @@ export function parseLocalComment(comment, commentThread = undefined) {
|
|||
replyToken = commentThread
|
||||
}
|
||||
|
||||
return {
|
||||
const parsed = {
|
||||
dataType: 'local',
|
||||
authorLink: comment.author.id,
|
||||
author: comment.author.name,
|
||||
authorId: comment.author.id,
|
||||
authorThumb: comment.author.best_thumbnail.url,
|
||||
isPinned: comment.is_pinned,
|
||||
isOwner: comment.author_is_channel_owner,
|
||||
isMember: comment.is_member,
|
||||
memberIconUrl: comment.is_member ? comment.sponsor_comment_badge.custom_badge[0].url : '',
|
||||
isOwner: !!comment.author_is_channel_owner,
|
||||
isMember: !!comment.is_member,
|
||||
text: Autolinker.link(parseLocalTextRuns(comment.content.runs, 16, { looseChannelNameDetection: true })),
|
||||
time: toLocalePublicationString({ publishText: comment.published.text.replace('(edited)', '').trim() }),
|
||||
likes: comment.vote_count,
|
||||
isHearted: comment.is_hearted,
|
||||
numReplies: comment.reply_count,
|
||||
isHearted: !!comment.is_hearted,
|
||||
hasOwnerReplied,
|
||||
replyToken,
|
||||
showReplies: false,
|
||||
replies: []
|
||||
replies: [],
|
||||
|
||||
// default values for the properties set below
|
||||
memberIconUrl: '',
|
||||
time: '',
|
||||
likes: 0,
|
||||
numReplies: 0
|
||||
}
|
||||
|
||||
if (comment.type === 'Comment') {
|
||||
/** @type {import('youtubei.js').YTNodes.Comment} */
|
||||
const comment_ = comment
|
||||
|
||||
parsed.memberIconUrl = comment_.is_member ? comment_.sponsor_comment_badge.custom_badge[0].url : ''
|
||||
parsed.time = toLocalePublicationString({ publishText: comment_.published.text.replace('(edited)', '').trim() })
|
||||
parsed.likes = comment_.vote_count
|
||||
parsed.numReplies = comment_.reply_count
|
||||
} else {
|
||||
/** @type {import('youtubei.js').YTNodes.CommentView} */
|
||||
const commentView = comment
|
||||
|
||||
parsed.memberIconUrl = commentView.is_member ? commentView.member_badge.url : ''
|
||||
parsed.time = toLocalePublicationString({ publishText: commentView.published_time.replace('(edited)', '').trim() })
|
||||
parsed.likes = commentView.like_count
|
||||
parsed.numReplies = parseLocalSubscriberCount(commentView.reply_count)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1088,32 +1144,40 @@ export function filterLocalFormats(formats, allowAv1 = false) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Really not a fan of this :(, YouTube returns the subscribers as "15.1M subscribers"
|
||||
* so we have to parse it somehow
|
||||
* @param {string} text
|
||||
*/
|
||||
export function parseLocalSubscriberCount(text) {
|
||||
const match = text
|
||||
.replace(',', '.')
|
||||
.toUpperCase()
|
||||
.match(/([\d.]+)\s*([KM]?)/)
|
||||
const match = text.match(/(\d+)(?:[,.](\d+))?\s?([BKMbkm])\b/)
|
||||
|
||||
let subscribers
|
||||
if (match) {
|
||||
subscribers = parseFloat(match[1])
|
||||
let multiplier = 0
|
||||
|
||||
if (match[2] === 'K') {
|
||||
subscribers *= 1000
|
||||
} else if (match[2] === 'M') {
|
||||
subscribers *= 1000_000
|
||||
switch (match[3]) {
|
||||
case 'K':
|
||||
case 'k':
|
||||
multiplier = 3
|
||||
break
|
||||
case 'M':
|
||||
case 'm':
|
||||
multiplier = 6
|
||||
break
|
||||
case 'B':
|
||||
case 'b':
|
||||
multiplier = 9
|
||||
break
|
||||
}
|
||||
|
||||
subscribers = Math.trunc(subscribers)
|
||||
} else {
|
||||
subscribers = extractNumberFromString(text)
|
||||
}
|
||||
let parsedDecimals
|
||||
if (typeof match[2] === 'undefined') {
|
||||
parsedDecimals = '0'.repeat(multiplier)
|
||||
} else {
|
||||
parsedDecimals = match[2].padEnd(multiplier, '0')
|
||||
}
|
||||
|
||||
return subscribers
|
||||
return parseInt(match[1] + parsedDecimals)
|
||||
} else {
|
||||
return extractNumberFromString(text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,7 +11,7 @@ import { getLocalChannel } from './api/local'
|
|||
*/
|
||||
async function findChannelById(id, backendOptions) {
|
||||
try {
|
||||
if (!process.env.IS_ELECTRON || backendOptions.preference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || backendOptions.preference === 'invidious') {
|
||||
return await invidiousGetChannelInfo(id)
|
||||
} else {
|
||||
return await getLocalChannel(id)
|
||||
|
@ -21,7 +21,7 @@ async function findChannelById(id, backendOptions) {
|
|||
if (err.message && err.message === 'This channel does not exist.') {
|
||||
return { invalid: true }
|
||||
}
|
||||
if (process.env.IS_ELECTRON && backendOptions.fallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && backendOptions.fallback) {
|
||||
if (backendOptions.preference === 'invidious') {
|
||||
return await getLocalChannel(id)
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export async function findChannelTagInfo(id, backendOptions) {
|
|||
if (!checkYoutubeChannelId(id)) return { invalidId: true }
|
||||
try {
|
||||
const channel = await findChannelById(id, backendOptions)
|
||||
if (!process.env.IS_ELECTRON || backendOptions.preference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || backendOptions.preference === 'invidious') {
|
||||
if (channel.invalid) return { invalidId: true }
|
||||
return {
|
||||
preferredName: channel.author,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import i18n from '../i18n/index'
|
||||
|
||||
export const colors = [
|
||||
{ name: 'Red', value: '#d50000' },
|
||||
{ name: 'Pink', value: '#C51162' },
|
||||
|
@ -38,14 +40,55 @@ export const colors = [
|
|||
{ name: 'CatppuccinMochaLavender', value: '#B4BEFE' }
|
||||
]
|
||||
|
||||
export function getColorTranslations() {
|
||||
return [
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Red'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Pink'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Purple'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Deep Purple'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Indigo'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Blue'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Light Blue'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Cyan'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Teal'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Green'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Light Green'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Lime'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Yellow'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Amber'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Orange'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Deep Orange'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Cyan'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Green'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Orange'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Pink'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Purple'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Red'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Dracula Yellow'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Rosewater'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Flamingo'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Pink'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Mauve'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Red'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Maroon'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Peach'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Yellow'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Green'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Teal'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Sky'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Sapphire'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Blue'),
|
||||
i18n.t('Settings.Theme Settings.Main Color Theme.Catppuccin Mocha Lavender')
|
||||
]
|
||||
}
|
||||
|
||||
export function getRandomColorClass() {
|
||||
const randomInt = Math.floor(Math.random() * colors.length)
|
||||
return 'main' + colors[randomInt].name
|
||||
return 'main' + getRandomColor().name
|
||||
}
|
||||
|
||||
export function getRandomColor() {
|
||||
const randomInt = Math.floor(Math.random() * colors.length)
|
||||
return colors[randomInt].value
|
||||
return colors[randomInt]
|
||||
}
|
||||
|
||||
export function calculateColorLuminance(colorValue) {
|
||||
|
|
|
@ -23,3 +23,32 @@ export function isKeyboardEventKeyPrintableChar(eventKey) {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
export function translateWindowTitle(title, i18n) {
|
||||
switch (title) {
|
||||
case 'Subscriptions':
|
||||
return i18n.t('Subscriptions.Subscriptions')
|
||||
case 'Channels':
|
||||
return i18n.t('Channels.Title')
|
||||
case 'Trending':
|
||||
return i18n.t('Trending.Trending')
|
||||
case 'Most Popular':
|
||||
return i18n.t('Most Popular')
|
||||
case 'Your Playlists':
|
||||
return i18n.t('User Playlists.Your Playlists')
|
||||
case 'History':
|
||||
return i18n.t('History.History')
|
||||
case 'Settings':
|
||||
return i18n.t('Settings.Settings')
|
||||
case 'About':
|
||||
return i18n.t('About.About')
|
||||
case 'Profile Settings':
|
||||
return i18n.t('Profile.Profile Settings')
|
||||
case 'Search Results':
|
||||
return i18n.t('Search Filters.Search Results')
|
||||
case 'Playlist':
|
||||
return i18n.t('Playlist.Playlist')
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,43 +92,67 @@ export function toLocalePublicationString ({ publishText, isLive = false, isUpco
|
|||
|
||||
const match = publishText.match(PUBLISHED_TEXT_REGEX)
|
||||
const singular = (match[1] === '1')
|
||||
let translationKey = ''
|
||||
let unit = ''
|
||||
switch (match[2].substring(0, 2)) {
|
||||
case 'se':
|
||||
case 's':
|
||||
translationKey = 'Video.Published.Second'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Second')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Seconds')
|
||||
}
|
||||
break
|
||||
case 'mi':
|
||||
case 'm':
|
||||
translationKey = 'Video.Published.Minute'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Minute')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Minutes')
|
||||
}
|
||||
break
|
||||
case 'ho':
|
||||
case 'h':
|
||||
translationKey = 'Video.Published.Hour'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Hour')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Hours')
|
||||
}
|
||||
break
|
||||
case 'da':
|
||||
case 'd':
|
||||
translationKey = 'Video.Published.Day'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Day')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Days')
|
||||
}
|
||||
break
|
||||
case 'we':
|
||||
case 'w':
|
||||
translationKey = 'Video.Published.Week'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Week')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Weeks')
|
||||
}
|
||||
break
|
||||
case 'mo':
|
||||
translationKey = 'Video.Published.Month'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Month')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Months')
|
||||
}
|
||||
break
|
||||
case 'ye':
|
||||
case 'y':
|
||||
translationKey = 'Video.Published.Year'
|
||||
if (singular) {
|
||||
unit = i18n.t('Video.Published.Year')
|
||||
} else {
|
||||
unit = i18n.t('Video.Published.Years')
|
||||
}
|
||||
break
|
||||
default:
|
||||
return publishText
|
||||
}
|
||||
if (!singular) {
|
||||
translationKey += 's'
|
||||
}
|
||||
|
||||
const unit = i18n.t(translationKey)
|
||||
return i18n.t('Video.Publicationtemplate', { number: match[1], unit })
|
||||
}
|
||||
|
||||
|
@ -574,8 +598,7 @@ export function extractNumberFromString(str) {
|
|||
}
|
||||
}
|
||||
|
||||
export function showExternalPlayerUnsupportedActionToast(externalPlayer, actionName) {
|
||||
const action = i18n.t(`Video.External Player.Unsupported Actions.${actionName}`)
|
||||
export function showExternalPlayerUnsupportedActionToast(externalPlayer, action) {
|
||||
const message = i18n.t('Video.External Player.UnsupportedActionTemplate', { externalPlayer, action })
|
||||
showToast(message)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,20 @@ Vue.use(VueI18n)
|
|||
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en-US',
|
||||
fallbackLocale: { default: 'en-US' }
|
||||
fallbackLocale: {
|
||||
// https://kazupon.github.io/vue-i18n/guide/fallback.html#explicit-fallback-with-decision-maps
|
||||
|
||||
// es_AR -> es -> en-US
|
||||
es_AR: ['es'],
|
||||
// es-MX -> es -> en-US
|
||||
'es-MX': ['es'],
|
||||
// pt-BR -> pt -> en-US
|
||||
'pt-BR': ['pt'],
|
||||
// pt-PT -> pt -> en-US
|
||||
'pt-PT': ['pt'],
|
||||
// any -> en-US
|
||||
default: ['en-US'],
|
||||
}
|
||||
})
|
||||
|
||||
export async function loadLocale(locale) {
|
||||
|
@ -18,6 +31,7 @@ export async function loadLocale(locale) {
|
|||
}
|
||||
if (!activeLocales.includes(locale)) {
|
||||
console.error(`Unable to load unknown locale: "${locale}"`)
|
||||
return
|
||||
}
|
||||
|
||||
// locales are only compressed in our production Electron builds
|
||||
|
@ -45,6 +59,4 @@ export async function loadLocale(locale) {
|
|||
}
|
||||
}
|
||||
|
||||
loadLocale('en-US')
|
||||
|
||||
export default i18n
|
||||
|
|
|
@ -63,7 +63,6 @@ import {
|
|||
faShareAlt,
|
||||
faSlidersH,
|
||||
faSortDown,
|
||||
faStar,
|
||||
faStepBackward,
|
||||
faStepForward,
|
||||
faSync,
|
||||
|
@ -143,7 +142,6 @@ library.add(
|
|||
faShareAlt,
|
||||
faSlidersH,
|
||||
faSortDown,
|
||||
faStar,
|
||||
faStepBackward,
|
||||
faStepForward,
|
||||
faSync,
|
||||
|
|
|
@ -23,7 +23,7 @@ const router = new Router({
|
|||
path: '/',
|
||||
name: 'default',
|
||||
meta: {
|
||||
title: 'Subscriptions.Subscriptions'
|
||||
title: 'Subscriptions'
|
||||
},
|
||||
component: Subscriptions
|
||||
},
|
||||
|
@ -31,7 +31,7 @@ const router = new Router({
|
|||
path: '/subscriptions',
|
||||
name: 'subscriptions',
|
||||
meta: {
|
||||
title: 'Subscriptions.Subscriptions'
|
||||
title: 'Subscriptions'
|
||||
},
|
||||
component: Subscriptions
|
||||
},
|
||||
|
@ -39,7 +39,7 @@ const router = new Router({
|
|||
path: '/subscribedchannels',
|
||||
name: 'subscribedChannels',
|
||||
meta: {
|
||||
title: 'Channels.Title'
|
||||
title: 'Channels'
|
||||
},
|
||||
component: SubscribedChannels
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ const router = new Router({
|
|||
path: '/trending',
|
||||
name: 'trending',
|
||||
meta: {
|
||||
title: 'Trending.Trending'
|
||||
title: 'Trending'
|
||||
},
|
||||
component: Trending
|
||||
},
|
||||
|
@ -63,7 +63,7 @@ const router = new Router({
|
|||
path: '/userplaylists',
|
||||
name: 'userPlaylists',
|
||||
meta: {
|
||||
title: 'User Playlists.Your Playlists'
|
||||
title: 'Your Playlists'
|
||||
},
|
||||
component: UserPlaylists
|
||||
},
|
||||
|
@ -71,7 +71,7 @@ const router = new Router({
|
|||
path: '/history',
|
||||
name: 'history',
|
||||
meta: {
|
||||
title: 'History.History'
|
||||
title: 'History'
|
||||
},
|
||||
component: History
|
||||
},
|
||||
|
@ -79,7 +79,7 @@ const router = new Router({
|
|||
path: '/settings',
|
||||
name: 'settings',
|
||||
meta: {
|
||||
title: 'Settings.Settings'
|
||||
title: 'Settings'
|
||||
},
|
||||
component: Settings
|
||||
},
|
||||
|
@ -87,7 +87,7 @@ const router = new Router({
|
|||
path: '/about',
|
||||
name: 'about',
|
||||
meta: {
|
||||
title: 'About.About'
|
||||
title: 'About'
|
||||
},
|
||||
component: About
|
||||
},
|
||||
|
@ -95,21 +95,21 @@ const router = new Router({
|
|||
path: '/settings/profile',
|
||||
name: 'profileSettings',
|
||||
meta: {
|
||||
title: 'Profile.Profile Settings'
|
||||
title: 'Profile Settings'
|
||||
},
|
||||
component: ProfileSettings
|
||||
},
|
||||
{
|
||||
path: '/search/:query',
|
||||
meta: {
|
||||
title: 'Search Filters.Search Results'
|
||||
title: 'Search Results'
|
||||
},
|
||||
component: Search
|
||||
},
|
||||
{
|
||||
path: '/playlist/:id',
|
||||
meta: {
|
||||
title: 'Playlist.Playlist'
|
||||
title: 'Playlist'
|
||||
},
|
||||
component: Playlist
|
||||
},
|
||||
|
|
|
@ -366,7 +366,7 @@ $watched-transition-duration: 0.5s;
|
|||
&:has(:focus-visible) .addToPlaylistIcon:not(.alwaysVisible),
|
||||
&:has(:focus-visible) .quickBookmarkVideoIcon:not(.alwaysVisible),
|
||||
&:has(:focus-visible) .externalPlayerIcon {
|
||||
opacity: $thumbnail-overlay-opacity;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover .optionsButton,
|
||||
|
@ -386,45 +386,6 @@ $watched-transition-duration: 0.5s;
|
|||
}
|
||||
}
|
||||
|
||||
.ft-list-channel {
|
||||
.infoAndSubscribe {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
&.grid {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.infoAndSubscribe {
|
||||
flex-flow: column wrap;
|
||||
align-items: center;
|
||||
|
||||
.info {
|
||||
margin-block-end: 12px;
|
||||
|
||||
.infoLine {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.list {
|
||||
.infoAndSubscribe {
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
|
||||
.channelSubscribeButton {
|
||||
margin-block: auto;
|
||||
margin-inline: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.videoWatched,
|
||||
.live,
|
||||
.upcoming {
|
||||
|
|
|
@ -28,7 +28,7 @@ const actions = {
|
|||
return !(instance[0].includes('.onion') ||
|
||||
instance[0].includes('.i2p') ||
|
||||
!instance[1].api ||
|
||||
(!process.env.IS_ELECTRON && !instance[1].cors))
|
||||
(!process.env.SUPPORTS_LOCAL_API && !instance[1].cors))
|
||||
}).map((instance) => {
|
||||
return instance[1].uri.replace(/\/$/, '')
|
||||
})
|
||||
|
@ -50,7 +50,7 @@ const actions = {
|
|||
console.warn('reading static file for invidious instances')
|
||||
const fileData = process.env.IS_ELECTRON ? await fs.readFile(filePath, 'utf8') : await (await fetch(createWebURL(filePath))).text()
|
||||
instances = JSON.parse(fileData).filter(e => {
|
||||
return process.env.IS_ELECTRON || e.cors
|
||||
return process.env.SUPPORTS_LOCAL_API || e.cors
|
||||
}).map(e => {
|
||||
return e.url
|
||||
})
|
||||
|
|
|
@ -54,7 +54,7 @@ const actions = {
|
|||
|
||||
if (profiles.length === 0) {
|
||||
// Create a default profile and persist it
|
||||
const randomColor = getRandomColor()
|
||||
const randomColor = getRandomColor().value
|
||||
const textColor = calculateColorLuminance(randomColor)
|
||||
const defaultProfile = {
|
||||
_id: MAIN_PROFILE_ID,
|
||||
|
|
|
@ -165,8 +165,8 @@ const state = {
|
|||
allSettingsSectionsExpandedByDefault: false,
|
||||
autoplayPlaylists: true,
|
||||
autoplayVideos: true,
|
||||
backendFallback: process.env.IS_ELECTRON,
|
||||
backendPreference: !process.env.IS_ELECTRON ? 'invidious' : 'local',
|
||||
backendFallback: process.env.SUPPORTS_LOCAL_API,
|
||||
backendPreference: !process.env.SUPPORTS_LOCAL_API ? 'invidious' : 'local',
|
||||
barColor: false,
|
||||
checkForBlogPosts: true,
|
||||
checkForUpdates: true,
|
||||
|
@ -237,10 +237,9 @@ const state = {
|
|||
proxyHostname: '127.0.0.1',
|
||||
proxyPort: '9050',
|
||||
proxyProtocol: 'socks5',
|
||||
proxyVideos: !process.env.IS_ELECTRON,
|
||||
proxyVideos: !process.env.SUPPORTS_LOCAL_API,
|
||||
region: 'US',
|
||||
rememberHistory: true,
|
||||
removeVideoMetaFiles: true,
|
||||
saveWatchedProgress: true,
|
||||
saveVideoHistoryWithLastViewedPlaylist: true,
|
||||
showFamilyFriendlyOnly: false,
|
||||
|
@ -353,7 +352,34 @@ const stateWithSideEffects = {
|
|||
}
|
||||
}
|
||||
|
||||
await loadLocale(targetLocale)
|
||||
const loadPromises = []
|
||||
|
||||
if (targetLocale !== defaultLocale) {
|
||||
// "en-US" is used as a fallback for missing strings in other locales
|
||||
loadPromises.push(
|
||||
loadLocale(defaultLocale)
|
||||
)
|
||||
}
|
||||
|
||||
// "es" is used as a fallback for "es_AR" and "es-MX"
|
||||
if (targetLocale === 'es_AR' || targetLocale === 'es-MX') {
|
||||
loadPromises.push(
|
||||
loadLocale('es')
|
||||
)
|
||||
}
|
||||
|
||||
// "pt" is used as a fallback for "pt-PT" and "pt-BR"
|
||||
if (targetLocale === 'pt-PT' || targetLocale === 'pt-BR') {
|
||||
loadPromises.push(
|
||||
loadLocale('pt')
|
||||
)
|
||||
}
|
||||
|
||||
loadPromises.push(
|
||||
loadLocale(targetLocale)
|
||||
)
|
||||
|
||||
await Promise.allSettled(loadPromises)
|
||||
|
||||
i18n.locale = targetLocale
|
||||
await dispatch('getRegionData', {
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
import { deepCopy } from '../../helpers/utils'
|
||||
|
||||
const defaultCacheEntryValueForForOneChannel = {
|
||||
videos: null,
|
||||
}
|
||||
|
||||
const state = {
|
||||
videoCache: {},
|
||||
liveCache: {},
|
||||
|
@ -77,7 +71,7 @@ const actions = {
|
|||
const mutations = {
|
||||
updateVideoCacheByChannel(state, { channelId, videos }) {
|
||||
const existingObject = state.videoCache[channelId]
|
||||
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
|
||||
const newObject = existingObject ?? { videos: null }
|
||||
if (videos != null) { newObject.videos = videos }
|
||||
state.videoCache[channelId] = newObject
|
||||
},
|
||||
|
@ -86,7 +80,7 @@ const mutations = {
|
|||
},
|
||||
updateShortsCacheByChannel(state, { channelId, videos }) {
|
||||
const existingObject = state.shortsCache[channelId]
|
||||
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
|
||||
const newObject = existingObject ?? { videos: null }
|
||||
if (videos != null) { newObject.videos = videos }
|
||||
state.shortsCache[channelId] = newObject
|
||||
},
|
||||
|
@ -120,7 +114,7 @@ const mutations = {
|
|||
},
|
||||
updateLiveCacheByChannel(state, { channelId, videos }) {
|
||||
const existingObject = state.liveCache[channelId]
|
||||
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
|
||||
const newObject = existingObject ?? { videos: null }
|
||||
if (videos != null) { newObject.videos = videos }
|
||||
state.liveCache[channelId] = newObject
|
||||
},
|
||||
|
@ -129,7 +123,7 @@ const mutations = {
|
|||
},
|
||||
updatePostsCacheByChannel(state, { channelId, posts }) {
|
||||
const existingObject = state.postsCache[channelId]
|
||||
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
|
||||
const newObject = existingObject ?? { posts: null }
|
||||
if (posts != null) { newObject.posts = posts }
|
||||
state.postsCache[channelId] = newObject
|
||||
},
|
||||
|
|
|
@ -47,7 +47,6 @@ const state = {
|
|||
duration: ''
|
||||
},
|
||||
externalPlayerNames: [],
|
||||
externalPlayerNameTranslationKeys: [],
|
||||
externalPlayerValues: [],
|
||||
externalPlayerCmdArguments: {}
|
||||
}
|
||||
|
@ -133,10 +132,6 @@ const getters = {
|
|||
return state.externalPlayerNames
|
||||
},
|
||||
|
||||
getExternalPlayerNameTranslationKeys () {
|
||||
return state.externalPlayerNameTranslationKeys
|
||||
},
|
||||
|
||||
getExternalPlayerValues () {
|
||||
return state.externalPlayerValues
|
||||
},
|
||||
|
@ -262,7 +257,7 @@ const actions = {
|
|||
}
|
||||
|
||||
if (parsedString !== replaceFilenameForbiddenChars(parsedString)) {
|
||||
reject(new Error('Forbidden Characters')) // use message as translation key
|
||||
reject(new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Forbidden Characters')))
|
||||
}
|
||||
|
||||
let filename
|
||||
|
@ -274,7 +269,7 @@ const actions = {
|
|||
}
|
||||
|
||||
if (!filename) {
|
||||
reject(new Error('Empty File Name'))
|
||||
reject(new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Empty File Name')))
|
||||
}
|
||||
|
||||
resolve(parsedString)
|
||||
|
@ -603,7 +598,7 @@ const actions = {
|
|||
const fileData = await fs.readFile(`${fileLocation}${fileName}`)
|
||||
|
||||
const externalPlayerMap = JSON.parse(fileData).map((entry) => {
|
||||
return { name: entry.name, nameTranslationKey: entry.nameTranslationKey, value: entry.value, cmdArguments: entry.cmdArguments }
|
||||
return { name: entry.name, value: entry.value, cmdArguments: entry.cmdArguments }
|
||||
})
|
||||
// Sort external players alphabetically & case-insensitive, keep default entry at the top
|
||||
const playerNone = externalPlayerMap.shift()
|
||||
|
@ -611,7 +606,6 @@ const actions = {
|
|||
externalPlayerMap.unshift(playerNone)
|
||||
|
||||
const externalPlayerNames = externalPlayerMap.map((entry) => { return entry.name })
|
||||
const externalPlayerNameTranslationKeys = externalPlayerMap.map((entry) => { return entry.nameTranslationKey })
|
||||
const externalPlayerValues = externalPlayerMap.map((entry) => { return entry.value })
|
||||
const externalPlayerCmdArguments = externalPlayerMap.reduce((result, item) => {
|
||||
result[item.value] = item.cmdArguments
|
||||
|
@ -619,7 +613,6 @@ const actions = {
|
|||
}, {})
|
||||
|
||||
commit('setExternalPlayerNames', externalPlayerNames)
|
||||
commit('setExternalPlayerNameTranslationKeys', externalPlayerNameTranslationKeys)
|
||||
commit('setExternalPlayerValues', externalPlayerValues)
|
||||
commit('setExternalPlayerCmdArguments', externalPlayerCmdArguments)
|
||||
},
|
||||
|
@ -667,7 +660,7 @@ const actions = {
|
|||
args.push(cmdArgs.startOffset, Math.trunc(payload.watchProgress))
|
||||
}
|
||||
} else if (!ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'starting video at offset')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.starting video at offset'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -675,7 +668,7 @@ const actions = {
|
|||
if (typeof cmdArgs.playbackRate === 'string') {
|
||||
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
|
||||
} else if (!ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'setting a playback rate')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.setting a playback rate'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -685,7 +678,7 @@ const actions = {
|
|||
if (typeof cmdArgs.playlistIndex === 'string') {
|
||||
args.push(`${cmdArgs.playlistIndex}${payload.playlistIndex}`)
|
||||
} else if (!ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'opening specific video in a playlist (falling back to opening the video)')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.opening specific video in a playlist (falling back to opening the video)'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -693,7 +686,7 @@ const actions = {
|
|||
if (typeof cmdArgs.playlistReverse === 'string') {
|
||||
args.push(cmdArgs.playlistReverse)
|
||||
} else if (!ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'reversing playlists')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.reversing playlists'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -701,7 +694,7 @@ const actions = {
|
|||
if (typeof cmdArgs.playlistShuffle === 'string') {
|
||||
args.push(cmdArgs.playlistShuffle)
|
||||
} else if (!ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'shuffling playlists')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.shuffling playlists'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -709,7 +702,7 @@ const actions = {
|
|||
if (typeof cmdArgs.playlistLoop === 'string') {
|
||||
args.push(cmdArgs.playlistLoop)
|
||||
} else if (!ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'looping playlists')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.looping playlists'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -721,7 +714,7 @@ const actions = {
|
|||
}
|
||||
} else {
|
||||
if (payload.playlistId != null && payload.playlistId !== '' && !ignoreWarnings) {
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, 'opening playlists')
|
||||
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.opening playlists'))
|
||||
}
|
||||
if (payload.videoId != null) {
|
||||
args.push(`${cmdArgs.videoUrl}https://www.youtube.com/watch?v=${payload.videoId}`)
|
||||
|
@ -874,10 +867,6 @@ const mutations = {
|
|||
state.externalPlayerNames = value
|
||||
},
|
||||
|
||||
setExternalPlayerNameTranslationKeys (state, value) {
|
||||
state.externalPlayerNameTranslationKeys = value
|
||||
},
|
||||
|
||||
setExternalPlayerValues (state, value) {
|
||||
state.externalPlayerValues = value
|
||||
},
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
--primary-shadow-color: rgba(232, 232, 232, 1);
|
||||
--title-color: #3f7ac6;
|
||||
--bg-color: #f1f1f1;
|
||||
--favorite-icon-color: #FFD600;
|
||||
--favorite-icon-color: #00CC00;
|
||||
--card-bg-color: #FFFFFF;
|
||||
--secondary-card-bg-color: #eeeeee;
|
||||
--scrollbar-color: #CCCCCC;
|
||||
|
@ -89,7 +89,7 @@
|
|||
--tertiary-text-color: #999;
|
||||
--title-color: #EEEEEE;
|
||||
--bg-color: #212121;
|
||||
--favorite-icon-color: #FFEA00;
|
||||
--favorite-icon-color: #00FF00;
|
||||
--card-bg-color: #303030;
|
||||
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
|
||||
--scrollbar-color: #414141;
|
||||
|
@ -106,7 +106,7 @@
|
|||
--tertiary-text-color: #EEEEEE;
|
||||
--title-color: #EEEEEE;
|
||||
--bg-color: #000000;
|
||||
--favorite-icon-color: #FFEA00;
|
||||
--favorite-icon-color: #00FF00;
|
||||
--card-bg-color: #000000;
|
||||
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
|
||||
--scrollbar-color: #515151;
|
||||
|
@ -137,7 +137,7 @@
|
|||
--tertiary-text-color: #e5e8f3;
|
||||
--title-color: #BD93F9;
|
||||
--bg-color: #282A36;
|
||||
--favorite-icon-color: #F1FA8C;
|
||||
--favorite-icon-color: #00FF00;
|
||||
--card-bg-color: #33353F;
|
||||
--secondary-card-bg-color: #282A36;
|
||||
--scrollbar-color: #44475A;
|
||||
|
@ -156,7 +156,7 @@
|
|||
--tertiary-text-color: #a6adc8;
|
||||
--title-color: var(--accent-color);
|
||||
--bg-color: #1e1e2e;
|
||||
--favorite-icon-color: #f9e2af;
|
||||
--favorite-icon-color: #00FF00;
|
||||
--card-bg-color: #181825;
|
||||
--secondary-card-bg-color: #1e1e2e;
|
||||
--scrollbar-color: #313244;
|
||||
|
@ -197,7 +197,7 @@
|
|||
--tertiary-text-color: #FFFF;
|
||||
--title-color: #000000;
|
||||
--bg-color: #ff008a;
|
||||
--favorite-icon-color: #FFEA00;
|
||||
--favorite-icon-color: #00FF00;
|
||||
--card-bg-color: #DE1C85;
|
||||
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
|
||||
--scrollbar-color: #FFFFFF;
|
||||
|
@ -248,7 +248,7 @@ it can be safely elided. This looks quite pleasant on this theme. */
|
|||
--tertiary-text-color: #EEEEEE;
|
||||
--title-color: #EEEEEE;
|
||||
--bg-color: #2b2f3a;
|
||||
--favorite-icon-color: #FFEA00;
|
||||
--favorite-icon-color: #00FF00;
|
||||
--card-bg-color: #2e3440;
|
||||
--secondary-card-bg-color: rgba(59, 66, 82, 0.75);
|
||||
--scrollbar-color: #4b566a;
|
||||
|
|
|
@ -105,15 +105,11 @@ export default defineComponent({
|
|||
errorMessage: '',
|
||||
showSearchBar: true,
|
||||
showShareMenu: true,
|
||||
videoLiveSelectValues: [
|
||||
videoLiveShortSelectValues: [
|
||||
'newest',
|
||||
'popular',
|
||||
'oldest'
|
||||
],
|
||||
shortSelectValues: [
|
||||
'newest',
|
||||
'popular'
|
||||
],
|
||||
playlistSelectValues: [
|
||||
'newest',
|
||||
'last'
|
||||
|
@ -184,7 +180,7 @@ export default defineComponent({
|
|||
return profileList[0].subscriptions.some((channel) => channel.id === this.id)
|
||||
},
|
||||
|
||||
videoLiveSelectNames: function () {
|
||||
videoLiveShortSelectNames: function () {
|
||||
return [
|
||||
this.$t('Channel.Videos.Sort Types.Newest'),
|
||||
this.$t('Channel.Videos.Sort Types.Most Popular'),
|
||||
|
@ -192,13 +188,6 @@ export default defineComponent({
|
|||
]
|
||||
},
|
||||
|
||||
shortSelectNames: function () {
|
||||
return [
|
||||
this.$t('Channel.Videos.Sort Types.Newest'),
|
||||
this.$t('Channel.Videos.Sort Types.Most Popular')
|
||||
]
|
||||
},
|
||||
|
||||
playlistSelectNames: function () {
|
||||
return [
|
||||
this.$t('Channel.Playlists.Sort Types.Newest'),
|
||||
|
@ -362,7 +351,7 @@ export default defineComponent({
|
|||
this.errorMessage = ''
|
||||
|
||||
// Re-enable auto refresh on sort value change AFTER update done
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getChannelInfoInvidious()
|
||||
this.autoRefreshOnSortByChangeEnabled = true
|
||||
} else {
|
||||
|
@ -460,7 +449,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Enable auto refresh on sort value change AFTER initial update done
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getChannelInfoInvidious()
|
||||
this.autoRefreshOnSortByChangeEnabled = true
|
||||
} else {
|
||||
|
@ -473,7 +462,7 @@ export default defineComponent({
|
|||
resolveChannelUrl: async function (url, tab = undefined) {
|
||||
let id
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
id = await invidiousGetChannelId(url)
|
||||
} else {
|
||||
id = await getLocalChannelId(url)
|
||||
|
@ -769,7 +758,7 @@ export default defineComponent({
|
|||
this.showVideoSortBy = videosTab.filters.length > 1
|
||||
|
||||
if (this.showVideoSortBy && this.videoSortBy !== 'newest') {
|
||||
const index = this.videoLiveSelectValues.indexOf(this.videoSortBy)
|
||||
const index = this.videoLiveShortSelectValues.indexOf(this.videoSortBy)
|
||||
videosTab = await videosTab.applyFilter(videosTab.filters[index])
|
||||
}
|
||||
|
||||
|
@ -837,7 +826,7 @@ export default defineComponent({
|
|||
this.showShortSortBy = shortsTab.filters.length > 1
|
||||
|
||||
if (this.showShortSortBy && this.shortSortBy !== 'newest') {
|
||||
const index = this.shortSelectValues.indexOf(this.shortSortBy)
|
||||
const index = this.videoLiveShortSelectValues.indexOf(this.shortSortBy)
|
||||
shortsTab = await shortsTab.applyFilter(shortsTab.filters[index])
|
||||
}
|
||||
|
||||
|
@ -905,7 +894,7 @@ export default defineComponent({
|
|||
this.showLiveSortBy = liveTab.filters.length > 1
|
||||
|
||||
if (this.showLiveSortBy && this.liveSortBy !== 'newest') {
|
||||
const index = this.videoLiveSelectValues.indexOf(this.liveSortBy)
|
||||
const index = this.videoLiveShortSelectValues.indexOf(this.liveSortBy)
|
||||
liveTab = await liveTab.applyFilter(liveTab.filters[index])
|
||||
}
|
||||
|
||||
|
@ -913,7 +902,16 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestLive = parseLocalChannelVideos(liveTab.videos, this.id, this.channelName)
|
||||
// work around YouTube bug where it will return a bunch of responses with only continuations in them
|
||||
// e.g. https://www.youtube.com/@TWLIVES/streams
|
||||
|
||||
let videos = liveTab.videos
|
||||
while (videos.length === 0 && liveTab.has_continuation) {
|
||||
liveTab = await liveTab.getContinuation()
|
||||
videos = liveTab.videos
|
||||
}
|
||||
|
||||
this.latestLive = parseLocalChannelVideos(videos, this.id, this.channelName)
|
||||
this.liveContinuationData = liveTab.has_continuation ? liveTab : null
|
||||
this.isElementListLoading = false
|
||||
|
||||
|
@ -1053,7 +1051,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getChannelLocal()
|
||||
} else {
|
||||
|
@ -1331,7 +1329,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
if (!this.channelInstance) {
|
||||
this.channelInstance = await getLocalChannel(this.id)
|
||||
|
@ -1372,7 +1370,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getChannelLocal()
|
||||
} else {
|
||||
|
@ -1451,7 +1449,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
if (!this.channelInstance) {
|
||||
this.channelInstance = await getLocalChannel(this.id)
|
||||
|
@ -1485,7 +1483,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getChannelLocal()
|
||||
} else {
|
||||
|
@ -1564,7 +1562,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
if (!this.channelInstance) {
|
||||
this.channelInstance = await getLocalChannel(this.id)
|
||||
|
@ -1598,7 +1596,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getChannelLocal()
|
||||
} else {
|
||||
|
@ -1718,7 +1716,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
if (!this.channelInstance) {
|
||||
this.channelInstance = await getLocalChannel(this.id)
|
||||
|
@ -1927,6 +1925,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
invidiousAPICall(payload).then((response) => {
|
||||
setPublishedTimestampsInvidious(response.filter(item => item.type === 'video'))
|
||||
if (this.hideChannelPlaylists) {
|
||||
this.searchResults = this.searchResults.concat(response.filter(item => item.type !== 'playlist'))
|
||||
} else {
|
||||
|
@ -1940,7 +1939,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.searchChannelLocal()
|
||||
} else {
|
||||
|
|
|
@ -233,27 +233,27 @@
|
|||
<ft-select
|
||||
v-if="showVideoSortBy"
|
||||
v-show="currentTab === 'videos' && latestVideos.length > 0"
|
||||
:value="videoLiveSelectValues[0]"
|
||||
:select-names="videoLiveSelectNames"
|
||||
:select-values="videoLiveSelectValues"
|
||||
:value="videoLiveShortSelectValues[0]"
|
||||
:select-names="videoLiveShortSelectNames"
|
||||
:select-values="videoLiveShortSelectValues"
|
||||
:placeholder="$t('Search Filters.Sort By.Sort By')"
|
||||
@change="videoSortBy = $event"
|
||||
/>
|
||||
<ft-select
|
||||
v-if="!hideChannelShorts && showShortSortBy"
|
||||
v-show="currentTab === 'shorts' && latestShorts.length > 0"
|
||||
:value="shortSelectValues[0]"
|
||||
:select-names="shortSelectNames"
|
||||
:select-values="shortSelectValues"
|
||||
:value="videoLiveShortSelectValues[0]"
|
||||
:select-names="videoLiveShortSelectNames"
|
||||
:select-values="videoLiveShortSelectValues"
|
||||
:placeholder="$t('Search Filters.Sort By.Sort By')"
|
||||
@change="shortSortBy = $event"
|
||||
/>
|
||||
<ft-select
|
||||
v-if="!hideLiveStreams && showLiveSortBy"
|
||||
v-show="currentTab === 'live' && latestLive.length > 0"
|
||||
:value="videoLiveSelectValues[0]"
|
||||
:select-names="videoLiveSelectNames"
|
||||
:select-values="videoLiveSelectValues"
|
||||
:value="videoLiveShortSelectValues[0]"
|
||||
:select-names="videoLiveShortSelectNames"
|
||||
:select-values="videoLiveShortSelectValues"
|
||||
:placeholder="$t('Search Filters.Sort By.Sort By')"
|
||||
@change="liveSortBy = $event"
|
||||
/>
|
||||
|
|
|
@ -85,7 +85,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${error}`, 10000, () => {
|
||||
copyToClipboard(error)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.resetData()
|
||||
this.getLocalHashtag(hashtag)
|
||||
|
|
|
@ -93,11 +93,13 @@ export default defineComponent({
|
|||
} else {
|
||||
const lowerCaseQuery = this.query.toLowerCase()
|
||||
const filteredQuery = this.historyCacheSorted.filter((video) => {
|
||||
if (typeof (video.title) !== 'string' || typeof (video.author) !== 'string') {
|
||||
return false
|
||||
} else {
|
||||
return video.title.toLowerCase().includes(lowerCaseQuery) || video.author.toLowerCase().includes(lowerCaseQuery)
|
||||
if (typeof (video.title) === 'string' && video.title.toLowerCase().includes(lowerCaseQuery)) {
|
||||
return true
|
||||
} else if (typeof (video.author) === 'string' && video.author.toLowerCase().includes(lowerCaseQuery)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}).sort((a, b) => {
|
||||
return b.timeWatched - a.timeWatched
|
||||
})
|
||||
|
|
|
@ -142,7 +142,13 @@ export default defineComponent({
|
|||
if (this.processedVideoSearchQuery === '') { return this.playlistItems }
|
||||
|
||||
return this.playlistItems.filter((v) => {
|
||||
return v.title.toLowerCase().includes(this.processedVideoSearchQuery)
|
||||
if (typeof (v.title) === 'string' && v.title.toLowerCase().includes(this.processedVideoSearchQuery)) {
|
||||
return true
|
||||
} else if (typeof (v.author) === 'string' && v.author.toLowerCase().includes(this.processedVideoSearchQuery)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
},
|
||||
visiblePlaylistItems: function () {
|
||||
|
@ -307,7 +313,7 @@ export default defineComponent({
|
|||
this.isLoading = false
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
console.warn('Error getting data with Invidious, falling back to local backend')
|
||||
this.getPlaylistLocal()
|
||||
} else {
|
||||
|
|
|
@ -52,7 +52,7 @@ export default defineComponent({
|
|||
methods: {
|
||||
openSettingsForNewProfile: function () {
|
||||
this.isNewProfileOpen = true
|
||||
const bgColor = getRandomColor()
|
||||
const bgColor = getRandomColor().value
|
||||
const textColor = calculateColorLuminance(bgColor)
|
||||
this.openSettingsProfile = {
|
||||
name: '',
|
||||
|
|
|
@ -251,7 +251,7 @@ export default defineComponent({
|
|||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.performSearchLocal(payload)
|
||||
} else {
|
||||
|
|
|
@ -39,14 +39,11 @@ export default defineComponent({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
usingElectron: process.env.IS_ELECTRON,
|
||||
unlocked: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
usingElectron: function () {
|
||||
return process.env.IS_ELECTRON
|
||||
},
|
||||
|
||||
settingsPassword: function () {
|
||||
return this.$store.getters.getSettingsPassword
|
||||
},
|
||||
|
|
|
@ -85,7 +85,7 @@ export default defineComponent({
|
|||
this.$store.commit('clearTrendingCache')
|
||||
}
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getTrendingInfoInvidious()
|
||||
} else {
|
||||
this.getTrendingInfoLocal()
|
||||
|
@ -161,7 +161,7 @@ export default defineComponent({
|
|||
copyToClipboard(err.responseText)
|
||||
})
|
||||
|
||||
if (process.env.IS_ELECTRON && (this.backendPreference === 'invidious' && this.backendFallback)) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && (this.backendPreference === 'invidious' && this.backendFallback)) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getTrendingInfoLocal()
|
||||
} else {
|
||||
|
|
|
@ -223,7 +223,7 @@ export default defineComponent({
|
|||
if (typeof (playlist.playlistName) !== 'string') { return false }
|
||||
|
||||
if (this.doSearchPlaylistsWithMatchingVideos) {
|
||||
if (playlist.videos.some((v) => v.title.toLowerCase().includes(this.lowerCaseQuery))) {
|
||||
if (playlist.videos.some((v) => v.author.toLowerCase().includes(this.lowerCaseQuery) || v.title.toLowerCase().includes(this.lowerCaseQuery))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import fs from 'fs/promises'
|
||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtVideoPlayer from '../../components/ft-video-player/ft-video-player.vue'
|
||||
import WatchVideoInfo from '../../components/watch-video-info/watch-video-info.vue'
|
||||
|
@ -12,14 +11,12 @@ import WatchVideoPlaylist from '../../components/watch-video-playlist/watch-vide
|
|||
import WatchVideoRecommendations from '../../components/watch-video-recommendations/watch-video-recommendations.vue'
|
||||
import FtAgeRestricted from '../../components/ft-age-restricted/ft-age-restricted.vue'
|
||||
import packageDetails from '../../../../package.json'
|
||||
import { pathExists } from '../../helpers/filesystem'
|
||||
import {
|
||||
buildVTTFileLocally,
|
||||
copyToClipboard,
|
||||
formatDurationAsTimestamp,
|
||||
formatNumber,
|
||||
getFormatsFromHLSManifest,
|
||||
getUserDataPath,
|
||||
showToast
|
||||
} from '../../helpers/utils'
|
||||
import {
|
||||
|
@ -141,9 +138,6 @@ export default defineComponent({
|
|||
rememberHistory: function () {
|
||||
return this.$store.getters.getRememberHistory
|
||||
},
|
||||
removeVideoMetaFiles: function () {
|
||||
return this.$store.getters.getRemoveVideoMetaFiles
|
||||
},
|
||||
saveWatchedProgress: function () {
|
||||
return this.$store.getters.getSaveWatchedProgress
|
||||
},
|
||||
|
@ -295,7 +289,7 @@ export default defineComponent({
|
|||
this.checkIfPlaylist()
|
||||
this.checkIfTimestamp()
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
|
||||
this.getVideoInformationInvidious()
|
||||
} else {
|
||||
this.getVideoInformationLocal()
|
||||
|
@ -703,7 +697,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (result.storyboards?.type === 'PlayerStoryboardSpec') {
|
||||
await this.createLocalStoryboardUrls(result.storyboards.boards.at(-1))
|
||||
this.createLocalStoryboardUrls(result.storyboards.boards.at(-1))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -876,8 +870,8 @@ export default defineComponent({
|
|||
this.audioTracks = []
|
||||
this.dashSrc = await this.createInvidiousDashManifest()
|
||||
|
||||
if (process.env.IS_ELECTRON && this.audioTracks.length > 0) {
|
||||
// when we are in Electron and the video has multiple audio tracks,
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.audioTracks.length > 0) {
|
||||
// when the local API is supported and the video has multiple audio tracks,
|
||||
// we populate the list inside createInvidiousDashManifest
|
||||
// as we need to work out the different audio tracks for the DASH manifest anyway
|
||||
this.audioSourceList = this.audioTracks.find(track => track.isDefault).sourceList
|
||||
|
@ -914,7 +908,7 @@ export default defineComponent({
|
|||
copyToClipboard(err.responseText)
|
||||
})
|
||||
console.error(err)
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getVideoInformationLocal()
|
||||
} else {
|
||||
|
@ -1239,7 +1233,7 @@ export default defineComponent({
|
|||
copyToClipboard(err)
|
||||
})
|
||||
console.error(err)
|
||||
if (!process.env.IS_ELECTRON || (this.backendPreference === 'local' && this.backendFallback)) {
|
||||
if (!process.env.SUPPORTS_LOCAL_API || (this.backendPreference === 'local' && this.backendFallback)) {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getVideoInformationInvidious()
|
||||
}
|
||||
|
@ -1400,9 +1394,7 @@ export default defineComponent({
|
|||
this.playNextCountDownIntervalId = setInterval(showCountDownMessage, 1000)
|
||||
},
|
||||
|
||||
handleRouteChange: async function (videoId) {
|
||||
// if the user navigates to another video, the ipc call for the userdata path
|
||||
// takes long enough for the video id to have already changed to the new one
|
||||
handleRouteChange: function (videoId) {
|
||||
// receiving it as an arg instead of accessing it ourselves means we always have the right one
|
||||
|
||||
clearTimeout(this.playNextTimeout)
|
||||
|
@ -1433,21 +1425,9 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
if (process.env.IS_ELECTRON && this.removeVideoMetaFiles) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const vttFileLocation = `static/storyboards/${videoId}.vtt`
|
||||
// only delete the file it actually exists
|
||||
if (await pathExists(vttFileLocation)) {
|
||||
await fs.rm(vttFileLocation)
|
||||
}
|
||||
} else {
|
||||
const userData = await getUserDataPath()
|
||||
const vttFileLocation = `${userData}/storyboards/${videoId}.vtt`
|
||||
|
||||
if (await pathExists(vttFileLocation)) {
|
||||
await fs.rm(vttFileLocation)
|
||||
}
|
||||
}
|
||||
if (this.videoStoryboardSrc.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.videoStoryboardSrc)
|
||||
this.videoStoryboardSrc = ''
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1491,7 +1471,7 @@ export default defineComponent({
|
|||
// If we are in Electron,
|
||||
// we can use YouTube.js' DASH manifest generator to generate the manifest.
|
||||
// Using YouTube.js' gives us support for multiple audio tracks (currently not supported by Invidious)
|
||||
if (process.env.IS_ELECTRON) {
|
||||
if (process.env.SUPPORTS_LOCAL_API) {
|
||||
// Invidious' API response doesn't include the height and width (and fps and qualityLabel for AV1) of video streams
|
||||
// so we need to extract them from Invidious' manifest
|
||||
const response = await fetch(url)
|
||||
|
@ -1612,36 +1592,14 @@ export default defineComponent({
|
|||
})
|
||||
},
|
||||
|
||||
createLocalStoryboardUrls: async function (storyboardInfo) {
|
||||
createLocalStoryboardUrls: function (storyboardInfo) {
|
||||
const results = buildVTTFileLocally(storyboardInfo, this.videoLengthSeconds)
|
||||
const userData = await getUserDataPath()
|
||||
let fileLocation
|
||||
let uriSchema
|
||||
|
||||
// Dev mode doesn't have access to the file:// schema, so we access
|
||||
// storyboards differently when run in dev
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
fileLocation = `static/storyboards/${this.videoId}.vtt`
|
||||
uriSchema = `storyboards/${this.videoId}.vtt`
|
||||
// if the location does not exist, writeFile will not create the directory, so we have to do that manually
|
||||
if (!(await pathExists('static/storyboards/'))) {
|
||||
fs.mkdir('static/storyboards/')
|
||||
} else if (await pathExists(fileLocation)) {
|
||||
await fs.rm(fileLocation)
|
||||
}
|
||||
// after the player migration, switch to using a data URI, as those don't need to be revoked
|
||||
|
||||
await fs.writeFile(fileLocation, results)
|
||||
} else {
|
||||
if (!(await pathExists(`${userData}/storyboards/`))) {
|
||||
await fs.mkdir(`${userData}/storyboards/`)
|
||||
}
|
||||
fileLocation = `${userData}/storyboards/${this.videoId}.vtt`
|
||||
uriSchema = `file://${fileLocation}`
|
||||
const blob = new Blob([results], { type: 'text/vtt;charset=UTF-8' })
|
||||
|
||||
await fs.writeFile(fileLocation, results)
|
||||
}
|
||||
|
||||
this.videoStoryboardSrc = uriSchema
|
||||
this.videoStoryboardSrc = URL.createObjectURL(blob)
|
||||
},
|
||||
|
||||
tryAddingTranslatedLocaleCaption: function (captionTracks, locale, baseUrl) {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
[
|
||||
{
|
||||
"name": "None",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.None.Name",
|
||||
"value": "",
|
||||
"cmdArguments": null
|
||||
},
|
||||
{
|
||||
"name": "mpv",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.mpv.Name",
|
||||
"value": "mpv",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "mpv",
|
||||
|
@ -25,7 +23,6 @@
|
|||
},
|
||||
{
|
||||
"name": "VLC",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.VLC.Name",
|
||||
"value": "vlc",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "vlc",
|
||||
|
@ -43,7 +40,6 @@
|
|||
},
|
||||
{
|
||||
"name": "iina",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.iina.Name",
|
||||
"value": "iina",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "iina",
|
||||
|
@ -61,7 +57,6 @@
|
|||
},
|
||||
{
|
||||
"name": "SMPlayer",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.SMPlayer.Name",
|
||||
"value": "smplayer",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "smplayer",
|
||||
|
@ -79,7 +74,6 @@
|
|||
},
|
||||
{
|
||||
"name": "MPC-BE",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.MPC-BE.Name",
|
||||
"value": "mpc-be",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "mpc-be64",
|
||||
|
@ -97,7 +91,6 @@
|
|||
},
|
||||
{
|
||||
"name": "MPC-HC",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.MPC-HC.Name",
|
||||
"value": "mpc-hc",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "mpc-hc64",
|
||||
|
@ -115,7 +108,6 @@
|
|||
},
|
||||
{
|
||||
"name": "PotPlayer",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.PotPlayer.Name",
|
||||
"value": "potplayer",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "potplayermini64",
|
||||
|
@ -133,7 +125,6 @@
|
|||
},
|
||||
{
|
||||
"name": "Clapper",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.Clapper.Name",
|
||||
"value": "clapper",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "clapper",
|
||||
|
@ -151,7 +142,6 @@
|
|||
},
|
||||
{
|
||||
"name": "Celluloid",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.Celluloid.Name",
|
||||
"value": "celluloid",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "celluloid",
|
||||
|
@ -169,7 +159,6 @@
|
|||
},
|
||||
{
|
||||
"name": "Haruna",
|
||||
"nameTranslationKey": "Settings.External Player Settings.Players.Haruna.Name",
|
||||
"value": "haruna",
|
||||
"cmdArguments": {
|
||||
"defaultExecutable": "haruna",
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.flokinet.to",
|
||||
"url": "https://invidious.nerdvpn.de",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://inv.bp.projectsegfau.lt",
|
||||
"url": "https://invidious.projectsegfau.lt",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
|
@ -20,11 +20,11 @@
|
|||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.io.lol",
|
||||
"url": "https://inv.tux.pizza",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://inv.tux.pizza",
|
||||
"url": "https://invidious.flokinet.to",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
|
@ -32,19 +32,11 @@
|
|||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://yt.artemislena.eu",
|
||||
"url": "https://iv.ggtyler.dev",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://vid.priv.au",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://onion.tube",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://yt.oelrichsgarcia.de",
|
||||
"url": "https://inv.nadeko.net",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
|
@ -52,29 +44,25 @@
|
|||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.asir.dev",
|
||||
"url": "https://iv.nboeck.de",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://iv.nboeck.de",
|
||||
"url": "https://yt.artemislena.eu",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.private.coffee",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://inv.n8pjl.ca",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://iv.datura.network",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://anontube.lvkaszus.pl",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://inv.us.projectsegfau.lt",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.perennialte.ch",
|
||||
"cors": true
|
||||
|
@ -83,16 +71,20 @@
|
|||
"url": "https://invidious.drgns.space",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.jing.rocks",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.einfachzocken.eu",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.slipfox.xyz",
|
||||
"url": "https://inv.oikei.net",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://invidious.no-logs.com",
|
||||
"url": "https://vid.lilay.dev",
|
||||
"cors": true
|
||||
},
|
||||
{
|
||||
|
@ -108,7 +100,7 @@
|
|||
"cors": true
|
||||
},
|
||||
{
|
||||
"url": "https://inv.citw.lgbt",
|
||||
"url": "https://invidious.privacyredirect.com",
|
||||
"cors": true
|
||||
}
|
||||
]
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'العربية'
|
||||
FreeTube: 'فري تيوب'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
هذا الجزء من التطبيق ليس جاهزاً بعد. عُد لاحقاً عندما يتم إحراز تقدم.
|
||||
|
@ -399,7 +398,6 @@ Settings:
|
|||
أنت متأكد أنك تريد إزالة جميع الاشتراكات والملفات الشخصية؟ لا يمكن التراجع عن
|
||||
هذا.
|
||||
Remove All Subscriptions / Profiles: إزالة جميع الاشتراكات \ الملفات الشخصية
|
||||
Automatically Remove Video Meta Files: إزالة ملفات تعريف الفيديو تلقائيًا
|
||||
Save Watched Videos With Last Viewed Playlist: حفظ مقاطع الفيديو التي تمت مشاهدتها
|
||||
مع آخر قائمة تشغيل تم عرضها
|
||||
All playlists have been removed: تمت إزالة جميع قوائم التشغيل
|
||||
|
@ -1068,9 +1066,6 @@ Tooltips:
|
|||
Allow DASH AV1 formats: قد تبدو تنسيقات DASH AV1 أفضل من تنسيقات DASH H.264. تتطلب
|
||||
تنسيقات DASH AV1 مزيدا من الطاقة للتشغيل! وهي غير متوفرة في جميع مقاطع الفيديو
|
||||
، وفي هذه الحالات سيستخدم المشغل تنسيقات DASH H.264 بدلا من ذلك.
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: عندما يمكن، يحذف Freetube تلقائيًا ملفات التعريف التي
|
||||
تم إنشاؤها أثناء تشغيل الفيديو ، عندما تكون صفحة المشاهدة مغلقة.
|
||||
Subscription Settings:
|
||||
Fetch Feeds from RSS: عند تفعيلها، سوف يستخدم فريتيوب طريقة RSS بدلًا من طريقته
|
||||
المعتادة لجلب صفحة اشتراكاتك. طريقة RSS أسرع وتتخطى حجب الآي بي IP، لكنها لا
|
||||
|
@ -1175,3 +1170,5 @@ Age Restricted:
|
|||
This channel is age restricted: هذه القناة مقيدة بالعمر
|
||||
This video is age restricted: هذا الفيديو مقيد بالفئة العمرية
|
||||
Close Banner: إغلاق الشعار
|
||||
checkmark: ✓
|
||||
Display Label: '{label}: {value}'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
FreeTube: 'ফ্ৰীটিউব'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
File: 'ফাইল'
|
||||
Quit: 'অন্ত কৰক'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Беларуская'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Гэтая частка праграмы яшчэ не гатова. Прыходзьце пазней.
|
||||
|
@ -307,7 +306,6 @@ Settings:
|
|||
Remember History: ''
|
||||
Save Watched Progress: ''
|
||||
Save Watched Videos With Last Viewed Playlist: ''
|
||||
Automatically Remove Video Meta Files: ''
|
||||
Clear Search Cache: ''
|
||||
Are you sure you want to clear out your search cache?: ''
|
||||
Search cache has been cleared: ''
|
||||
|
@ -802,8 +800,6 @@ Tooltips:
|
|||
Subscription Settings:
|
||||
Fetch Feeds from RSS: ''
|
||||
Fetch Automatically: ''
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: ''
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: ''
|
||||
SponsorBlock Settings:
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Български'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Тази част от приложението все още не е готова. Върнете се по-късно, когато може
|
||||
|
@ -44,6 +43,8 @@ Global:
|
|||
Subscriber Count: 1 абонат | {count} абоната
|
||||
View Count: 1 преглед | {count} прегледа
|
||||
Watching Count: 1 гледане | {count} гледания
|
||||
Input Tags:
|
||||
Length Requirement: Дължината на етикета трябва да бъде поне {number} знака
|
||||
Version {versionNumber} is now available! Click for more details: 'Версия {versionNumber}
|
||||
е вече налична! Щракнете за повече детайли'
|
||||
Download From Site: 'Сваляне от сайта'
|
||||
|
@ -127,7 +128,7 @@ User Playlists:
|
|||
изброени само видеа, които сте запазили или избрали за предпочитани. Когато работата
|
||||
приключи, всички видеа, които в момента са тук, ще бъдат преместени в плейлист
|
||||
„Предпочитани“.
|
||||
Search bar placeholder: Търсене в плейлиста
|
||||
Search bar placeholder: Търсене за плейлисти
|
||||
Empty Search Message: В този плейлист няма видеа, които да отговарят на търсенето
|
||||
ви
|
||||
This playlist currently has no videos.: В този плейлист в момента няма видеа.
|
||||
|
@ -166,6 +167,7 @@ User Playlists:
|
|||
This playlist is protected and cannot be removed.: Този плейлист е защитен и
|
||||
не може да бъде премахнат.
|
||||
This playlist does not exist: Този плейлист не съществува
|
||||
Search for Videos: Търсене за видеа
|
||||
CreatePlaylistPrompt:
|
||||
Toast:
|
||||
Playlist {playlistName} has been successfully created.: Плейлиста {playlistName}
|
||||
|
@ -207,6 +209,7 @@ User Playlists:
|
|||
към {playlistCount} плейлиста | Добавени {videoCount} видеа към {playlistCount}
|
||||
плейлиста
|
||||
Save: Запазване
|
||||
Added {count} Times: Добавено {count} път | Добавено {count} пъти
|
||||
Remove from Playlist: Премахване от плейлиста за изпълнение
|
||||
Playlist Name: Име на плейлиста
|
||||
Save Changes: Запазване на промените
|
||||
|
@ -218,6 +221,7 @@ User Playlists:
|
|||
Delete Playlist: Изтриване на плейлиста
|
||||
Remove from Favorites: Премахване от {playlistName}
|
||||
Move Video Down: Преместване надолу
|
||||
Playlists with Matching Videos: Плейлисти със съвпадащи видеа
|
||||
History:
|
||||
# On History Page
|
||||
History: 'История'
|
||||
|
@ -287,6 +291,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Пастелно розово
|
||||
Hot Pink: Горещо розово
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Основна цветова тема'
|
||||
Red: 'Червено'
|
||||
|
@ -409,7 +414,6 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Сигурни
|
||||
ли сте, че искате да премахнете всички абонаменти и профили? Това не може да
|
||||
бъде възстановено.'
|
||||
Automatically Remove Video Meta Files: Автоматично премахване на видео метафайловете
|
||||
Save Watched Videos With Last Viewed Playlist: Запазване на гледани видеа с последно
|
||||
гледан плейлист
|
||||
Remove All Playlists: Премахване на всички плейлисти
|
||||
|
@ -528,7 +532,7 @@ Settings:
|
|||
Hide Channels: Скриване видеата от каналите
|
||||
Hide Channels Placeholder: Идентификатор на канала
|
||||
Display Titles Without Excessive Capitalisation: Показване на заглавията без излишни
|
||||
главни букви
|
||||
главни букви и препинателни знаци
|
||||
Hide Featured Channels: Скриване на препоръчаните канали
|
||||
Hide Channel Playlists: Скриване на плейлистите на каналите
|
||||
Hide Channel Community: Скриване на общността на каналите
|
||||
|
@ -553,6 +557,10 @@ Settings:
|
|||
идентификатор. Моля, проверете отново дали идентификаторът е правилен.
|
||||
Hide Channels Invalid: Предоставеният идентификатор на канала е невалиден
|
||||
Hide Channels Already Exists: Идентификаторът на канала вече съществува
|
||||
Hide Videos and Playlists Containing Text: Скриване на видеа и плейлисти, съдържащи
|
||||
текст
|
||||
Hide Videos and Playlists Containing Text Placeholder: Дума, част от дума или
|
||||
фраза
|
||||
The app needs to restart for changes to take effect. Restart and apply change?: Приложението
|
||||
трябва да се рестартира за да се приложат промените. Рестартиране?
|
||||
Proxy Settings:
|
||||
|
@ -780,6 +788,7 @@ Channel:
|
|||
votes: '{votes} гласа'
|
||||
Reveal Answers: Разкриване на отговорите
|
||||
Hide Answers: Скриване на отговорите
|
||||
Video hidden by FreeTube: Видео, скрито от FreeTube
|
||||
This channel does not exist: Каналът не съществува
|
||||
This channel does not allow searching: Каналът не позволява търсене
|
||||
This channel is age-restricted and currently cannot be viewed in FreeTube.: Каналът
|
||||
|
@ -866,7 +875,7 @@ Video:
|
|||
Upcoming: 'Премиера на'
|
||||
Less than a minute: По-малко от минута
|
||||
In less than a minute: След по-малко от минута
|
||||
Published on: 'Публикуван на'
|
||||
Published on: 'Публикувано на'
|
||||
Publicationtemplate: 'Преди {number} {unit}'
|
||||
#& Videos
|
||||
Audio:
|
||||
|
@ -944,6 +953,7 @@ Video:
|
|||
Pause on Current Video: Пауза на текущото видео
|
||||
Unhide Channel: Показване на канала
|
||||
Hide Channel: Скриване на канала
|
||||
More Options: Още опции
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1087,10 +1097,6 @@ Tooltips:
|
|||
External Link Handling: "Избор на поведението по подразбиране, когато щракнете
|
||||
върху връзка, която не може да бъде отворена във FreeTube.\nПо подразбиране
|
||||
FreeTube ще отвори връзката в браузъра по подразбиране.\n"
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: Когато страницата за гледане бъде затворена, FreeTube
|
||||
автоматично ще изтрива метафайловете, създадени по време на възпроизвеждане
|
||||
на видеото.
|
||||
External Player Settings:
|
||||
Custom External Player Arguments: Всички персонализирани аргументи от командния
|
||||
ред, разделени с точка и запетая (";"), които искате да бъдат предадени на външния
|
||||
|
@ -1119,6 +1125,10 @@ Tooltips:
|
|||
и малки букви.
|
||||
Hide Subscriptions Live: Тази настройка се отменя от настройката за цялото приложение
|
||||
"{appWideSetting}" в секция "{subsection}" на "{settingsSection}"
|
||||
Hide Videos and Playlists Containing Text: Въведете дума, част от дума или фраза
|
||||
(без значение от главни и малки букви), за да скриете всички видеа и плейлисти,
|
||||
чиито оригинални заглавия ги съдържат в целия FreeTube, с изключение само на
|
||||
Историята, Вашите плейлисти и видеата в плейлистите.
|
||||
SponsorBlock Settings:
|
||||
UseDeArrowTitles: Заменя заглавията на видеата с подадени от потребителите заглавия
|
||||
от DeArrow.
|
||||
|
@ -1179,3 +1189,10 @@ Playlist will not pause when current video is finished: Плейлистът н
|
|||
Channel Hidden: '{channel} е добавен към филтъра за канали'
|
||||
Channel Unhidden: '{channel} е премахнат от филтъра за канали'
|
||||
Go to page: Отиване на {page}
|
||||
Tag already exists: Етикетът "{tagName}" вече съществува
|
||||
Trimmed input must be at least N characters long: Орязаният вход трябва да бъде дълъг
|
||||
поне 1 символ | Орязаният вход трябва да бъде дълъг поне {length} символа
|
||||
Age Restricted:
|
||||
This channel is age restricted: Този канал е с възрастово ограничение
|
||||
This video is age restricted: Това видео е с възрастово ограничение
|
||||
Close Banner: Затваряне на банер
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'বাংলা'
|
||||
FreeTube: 'ফ্রিটিউব'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
অ্যাপের এই অংশটি এখনও প্রস্তুত নয়। পরে ফিরে এসো যখন অগ্রগতি হয়েছে।
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Bosanski'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Ovaj dio aplikacije još nije spreman. Vratite se kasnije kad se postigne napredak.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'català'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Aquesta secció de l'aplicació encara no està llesta. Torna més endavant quan s'hagin
|
||||
|
@ -266,8 +265,6 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Esteu
|
||||
segur que voleu esborrar totes les subscripcions i perfils? Aquesta acció no
|
||||
es pot desfer.'
|
||||
Automatically Remove Video Meta Files: Suprimeix automàticament les metadades
|
||||
dels vídeos
|
||||
Subscription Settings:
|
||||
Subscription Settings: 'Configuració de les subscripcions'
|
||||
Hide Videos on Watch: 'Oculta els vídeos visualitzats'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'ئیگلیزی (وڵاتە یەکگرتووەکانی ئەمریکا)'
|
||||
FreeTube: 'فریتیوب'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
بەشێک لە نەرمەواڵەکە هێشتا ئامادە نییە. کە ڕەوتەکە درووست کرا دووبارە وەرەوە.
|
||||
|
@ -311,7 +310,6 @@ Settings:
|
|||
Remember History: ''
|
||||
Save Watched Progress: ''
|
||||
Save Watched Videos With Last Viewed Playlist: ''
|
||||
Automatically Remove Video Meta Files: ''
|
||||
Clear Search Cache: ''
|
||||
Are you sure you want to clear out your search cache?: ''
|
||||
Search cache has been cleared: ''
|
||||
|
@ -818,8 +816,6 @@ Tooltips:
|
|||
Subscription Settings:
|
||||
Fetch Feeds from RSS: ''
|
||||
Fetch Automatically: ''
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: ''
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: ''
|
||||
SponsorBlock Settings:
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Čeština'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Tato část aplikace ještě není hotova. Vraťte se později.
|
||||
|
@ -411,7 +410,6 @@ Settings:
|
|||
Remove All Subscriptions / Profiles: 'Odstranit všechny odběry / profily'
|
||||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Opravdu
|
||||
chcete odstranit všechny odběry a profily? Tato akce je nevratná.'
|
||||
Automatically Remove Video Meta Files: Automaticky odstranit meta soubory videa
|
||||
Save Watched Videos With Last Viewed Playlist: Uložit zhlédnutá videa s naposledy
|
||||
zobrazeným playlistem
|
||||
All playlists have been removed: Všechny playlisty byly odstraněny
|
||||
|
@ -1057,9 +1055,6 @@ Tooltips:
|
|||
# Toast Messages
|
||||
Fetch Automatically: Při povolení tohoto nastavení bude FreeTube automaticky načítat
|
||||
vaše odběry při otevření nového okna a při přepínání profilů.
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: Pokud je povoleno, FreeTube automaticky odstraní meta
|
||||
soubory vytvořené během přehrávání videa, když se stránka sledování zavře.
|
||||
External Player Settings:
|
||||
Ignore Warnings: Potlačuje varování, kdy současný externí přehrávač nepodporuje
|
||||
aktuální akci (např. obrácení seznamů skladeb apod.).
|
||||
|
@ -1173,3 +1168,5 @@ Close Banner: Zavřít panel
|
|||
Age Restricted:
|
||||
This channel is age restricted: Tento kanál je omezen věkem
|
||||
This video is age restricted: Toto video je omezeno věkem
|
||||
checkmark: ✓
|
||||
Display Label: '{label}: {value}'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Cymraeg'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >
|
||||
|
||||
|
@ -314,7 +313,6 @@ Settings:
|
|||
Remember History: 'Cadw Hanes'
|
||||
Save Watched Progress: ''
|
||||
Save Watched Videos With Last Viewed Playlist: ''
|
||||
Automatically Remove Video Meta Files: ''
|
||||
Clear Search Cache: ''
|
||||
Are you sure you want to clear out your search cache?: ''
|
||||
Search cache has been cleared: ''
|
||||
|
@ -820,8 +818,6 @@ Tooltips:
|
|||
Subscription Settings:
|
||||
Fetch Feeds from RSS: ''
|
||||
Fetch Automatically: ''
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: ''
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: ''
|
||||
SponsorBlock Settings:
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'dansk'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Denne del af app'en er ikke klar endnu. Kom tilbage senere, når der er gjort fremskridt.
|
||||
|
@ -383,7 +382,6 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Er
|
||||
du sikker på, at du vil fjerne alle abonnementer og profiler? Dette kan ikke
|
||||
fortrydes.'
|
||||
Automatically Remove Video Meta Files: Fjern Automatisk Video Metafiler
|
||||
All playlists have been removed: Alle playlister blev fjernet
|
||||
Are you sure you want to remove all your playlists?: Er du sikker på, at du vil
|
||||
fjerne alle dine playlister?
|
||||
|
@ -1010,9 +1008,6 @@ Tooltips:
|
|||
Advarsel: Invidious-indstillinger har ingen effekt på eksterne afspillere.'
|
||||
Ignore Warnings: Undertryk advarsler for når den eksterne afspiller ikke understøtter
|
||||
den gældende handling (fx vende playlister om, etc.).
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: Når det er aktiveret, sletter FreeTube automatisk metafiler
|
||||
oprettet under videoafspilning, når siden lukkes.
|
||||
Distraction Free Settings:
|
||||
Hide Channels: Indtast et kanal-ID for at skjule alle videoer, playlister og selve
|
||||
kanalen fra at blive vist i søgning, trending, mest populære og anbefalet. Det
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
FreeTube: FreeTube
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Dieser Teil der App ist noch nicht bereit. Komme später wieder, wenn Fortschritte
|
||||
|
@ -461,7 +460,6 @@ Settings:
|
|||
du sicher, dass du alle Abos und Profile löschen möchtest? Diese Aktion kann
|
||||
nicht rückgängig gemacht werden.
|
||||
Remove All Subscriptions / Profiles: Alle Abos / Profile entfernen
|
||||
Automatically Remove Video Meta Files: Video-Metadateien automatisch entfernen
|
||||
Save Watched Videos With Last Viewed Playlist: Angesehene Videos mit der zuletzt
|
||||
angesehenen Wiedergabeliste speichern
|
||||
Remove All Playlists: Alle Wiedergabelisten entfernen
|
||||
|
@ -1140,10 +1138,6 @@ Tooltips:
|
|||
Die DASH AV1-Formate benötigen mehr Leistung für die Wiedergabe! Sie sind nicht
|
||||
bei allen Videos verfügbar. In diesen Fällen verwendet der Abspieler stattdessen
|
||||
die DASH H.264-Formate.
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: Wenn aktiviert, löscht FreeTube automatisch die während
|
||||
der Videowiedergabe erstellten Metadateien, wenn die Abspielseite geschlossen
|
||||
wird.
|
||||
External Player Settings:
|
||||
Custom External Player Arguments: Alle benutzerdefinierten Befehlszeilenargumente,
|
||||
getrennt durch Semikolon (';'), die an den externen Abspieler übergeben werden
|
||||
|
@ -1249,3 +1243,5 @@ Close Banner: Banner schließen
|
|||
Age Restricted:
|
||||
This channel is age restricted: Dieser Kanal ist altersbeschränkt
|
||||
This video is age restricted: Dieses Video ist altersbeschränkt
|
||||
Display Label: '{label}: {value}'
|
||||
checkmark: ✓
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Ελληνικά (EL)'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Αυτό το μέρος της εφαρμογής δεν είναι ακόμη έτοιμο. Επιστρέψτε αργότερα όταν έχει
|
||||
|
@ -323,7 +322,6 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Είστε
|
||||
βέβαιοι ότι θέλετε να καταργήσετε όλες τις συνδρομές και τα προφίλ; Αυτό δεν
|
||||
μπορεί να αναιρεθεί.'
|
||||
Automatically Remove Video Meta Files: Αυτόματη αφαίρεση μετα-αρχείων βίντεο
|
||||
Save Watched Videos With Last Viewed Playlist: Αποθήκευση Βίντεο που Παρακολουθήσατε
|
||||
Με τη Λίστα Αναπαραγωγής Τελευταίας Προβολής
|
||||
Subscription Settings:
|
||||
|
@ -1019,10 +1017,6 @@ Tooltips:
|
|||
σε έναν σύνδεσμο, ο οποίος δεν μπορεί να ανοίξει στο FreeTube.\nΑπό προεπιλογή,
|
||||
το FreeTube θα ανοίξει τον σύνδεσμο που έχει πατηθεί στο προεπιλεγμένο πρόγραμμα
|
||||
περιήγησης.\n"
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: Όταν είναι ενεργοποιημένο, το FreeTube διαγράφει αυτόματα
|
||||
τα μετα-αρχεία που δημιουργήθηκαν κατά την αναπαραγωγή βίντεο, όταν η σελίδα
|
||||
παρακολούθησης είναι κλειστή.
|
||||
External Player Settings:
|
||||
Custom External Player Executable: Από προεπιλογή, το FreeTube θα υποθέσει ότι
|
||||
το επιλεγμένο εξωτερικό πρόγραμμα αναπαραγωγής μπορεί να βρεθεί μέσω της μεταβλητής
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: English (US)
|
||||
FreeTube: FreeTube
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
This part of the app is not ready yet. Come back later when progress has been made.
|
||||
|
@ -413,7 +412,6 @@ Settings:
|
|||
Remember History: Remember History
|
||||
Save Watched Progress: Save Watched Progress
|
||||
Save Watched Videos With Last Viewed Playlist: Save Watched Videos With Last Viewed Playlist
|
||||
Automatically Remove Video Meta Files: Automatically Remove Video Meta Files
|
||||
Clear Search Cache: Clear Search Cache
|
||||
Are you sure you want to clear out your search cache?: Are you sure you want to
|
||||
clear out your search cache?
|
||||
|
@ -1003,9 +1001,6 @@ Tooltips:
|
|||
but doesn't provide certain information like video duration or live status
|
||||
Fetch Automatically: When enabled, FreeTube will automatically fetch
|
||||
your subscription feed when a new window is opened and when switching profile.
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: When enabled, FreeTube automatically deletes meta files created during video playback,
|
||||
when the watch page is closed.
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: Disables Electron's disk based HTTP cache and enables a custom in-memory image cache. Will lead to increased RAM usage.
|
||||
SponsorBlock Settings:
|
||||
|
@ -1060,3 +1055,7 @@ Hashtag:
|
|||
Yes: Yes
|
||||
No: No
|
||||
Ok: Ok
|
||||
# symbol used to indicate that an item is correct
|
||||
checkmark: ✓
|
||||
# French is the only language that should change this (they have a space before the colon)
|
||||
Display Label: '{label}: {value}'
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'English (UK)'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
This part of the app is not ready yet. Come back later when progress has been made.
|
||||
|
@ -417,7 +416,6 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Are
|
||||
you sure you want to remove all subscriptions and profiles? This cannot be
|
||||
undone.'
|
||||
Automatically Remove Video Meta Files: Automatically Remove Video Meta Files
|
||||
Save Watched Videos With Last Viewed Playlist: Save Watched Videos With Last Viewed
|
||||
Playlist
|
||||
Remove All Playlists: Remove all playlists
|
||||
|
@ -1084,9 +1082,6 @@ Tooltips:
|
|||
Custom External Player Arguments: Any custom command line arguments, separated
|
||||
by semicolons (';'), you want to be passed to the external player.
|
||||
DefaultCustomArgumentsTemplate: '(Default: ‘{defaultCustomArguments}’)'
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: When enabled, FreeTube automatically deletes meta files
|
||||
created during video playback, when the watch page is closed.
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: Disables Electron's disk-based HTTP cache and enables a custom
|
||||
in-memory image cache. Will lead to increased RAM usage.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'Esperanto'
|
||||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
File: 'Dosiero'
|
||||
Quit: 'Eliri'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
FreeTube: 'FreeTube'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
Esta parte de la aplicación aún no está lista. Vuelva más tarde cuando se haya avanzado.
|
||||
|
@ -290,7 +289,6 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: ¿Estás
|
||||
seguro de que deseas eliminar todos los suscripciones y perfiles? Esto no puede
|
||||
ser deshecho.
|
||||
Automatically Remove Video Meta Files: Borrar automáticamente metadatos del video
|
||||
Data Settings:
|
||||
How do I import my subscriptions?: ¿Cómo puedo importar mis suscripciones?
|
||||
Export History: Exportar Historia
|
||||
|
@ -796,9 +794,6 @@ Tooltips:
|
|||
presionada la tecla Ctrl (tecla Comando en Mac) y haga clic izquierdo para reestablecer
|
||||
la velocidad de reproducción predeterminada (que es de 1x, a menos que la haya
|
||||
cambiado en configuración).
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: Si se habilita, FreeTube borrará automáticamente los
|
||||
metadatos creados al abrir el video, una vez que la ventana se cierre.
|
||||
Subscription Settings:
|
||||
Fetch Feeds from RSS: Si se habilita, FreeTube usará RSS en lugar del método predeterminado
|
||||
para recibir videos de sus suscripciones. RSS es más rápido y previene que bloqueen
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue