+
+
Screenshots • How does it work? • Features • Building and testing • Contributing • Localization • Contact • Donate • License
+Website • Blog • Documentation • FAQ • Discussions
+
+
+
+
+
+
+
+### Automated Builds (Nightly / Weekly)
+Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/MarmadileManteater/FreeTubeCordova/actions/workflows/buildCordova.yml).
+
+The first build with a green check mark is the latest build. You will need to have a GitHub account to download these builds.
+
+## How to build and test
+### Commands for the APK
+```bash
+# 📦 Packs the project using `webpack.android.config.js`
+yarn pack:android
+# 🤖 Packs the botguard script
+yarn pack:botGuardScript:android
+# 🚧 for development
+yarn pack:android:dev
+```
+> [!NOTE]
+> These commands only build the assets necessary for the project located in `android/` to be built. In order to obtain a complete build, you will need to build the project located in `android/` with `gradle`.
+### Commands for the PWA
+```bash
+# 🐛 Debugs the project using `webpack.web.config.js`
+yarn dev:web
+# 📦 Packs the project using `webpack.web.config.js`
+yarn pack:web
+```
+
+### Commands for the PWA Docker Image
+```bash
+
+# 💨 Creates and runs the image locally. Add `--platform=linux/arm64` to docker build for ARM64 devices including Apple Silicon
+docker build -t freetubecordova . # Warning, might take a while on Apple Silicon
+docker run --name ftcordova -d -p 8080:80 freetubecordova
+
+# ⬇ Pulls the latest from the Github Container Registry (ghcr.io)
+docker pull ghcr.io/marmadilemanteater/freetubecordova:latest
+# 👟 Runs the image from ghcr.io
+docker run --name ftcordova -d -p 8080:80 ghcr.io/marmadilemanteater/freetubecordova:latest
+
+# 🏃 Runs the image from Docker Hub.
+docker run --name ftcordova -d -p 8080:80 owentruong/freetubecordova:latest
+
+# 🏃♂️ Runs the image from Docker Hub (ARM64)
+docker run --platform=linux/arm64 --name ftcordova -d -p 8080:80 owentruong/freetubecordova:latest-arm64
+```
+## Contributing
+
+**NOTICE: MOST CHANGES SHOULD PROBABLY BE MADE TO [UPSTREAM](https://www.github.com/freetubeapp/freetube) UNLESS DIRECTLY RELATED TO CORDOVA CODE OR WORKFLOWS.**
+
+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/MarmadileManteater/FreeTubeCordova/blob/development/CONTRIBUTING.md)
+before sending your pull request.
+
+
+## Localization
+
+
+
+
+If you'd like to localize FreeTube Android, please send submissions to [FreeTube's weblate](https://hosted.weblate.org/engage/free-tube/).
+
+## Contact
+If you ever have any questions, feel free to make an issue here on GitHub.
+
+## Upstream Donations
+If you enjoy using FreeTube Android, you're welcome to leave a donation using the following methods to support upstream development and maintenance.
+
+* Bitcoin Address: `1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS`
+
+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.
+
+## License
+[](https://www.gnu.org/licenses/agpl-3.0.html)
+
+FreeTube is Free Software: You can use, study share and improve it at your
+will. Specifically you can redistribute and/or modify it under the terms of the
+[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as
+published by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
diff --git a/_icons/.icon-set/background.png b/_icons/.icon-set/background.png
new file mode 100644
index 0000000..23a4f00
Binary files /dev/null and b/_icons/.icon-set/background.png differ
diff --git a/_icons/.icon-set/background_128x128.png b/_icons/.icon-set/background_128x128.png
new file mode 100644
index 0000000..5917ef3
Binary files /dev/null and b/_icons/.icon-set/background_128x128.png differ
diff --git a/_icons/.icon-set/background_16x16.png b/_icons/.icon-set/background_16x16.png
new file mode 100644
index 0000000..b9d2024
Binary files /dev/null and b/_icons/.icon-set/background_16x16.png differ
diff --git a/_icons/.icon-set/background_32x32.png b/_icons/.icon-set/background_32x32.png
new file mode 100644
index 0000000..bbb05af
Binary files /dev/null and b/_icons/.icon-set/background_32x32.png differ
diff --git a/_icons/.icon-set/background_48x48.png b/_icons/.icon-set/background_48x48.png
new file mode 100644
index 0000000..dda9e01
Binary files /dev/null and b/_icons/.icon-set/background_48x48.png differ
diff --git a/_icons/.icon-set/background_64x64.png b/_icons/.icon-set/background_64x64.png
new file mode 100644
index 0000000..c107c20
Binary files /dev/null and b/_icons/.icon-set/background_64x64.png differ
diff --git a/_icons/.icon-set/iconColor_256.png b/_icons/.icon-set/iconColor_256.png
new file mode 100644
index 0000000..758e4fd
Binary files /dev/null and b/_icons/.icon-set/iconColor_256.png differ
diff --git a/_icons/.icon-set/iconColor_256_padded.png b/_icons/.icon-set/iconColor_256_padded.png
new file mode 100644
index 0000000..fb5c4b8
Binary files /dev/null and b/_icons/.icon-set/iconColor_256_padded.png differ
diff --git a/_icons/.icon-set/icon_128x128.png b/_icons/.icon-set/icon_128x128.png
new file mode 100644
index 0000000..79a2f84
Binary files /dev/null and b/_icons/.icon-set/icon_128x128.png differ
diff --git a/_icons/.icon-set/icon_128x128_padded.png b/_icons/.icon-set/icon_128x128_padded.png
new file mode 100644
index 0000000..20631d1
Binary files /dev/null and b/_icons/.icon-set/icon_128x128_padded.png differ
diff --git a/_icons/.icon-set/icon_16x16.png b/_icons/.icon-set/icon_16x16.png
new file mode 100644
index 0000000..6a3daea
Binary files /dev/null and b/_icons/.icon-set/icon_16x16.png differ
diff --git a/_icons/.icon-set/icon_16x16_padded.png b/_icons/.icon-set/icon_16x16_padded.png
new file mode 100644
index 0000000..3049ea8
Binary files /dev/null and b/_icons/.icon-set/icon_16x16_padded.png differ
diff --git a/_icons/.icon-set/icon_32x32.png b/_icons/.icon-set/icon_32x32.png
new file mode 100644
index 0000000..374fb61
Binary files /dev/null and b/_icons/.icon-set/icon_32x32.png differ
diff --git a/_icons/.icon-set/icon_32x32_padded.png b/_icons/.icon-set/icon_32x32_padded.png
new file mode 100644
index 0000000..210ad05
Binary files /dev/null and b/_icons/.icon-set/icon_32x32_padded.png differ
diff --git a/_icons/.icon-set/icon_48x48.png b/_icons/.icon-set/icon_48x48.png
new file mode 100644
index 0000000..8ce9af1
Binary files /dev/null and b/_icons/.icon-set/icon_48x48.png differ
diff --git a/_icons/.icon-set/icon_48x48_padded.png b/_icons/.icon-set/icon_48x48_padded.png
new file mode 100644
index 0000000..8005764
Binary files /dev/null and b/_icons/.icon-set/icon_48x48_padded.png differ
diff --git a/_icons/.icon-set/icon_64x64.png b/_icons/.icon-set/icon_64x64.png
new file mode 100644
index 0000000..2419b7e
Binary files /dev/null and b/_icons/.icon-set/icon_64x64.png differ
diff --git a/_icons/.icon-set/icon_64x64_padded.png b/_icons/.icon-set/icon_64x64_padded.png
new file mode 100644
index 0000000..74f27c3
Binary files /dev/null and b/_icons/.icon-set/icon_64x64_padded.png differ
diff --git a/_icons/192x192.png b/_icons/192x192.png
new file mode 100644
index 0000000..fd10580
Binary files /dev/null and b/_icons/192x192.png differ
diff --git a/_icons/256x256.png b/_icons/256x256.png
new file mode 100644
index 0000000..1cfc859
Binary files /dev/null and b/_icons/256x256.png differ
diff --git a/_icons/512x512.png b/_icons/512x512.png
new file mode 100644
index 0000000..9f580c5
Binary files /dev/null and b/_icons/512x512.png differ
diff --git a/_icons/icon.icns b/_icons/icon.icns
new file mode 100644
index 0000000..7f40fce
Binary files /dev/null and b/_icons/icon.icns differ
diff --git a/_icons/icon.svg b/_icons/icon.svg
new file mode 100644
index 0000000..871bb00
--- /dev/null
+++ b/_icons/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconBlack.svg b/_icons/iconBlack.svg
new file mode 100644
index 0000000..25dc9aa
--- /dev/null
+++ b/_icons/iconBlack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconBlackSmall.svg b/_icons/iconBlackSmall.svg
new file mode 100644
index 0000000..cd569f1
--- /dev/null
+++ b/_icons/iconBlackSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconCatppuccinFrappeDarkSmall.svg b/_icons/iconCatppuccinFrappeDarkSmall.svg
new file mode 100644
index 0000000..c27e4b4
--- /dev/null
+++ b/_icons/iconCatppuccinFrappeDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconCatppuccinFrappeLightSmall.svg b/_icons/iconCatppuccinFrappeLightSmall.svg
new file mode 100644
index 0000000..d0a3892
--- /dev/null
+++ b/_icons/iconCatppuccinFrappeLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconCatppuccinMochaDarkSmall.svg b/_icons/iconCatppuccinMochaDarkSmall.svg
new file mode 100644
index 0000000..5607b26
--- /dev/null
+++ b/_icons/iconCatppuccinMochaDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconCatppuccinMochaLightSmall.svg b/_icons/iconCatppuccinMochaLightSmall.svg
new file mode 100644
index 0000000..2ae10e8
--- /dev/null
+++ b/_icons/iconCatppuccinMochaLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconColor.icns b/_icons/iconColor.icns
new file mode 100644
index 0000000..e4cc274
Binary files /dev/null and b/_icons/iconColor.icns differ
diff --git a/_icons/iconColor.ico b/_icons/iconColor.ico
new file mode 100644
index 0000000..e04db49
Binary files /dev/null and b/_icons/iconColor.ico differ
diff --git a/_icons/iconColor.png b/_icons/iconColor.png
new file mode 100644
index 0000000..407353a
Binary files /dev/null and b/_icons/iconColor.png differ
diff --git a/_icons/iconColor.svg b/_icons/iconColor.svg
new file mode 100644
index 0000000..013d30a
--- /dev/null
+++ b/_icons/iconColor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconColorSmall.svg b/_icons/iconColorSmall.svg
new file mode 100644
index 0000000..7babd8b
--- /dev/null
+++ b/_icons/iconColorSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconDraculaDarkSmall.svg b/_icons/iconDraculaDarkSmall.svg
new file mode 100644
index 0000000..a203848
--- /dev/null
+++ b/_icons/iconDraculaDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconDraculaLightSmall.svg b/_icons/iconDraculaLightSmall.svg
new file mode 100644
index 0000000..a02231b
--- /dev/null
+++ b/_icons/iconDraculaLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconGruvboxDarkSmall.svg b/_icons/iconGruvboxDarkSmall.svg
new file mode 100644
index 0000000..e6dd6ad
--- /dev/null
+++ b/_icons/iconGruvboxDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconGruvboxLightSmall.svg b/_icons/iconGruvboxLightSmall.svg
new file mode 100644
index 0000000..a08e5c3
--- /dev/null
+++ b/_icons/iconGruvboxLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconMac.icns b/_icons/iconMac.icns
new file mode 100644
index 0000000..c49e2e4
Binary files /dev/null and b/_icons/iconMac.icns differ
diff --git a/_icons/iconNordicLightSmall.svg b/_icons/iconNordicLightSmall.svg
new file mode 100644
index 0000000..b5dd535
--- /dev/null
+++ b/_icons/iconNordicLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconSolarizedDarkSmall.svg b/_icons/iconSolarizedDarkSmall.svg
new file mode 100644
index 0000000..46e685e
--- /dev/null
+++ b/_icons/iconSolarizedDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconSolarizedLightSmall.svg b/_icons/iconSolarizedLightSmall.svg
new file mode 100644
index 0000000..584b8b8
--- /dev/null
+++ b/_icons/iconSolarizedLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconWhite.svg b/_icons/iconWhite.svg
new file mode 100644
index 0000000..6267305
--- /dev/null
+++ b/_icons/iconWhite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/iconWhiteSmall.svg b/_icons/iconWhiteSmall.svg
new file mode 100644
index 0000000..ecc99d8
--- /dev/null
+++ b/_icons/iconWhiteSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/logoBlack.svg b/_icons/logoBlack.svg
new file mode 100644
index 0000000..08c0d9d
--- /dev/null
+++ b/_icons/logoBlack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/logoColor.svg b/_icons/logoColor.svg
new file mode 100644
index 0000000..09a2399
--- /dev/null
+++ b/_icons/logoColor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/logoWhite.svg b/_icons/logoWhite.svg
new file mode 100644
index 0000000..e67d2c4
--- /dev/null
+++ b/_icons/logoWhite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/mejs-controls.svg b/_icons/mejs-controls.svg
new file mode 100644
index 0000000..1cd47e8
--- /dev/null
+++ b/_icons/mejs-controls.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textBlack.svg b/_icons/textBlack.svg
new file mode 100644
index 0000000..5569986
--- /dev/null
+++ b/_icons/textBlack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textBlackSmall.svg b/_icons/textBlackSmall.svg
new file mode 100644
index 0000000..494a206
--- /dev/null
+++ b/_icons/textBlackSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textCatppuccinFrappeDarkSmall.svg b/_icons/textCatppuccinFrappeDarkSmall.svg
new file mode 100644
index 0000000..b67b41a
--- /dev/null
+++ b/_icons/textCatppuccinFrappeDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textCatppuccinFrappeLightSmall.svg b/_icons/textCatppuccinFrappeLightSmall.svg
new file mode 100644
index 0000000..47d0d38
--- /dev/null
+++ b/_icons/textCatppuccinFrappeLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textCatppuccinMochaDarkSmall.svg b/_icons/textCatppuccinMochaDarkSmall.svg
new file mode 100644
index 0000000..b9c2cfc
--- /dev/null
+++ b/_icons/textCatppuccinMochaDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textCatppuccinMochaLightSmall.svg b/_icons/textCatppuccinMochaLightSmall.svg
new file mode 100644
index 0000000..407fef3
--- /dev/null
+++ b/_icons/textCatppuccinMochaLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textColor.svg b/_icons/textColor.svg
new file mode 100644
index 0000000..0a02333
--- /dev/null
+++ b/_icons/textColor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textColorSmall.svg b/_icons/textColorSmall.svg
new file mode 100644
index 0000000..9390084
--- /dev/null
+++ b/_icons/textColorSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textDraculaDarkSmall.svg b/_icons/textDraculaDarkSmall.svg
new file mode 100644
index 0000000..a3c73e1
--- /dev/null
+++ b/_icons/textDraculaDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textDraculaLightSmall.svg b/_icons/textDraculaLightSmall.svg
new file mode 100644
index 0000000..1206d4d
--- /dev/null
+++ b/_icons/textDraculaLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textGruvboxDarkSmall.svg b/_icons/textGruvboxDarkSmall.svg
new file mode 100644
index 0000000..00e3080
--- /dev/null
+++ b/_icons/textGruvboxDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textGruvboxLightSmall.svg b/_icons/textGruvboxLightSmall.svg
new file mode 100644
index 0000000..434c116
--- /dev/null
+++ b/_icons/textGruvboxLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textNordicLightSmall.svg b/_icons/textNordicLightSmall.svg
new file mode 100644
index 0000000..b7b97ba
--- /dev/null
+++ b/_icons/textNordicLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textSolarizedDarkSmall.svg b/_icons/textSolarizedDarkSmall.svg
new file mode 100644
index 0000000..2d57d96
--- /dev/null
+++ b/_icons/textSolarizedDarkSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textSolarizedLightSmall.svg b/_icons/textSolarizedLightSmall.svg
new file mode 100644
index 0000000..b6906ee
--- /dev/null
+++ b/_icons/textSolarizedLightSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textWhite.svg b/_icons/textWhite.svg
new file mode 100644
index 0000000..a00bff6
--- /dev/null
+++ b/_icons/textWhite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_icons/textWhiteSmall.svg b/_icons/textWhiteSmall.svg
new file mode 100644
index 0000000..5cec3ef
--- /dev/null
+++ b/_icons/textWhiteSmall.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/_scripts/ProcessLocalesPlugin.js b/_scripts/ProcessLocalesPlugin.js
new file mode 100644
index 0000000..d08bb37
--- /dev/null
+++ b/_scripts/ProcessLocalesPlugin.js
@@ -0,0 +1,190 @@
+const { existsSync, readFileSync } = require('fs')
+const { readFile } = require('fs/promises')
+const { join } = require('path')
+const { brotliCompress, constants } = require('zlib')
+const { promisify } = require('util')
+const { load: loadYaml } = require('js-yaml')
+
+const brotliCompressAsync = promisify(brotliCompress)
+
+const PLUGIN_NAME = 'ProcessLocalesPlugin'
+
+class ProcessLocalesPlugin {
+ constructor(options = {}) {
+ this.compress = !!options.compress
+ this.hotReload = !!options.hotReload
+
+ if (typeof options.inputDir !== 'string') {
+ throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.')
+ } else if (!existsSync(options.inputDir)) {
+ throw new Error('ProcessLocalesPlugin: the specified input directory does not exist.')
+ }
+ this.inputDir = options.inputDir
+
+ if (typeof options.outputDir !== 'string') {
+ throw new Error('ProcessLocalesPlugin: no output directory `outputDir` specified.')
+ }
+ this.outputDir = options.outputDir
+
+ /** @type {Map| + {{ $t('Channel.About.Joined') }} + | +{{ formattedJoined }} | +
|---|---|
| + {{ $t('Video.Views') }} + | +{{ formattedViews }} | +
| + {{ $t('Global.Videos') }} + | +{{ formattedVideos }} | +
| + {{ $t('Channel.About.Location') }} + | +{{ location }} | +
+ {{ $tc('Global.Counts.Subscriber Count', subCount, { count: formattedSubCount }) }} +
++ {{ shelf.subtitle }} +
++ {{ $t('Settings.Experimental Settings.Warning') }} +
++ {{ '[' + $t('Channel.Community.Video hidden by FreeTube') + ']' }} +
++ {{ label }} +
++ {{ shortcut }} +
++ + {{ extraLabel }} + +
++ {{ profile.name }} +
++ {{ chapter.title }} +
++ {{ $t('Data Settings.Data Is Currently Stored In') }} +
++ {{ dataDirectory }} +
++ + {{ $t("Settings.Data Settings.How do I import my subscriptions?") }} + +
++ {{ $t('User Playlists.SinglePlaylistView.Toast["Playlist name cannot be empty. Please input a name."]') }} +
++ {{ $t('User Playlists.CreatePlaylistPrompt.Toast["There is already a playlist with this name. Please pick a different name."]') }} +
+
+
+ {{ $tc('User Playlists.AddVideoPrompt.N playlists selected', selectedPlaylistCount, { + playlistCount: selectedPlaylistCount, + }) }} +
++ {{ selectedText }} +
++ {{ selectedText }} +
++ {{ translatedProfileName(profile) }} +
++ {{ $t('Feed.Feed Last Updated', { feedName: title, date: lastRefreshTimestamp }) }} +
+
+
+ {{ $t('Video.Player.You appear to be offline') }}
+
+
+
+ {{ $t('Video.Player.Playback will resume automatically when your connection comes back') }}
+
+
+ {{ $t('Video.Player.Skipped segment', { segmentCategory: translatedCategory }) }} +
+${stat[0]}: ${stat[1]}
` + listContentHTML += content + }) + return listContentHTML + }, + + /** + * determines whether the jump to the previous or next chapter + * with the the keyboard shortcuts, should be done + * first it checks whether there are any chapters (the array is also empty if chapters are hidden) + * it also checks that the approprate combination was used ALT/OPTION on macOS and CTRL everywhere else + * @param {KeyboardEvent} event the keyboard event + * @param {string} direction the direction of the jump either previous or next + * @returns {boolean} + */ + canChapterJump: function (event, direction) { + const currentChapter = this.currentChapterIndex + return this.chapters.length > 0 && + (direction === 'previous' ? currentChapter > 0 : this.chapters.length - 1 !== currentChapter) && + ((process.platform !== 'darwin' && event.ctrlKey) || + (process.platform === 'darwin' && event.metaKey)) + }, + + playVideo(thenFunc = null) { + // It can be called in `setTimeout` & user can navigate to other pages before it runs + // Which makes `this.player` become `null` + if (this.player == null) { return } + + let promise = this.player.play() + if (typeof thenFunc === 'function') { + promise = promise.then(thenFunc) + } + promise + .catch(err => { + if (EXPECTED_PLAY_RELATED_ERROR_MESSAGES.some(msg => err.message.includes(msg))) { + // Ignoring expected exception + // console.debug('Ignoring expected error') + // console.debug(err) + return + } + + // 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) + }) + }) + }, + + // This function should always be at the bottom of this file + /** + * @param {KeyboardEvent} event + */ + keyboardShortcutHandler: function (event) { + if (document.activeElement.classList.contains('ft-input') || event.altKey) { + return + } + + // allow chapter jump keyboard shortcuts + if (event.ctrlKey && (process.platform === 'darwin' || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight'))) { + return + } + + // allow copying text + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') { + return + } + + if (this.player !== null) { + switch (event.key) { + case ' ': + case 'Spacebar': // older browsers might return spacebar instead of a space character + // Toggle Play/Pause + event.preventDefault() + this.togglePlayPause() + break + case 'J': + case 'j': + // Rewind by 2x the time-skip interval (in seconds) + event.preventDefault() + this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate() * 2) + break + case 'K': + case 'k': + // Toggle Play/Pause + event.preventDefault() + this.togglePlayPause() + break + case 'L': + case 'l': + // Fast-Forward by 2x the time-skip interval (in seconds) + event.preventDefault() + this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate() * 2) + break + case 'O': + case 'o': + // Decrease playback rate by 0.25x + event.preventDefault() + this.changePlayBackRate(-this.videoPlaybackRateInterval) + break + case 'P': + case 'p': + // Increase playback rate by 0.25x + event.preventDefault() + this.changePlayBackRate(this.videoPlaybackRateInterval) + break + case 'F': + case 'f': + // Toggle Fullscreen Playback + event.preventDefault() + this.toggleFullscreen() + break + case 'M': + case 'm': + // Toggle Mute + if (!event.metaKey) { + event.preventDefault() + this.toggleMute() + } + break + case 'C': + case 'c': + // Toggle Captions + event.preventDefault() + this.toggleCaptions() + break + case 'ArrowUp': + // Increase volume + event.preventDefault() + this.changeVolume(0.05) + break + case 'ArrowDown': + // Decrease Volume + event.preventDefault() + this.changeVolume(-0.05) + break + case 'ArrowLeft': + event.preventDefault() + if (this.canChapterJump(event, 'previous')) { + // Jump to the previous chapter + this.player.currentTime(this.chapters[this.currentChapterIndex - 1].startSeconds) + } else { + // Rewind by the time-skip interval (in seconds) + this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate()) + } + break + case 'ArrowRight': + event.preventDefault() + if (this.canChapterJump(event, 'next')) { + // Jump to the next chapter + this.player.currentTime(this.chapters[this.currentChapterIndex + 1].startSeconds) + } else { + // Fast-Forward by the time-skip interval (in seconds) + this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate()) + } + break + case 'I': + case 'i': + event.preventDefault() + // Toggle Picture in Picture Mode + if (this.format !== 'audio' && !this.player.isInPictureInPicture()) { + this.player.requestPictureInPicture() + } else if (this.player.isInPictureInPicture()) { + this.player.exitPictureInPicture() + } + break + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + // Jump to percentage in the video + event.preventDefault() + + const percentage = parseInt(event.key) / 10 + const duration = this.player.duration() + const newTime = duration * percentage + + this.player.currentTime(newTime) + break + } + case ',': + // Return to previous frame + this.framebyframe(-1) + break + case '.': + // Advance to next frame + this.framebyframe(1) + break + case 'D': + case 'd': + event.preventDefault() + this.toggleShowStatsModal() + break + case 'Escape': + // Exit full window + event.preventDefault() + this.exitFullWindow() + break + case 'S': + case 's': + // Toggle Full Window Mode + this.toggleFullWindow() + break + case 'T': + case 't': + // Toggle Theatre Mode + this.toggleTheatreMode() + break + case 'U': + case 'u': + // Take screenshot + this.takeScreenshot() + break + } + } + }, + + stopPowerSaveBlocker: function () { + if (process.env.IS_ELECTRON && this.powerSaveBlocker !== null) { + const { ipcRenderer } = require('electron') + ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker) + this.powerSaveBlocker = null + } + }, + ...mapMutations([ + 'setUsingTouch' + ]), + ...mapActions([ + 'updateDefaultCaptionSettings', + 'parseScreenshotCustomFileName', + 'updateScreenshotFolderPath', + ]) + } +}) diff --git a/src/renderer/components/fta-log-viewer/fta-log-viewer.css b/src/renderer/components/fta-log-viewer/fta-log-viewer.css new file mode 100644 index 0000000..3cfc0b2 --- /dev/null +++ b/src/renderer/components/fta-log-viewer/fta-log-viewer.css @@ -0,0 +1,93 @@ + +.level { + padding-inline: 5px; +} + +.logs { + max-block-size: calc(100vh - 164px); + overflow-y: scroll; + display: flex; + flex-direction: column-reverse; +} + +.logs-wrapper { + display: flex; + flex-direction: column; +} + +.logs-wrapper .actions-container { + padding-block-start: 12px; +} + +.logs > .log, +.logs > .warning, +.logs > .error { + padding: 10px; +} + +.logs > .log { + background-color: var(--card-bg-color); + border-block-end: 2px solid var(--secondary-card-bg-color); +} + +.logs > .warning { + background-color: #332b00; + color: #ee9836; + border-block-end: 1px solid #ee9836; +} + +.logs > .error { + background-color: #290000; + color: #e46962; + border-block-end: 1px solid #e46962; +} + +[data-theme="light"] .logs > .warning { + background-color: #fef6d5; + color: #3e2f00; + border-block-end: 2px solid var(--card-bg-color); +} + +.content { + overflow-wrap: break-word; +} + +.source { + display: block; + direction: rtl; + overflow: hidden; + text-wrap: nowrap; + text-overflow: ellipsis; +} + +.log .source { + font-weight: bold; +} + +.timestamp { + text-align: end; + display: block; +} + +.error .source, .warning .source { + color: #fff; +} + +[data-theme="light"] .source, +[data-theme="light"] .error { + color: #000; +} + +[data-theme="light"] .logs > .error { + background-color: #fcebeb; + color: #410e0b; + border-block-end: 2px solid var(--card-bg-color); +} + + +@media (width <= 420px) { + .logs-wrapper .actions-container { + padding: 23px; + flex-grow: 1; + } +} diff --git a/src/renderer/components/fta-log-viewer/fta-log-viewer.js b/src/renderer/components/fta-log-viewer/fta-log-viewer.js new file mode 100644 index 0000000..d4a61cd --- /dev/null +++ b/src/renderer/components/fta-log-viewer/fta-log-viewer.js @@ -0,0 +1,129 @@ +import { defineComponent } from 'vue' +import { mapActions } from 'vuex' +import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' +import FtPrompt from '../FtPrompt/FtPrompt.vue' +import FtButton from '../ft-button/ft-button.vue' +import { getConsoleLogs, isColourDark } from '../../helpers/android' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' + +export default defineComponent({ + name: 'FtaLogViewer', + components: { + 'ft-flex-box': FtFlexBox, + 'ft-prompt': FtPrompt, + 'ft-button': FtButton, + 'font-awesome-icon': FontAwesomeIcon + }, + props: { + logLimit: { + type: Number, + default: 50 + } + }, + data() { + return { + usingAndroid: process.env.IS_ANDROID, + usingRelease: process.env.IS_RELEASE, + theme: this.getThemeFromBody(), + logs: [] + } + }, + computed: { + baseTheme() { + return this.$store.getters.getBaseTheme + }, + shown() { + return this.$store.getters.getShowLogViewer + }, + hidden() { + return !this.$store.getters.getShowLogViewer + }, + logsReversed() { + const result = [] + for (let i = this.logs.length - 1; i >= 0; i--) { + result.push(this.logs[i]) + } + return result + } + }, + watch: { + baseTheme() { + this.theme = this.getThemeFromBody() + } + }, + mounted() { + window.addEventListener('enabled-light-mode', this.onLightModeEnabled) + window.addEventListener('enabled-dark-mode', this.onDarkModeEnabled) + // when mounted, backfill the logs so far + this.logs.push(...getConsoleLogs()) + window.addEventListener('console-message', this.onConsoleMessage) + }, + beforeDestroy() { + window.removeEventListener('enabled-light-mode', this.onLightModeEnabled) + window.removeEventListener('enabled-dark-mode', this.onDarkModeEnabled) + this.logs = [] + window.removeEventListener('console-message', this.onConsoleMessage) + }, + methods: { + getFaIconFromLevel(level) { + switch (level) { + case 'WARNING': + return 'triangle-exclamation' + case 'ERROR': + return 'circle-xmark' + default: + return null + } + }, + removeQueryString(path) { + if (path.indexOf('?') !== -1) { + return path.split('?')[0] + } else { + return path + } + }, + onLightModeEnabled() { + if (this.$store.getters.getBaseTheme === 'system') { + this.theme = 'light' + } + }, + onDarkModeEnabled() { + if (this.$store.getters.getBaseTheme === 'system') { + this.theme = 'dark' + } + }, + getThemeFromBody() { + const bodyStyle = getComputedStyle(document.body) + const text = bodyStyle.getPropertyValue('--primary-text-color') + const isDark = isColourDark(text) + return isDark ? 'dark' : 'light' + }, + onConsoleMessage({ data }) { + if ('content' in data && data.content !== null) { + if (data.content.indexOf('found in') === -1 && data.content.indexOf('--->+ {{ $t('Settings.General Settings.The currently set default instance is {instance}', { instance: defaultInvidiousInstance }) }} +
+ ++ {{ $t('Settings.General Settings.No default instance has been set') }} +
++ {{ $t('Settings.General Settings.Current instance will be randomized on startup') }} +
+ ++ {{ $t('Settings.Player Settings.Screenshot.Folder Label') }} +
+
+ {{ $t('Settings.Player Settings.Screenshot.File Name Label') }}
+
+ {{ $t('User Playlists.SinglePlaylistView.Toast["Playlist name cannot be empty. Please input a name."]') }} +
++ {{ $t('User Playlists.CreatePlaylistPrompt.Toast["There is already a playlist with this name. Please pick a different name."]') }} +
++ {{ $tc('Global.Counts.Video Count', videoCount, {count: parsedVideoCount}) }} + + - {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }} + + - + + {{ $t("Playlist.Last Updated On") }} + + {{ lastUpdated }} +
+ +
+
+ {{ $t('Settings.Proxy Settings.Clicking on Test Proxy will send a request to') }} {{ proxyTestUrl }} +
++ {{ $t('Display Label', { label: $t('Settings.Proxy Settings.Ip'), value: proxyIp }) }} +
++ {{ $t('Display Label', { label: $t('Settings.Proxy Settings.Country'), value: proxyCountry }) }} +
++ {{ $t('Display Label', { label: $t('Settings.Proxy Settings.Region'), value: proxyRegion }) }} +
++ {{ $t('Display Label', { label: $t('Settings.Proxy Settings.City'), value: proxyCity }) }} +
++ + {{ comment.superChat.amount }} + +
++ {{ superChat.author.name }} +
++ {{ superChat.superChat.amount }} +
++ {{ comment.author.name }} +
++ {{ comment.superChat.amount }} +
+
+
+ {{ comment.author.name }}
+
+
+
+
+
+
+
+ {{ $t("Comments.Pinned by") }} {{ channelName }}
+
++
+ {{ comment.author }}
+
+
+
+
+ {{ comment.time }}
+
+
++ +
+ {{ comment.likes }}
+
+
+
+
+
+
+
+ {{ $t("Comments.View") }}
+ {{ $t("Comments.Hide") }}
+ {{ comment.numReplies }}
+ {{ $t("Comments.Reply").toLowerCase() }}
+ {{ $t("Comments.Replies").toLowerCase() }}
+ {{ $t("Comments.From {channelName}", { channelName }) }}
+ {{ $t("Comments.And others") }}
+
+
++
+ {{ reply.author }}
+
+
+
+
+ {{ reply.time }}
+
+
++ +
+ {{ reply.likes }}
+
+
++ {{ $t('Comments.View {replyCount} replies', { replyCount: reply.numReplies }) }} +
+