Merge branch 'development' into add-command-line-arg-to-search

This commit is contained in:
ChunkyProgrammer 2024-04-12 09:12:21 -04:00
commit 11973f9597
150 changed files with 1331 additions and 1074 deletions

View File

@ -4,8 +4,8 @@
"@babel/env",
{
"targets": {
"chrome": "106",
"node": "16.16.0"
"chrome": "122",
"node": "20.9.0"
}
}
]

View File

@ -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'
}
}
}

View File

@ -92,7 +92,7 @@ body:
- Portable
- .rpm
- .zip
- .apk (Android, FreeTubeCordova Unofficial)
- .apk (FreeTubeAndroid Unofficial)
- AUR (Unofficial)
- Chocolatey (Unofficial)
- Homebrew (Unofficial)

View File

@ -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

View File

@ -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)'

View File

@ -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/"]'

View File

@ -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

View File

@ -12,7 +12,7 @@ jobs:
build:
strategy:
matrix:
node-version: [18.x]
node-version: [20.x]
runtime:
- linux-x64
- linux-armv7l

View File

@ -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

View File

@ -12,7 +12,7 @@ jobs:
build:
strategy:
matrix:
node-version: [18.x]
node-version: [20.x]
runtime:
- linux-x64
- linux-armv7l

View File

@ -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> &bull; <a href="https://blog.freetubeapp.io/">Blog</a> &bull; <a href="https://docs.freetubeapp.io/">Documentation</a> &bull; <a href="https://docs.freetubeapp.io/faq/">FAQ</a> &bull; <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)

View File

@ -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/**'],
},
},
]

View File

@ -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({

View File

@ -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({

View File

@ -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",

View File

@ -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 = {

View File

@ -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>

View File

@ -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
// *********** //

View File

@ -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'))
}
}
})

View File

@ -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 []

View File

@ -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')
}
}
})

View File

@ -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

View File

@ -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')
}
}
})

View File

@ -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)
})

View File

@ -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;

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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()
},

View File

@ -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
}

View File

@ -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
},

View File

@ -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">

View File

@ -13,10 +13,7 @@
@change="$emit('change', currentValue)"
>
<span>
{{ label }}:
<span>
{{ displayLabel }}
</span>
{{ $t('Display Label', {label: label, value: displayLabel}) }}
</span>
</label>
</template>

View File

@ -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 ''
}
}
},

View File

@ -4,7 +4,7 @@
:id="sanitizedId"
class="sponsorTitle"
>
{{ $t("Video.Sponsor Block category." + categoryName) }}
{{ translatedCategoryName }}
</div>
<ft-select
:sanitized-id="sanitizedId + 'categoryColor'"

View File

@ -63,7 +63,7 @@
<div
class="initial"
>
{{ isProfileSubscribed(profile) ? '✓' : profileInitials[index] }}
{{ isProfileSubscribed(profile) ? $t('checkmark') : profileInitials[index] }}
</div>
</div>
<p

View File

@ -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)

View File

@ -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 () {

View File

@ -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
})
},

View File

@ -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',

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 () {

View File

@ -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'
)

View File

@ -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) {

View File

@ -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')"

View File

@ -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 {

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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]
}

View File

@ -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)
}
}
/**

View File

@ -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,

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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,

View File

@ -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
},

View File

@ -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 {

View File

@ -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
})

View File

@ -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,

View File

@ -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', {

View File

@ -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
},

View File

@ -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
},

View File

@ -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;

View File

@ -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 {

View File

@ -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"
/>

View File

@ -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)

View File

@ -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
})

View File

@ -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 {

View File

@ -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: '',

View File

@ -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 {

View File

@ -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
},

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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",

View File

@ -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
}
]

View File

@ -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}'

View File

@ -1,4 +1,3 @@
FreeTube: 'ফ্ৰীটিউব'
# Currently on Subscriptions, Playlists, and History
File: 'ফাইল'
Quit: 'অন্ত কৰক'

View File

@ -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:

View File

@ -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: Затваряне на банер

View File

@ -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.': >-
অ্যাপের এই অংশটি এখনও প্রস্তুত নয়। পরে ফিরে এসো যখন অগ্রগতি হয়েছে।

View File

@ -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.

View File

@ -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'

View File

@ -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:

View File

@ -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}'

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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 θα υποθέσει ότι
το επιλεγμένο εξωτερικό πρόγραμμα αναπαραγωγής μπορεί να βρεθεί μέσω της μεταβλητής

View File

@ -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}'

View File

@ -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.

View File

@ -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'

View File

@ -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