Compare commits

...

236 Commits

Author SHA1 Message Date
dependabot[bot]
c3941f219c Bump shaka-player from 4.16.9 to 4.16.10 (#8345)
Bumps [shaka-player](https://github.com/shaka-project/shaka-player) from 4.16.9 to 4.16.10.
- [Release notes](https://github.com/shaka-project/shaka-player/releases)
- [Changelog](https://github.com/shaka-project/shaka-player/blob/v4.16.10/CHANGELOG.md)
- [Commits](https://github.com/shaka-project/shaka-player/compare/v4.16.9...v4.16.10)

---
updated-dependencies:
- dependency-name: shaka-player
  dependency-version: 4.16.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 23:16:53 +01:00
dependabot[bot]
f87892c7bf Bump the stylelint group with 2 updates (#8334)
Bumps the stylelint group with 2 updates: [@double-great/stylelint-a11y](https://github.com/double-great/stylelint-a11y) and [stylelint](https://github.com/stylelint/stylelint).


Updates `@double-great/stylelint-a11y` from 3.4.0 to 3.4.1
- [Release notes](https://github.com/double-great/stylelint-a11y/releases)
- [Changelog](https://github.com/double-great/stylelint-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/double-great/stylelint-a11y/compare/v3.4.0...v3.4.1)

Updates `stylelint` from 16.25.0 to 16.26.0
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/16.25.0...16.26.0)

---
updated-dependencies:
- dependency-name: "@double-great/stylelint-a11y"
  dependency-version: 3.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: stylelint
- dependency-name: stylelint
  dependency-version: 16.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: stylelint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 23:16:11 +01:00
dependabot[bot]
905c308ff5 Bump the eslint group with 2 updates (#8333)
Bumps the eslint group with 2 updates: [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue).


Updates `eslint-plugin-jsdoc` from 61.2.1 to 61.4.1
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v61.2.1...v61.4.1)

Updates `eslint-plugin-vue` from 10.5.1 to 10.6.0
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Changelog](https://github.com/vuejs/eslint-plugin-vue/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v10.5.1...v10.6.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 61.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint-plugin-vue
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 22:45:04 +01:00
dependabot[bot]
a3693ecb1e Bump vue from 3.5.24 to 3.5.25 (#8336)
Bumps [vue](https://github.com/vuejs/core) from 3.5.24 to 3.5.25.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.24...v3.5.25)

---
updated-dependencies:
- dependency-name: vue
  dependency-version: 3.5.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 22:42:43 +01:00
dependabot[bot]
5de006bca5 Bump electron-builder from 26.2.0 to 26.3.0 (#8341)
Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 26.2.0 to 26.3.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.3.0/packages/electron-builder)

---
updated-dependencies:
- dependency-name: electron-builder
  dependency-version: 26.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 19:45:19 +01:00
dependabot[bot]
52cb9b7f94 Bump marked from 17.0.0 to 17.0.1 (#8339)
Bumps [marked](https://github.com/markedjs/marked) from 17.0.0 to 17.0.1.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v17.0.0...v17.0.1)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 17.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 19:29:01 +01:00
dependabot[bot]
c4dd47bedd Bump webpack from 5.102.1 to 5.103.0 (#8340)
Bumps [webpack](https://github.com/webpack/webpack) from 5.102.1 to 5.103.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.102.1...v5.103.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.103.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 19:23:11 +01:00
Hosted Weblate
b12c3527c5 Merge branch 'origin/development' into Weblate. 2025-11-24 18:51:27 +01:00
Andi Chandler
fb78cf5240 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Andi Chandler <andi@gowling.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/en_GB/
Translation: FreeTube/Translations
2025-11-24 18:51:18 +01:00
dependabot[bot]
863eb9e760 Bump html-webpack-plugin from 5.6.4 to 5.6.5 (#8342)
Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 5.6.4 to 5.6.5.
- [Release notes](https://github.com/jantimon/html-webpack-plugin/releases)
- [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v5.6.4...v5.6.5)

---
updated-dependencies:
- dependency-name: html-webpack-plugin
  dependency-version: 5.6.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 17:25:28 +01:00
dependabot[bot]
654e24ec4c Bump mikefarah/yq from 4.48.1 to 4.49.1 (#8343)
Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.48.1 to 4.49.1.
- [Release notes](https://github.com/mikefarah/yq/releases)
- [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt)
- [Commits](https://github.com/mikefarah/yq/compare/v4.48.1...v4.49.1)

---
updated-dependencies:
- dependency-name: mikefarah/yq
  dependency-version: 4.49.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 17:25:06 +01:00
dependabot[bot]
92cac18c39 Bump actions/checkout from 5 to 6 (#8344)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 17:24:49 +01:00
dependabot[bot]
8847b35e81 Bump electron from 39.2.1 to 39.2.3 (#8337)
Bumps [electron](https://github.com/electron/electron) from 39.2.1 to 39.2.3.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v39.2.1...v39.2.3)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 39.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 17:18:16 +01:00
dependabot[bot]
2354fb0a88 Bump sass from 1.94.0 to 1.94.2 (#8335)
Bumps [sass](https://github.com/sass/dart-sass) from 1.94.0 to 1.94.2.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.94.0...1.94.2)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.94.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 15:38:18 +00:00
daangamz
585d63f543 Translated using Weblate (Dutch)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: daangamz <daandenhartog@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/nl/
Translation: FreeTube/Translations
2025-11-24 13:51:19 +00:00
Jeff Huang
b59323dacc Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Jeff Huang <s8321414@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hant/
Translation: FreeTube/Translations
2025-11-24 03:51:19 +00:00
absidue
e71a824e6b Fix dropdownShown error on the watch page for live streams (#8331) 2025-11-24 09:31:35 +08:00
absidue
663273ea02 Fix view count with RSS feeds (#8328) 2025-11-23 19:50:36 -05:00
absidue
1453e0bf8d Add support for exporting watch history in YouTube's JSON format (#8323) 2025-11-23 19:49:21 -05:00
efb4f5ff-1298-471a-8973-3d47447115dc
605febdafa Cleanup leftover import toast (#8325)
* cleanup leftover toast

* cleanup locales
2025-11-23 19:48:11 -05:00
delvani
6f27f2429f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: delvani <del.cidrak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt_BR/
Translation: FreeTube/Translations
2025-11-23 23:51:18 +00:00
absidue
36501ba563 Wrap interactive FontAwesomeIcons in FtInput in actual buttons (#8324) 2025-11-23 23:39:23 +00:00
PikachuEXE
66e9fb74d8 Fix playlist page width & long playlist name handling (#8313)
* ! Fix playlist page width & long playlist name handling

* ! Fix layout issue in some widths

Also use `gap` instead of `margin-inline` for spacing

* ! Fix layout issue for local playlist in some widths
2025-11-23 18:04:20 +01:00
Milan
6db9c7352d Translated using Weblate (Slovak)
Currently translated at 67.5% (661 of 978 strings)

Co-authored-by: Milan <mobrcian@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/sk/
Translation: FreeTube/Translations
2025-11-23 16:51:18 +00:00
Milan
5dcc7b5dde Translated using Weblate (Slovak)
Currently translated at 67.5% (661 of 978 strings)

Co-authored-by: Milan <mobrcian@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/sk/
Translation: FreeTube/Translations
2025-11-23 15:51:19 +01:00
Milan
6452db219b Translated using Weblate (Slovak)
Currently translated at 59.5% (582 of 978 strings)

Co-authored-by: Milan <mobrcian@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/sk/
Translation: FreeTube/Translations
2025-11-23 12:51:18 +00:00
Sveinn í Felli
6590a18c46 Translated using Weblate (Icelandic)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/is/
Translation: FreeTube/Translations
2025-11-23 11:51:19 +01:00
Eder Etxebarria Rojo
34f0f54faf Translated using Weblate (Basque)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Eder Etxebarria Rojo <eder@betxepare.eus>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/eu/
Translation: FreeTube/Translations
2025-11-23 08:51:34 +01:00
cyberboh
9f5637215a Translated using Weblate (Indonesian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: cyberboh <cybermay686@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/id/
Translation: FreeTube/Translations
2025-11-23 08:51:32 +01:00
Massimo Pissarello
deef6d8f53 Translated using Weblate (Italian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/it/
Translation: FreeTube/Translations
2025-11-23 08:51:31 +01:00
SquattedWasp747
13d4acf664 Translated using Weblate (Russian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: SquattedWasp747 <squattedwasp747@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-11-23 01:51:21 +00:00
Maxim
1137add185 Translated using Weblate (Russian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Maxim <lixngmax@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-11-23 01:51:19 +00:00
Grzegorz Wójcicki
64796c3e6a Translated using Weblate (Polish)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Grzegorz Wójcicki <terkaz@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pl/
Translation: FreeTube/Translations
2025-11-22 23:51:23 +01:00
Telaneo
aa0b576388 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (978 of 978 strings)

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

Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-22 18:51:19 +00:00
Ghost of Sparta
bf9c5cdaca Translated using Weblate (Hungarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-22 16:51:17 +00:00
ColorfulRhino
602d8d41e8 Translated using Weblate (German)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: ColorfulRhino <131405023+ColorfulRhino@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-22 14:51:23 +01:00
Oğuz Ersen
8f89e39926 Translated using Weblate (Turkish)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/tr/
Translation: FreeTube/Translations
2025-11-22 14:51:22 +01:00
Fjuro
85c38edefb Translated using Weblate (Czech)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cs/
Translation: FreeTube/Translations
2025-11-22 14:51:20 +01:00
Loc Huynh
50c7d0f2e4 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Loc Huynh <huynhloc.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/vi/
Translation: FreeTube/Translations
2025-11-22 12:51:26 +01:00
summoner001
888c007573 Translated using Weblate (Hungarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: summoner001 <summoner@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-22 12:51:24 +01:00
Cloud Esp
d7ca9f658a Translated using Weblate (French)
Currently translated at 99.8% (977 of 978 strings)

Co-authored-by: Cloud Esp <Frederic.Darboux@inrae.fr>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-22 12:51:22 +01:00
大王叫我来巡山
636a50be46 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-22 10:51:29 +01:00
Priit Jõerüüt
a95276784b Translated using Weblate (Estonian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/et/
Translation: FreeTube/Translations
2025-11-22 10:51:27 +01:00
Ghost of Sparta
5dc1270787 Translated using Weblate (Hungarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-22 10:51:25 +01:00
Sveinn í Felli
facc91546a Translated using Weblate (Icelandic)
Currently translated at 100.0% (977 of 977 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/is/
Translation: FreeTube/Translations
2025-11-22 10:51:23 +01:00
Aditya Mishra
4a5d8fa453 feat: added share button to community posts (#8317)
* feat: add share button to community posts

* Updated FtCommunityPost.scss so share button UI is consistant

* Shifted CSS code to FtCommunityPost.scss

* Move FtShareButton to a new position in the template

* Refactor FtCommunityPost.scss styles

Removed header share button styling and adjusted bottom section layout.

* Cleanup

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-11-22 16:43:20 +08:00
Oğuz Ersen
7c28dbb0d4 Translated using Weblate (Turkish)
Currently translated at 100.0% (977 of 977 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/tr/
Translation: FreeTube/Translations
2025-11-22 07:51:18 +01:00
Mona Lisa
105d9b7d0c Translated using Weblate (Swedish)
Currently translated at 100.0% (977 of 977 strings)

Co-authored-by: Mona Lisa <nickwick@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/sv/
Translation: FreeTube/Translations
2025-11-22 04:51:18 +00:00
大王叫我来巡山
5e599ea721 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (977 of 977 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-22 03:51:21 +01:00
Massimo Pissarello
44f0e5ca92 Translated using Weblate (Italian)
Currently translated at 100.0% (977 of 977 strings)

Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/it/
Translation: FreeTube/Translations
2025-11-22 03:51:19 +01:00
ozrendev
4253ada1e5 Fix number of videos to be added to playlist is wrong (#8262)
* change toast popup var

* remove videoCount from toast popup

* update locale labels

* remove videoCount var from locales

* fix linter

* use only one locale for toast

* remove un-used locale

* update correct locale value

* remove updated locale values

* fix blank values to empty values linter

* Apply suggestion from @PikachuEXE

---------

Co-authored-by: PikachuEXE <git@pikachuexe.net>
2025-11-22 00:00:17 +01:00
absidue
c6722b8714 Fix the spacing between playlist metadata entries (#8311) 2025-11-21 23:14:22 +01:00
Andi Chandler
39bf9126bd Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Andi Chandler <andi@gowling.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/en_GB/
Translation: FreeTube/Translations
2025-11-20 18:51:19 +00:00
Juan Carlos Alfonso Vina
5bf3a5a796 Translated using Weblate (Spanish)
Currently translated at 97.4% (953 of 978 strings)

Co-authored-by: Juan Carlos Alfonso Vina <jalfo53@wgu.edu>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/es/
Translation: FreeTube/Translations
2025-11-20 17:51:17 +01:00
ColorfulRhino
e303ff8b56 Translated using Weblate (German)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: ColorfulRhino <131405023+ColorfulRhino@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-20 12:51:18 +00:00
Jeff Huang
bcaf110d6d Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Jeff Huang <s8321414@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hant/
Translation: FreeTube/Translations
2025-11-20 04:51:19 +01:00
Maxim
f076a552df Translated using Weblate (Russian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Maxim <lixngmax@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-11-19 21:54:56 +01:00
Loc Huynh
2e9aeae057 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Loc Huynh <huynhloc.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/vi/
Translation: FreeTube/Translations
2025-11-19 17:51:23 +00:00
Andi Chandler
bd792a1f2f Translated using Weblate (English (United Kingdom))
Currently translated at 99.3% (972 of 978 strings)

Co-authored-by: Andi Chandler <andi@gowling.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/en_GB/
Translation: FreeTube/Translations
2025-11-19 17:51:21 +00:00
Andi Chandler
035a1c4748 Translated using Weblate (English (United Kingdom))
Currently translated at 97.6% (955 of 978 strings)

Co-authored-by: Andi Chandler <andi@gowling.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/en_GB/
Translation: FreeTube/Translations
2025-11-19 13:51:19 +00:00
Andi Chandler
bf64152ff0 Translated using Weblate (English (United Kingdom))
Currently translated at 97.5% (954 of 978 strings)

Co-authored-by: Andi Chandler <andi@gowling.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/en_GB/
Translation: FreeTube/Translations
2025-11-19 11:51:24 +01:00
Eder Etxebarria Rojo
e1bae5dc27 Translated using Weblate (Basque)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Eder Etxebarria Rojo <eder@betxepare.eus>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/eu/
Translation: FreeTube/Translations
2025-11-19 11:51:21 +01:00
Grzegorz Wójcicki
31bc5c541b Translated using Weblate (Polish)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Grzegorz Wójcicki <terkaz@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pl/
Translation: FreeTube/Translations
2025-11-19 01:51:20 +01:00
Rusi Dimitrov
3fdbd43b8a Translated using Weblate (Bulgarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Rusi Dimitrov <astral_86@mail.bg>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/bg/
Translation: FreeTube/Translations
2025-11-19 01:51:18 +01:00
Sveinn í Felli
b91a15277f Translated using Weblate (Icelandic)
Currently translated at 100.0% (978 of 978 strings)

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

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/tr/
Translation: FreeTube/Translations
2025-11-18 17:52:50 +01:00
Hosted Weblate
ec724382de Merge branch 'origin/development' into Weblate. 2025-11-18 15:51:48 +01:00
Fjuro
e9c48fbb58 Translated using Weblate (Czech)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cs/
Translation: FreeTube/Translations
2025-11-18 15:51:27 +01:00
Priit Jõerüüt
aa745d5ead Translated using Weblate (Estonian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/et/
Translation: FreeTube/Translations
2025-11-18 15:51:24 +01:00
dependabot[bot]
ee2ff267d1 Bump sass from 1.93.3 to 1.94.0 (#8300)
Bumps [sass](https://github.com/sass/dart-sass) from 1.93.3 to 1.94.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.93.3...1.94.0)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.94.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 12:42:02 +00:00
absidue
24593d425e Switch to Vue's useTemplateRef() function (#8308) 2025-11-18 06:09:36 -05:00
Massimo Pissarello
92f79616d1 Translated using Weblate (Italian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/it/
Translation: FreeTube/Translations
2025-11-18 03:51:17 +01:00
delvani
c092104596 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: delvani <del.cidrak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt_BR/
Translation: FreeTube/Translations
2025-11-18 03:51:17 +01:00
大王叫我来巡山
ac0a8b0134 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-18 01:51:20 +01:00
absidue
f1d4991ca6 Workaround ready-to-show not firing consistently on wayland (#8294) 2025-11-18 08:29:14 +08:00
dependabot[bot]
c14e332051 Bump electron from 38.4.0 to 39.2.1 (#8303)
Bumps [electron](https://github.com/electron/electron) from 38.4.0 to 39.2.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v38.4.0...v39.2.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 39.2.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 08:28:19 +08:00
absidue
e7280db2b9 Migrate the WatchVideoInfo component to the composition API (#8295) 2025-11-17 19:04:22 -05:00
summoner001
bf340071ad Translated using Weblate (Hungarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: summoner001 <summoner@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-17 22:51:34 +01:00
Telaneo
ea71f45be5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (978 of 978 strings)

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

Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-17 22:51:30 +01:00
dependabot[bot]
3b379dbcb4 Bump eslint-plugin-jsdoc from 61.1.12 to 61.2.1 in the eslint group (#8298)
Bumps the eslint group with 1 update: [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc).


Updates `eslint-plugin-jsdoc` from 61.1.12 to 61.2.1
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v61.1.12...v61.2.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 61.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 20:55:29 +01:00
absidue
8695da836b Replace broken trending with a local API only equivalent (#8289) 2025-11-17 14:42:31 -05:00
absidue
a711a9730b Fix settings layout issues after the Vue 3 migration (#8292) 2025-11-17 20:17:49 +01:00
dependabot[bot]
cb14c8c4ce Bump lefthook from 2.0.3 to 2.0.4 (#8299)
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 17:51:39 +01:00
dependabot[bot]
8be984c2fc Bump shaka-player from 4.16.8 to 4.16.9 (#8301)
Bumps [shaka-player](https://github.com/shaka-project/shaka-player) from 4.16.8 to 4.16.9.
- [Release notes](https://github.com/shaka-project/shaka-player/releases)
- [Changelog](https://github.com/shaka-project/shaka-player/blob/v4.16.9/CHANGELOG.md)
- [Commits](https://github.com/shaka-project/shaka-player/compare/v4.16.8...v4.16.9)

---
updated-dependencies:
- dependency-name: shaka-player
  dependency-version: 4.16.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 17:43:38 +01:00
dependabot[bot]
fadaa2457b Bump electron-builder from 26.1.0 to 26.2.0 (#8302)
Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 26.1.0 to 26.2.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.2.0/packages/electron-builder)

---
updated-dependencies:
- dependency-name: electron-builder
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 17:35:14 +01:00
absidue
fde2ade504 Fix playlist video view counts on the local API (#8290) 2025-11-16 20:39:52 -05:00
ozrendev
5e243c68ec change hardcoded color app text logoColor (#8288) 2025-11-17 09:07:19 +08:00
Maxim
44850093cb Translated using Weblate (Russian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Maxim <lixngmax@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-11-16 13:51:16 +01:00
ColorfulRhino
05cb312818 Translated using Weblate (German)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: ColorfulRhino <131405023+ColorfulRhino@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-16 01:51:16 +01:00
ozrendev
9e832a1302 wrap long playlist strings (#8275) 2025-11-16 08:30:56 +08:00
Rusi Dimitrov
1bc8e8861e Translated using Weblate (Bulgarian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Rusi Dimitrov <astral_86@mail.bg>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/bg/
Translation: FreeTube/Translations
2025-11-15 21:51:19 +01:00
dependabot[bot]
30bca40908 Bump js-yaml from 4.1.0 to 4.1.1 (#8284)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-15 16:29:50 +01:00
efb4f5ff-1298-471a-8973-3d47447115dc
010aa50acd bump workflows node-version to 24 (#8279) 2025-11-15 00:00:06 +00:00
Eder Etxebarria Rojo
774e370981 Translated using Weblate (Basque)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Eder Etxebarria Rojo <eder@betxepare.eus>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/eu/
Translation: FreeTube/Translations
2025-11-14 12:51:24 +01:00
Dao Duy Tin
2aef9ad424 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Dao Duy Tin <duytin095@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/vi/
Translation: FreeTube/Translations
2025-11-13 14:51:28 +01:00
Ghost of Sparta
7d24561df9 Translated using Weblate (Hungarian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-12 22:52:49 +01:00
Ghost of Sparta
94693c9f00 Translated using Weblate (Hungarian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-12 07:51:21 +00:00
efb4f5ff-1298-471a-8973-3d47447115dc
ec473ab875 Remove upperCase on various places (#8267)
* remove uppercase on various places

* bring back user-select for upcoming and watched

Co-Authored-By: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>

---------

Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
2025-11-12 01:29:19 +00:00
Mickaël Binos
050611cae3 Translated using Weblate (French)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-11 19:51:15 +01:00
PikachuEXE
8e66ac2550 Fix subscription count display on watch page with IV API (#8255)
* ! Fix subscription count display on watch page with IV API

* * Use same subscription count handling as local API
2025-11-11 18:26:02 +01:00
absidue
10829adc08 Specify window position in constructor for better wayland compatibility (#8238) 2025-11-11 12:21:06 -05:00
efb4f5ff-1298-471a-8973-3d47447115dc
d50e3d4da4 Change profile icon to include lowercase letters (#8253)
* change profile icon

* fix subscribe button
2025-11-11 08:31:26 -05:00
Markus Gaugg
1ed5cd584e Translated using Weblate (German)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Markus Gaugg <gaugg@gmx.at>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-11 12:51:27 +00:00
Jeff Huang
a0bb74fedc Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Jeff Huang <s8321414@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hant/
Translation: FreeTube/Translations
2025-11-11 04:51:21 +00:00
delvani
4b1bb5d651 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: delvani <del.cidrak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt_BR/
Translation: FreeTube/Translations
2025-11-11 02:51:22 +01:00
Oğuz Ersen
cf4f48b17f Translated using Weblate (Turkish)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/tr/
Translation: FreeTube/Translations
2025-11-10 19:51:22 +01:00
PikachuEXE
047c3e6d28 ! Fix top nav cannot search in new window (#8254) 2025-11-10 16:52:11 +01:00
dependabot[bot]
a5b7e77588 Bump vue from 3.5.22 to 3.5.24 (#8257)
Bumps [vue](https://github.com/vuejs/core) from 3.5.22 to 3.5.24.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.22...v3.5.24)

---
updated-dependencies:
- dependency-name: vue
  dependency-version: 3.5.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 16:42:08 +01:00
Fjuro
9895c8b563 Translated using Weblate (Czech)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cs/
Translation: FreeTube/Translations
2025-11-10 15:51:29 +01:00
Massimo Pissarello
a9c6df7256 Translated using Weblate (Italian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/it/
Translation: FreeTube/Translations
2025-11-10 15:51:28 +01:00
Grzegorz Wójcicki
60d5f61785 Translated using Weblate (Polish)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Grzegorz Wójcicki <terkaz@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pl/
Translation: FreeTube/Translations
2025-11-10 15:51:26 +01:00
Hosted Weblate
1f59f22231 Merge branch 'origin/development' into Weblate. 2025-11-10 12:51:49 +00:00
Ghost of Sparta
c06738b1ae Translated using Weblate (Hungarian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-10 13:51:37 +01:00
Telaneo
ae5deb9844 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Telaneo <post@telaneo.net>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/nb_NO/
Translation: FreeTube/Translations
2025-11-10 13:51:35 +01:00
dependabot[bot]
c687a1c9f3 Bump lefthook from 2.0.2 to 2.0.3 (#8258)
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 05:51:55 -05:00
dependabot[bot]
17650fdf34 Bump the eslint group with 2 updates (#8256)
Bumps the eslint group with 2 updates: [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) and [eslint](https://github.com/eslint/eslint).


Updates `@eslint/js` from 9.39.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.1/packages/js)

Updates `eslint` from 9.39.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.0...v9.39.1)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 05:45:39 -05:00
dependabot[bot]
7352534ed0 Bump marked from 16.4.1 to 17.0.0 (#8260)
Bumps [marked](https://github.com/markedjs/marked) from 16.4.1 to 17.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v16.4.1...v17.0.0)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 17.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 05:43:59 -05:00
Florent
0f3ae6e720 Translated using Weblate (Breton)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Florent <florent.grouin+osmand@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/br/
Translation: FreeTube/Translations
2025-11-10 10:51:22 +01:00
Priit Jõerüüt
26d62ba8e2 Translated using Weblate (Estonian)
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/et/
Translation: FreeTube/Translations
2025-11-10 10:51:22 +01:00
Cloud Esp
b3c14bf2b4 Translated using Weblate (French)
Currently translated at 99.7% (978 of 980 strings)

Co-authored-by: Cloud Esp <Frederic.Darboux@inrae.fr>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-10 10:51:21 +01:00
大王叫我来巡山
80a0af668b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (980 of 980 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-10 10:51:21 +01:00
absidue
676cf13159 Add support for importing and exporting search history (#8237)
* Add support for importing and exporting search history

* Fix errors caused by passing reactive objects through IPC channels
2025-11-10 15:09:08 +08:00
absidue
075c111eb5 Invidious API: Fix published dates on recommended videos (#8251) 2025-11-10 13:12:15 +08:00
大王叫我来巡山
13c2c87122 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-10 04:51:20 +01:00
PikachuEXE
0c1ffe2d38 ! Fix playlist progress bar preview broken in IV API (#8245) 2025-11-10 03:33:54 +00:00
absidue
fcb311b26d Wrap interactive FontAwesomeIcons in actual buttons (#8206)
* Wrap interactive FontAwesomeIcons in actual buttons

* Fix notification banner and top nav arrows
2025-11-10 09:59:05 +08:00
ozrendev
31290be062 Add Catpuccin Latte Theme (#7943)
* feat: add catppuccin latte theme

* add case to index.js

* remove metadata from svg

* fix alphabet order

* remove colors that don't conform to WCAG 4.5:1

* Update src/renderer/themes.css

Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>

* set scrollbar-color

---------

Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>
2025-11-09 20:54:25 -05:00
absidue
eff85a5f06 Validate sender in all IPC event handlers (#8248) 2025-11-09 20:52:37 -05:00
Priit Jõerüüt
d584ad350d Translated using Weblate (Estonian)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/et/
Translation: FreeTube/Translations
2025-11-10 00:51:21 +00:00
Telaneo
b8599c655d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (971 of 971 strings)

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

Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-09 17:51:22 +00:00
Fjuro
00be448e3f Translated using Weblate (Czech)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cs/
Translation: FreeTube/Translations
2025-11-09 15:51:18 +00:00
PikachuEXE
09c265e445 ! Fix watch page playlist component not scrolled to current video on load (#8239) 2025-11-09 14:41:04 +00:00
Rusi Dimitrov
d9e3461cdc Translated using Weblate (Bulgarian)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: Rusi Dimitrov <astral_86@mail.bg>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/bg/
Translation: FreeTube/Translations
2025-11-09 13:51:18 +00:00
summoner001
81cc935ecf Translated using Weblate (Hungarian)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: summoner001 <summoner@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-09 12:51:34 +01:00
Cloud Esp
a71a6b69ae Translated using Weblate (French)
Currently translated at 99.5% (967 of 971 strings)

Co-authored-by: Cloud Esp <Frederic.Darboux@inrae.fr>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-09 12:51:32 +01:00
ColorfulRhino
5f2a951645 Translated using Weblate (German)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: ColorfulRhino <131405023+ColorfulRhino@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-09 12:51:29 +01:00
Massimo Pissarello
61ac1559c0 Translated using Weblate (Italian)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/it/
Translation: FreeTube/Translations
2025-11-09 09:51:18 +01:00
Oğuz Ersen
eff357772f Translated using Weblate (Turkish)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/tr/
Translation: FreeTube/Translations
2025-11-09 07:51:15 +01:00
大王叫我来巡山
655074f102 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-09 03:51:21 +01:00
delvani
e0bdbb1fcf Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: delvani <del.cidrak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt_BR/
Translation: FreeTube/Translations
2025-11-09 00:51:15 +01:00
Florent
3abd8e6a27 Translated using Weblate (Breton)
Currently translated at 100.0% (971 of 971 strings)

Co-authored-by: Florent <florent.grouin+osmand@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/br/
Translation: FreeTube/Translations
2025-11-08 22:53:56 +01:00
PikachuEXE
b01f851d1c Fix comment toggle link text (#8210)
* ! Fix comment toggle link text

* - Remove unused locale entries

* fix linter warning

* ! Fix incorrect nesting in template

* $ Use function to generate link text instead of using if-else in template

Also add/update type definition for comment objects

---------

Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>
2025-11-08 15:43:51 -05:00
PikachuEXE
614da2880f Fix scroll to current chapter on expand (#8242)
* ! Fix scroll to current chapter on expand

* Reuse existing variable

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-11-08 17:05:17 +00:00
ozrendev
f98d411fc5 Fix: Large fast forward/rewind value popup on player (#8236)
* round playback value

* Update src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js

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

---------

Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>
Co-authored-by: PikachuEXE <git@pikachuexe.net>
2025-11-08 13:40:22 +01:00
Ramón Ortiz Castañeda
f503b0b71c Translated using Weblate (Spanish (Mexico))
Currently translated at 61.8% (602 of 974 strings)

Co-authored-by: Ramón Ortiz Castañeda <ramon.o@ciencias.unam.mx>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/es_MX/
Translation: FreeTube/Translations
2025-11-08 03:51:21 +01:00
ozrendev
03c75f373e add and set show-tags for ext vid args (#8232) 2025-11-08 09:05:50 +08:00
ozrendev
2bdce9b98a Change clear filter button from text to icon (#8205)
* change clear filter button text to icon

* wrap icon in button elem

* add hover css

* refactor css

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

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

* Update src/renderer/components/FtSearchFilters/FtSearchFilters.css

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* Apply suggestion from @PikachuEXE

* Apply suggestion from @PikachuEXE

---------

Co-authored-by: PikachuEXE <git@pikachuexe.net>
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-11-05 23:53:17 +01:00
Nish Patel
fa79120f0a Feat clickable playlist progress bar (#7782)
* Feat clickable playlist progress bar

* * Make clicking on progress bar scroll the view instead of playing the video

* * Update thumbnail size to be slightly bigger w/ aspect ratio 4/3 like small width one

* * Use smaller iamges for preview

* * Use template ref instead of querySelector

* $ Refactor style & remove outdated comment

* * Remove usage of template ref array as ordered array

* ! Workaround preview covered by other elements when previewing earlier items

* ! Fix preview out of screen

* ! Fix preview out of screen 2

* ! Fix preview out of screen 3

* ! Fix preview out of screen 4

* * Update preview box box-shadow, remove arrow below & "ticks"

* * Use fixed width preview box (diff value for smaller view port)

* * Disable the preview for pointing device of limited accuracy

* * Update preview style for smaller width viewport

* $ Refactor style

* ! Fix children elements handling when scrolling to current video

---------

Co-authored-by: PikachuEXE <git@pikachuexe.net>
2025-11-05 23:09:30 +01:00
ozrendev
5e4a93b730 Add Hide channels based on text (#7948)
* add toggle to hide channels based on text

* fix lint warnings

* hide channels by default, remove toggle

* change label text to include channels

* hide videos on community tab

* hide posts on community tab

* filter subscription posts array

* filter subscription posts array on refresh

* set forbiddenTitles toLowerCase

* set forbiddenTitles toLowerCase in Watch.js

* Update src/renderer/components/SubscriptionsPosts.vue

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* rename translation keys

* one other translation key renamed

* changed translation keys after latest merge

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-11-05 23:05:11 +01:00
absidue
f2f2e9325c Clean up defunct shaka-player types patching (#8224) 2025-11-04 09:59:46 +01:00
ColorfulRhino
621a9ad30a Translated using Weblate (German)
Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: ColorfulRhino <131405023+ColorfulRhino@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-03 23:51:19 +01:00
Markus Gaugg
93eb7394b2 Translated using Weblate (German)
Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: Markus Gaugg <gaugg@gmx.at>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-11-03 23:51:16 +01:00
dependabot[bot]
fff5cb89d0 Bump shaka-player from 4.16.6 to 4.16.8 (#8223)
Bumps [shaka-player](https://github.com/shaka-project/shaka-player) from 4.16.6 to 4.16.8.
- [Release notes](https://github.com/shaka-project/shaka-player/releases)
- [Changelog](https://github.com/shaka-project/shaka-player/blob/v4.16.8/CHANGELOG.md)
- [Commits](https://github.com/shaka-project/shaka-player/compare/v4.16.6...v4.16.8)

---
updated-dependencies:
- dependency-name: shaka-player
  dependency-version: 4.16.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 06:13:45 +08:00
Milo Ivir
c34d8e934d Translated using Weblate (Croatian)
Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hr/
Translation: FreeTube/Translations
2025-11-03 21:51:14 +01:00
dependabot[bot]
3b08213ab1 Bump globals from 16.4.0 to 16.5.0 (#8216)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.4.0 to 16.5.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.4.0...v16.5.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 17:57:59 +01:00
dependabot[bot]
bde7acfd10 Bump eslint-plugin-jsdoc from 61.1.11 to 61.1.12 in the eslint group (#8222)
Bumps the eslint group with 1 update: [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc).


Updates `eslint-plugin-jsdoc` from 61.1.11 to 61.1.12
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v61.1.11...v61.1.12)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 61.1.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 16:47:00 +00:00
Fjuro
2b2869a3d4 Translated using Weblate (Czech)
Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cs/
Translation: FreeTube/Translations
2025-11-03 15:51:21 +00:00
Priit Jõerüüt
6a736d4b5e Translated using Weblate (Estonian)
Currently translated at 100.0% (974 of 974 strings)

Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/et/
Translation: FreeTube/Translations
2025-11-03 15:51:19 +00:00
dependabot[bot]
ebae6b9875 Bump lefthook from 2.0.1 to 2.0.2 (#8213)
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 05:57:57 -05:00
dependabot[bot]
d01c864c2f Bump sass from 1.93.2 to 1.93.3 (#8214)
Bumps [sass](https://github.com/sass/dart-sass) from 1.93.2 to 1.93.3.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.93.2...1.93.3)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.93.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 05:55:00 -05:00
dependabot[bot]
06a4aa4dc5 Bump the eslint group with 3 updates (#8211)
Bumps the eslint group with 3 updates: [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js), [eslint](https://github.com/eslint/eslint) and [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc).


Updates `@eslint/js` from 9.38.0 to 9.39.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.0/packages/js)

Updates `eslint` from 9.38.0 to 9.39.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.38.0...v9.39.0)

Updates `eslint-plugin-jsdoc` from 61.1.9 to 61.1.11
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v61.1.9...v61.1.11)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint
  dependency-version: 9.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 61.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 05:53:13 -05:00
absidue
caf2db57fb Remove broken restart window keyboard shortcut (#7281) 2025-11-03 05:52:52 -05:00
Loc Huynh
6dd00feb16 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Loc Huynh <huynhloc.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/vi/
Translation: FreeTube/Translations
2025-11-03 06:51:11 +01:00
Yaron Shahrabani
0d36e7b89f Translated using Weblate (Hebrew)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/he/
Translation: FreeTube/Translations
2025-11-03 04:51:20 +01:00
Jeff Huang
e6b1c20f42 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Jeff Huang <s8321414@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hant/
Translation: FreeTube/Translations
2025-11-03 04:51:18 +01:00
absidue
bf2f9b4fb3 Migrate FtInput to the composition API (#8208) 2025-11-03 03:37:43 +00:00
absidue
4c72a2358d Replace vue-portal dependency with Vue's built-in <Teleport> component (#8207) 2025-11-03 09:05:42 +08:00
absidue
d829cc2b16 Update to Vue 3 (#8094)
* Update to Vue 3

* Fix toasts and removing videos from playlists

* Fix duplicate app ID

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

* Fix various errors

* Fix toasts and hiding watched videos

* Update vue-router to 4.6.3

---------

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

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

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

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

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

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

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

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

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

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

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

* fix trimmed issues

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

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

* Update static/locales/en-US.yaml

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

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

---------

Co-authored-by: PikachuEXE <git@pikachuexe.net>
2025-11-01 15:06:15 +00:00
Philip Goto
bc4d8ce2fa Translated using Weblate (Dutch)
Currently translated at 99.3% (972 of 978 strings)

Co-authored-by: Philip Goto <philip.goto@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/nl/
Translation: FreeTube/Translations
2025-10-31 16:02:52 +01:00
TheAssassin
adbc650df0 Add support for AppImageUpdate (#8153) 2025-10-31 06:43:31 +08:00
Devenor
faaf92a967 Added New Window option to Tray (#7995)
* Added new window option to the tray

* Added special case for Linux

* Added special case for Linux

* Fix

* New window set as main window only if main window is in tray

* mainWindow gets updated when moving windows in and out of the tray

---------

Co-authored-by: Devenor <@>
2025-10-30 19:07:08 +01:00
Kyotaro Iijima
76ceac9efa Translated using Weblate (Japanese)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Kyotaro Iijima <kyotaro.eyes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ja/
Translation: FreeTube/Translations
2025-10-30 13:02:49 +01:00
Milo Ivir
8ad9964fed Translated using Weblate (Croatian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hr/
Translation: FreeTube/Translations
2025-10-30 05:24:49 +01:00
Lesser
d4733b1ae0 Translated using Weblate (Russian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Lesser <lesserl.persona@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-10-28 00:02:47 +01:00
dependabot[bot]
9d0577f13b Bump the eslint group with 2 updates (#8184)
Bumps the eslint group with 2 updates: [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn).


Updates `eslint-plugin-jsdoc` from 61.1.5 to 61.1.9
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v61.1.5...v61.1.9)

Updates `eslint-plugin-unicorn` from 61.0.2 to 62.0.0
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v61.0.2...v62.0.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 61.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint-plugin-unicorn
  dependency-version: 62.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 19:54:19 +01:00
dependabot[bot]
543a597497 Bump the babel group with 2 updates (#8185)
Bumps the babel group with 2 updates: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) and [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env).


Updates `@babel/core` from 7.28.4 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-core)

Updates `@babel/preset-env` from 7.28.3 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:48:04 +01:00
dependabot[bot]
ba54bfc997 Bump sass-loader from 16.0.5 to 16.0.6 (#8186)
Bumps [sass-loader](https://github.com/webpack/sass-loader) from 16.0.5 to 16.0.6.
- [Release notes](https://github.com/webpack/sass-loader/releases)
- [Changelog](https://github.com/webpack/sass-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/sass-loader/compare/v16.0.5...v16.0.6)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-version: 16.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:38:23 +01:00
dependabot[bot]
b0469c4f62 Bump electron from 38.3.0 to 38.4.0 (#8187)
Bumps [electron](https://github.com/electron/electron) from 38.3.0 to 38.4.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v38.3.0...v38.4.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 38.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:38:05 +01:00
dependabot[bot]
fb1be7d7b0 Bump swiper from 12.0.2 to 12.0.3 (#8189)
Bumps [swiper](https://github.com/nolimits4web/Swiper) from 12.0.2 to 12.0.3.
- [Release notes](https://github.com/nolimits4web/Swiper/releases)
- [Changelog](https://github.com/nolimits4web/swiper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nolimits4web/Swiper/compare/v12.0.2...v12.0.3)

---
updated-dependencies:
- dependency-name: swiper
  dependency-version: 12.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:17:44 +01:00
dependabot[bot]
6e79f3d541 Bump actions/upload-artifact from 4 to 5 (#8188)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:17:18 +01:00
dependabot[bot]
ac0388e808 Bump lefthook from 2.0.0 to 2.0.1 (#8190)
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 17:17:04 +01:00
efb4f5ff-1298-471a-8973-3d47447115dc
74cb414be8 Handle lockup views with no published text or views (#8170)
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-10-27 06:03:45 -04:00
Lesser
d51c8d3ac3 Translated using Weblate (Russian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Lesser <lesserl.persona@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-10-27 10:03:08 +01:00
Lesser
0954d8cda8 Translated using Weblate (Russian)
Currently translated at 98.7% (966 of 978 strings)

Co-authored-by: Lesser <lesserl.persona@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/ru/
Translation: FreeTube/Translations
2025-10-27 07:02:53 +00:00
ozrendev
890ba6f1e6 Remove Hide Channel from the kebab menu on the Subscriptions page (#8175)
* video options conditional based on subscriptions page

* proper page validation using routes

* change if inSubscriptions position

* Update src/renderer/components/ft-list-video/ft-list-video.js

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-10-27 09:07:24 +08:00
J. Lavoie
53bd699e8d Translated using Weblate (French)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-10-25 22:02:48 +00:00
ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝)
8fc7b357c6 Translated using Weblate (Latvian)
Currently translated at 56.2% (550 of 978 strings)

Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/lv/
Translation: FreeTube/Translations
2025-10-25 17:02:49 +02:00
ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝)
78624fde9c Translated using Weblate (Latvian)
Currently translated at 56.3% (551 of 978 strings)

Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/lv/
Translation: FreeTube/Translations
2025-10-25 15:02:42 +02:00
danssmnt
8714cff13b Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: danssmnt <danimunt07@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt_PT/
Translation: FreeTube/Translations
2025-10-25 03:03:03 +02:00
dependabot[bot]
46da1cf840 Bump lefthook from 1.13.6 to 2.0.0 (#8166)
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 1.13.6 to 2.0.0.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v1.13.6...v2.0.0)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-22 11:40:03 +02:00
dependabot[bot]
ade259b9bc Bump the eslint group with 4 updates (#8158)
Bumps the eslint group with 4 updates: [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js), [eslint](https://github.com/eslint/eslint), [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue).


Updates `@eslint/js` from 9.37.0 to 9.38.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.38.0/packages/js)

Updates `eslint` from 9.37.0 to 9.38.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.37.0...v9.38.0)

Updates `eslint-plugin-jsdoc` from 61.1.1 to 61.1.4
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v61.1.1...v61.1.4)

Updates `eslint-plugin-vue` from 10.5.0 to 10.5.1
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Changelog](https://github.com/vuejs/eslint-plugin-vue/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v10.5.0...v10.5.1)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint
  dependency-version: 9.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 61.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint-plugin-vue
  dependency-version: 10.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-21 06:28:11 +08:00
ColorfulRhino
a29031faec Translated using Weblate (German)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: ColorfulRhino <131405023+ColorfulRhino@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-10-21 00:02:46 +02:00
dependabot[bot]
c718ab2a3f Bump shaka-player from 4.16.4 to 4.16.6 (#8167)
* Bump shaka-player from 4.16.4 to 4.16.6

Bumps [shaka-player](https://github.com/shaka-project/shaka-player) from 4.16.4 to 4.16.6.
- [Release notes](https://github.com/shaka-project/shaka-player/releases)
- [Changelog](https://github.com/shaka-project/shaka-player/blob/v4.16.6/CHANGELOG.md)
- [Commits](https://github.com/shaka-project/shaka-player/compare/v4.16.4...v4.16.6)

---
updated-dependencies:
- dependency-name: shaka-player
  dependency-version: 4.16.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Revert "Fix playback rate reset when video ends (#7718)"

This reverts commit d4117344e4.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>
2025-10-20 19:50:04 +00:00
dependabot[bot]
963f42825d Bump marked from 16.4.0 to 16.4.1 (#8159)
Bumps [marked](https://github.com/markedjs/marked) from 16.4.0 to 16.4.1.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v16.4.0...v16.4.1)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 16.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 15:29:21 +02:00
dependabot[bot]
8b63948bbb Bump electron from 38.2.2 to 38.3.0 (#8161)
Bumps [electron](https://github.com/electron/electron) from 38.2.2 to 38.3.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v38.2.2...v38.3.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 38.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 14:30:39 +02:00
dependabot[bot]
e6599cfb30 Bump actions/setup-node from 5 to 6 (#8162)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 14:29:51 +02:00
dependabot[bot]
c5b0be28c4 Bump youtubei.js from 16.0.0 to 16.0.1 (#8163)
Bumps [youtubei.js](https://github.com/LuanRT/YouTube.js) from 16.0.0 to 16.0.1.
- [Release notes](https://github.com/LuanRT/YouTube.js/releases)
- [Changelog](https://github.com/LuanRT/YouTube.js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/LuanRT/YouTube.js/compare/v16.0.0...v16.0.1)

---
updated-dependencies:
- dependency-name: youtubei.js
  dependency-version: 16.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 18:12:40 +08:00
KING APPS
1baa962201 Translated using Weblate (Persian)
Currently translated at 99.0% (969 of 978 strings)

Co-authored-by: KING APPS <kiperking1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fa/
Translation: FreeTube/Translations
2025-10-20 04:50:45 +00:00
Fjuro
960677aabc Translated using Weblate (Czech)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cs/
Translation: FreeTube/Translations
2025-10-20 04:50:42 +00:00
Rusi Dimitrov
f0a49c6bd5 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Rusi Dimitrov <astral_86@mail.bg>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/bg/
Translation: FreeTube/Translations
2025-10-20 04:50:40 +00:00
absidue
6628c3e4ba Add missing IS_ELECTRON and SUPPORTS_LOCAL_API checks (#8147) 2025-10-17 07:11:00 +08:00
absidue
d170945d11 Simplify the NATIVE_THEME_UPDATE event handler (#8140) 2025-10-16 18:21:55 -04:00
efb4f5ff-1298-471a-8973-3d47447115dc
e46bdedb75 Bump package version from 0.23.11 to 0.23.12 (#8138) 2025-10-16 06:35:47 +08:00
absidue
8799b4990a Add error handling to the deciphering code (#8139) 2025-10-15 18:22:01 -04:00
PikachuEXE
fd96c7ad6f Fix video playback by using video ID bound poToken (#8137)
* ! Fix video playback by using video ID bound poToken

* Clean up now unused session PO token code

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2025-10-15 22:01:43 +00:00
Adrián Gelmotto Ruiz
03d44792a7 Hide subscriptions wrapper on mobile and center bottom SideNav items (#8133) 2025-10-15 21:11:29 +02:00
PikachuEXE
95ec469d72 ! Fix sigFrameScript on dev in windows (#8134) 2025-10-15 06:04:01 -04:00
Priit Jõerüüt
3e395f9586 Translated using Weblate (Estonian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/et/
Translation: FreeTube/Translations
2025-10-14 20:43:02 +00:00
dependabot[bot]
7d0b48c27c Bump github/codeql-action from 3 to 4 (#8124)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 18:36:51 +02:00
Rhoslyn Prys
19600d7076 Translated using Weblate (Welsh)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Rhoslyn Prys <rprys@posteo.net>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/cy/
Translation: FreeTube/Translations
2025-10-14 12:07:29 +00:00
dependabot[bot]
15d0e644e5 Bump mikefarah/yq from 4.47.2 to 4.48.1 (#8123)
Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.47.2 to 4.48.1.
- [Release notes](https://github.com/mikefarah/yq/releases)
- [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt)
- [Commits](https://github.com/mikefarah/yq/compare/v4.47.2...v4.48.1)

---
updated-dependencies:
- dependency-name: mikefarah/yq
  dependency-version: 4.48.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 08:44:00 +00:00
dependabot[bot]
52660a8f2b Bump stefanzweifel/git-auto-commit-action from 6 to 7 (#8125)
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 6 to 7.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 07:21:15 +00:00
efb4f5ff-1298-471a-8973-3d47447115dc
279f3bd7b3 Bump package version from 0.23.10 to 0.23.11 (#8126) 2025-10-14 07:46:39 +02:00
Ettore Atalan
03e0c47a3b Translated using Weblate (German)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/de/
Translation: FreeTube/Translations
2025-10-13 21:07:28 +00:00
absidue
76522d2685 Fix importing and exporting data after the Electron 38.2.0 update (#8106) 2025-10-13 18:10:07 +00:00
Florent
b47aaa6563 Translated using Weblate (Breton)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Florent <florent.grouin+osmand@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/br/
Translation: FreeTube/Translations
2025-10-13 18:07:31 +00:00
Grzegorz Wójcicki
cab43cbbdb Translated using Weblate (Polish)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Grzegorz Wójcicki <terkaz@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pl/
Translation: FreeTube/Translations
2025-10-13 18:07:30 +00:00
Grzegorz Wójcicki
b024a16deb Translated using Weblate (Polish)
Currently translated at 98.7% (966 of 978 strings)

Co-authored-by: Grzegorz Wójcicki <terkaz@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pl/
Translation: FreeTube/Translations
2025-10-13 16:07:28 +00:00
Sveinn í Felli
f151e17efb Translated using Weblate (Icelandic)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/is/
Translation: FreeTube/Translations
2025-10-13 08:07:29 +00:00
236 changed files with 5327 additions and 4966 deletions

View File

@@ -12,7 +12,7 @@ jobs:
build:
strategy:
matrix:
node-version: [22.x]
node-version: [24.x]
runtime:
- linux-x64
- linux-armv7l
@@ -46,14 +46,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Use faster D: drive for yarn cache on Windows'
if: startsWith(matrix.os, 'windows')
run: yarn config set cache-folder D:\ft_yarn_cache
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
@@ -121,91 +121,91 @@ jobs:
rm -rf ./squashfs-root ./appimagetool.AppImage
- name: Upload Linux .zip x64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-linux-x64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}.zip
- name: Upload Linux .7z x64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-linux-x64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}.7z
- name: Upload Linux .zip ARMv7l Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-linux-armv7l-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.zip
- name: Upload Linux .7z ARMv7l Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-linux-armv7l-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.7z
- name: Upload Linux .zip ARM64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-linux-arm64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.zip
- name: Upload Linux .7z ARM64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-linux-arm64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.7z
- name: Upload .deb x64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb
path: build/freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb
- name: Upload .deb ARMv7l Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb
path: build/freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb
- name: Upload .deb ARM64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb
path: build/freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb
- name: Upload AppImage x64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-amd64.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}.AppImage
- name: Upload AppImage ARMv7l Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-armv7l.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-armv7l.AppImage
- name: Upload AppImage ARM64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-arm64.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-arm64.AppImage
- name: Upload .rpm x64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}.amd64.rpm
@@ -214,119 +214,119 @@ jobs:
# rpm are not built for armv7l
- name: Upload .rpm ARM64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}.arm64.rpm
path: build/freetube-${{ steps.versionNumber.outputs.result }}.aarch64.rpm
- name: Upload Pacman .pacman x64 Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-amd64.pacman
path: build/freetube-${{ steps.versionNumber.outputs.result }}.pacman
# - name: Upload Web Build
# uses: actions/upload-artifact@v4
# uses: actions/upload-artifact@v5
# if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
# with:
# name: freetube-${{ steps.versionNumber.outputs.result }}-static-web
# path: dist/web
- name: Upload Windows x64 .exe Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-setup-x64.exe
path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Windows x64 Portable Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable.exe
path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Windows x64 .zip Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.zip
- name: Upload Windows x64 .7z Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.7z
- name: Upload Windows arm64 .exe Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-setup-arm64.exe
path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Windows arm64 Portable Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable.exe
path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe
- name: Upload Windows arm64 .zip Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.zip
- name: Upload Windows arm64 .7z Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.7z
- name: Upload Mac x64 .dmg Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.dmg
path: build/freetube-${{ steps.versionNumber.outputs.result }}.dmg
- name: Upload Mac x64 .zip Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.zip
- name: Upload Mac x64 .7z Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.7z
path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.7z
- name: Upload Mac arm64 .dmg Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.dmg
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.dmg
- name: Upload Mac arm64 .zip Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.zip
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-mac.zip
- name: Upload Mac arm64 .7z Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.7z

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@main

View File

@@ -27,11 +27,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -44,7 +44,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -57,6 +57,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: flathub/io.freetubeapp.FreeTube
token: ${{ secrets.FLATHUB_TOKEN }}
@@ -76,22 +76,22 @@ jobs:
date +"%Y-%m-%d" >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Update x64 File Location in yml File
uses: mikefarah/yq@v4.47.2
uses: mikefarah/yq@v4.49.1
with:
# The Command which should be run
cmd: yq -i '.modules[0].sources[0].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-beta-linux-x64-portable.zip"' io.freetubeapp.FreeTube.yml
- name: Update x64 Hash in yml File
uses: mikefarah/yq@v4.47.2
uses: mikefarah/yq@v4.49.1
with:
# The Command which should be run
cmd: yq -i '.modules[0].sources[0].sha256 = "${{ env.HASH_X64 }}"' io.freetubeapp.FreeTube.yml
- name: Update ARM File Location in yml File
uses: mikefarah/yq@v4.47.2
uses: mikefarah/yq@v4.49.1
with:
# The Command which should be run
cmd: yq -i '.modules[0].sources[1].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v${{ steps.sub.outputs.result }}-beta/freetube-${{ steps.sub.outputs.result }}-beta-linux-arm64-portable.zip"' io.freetubeapp.FreeTube.yml
- name: Update ARM Hash in yml File
uses: mikefarah/yq@v4.47.2
uses: mikefarah/yq@v4.49.1
with:
# The Command which should be run
cmd: yq -i '.modules[0].sources[1].sha256 = "${{ env.HASH_ARM64 }}"' io.freetubeapp.FreeTube.yml
@@ -102,7 +102,7 @@ jobs:
rm freetube-${{ steps.sub.outputs.result }}-beta-linux-x64-portable.zip
rm freetube-${{ steps.sub.outputs.result }}-beta-linux-arm64-portable.zip
- name: Commit Files
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@v7
with:
# Optional but recommended
# Defaults to "Apply automatic changes"

View File

@@ -17,11 +17,11 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v5
- name: Use Node.js 22.x
uses: actions/setup-node@v5
- uses: actions/checkout@v6
- name: Use Node.js 24.x
uses: actions/setup-node@v6
with:
node-version: 22.x
node-version: 24.x
cache: "yarn"
- run: yarn run ci
env:

View File

@@ -17,7 +17,7 @@ jobs:
build:
strategy:
matrix:
node-version: [22.x]
node-version: [24.x]
runtime:
- linux-x64
- linux-armv7l
@@ -51,14 +51,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Use faster D: drive for yarn cache on Windows'
if: startsWith(matrix.os, 'windows')
run: yarn config set cache-folder D:\ft_yarn_cache
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
@@ -87,7 +87,7 @@ jobs:
if: contains(matrix.runtime, 'arm64')
run: yarn run build:arm64
- name: Convert X64 AppImage to static runtime
- name: Convert X64 AppImage to static runtime and add update information
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
run: |
sudo apt install desktop-file-utils
@@ -95,11 +95,42 @@ jobs:
appimage="FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
chmod +x ./"$appimage" ./appimagetool.AppImage
update_information="gh-releases-zsync|FreeTubeApp|FreeTube|latest-all|freetube-*-amd64.AppImage.zsync"
./"$appimage" --appimage-extract && rm -f ./"$appimage"
./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-n ./squashfs-root ./"$appimage"
-u "$update_information" -n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage
- name: Convert ARMv7l AppImage to static runtime and add update information
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
run: |
sudo apt install desktop-file-utils
cd build
appimage="FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
wget "https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" -O runtime
chmod +x ./"$appimage" ./appimagetool.AppImage ./runtime
update_information="gh-releases-zsync|FreeTubeApp|FreeTube|latest-all|freetube-*-armv7l.AppImage.zsync"
TARGET_APPIMAGE=$appimage" ./runtime --appimage-extract && rm -f ./"$appimage"
ARCH=armhf ./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-u "$update_information" -n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage ./runtime
- name: Convert ARM64 AppImage to static runtime and add update information
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
run: |
sudo apt install desktop-file-utils
cd build
appimage="FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
wget "https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" -O runtime
chmod +x ./"$appimage" ./appimagetool.AppImage ./runtime
update_information="gh-releases-zsync|FreeTubeApp|FreeTube|latest-all|freetube-*-arm64.AppImage.zsync"
TARGET_APPIMAGE=$appimage" ./runtime --appimage-extract && rm -f ./"$appimage"
ARCH=aarch64 ./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-u "$update_information" -n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage ./runtime
- name: Upload Linux .zip x64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
@@ -210,6 +241,17 @@ jobs:
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage
asset_content_type: application/vnd.appimage
- name: Upload AppImage .zsync x64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-amd64.AppImage.zsync
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage.zsync
asset_content_type: application/x-zsync
- name: Upload AppImage ARMv7l Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
@@ -221,6 +263,17 @@ jobs:
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-armv7l.AppImage
asset_content_type: application/vnd.appimage
- name: Upload AppImage .zsync ARMv7l Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-armv7l.AppImage.zsync
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-armv7l.AppImage.zsync
asset_content_type: application/x-zsync
- name: Upload AppImage ARM64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
@@ -232,6 +285,17 @@ jobs:
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-arm64.AppImage
asset_content_type: application/vnd.appimage
- name: Upload AppImage .zsync ARM64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-arm64.AppImage.zsync
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-arm64.AppImage.zsync
asset_content_type: application/x-zsync
- name: Upload Linux .rpm x64 Release
uses: actions/upload-release-asset@v1
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: FreeTubeApp/FreeTubeApp.io
token: ${{ secrets.FLATHUB_TOKEN }}
@@ -54,7 +54,7 @@ jobs:
run: |
sed -i 's/${{ steps.previous.outputs.result }}/${{ steps.current.outputs.result }}/g' src/index.php
- name: Commit Files
uses: stefanzweifel/git-auto-commit-action@v6
uses: stefanzweifel/git-auto-commit-action@v7
with:
# Optional but recommended
# Defaults to "Apply automatic changes"

View File

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

View File

@@ -0,0 +1 @@
<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M4.903 0C5.84 0 6.6.76 6.6 1.697V25H4.27A4.268 4.268 0 0 1 0 20.73V1.697C0 .76.76 0 1.697 0h3.206zM25 0v2.022A4.577 4.577 0 0 1 20.422 6.6H9.38A1.68 1.68 0 0 1 7.7 4.92V1.68C7.7.752 8.452 0 9.38 0H25zm-7.064 12.223a.645.645 0 0 1 0 1.154l-9.273 4.596a.668.668 0 0 1-.963-.597V8.224a.667.667 0 0 1 .963-.597l9.273 4.596z" style="fill:#4c4f69"/></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@@ -0,0 +1 @@
<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M4.903 0C5.84 0 6.6.76 6.6 1.697V25H4.27A4.268 4.268 0 0 1 0 20.73V1.697C0 .76.76 0 1.697 0h3.206zM25 0v2.022A4.577 4.577 0 0 1 20.422 6.6H9.38A1.68 1.68 0 0 1 7.7 4.92V1.68C7.7.752 8.452 0 9.38 0H25zm-7.064 12.223a.645.645 0 0 1 0 1.154l-9.273 4.596a.668.668 0 0 1-.963-.597V8.224a.667.667 0 0 1 .963-.597l9.273 4.596z" style="fill:#eff1f5"/></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@@ -0,0 +1 @@
<svg width="100" height="49" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M3.029 33.471c-.452 0-.815-.141-1.088-.422-.272-.282-.409-.65-.409-1.104V18.21c0-.454.129-.806.386-1.057.257-.25.612-.376 1.064-.376h8.204c.951 0 1.426.407 1.426 1.221 0 .799-.475 1.198-1.426 1.198H4.501v4.508h6.218c.95 0 1.425.407 1.425 1.221 0 .798-.475 1.197-1.425 1.197H4.501v5.823c0 .454-.132.822-.397 1.104-.265.281-.624.422-1.075.422zm16.403-11.857c.367-.031.656.055.868.258.211.204.317.509.317.916 0 .423-.092.736-.275.939-.183.204-.515.337-.994.399l-.635.071c-.832.094-1.441.407-1.829.939-.388.532-.582 1.197-.582 1.996v4.93c0 .454-.127.803-.381 1.045-.253.243-.571.364-.951.364-.381 0-.695-.121-.941-.364-.247-.242-.37-.591-.37-1.045v-9.086c0-.438.123-.775.37-1.01.246-.234.553-.352.92-.352.366 0 .662.114.888.34.226.228.338.552.338.975v.939c.268-.689.667-1.221 1.195-1.597a3.419 3.419 0 0 1 1.766-.633l.296-.024zm12.444 8.57c.277 0 .501.102.672.305.171.204.257.478.257.822 0 .485-.302.892-.905 1.221a8.382 8.382 0 0 1-1.882.716 8.06 8.06 0 0 1-2.004.27c-1.923 0-3.447-.532-4.571-1.596-1.125-1.065-1.687-2.521-1.687-4.368 0-1.174.244-2.215.734-3.122a5.2 5.2 0 0 1 2.065-2.113c.888-.502 1.894-.752 3.019-.752 1.075 0 2.012.227 2.811.681a4.7 4.7 0 0 1 1.857 1.925c.44.83.66 1.808.66 2.935 0 .673-.309 1.01-.928 1.01h-7.211c.097 1.08.415 1.874.953 2.383.538.509 1.32.763 2.347.763.521 0 .981-.063 1.381-.188.399-.125.851-.297 1.356-.516.489-.251.848-.376 1.076-.376zm-4.229-6.551c-.831 0-1.495.251-1.992.752-.497.501-.795 1.22-.892 2.16h5.524c-.033-.955-.277-1.679-.733-2.172-.457-.493-1.092-.74-1.907-.74zm17.487 6.551c.277 0 .501.102.672.305.171.204.257.478.257.822 0 .485-.302.892-.905 1.221a8.382 8.382 0 0 1-1.882.716c-.7.18-1.369.27-2.004.27-1.923 0-3.447-.532-4.571-1.596-1.125-1.065-1.687-2.521-1.687-4.368 0-1.174.245-2.215.734-3.122a5.2 5.2 0 0 1 2.065-2.113c.888-.502 1.894-.752 3.019-.752 1.075 0 2.012.227 2.811.681a4.7 4.7 0 0 1 1.857 1.925c.44.83.66 1.808.66 2.935 0 .673-.309 1.01-.928 1.01h-7.211c.098 1.08.415 1.874.953 2.383.538.509 1.32.763 2.347.763.521 0 .981-.063 1.381-.188.399-.125.851-.297 1.356-.516.489-.251.848-.376 1.076-.376zm-4.229-6.551c-.831 0-1.495.251-1.992.752-.497.501-.794 1.22-.892 2.16h5.524c-.033-.955-.277-1.679-.733-2.172-.456-.493-1.092-.74-1.907-.74zm13.402 9.838c-.521 0-1.186-.164-1.506-.494-.32-.329-.48-.768-.48-1.316V19.763h-3.374c-1.102 0-1.652-.501-1.652-1.504 0-.988.55-1.482 1.652-1.482h10.719c1.102 0 1.653.494 1.653 1.482 0 1.003-.551 1.504-1.653 1.504h-3.374v11.898c0 .548-.156.987-.468 1.316-.313.33-.981.494-1.517.494zM69.693 21.69c.545 0 .978.148 1.297.446.32.297.479.704.479 1.221v8.616c0 .486-.167.877-.502 1.175-.335.297-.768.446-1.297.446-.499 0-.896-.141-1.192-.423-.297-.282-.445-.658-.445-1.127v-.235a3.63 3.63 0 0 1-1.414 1.362c-.584.313-1.242.469-1.975.469-1.449 0-2.528-.403-3.237-1.209-.709-.806-1.064-2.023-1.064-3.651v-5.423c0-.517.16-.924.479-1.221.32-.298.752-.446 1.298-.446.545 0 .974.148 1.285.446.312.297.468.704.468 1.221v5.494c0 .688.144 1.197.432 1.526.288.329.729.493 1.321.493.685 0 1.243-.235 1.671-.704.429-.47.643-1.088.643-1.855v-4.954c0-.517.156-.924.467-1.221.312-.298.741-.446 1.286-.446zm11.736-.047c1.017 0 1.915.242 2.694.727.779.486 1.387 1.178 1.824 2.079.437.899.655 1.944.655 3.134s-.218 2.242-.655 3.158c-.437.915-1.049 1.628-1.836 2.137-.787.508-1.681.762-2.682.762-.811 0-1.542-.168-2.194-.504a3.633 3.633 0 0 1-1.502-1.397v.187c0 .501-.159.904-.477 1.21-.317.305-.747.458-1.287.458-.54 0-.974-.153-1.299-.458-.326-.306-.489-.709-.489-1.21V18.402c0-.485.171-.876.512-1.173.342-.298.791-.447 1.347-.447.525 0 .946.141 1.264.423.318.282.477.657.477 1.127v5.142a3.534 3.534 0 0 1 1.49-1.35c.644-.321 1.363-.481 2.158-.481zm-1.049 9.298c.842 0 1.494-.294 1.955-.881.461-.587.691-1.412.691-2.477 0-1.049-.23-1.851-.691-2.407-.461-.555-1.113-.833-1.955-.833-.843 0-1.494.286-1.955.857-.461.571-.692 1.381-.692 2.43 0 1.064.231 1.882.692 2.454.461.571 1.112.857 1.955.857zm18.404-.916c.337 0 .612.125.823.375.211.251.317.572.317.963 0 .266-.084.513-.254.74a2.005 2.005 0 0 1-.709.575 9.445 9.445 0 0 1-2.002.692c-.76.181-1.461.27-2.103.27-1.351 0-2.53-.242-3.535-.727-1.005-.486-1.778-1.178-2.318-2.078-.541-.9-.811-1.961-.811-3.182 0-1.174.262-2.214.786-3.122a5.526 5.526 0 0 1 2.179-2.125c.929-.509 1.984-.763 3.167-.763 1.132 0 2.124.231 2.977.692a4.895 4.895 0 0 1 1.989 1.973c.473.853.71 1.85.71 2.993 0 .345-.089.607-.266.787-.177.18-.427.27-.748.27h-7.095c.119.923.423 1.593.913 2.007.49.415 1.191.622 2.103.622.49 0 .929-.054 1.317-.164.389-.11.82-.258 1.293-.446.236-.094.464-.176.684-.247.219-.07.414-.105.583-.105zm-4.359-5.941c-.726 0-1.309.216-1.748.646-.439.431-.701 1.053-.786 1.867h4.891c-.051-.83-.275-1.456-.672-1.878-.397-.423-.958-.635-1.685-.635z" style="fill-rule:nonzero;fill:#4c4f69"/></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1 @@
<svg width="100" height="49" xmlns="http://www.w3.org/2000/svg" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M3.029 33.471c-.452 0-.815-.141-1.088-.422-.272-.282-.409-.65-.409-1.104V18.21c0-.454.129-.806.386-1.057.257-.25.612-.376 1.064-.376h8.204c.951 0 1.426.407 1.426 1.221 0 .799-.475 1.198-1.426 1.198H4.501v4.508h6.218c.95 0 1.425.407 1.425 1.221 0 .798-.475 1.197-1.425 1.197H4.501v5.823c0 .454-.132.822-.397 1.104-.265.281-.624.422-1.075.422zm16.403-11.857c.367-.031.656.055.868.258.211.204.317.509.317.916 0 .423-.092.736-.275.939-.183.204-.515.337-.994.399l-.635.071c-.832.094-1.441.407-1.829.939-.388.532-.582 1.197-.582 1.996v4.93c0 .454-.127.803-.381 1.045-.253.243-.571.364-.951.364-.381 0-.695-.121-.941-.364-.247-.242-.37-.591-.37-1.045v-9.086c0-.438.123-.775.37-1.01.246-.234.553-.352.92-.352.366 0 .662.114.888.34.226.228.338.552.338.975v.939c.268-.689.667-1.221 1.195-1.597a3.419 3.419 0 0 1 1.766-.633l.296-.024zm12.444 8.57c.277 0 .501.102.672.305.171.204.257.478.257.822 0 .485-.302.892-.905 1.221a8.382 8.382 0 0 1-1.882.716 8.06 8.06 0 0 1-2.004.27c-1.923 0-3.447-.532-4.571-1.596-1.125-1.065-1.687-2.521-1.687-4.368 0-1.174.244-2.215.734-3.122a5.2 5.2 0 0 1 2.065-2.113c.888-.502 1.894-.752 3.019-.752 1.075 0 2.012.227 2.811.681a4.7 4.7 0 0 1 1.857 1.925c.44.83.66 1.808.66 2.935 0 .673-.309 1.01-.928 1.01h-7.211c.097 1.08.415 1.874.953 2.383.538.509 1.32.763 2.347.763.521 0 .981-.063 1.381-.188.399-.125.851-.297 1.356-.516.489-.251.848-.376 1.076-.376zm-4.229-6.551c-.831 0-1.495.251-1.992.752-.497.501-.795 1.22-.892 2.16h5.524c-.033-.955-.277-1.679-.733-2.172-.457-.493-1.092-.74-1.907-.74zm17.487 6.551c.277 0 .501.102.672.305.171.204.257.478.257.822 0 .485-.302.892-.905 1.221a8.382 8.382 0 0 1-1.882.716c-.7.18-1.369.27-2.004.27-1.923 0-3.447-.532-4.571-1.596-1.125-1.065-1.687-2.521-1.687-4.368 0-1.174.245-2.215.734-3.122a5.2 5.2 0 0 1 2.065-2.113c.888-.502 1.894-.752 3.019-.752 1.075 0 2.012.227 2.811.681a4.7 4.7 0 0 1 1.857 1.925c.44.83.66 1.808.66 2.935 0 .673-.309 1.01-.928 1.01h-7.211c.098 1.08.415 1.874.953 2.383.538.509 1.32.763 2.347.763.521 0 .981-.063 1.381-.188.399-.125.851-.297 1.356-.516.489-.251.848-.376 1.076-.376zm-4.229-6.551c-.831 0-1.495.251-1.992.752-.497.501-.794 1.22-.892 2.16h5.524c-.033-.955-.277-1.679-.733-2.172-.456-.493-1.092-.74-1.907-.74zm13.402 9.838c-.521 0-1.186-.164-1.506-.494-.32-.329-.48-.768-.48-1.316V19.763h-3.374c-1.102 0-1.652-.501-1.652-1.504 0-.988.55-1.482 1.652-1.482h10.719c1.102 0 1.653.494 1.653 1.482 0 1.003-.551 1.504-1.653 1.504h-3.374v11.898c0 .548-.156.987-.468 1.316-.313.33-.981.494-1.517.494zM69.693 21.69c.545 0 .978.148 1.297.446.32.297.479.704.479 1.221v8.616c0 .486-.167.877-.502 1.175-.335.297-.768.446-1.297.446-.499 0-.896-.141-1.192-.423-.297-.282-.445-.658-.445-1.127v-.235a3.63 3.63 0 0 1-1.414 1.362c-.584.313-1.242.469-1.975.469-1.449 0-2.528-.403-3.237-1.209-.709-.806-1.064-2.023-1.064-3.651v-5.423c0-.517.16-.924.479-1.221.32-.298.752-.446 1.298-.446.545 0 .974.148 1.285.446.312.297.468.704.468 1.221v5.494c0 .688.144 1.197.432 1.526.288.329.729.493 1.321.493.685 0 1.243-.235 1.671-.704.429-.47.643-1.088.643-1.855v-4.954c0-.517.156-.924.467-1.221.312-.298.741-.446 1.286-.446zm11.736-.047c1.017 0 1.915.242 2.694.727.779.486 1.387 1.178 1.824 2.079.437.899.655 1.944.655 3.134s-.218 2.242-.655 3.158c-.437.915-1.049 1.628-1.836 2.137-.787.508-1.681.762-2.682.762-.811 0-1.542-.168-2.194-.504a3.633 3.633 0 0 1-1.502-1.397v.187c0 .501-.159.904-.477 1.21-.317.305-.747.458-1.287.458-.54 0-.974-.153-1.299-.458-.326-.306-.489-.709-.489-1.21V18.402c0-.485.171-.876.512-1.173.342-.298.791-.447 1.347-.447.525 0 .946.141 1.264.423.318.282.477.657.477 1.127v5.142a3.534 3.534 0 0 1 1.49-1.35c.644-.321 1.363-.481 2.158-.481zm-1.049 9.298c.842 0 1.494-.294 1.955-.881.461-.587.691-1.412.691-2.477 0-1.049-.23-1.851-.691-2.407-.461-.555-1.113-.833-1.955-.833-.843 0-1.494.286-1.955.857-.461.571-.692 1.381-.692 2.43 0 1.064.231 1.882.692 2.454.461.571 1.112.857 1.955.857zm18.404-.916c.337 0 .612.125.823.375.211.251.317.572.317.963 0 .266-.084.513-.254.74a2.005 2.005 0 0 1-.709.575 9.445 9.445 0 0 1-2.002.692c-.76.181-1.461.27-2.103.27-1.351 0-2.53-.242-3.535-.727-1.005-.486-1.778-1.178-2.318-2.078-.541-.9-.811-1.961-.811-3.182 0-1.174.262-2.214.786-3.122a5.526 5.526 0 0 1 2.179-2.125c.929-.509 1.984-.763 3.167-.763 1.132 0 2.124.231 2.977.692a4.895 4.895 0 0 1 1.989 1.973c.473.853.71 1.85.71 2.993 0 .345-.089.607-.266.787-.177.18-.427.27-.748.27h-7.095c.119.923.423 1.593.913 2.007.49.415 1.191.622 2.103.622.49 0 .929-.054 1.317-.164.389-.11.82-.258 1.293-.446.236-.094.464-.176.684-.247.219-.07.414-.105.583-.105zm-4.359-5.941c-.726 0-1.309.216-1.748.646-.439.431-.701 1.053-.786 1.867h4.891c-.051-.83-.275-1.456-.672-1.878-.397-.423-.958-.635-1.685-.635z" style="fill-rule:nonzero;fill:#eff1f5"/></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,60 +1,12 @@
// This script fixes shaka not exporting its type definitions and referencing the Roboto font on google fonts in its CSS
// by adding an export line to the type definitions and updating the CSS to point to the local Roboto font
// This script fixes shaka-player referencing the Roboto font on google fonts in its CSS
// by updating the CSS to point to the local Roboto font
// this script only makes changes if they are needed, so running it multiple times doesn't cause any problems
import { appendFileSync, closeSync, ftruncateSync, openSync, readFileSync, writeSync } from 'fs'
import { closeSync, ftruncateSync, openSync, readFileSync, writeSync } from 'fs'
import { resolve } from 'path'
const SHAKA_DIST_DIR = resolve(import.meta.dirname, '../node_modules/shaka-player/dist')
function fixTypes() {
let fixedTypes = false
let fileHandleNormal
try {
fileHandleNormal = openSync(`${SHAKA_DIST_DIR}/shaka-player.ui.d.ts`, 'a+')
const contents = readFileSync(fileHandleNormal, 'utf-8')
// This script is run after every `yarn install`, even if shaka-player wasn't updated
// So we want to check first, if we actually need to make any changes
// or if the ones from the previous run are still intact
if (!contents.includes('export default shaka')) {
appendFileSync(fileHandleNormal, 'export default shaka;\n')
fixedTypes = true
}
} finally {
if (typeof fileHandleNormal !== 'undefined') {
closeSync(fileHandleNormal)
}
}
let fileHandleDebug
try {
fileHandleDebug = openSync(`${SHAKA_DIST_DIR}/shaka-player.ui.debug.d.ts`, 'a+')
const contents = readFileSync(fileHandleDebug, 'utf-8')
// This script is run after every `yarn install`, even if shaka-player wasn't updated
// So we want to check first, if we actually need to make any changes
// or if the ones from the previous run are still intact
if (!contents.includes('export default shaka')) {
appendFileSync(fileHandleDebug, 'export default shaka;\n')
fixedTypes = true
}
} finally {
if (typeof fileHandleDebug !== 'undefined') {
closeSync(fileHandleDebug)
}
}
if (fixedTypes) {
console.log('Fixed shaka-player types')
}
}
function removeRobotoFont() {
let cssFileHandle
try {
@@ -72,11 +24,10 @@ function removeRobotoFont() {
console.log('Removed shaka-player Roboto font, so it uses ours')
}
} finally {
if (typeof cssFileHandle !== 'undefined') {
if (cssFileHandle !== undefined) {
closeSync(cssFileHandle)
}
}
}
fixTypes()
removeRobotoFont()

View File

@@ -5,9 +5,7 @@ const { readFileSync } = require('fs')
const path = join(__dirname, '../src/renderer/sigFrameScript.js')
const rawScript = readFileSync(path, 'utf8')
const script = process.env.NODE_ENV === 'development'
? rawScript
: require('terser').minify_sync({ [path]: rawScript }).code
const script = require('terser').minify_sync({ [path]: rawScript }).code
module.exports.sigFrameTemplateParameters = {
sigFrameSrc: `data:text/html,${encodeURIComponent(`<!doctype html><script>${script}</script>`)}`,

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"name": "freetube",
"productName": "FreeTube",
"description": "A private YouTube client",
"version": "0.23.10",
"version": "0.23.12",
"license": "AGPL-3.0-or-later",
"main": "./dist/main.js",
"private": true,
@@ -58,64 +58,62 @@
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/vue-fontawesome": "^2.0.10",
"@fortawesome/vue-fontawesome": "^3.0.8",
"@seald-io/nedb": "^4.1.2",
"autolinker": "^4.1.5",
"bgutils-js": "^3.2.0",
"electron-context-menu": "^4.1.1",
"marked": "^16.4.0",
"portal-vue": "^2.1.7",
"marked": "^17.0.1",
"process": "^0.11.10",
"shaka-player": "^4.16.4",
"swiper": "^12.0.2",
"vue": "^2.7.16",
"vue-i18n": "^8.28.2",
"vue-observe-visibility": "^1.0.0",
"vue-router": "^3.6.5",
"vuex": "^3.6.2",
"youtubei.js": "^16.0.0"
"shaka-player": "^4.16.10",
"swiper": "^12.0.3",
"vue": "^3.5.25",
"vue-i18n": "^11.1.12",
"vue-observe-visibility": "^2.0.0-alpha.1",
"vue-router": "^4.6.3",
"vuex": "^4.1.0",
"youtubei.js": "^16.0.1"
},
"devDependencies": {
"@babel/core": "^7.28.4",
"@babel/preset-env": "^7.28.3",
"@double-great/stylelint-a11y": "^3.4.0",
"@eslint/js": "^9.37.0",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@double-great/stylelint-a11y": "^3.4.1",
"@eslint/js": "^9.39.1",
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
"babel-loader": "^10.0.0",
"copy-webpack-plugin": "^13.0.1",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
"electron": "^38.2.2",
"electron-builder": "^26.1.0",
"eslint": "^9.37.0",
"eslint-plugin-jsdoc": "^61.1.1",
"electron": "^39.2.3",
"electron-builder": "^26.3.0",
"eslint": "^9.39.1",
"eslint-plugin-jsdoc": "^61.4.1",
"eslint-plugin-jsonc": "^2.21.0",
"eslint-plugin-unicorn": "^61.0.2",
"eslint-plugin-vue": "^10.5.0",
"eslint-plugin-unicorn": "^62.0.0",
"eslint-plugin-vue": "^10.6.0",
"eslint-plugin-vuejs-accessibility": "^2.4.1",
"eslint-plugin-yml": "^1.19.0",
"globals": "^16.4.0",
"html-webpack-plugin": "^5.6.4",
"js-yaml": "^4.1.0",
"globals": "^16.5.0",
"html-webpack-plugin": "^5.6.5",
"js-yaml": "^4.1.1",
"json-minimizer-webpack-plugin": "^5.0.1",
"lefthook": "^1.13.6",
"lefthook": "^2.0.4",
"mini-css-extract-plugin": "^2.9.4",
"neostandard": "^0.12.2",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.6",
"postcss-scss": "^4.0.9",
"sass": "^1.93.2",
"sass-loader": "^16.0.5",
"stylelint": "^16.25.0",
"sass": "^1.94.2",
"sass-loader": "^16.0.6",
"stylelint": "^16.26.0",
"stylelint-config-sass-guidelines": "^12.1.0",
"stylelint-config-standard": "^39.0.1",
"stylelint-high-performance-animation": "^1.11.0",
"stylelint-use-logical-spec": "^5.0.1",
"tree-kill": "1.2.2",
"vue-devtools": "^5.1.4",
"vue-eslint-parser": "^10.2.0",
"vue-loader": "^15.10.0",
"webpack": "^5.102.1",
"vue-loader": "^17.4.2",
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",
"yaml-eslint-parser": "^1.3.0"

View File

@@ -6,10 +6,9 @@ import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js'
/**
* Based on: https://github.com/LuanRT/BgUtils/blob/main/examples/node/innertube-challenge-fetcher-example.ts
* @param {string} videoId
* @param {string} visitorData
* @param {import('youtubei.js').Session['context']} context
*/
export default async function (videoId, visitorData, context) {
export default async function (videoId, context) {
const requestKey = 'O43z0dpjhgX20SCx4KAo'
const challengeResponse = await fetch(
@@ -19,7 +18,7 @@ export default async function (videoId, visitorData, context) {
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
'X-Goog-Visitor-Id': visitorData,
'X-Goog-Visitor-Id': context.client.visitorData,
'X-Youtube-Client-Version': context.client.clientVersion,
'X-Youtube-Client-Name': '1'
},
@@ -83,8 +82,5 @@ export default async function (videoId, visitorData, context) {
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken: response[0] }, webPoSignalOutput)
const contentPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(visitorData)
return { contentPoToken, sessionPoToken }
return await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
}

View File

@@ -40,7 +40,7 @@ const IpcChannels = {
SET_INVIDIOUS_AUTHORIZATION: 'set-invidious-authorization',
GENERATE_PO_TOKENS: 'generate-po-tokens',
GENERATE_PO_TOKEN: 'generate-po-token',
GET_SCREENSHOT_FALLBACK_FOLDER: 'get-screenshot-fallback-folder',
CHOOSE_DEFAULT_FOLDER: 'choose-default-folder',
@@ -57,13 +57,13 @@ const DBActions = {
UPSERT: 2,
DELETE: 3,
DELETE_MULTIPLE: 4,
DELETE_ALL: 5
DELETE_ALL: 5,
OVERWRITE: 6
},
HISTORY: {
OVERWRITE: 20,
UPDATE_WATCH_PROGRESS: 21,
UPDATE_PLAYLIST: 22,
UPDATE_WATCH_PROGRESS: 20,
UPDATE_PLAYLIST: 21,
},
PROFILES: {
@@ -97,13 +97,13 @@ const SyncEvents = {
UPSERT: 1,
DELETE: 2,
DELETE_MULTIPLE: 3,
DELETE_ALL: 4
DELETE_ALL: 4,
OVERWRITE: 5,
},
HISTORY: {
OVERWRITE: 20,
UPDATE_WATCH_PROGRESS: 21,
UPDATE_PLAYLIST: 22,
UPDATE_WATCH_PROGRESS: 20,
UPDATE_PLAYLIST: 21,
},
PROFILES: {
@@ -152,8 +152,6 @@ const KeyboardShortcuts = {
NEW_WINDOW: 'ctrl+N',
MINIMIZE_WINDOW: 'ctrl+M',
CLOSE_WINDOW: 'ctrl+W',
RESTART_WINDOW: 'ctrl+R',
FORCE_RESTART_WINDOW: 'ctrl+shift+R',
TOGGLE_DEVTOOLS: 'ctrl+shift+I',
FOCUS_SEARCH: 'alt+D',
SEARCH_IN_NEW_WINDOW: 'shift+enter',

View File

@@ -272,6 +272,12 @@ class SearchHistory {
return db.searchHistory.updateAsync({ _id: searchHistoryEntry._id }, searchHistoryEntry, { upsert: true })
}
static async overwrite(records) {
await db.searchHistory.removeAsync({}, { multi: true })
await db.searchHistory.insertAsync(records)
}
static delete(_id) {
return db.searchHistory.removeAsync({ _id: _id })
}

View File

@@ -20,7 +20,7 @@ class History {
}
static overwrite(records) {
return window.ftElectron.dbHistory(DBActions.HISTORY.OVERWRITE, records)
return window.ftElectron.dbHistory(DBActions.GENERAL.OVERWRITE, records)
}
static updateWatchProgress(videoId, watchProgress) {
@@ -139,6 +139,10 @@ class SearchHistory {
return window.ftElectron.dbSearchHistory(DBActions.GENERAL.UPSERT, searchHistoryEntry)
}
static overwrite(records) {
return window.ftElectron.dbSearchHistory(DBActions.GENERAL.OVERWRITE, records)
}
static delete(_id) {
return window.ftElectron.dbSearchHistory(DBActions.GENERAL.DELETE, _id)
}

View File

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

View File

@@ -13,7 +13,7 @@
<body>
<div id="app"></div>
<% if (process.env.SUPPORTS_LOCAL_API) { %>
<% if (process.env.IS_ELECTRON) { %>
<iframe
id="sigFrame"
src="<%= sigFrameSrc %>"
@@ -24,8 +24,7 @@
style="display: none; pointer-events: none"
tabindex="-1"
></iframe>
<% } %>
<% if (!process.env.IS_ELECTRON) { %>
<% } else { %>
<script>
// This is the service worker with the Advanced caching

View File

@@ -351,9 +351,19 @@ function runApp() {
replaceMainWindow: false,
showWindowNow: true,
})
ipcMain.once(IpcChannels.APP_READY, () => {
newWindow.webContents.send(IpcChannels.OPEN_URL, newStartupUrl)
})
/**
* @param {import('electron').IpcMainEvent} event
*/
const readyHandler = (event) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
newWindow.webContents.ipc.off(IpcChannels.APP_READY, readyHandler)
event.reply(IpcChannels.OPEN_URL, newStartupUrl)
}
}
newWindow.webContents.ipc.on(IpcChannels.APP_READY, readyHandler)
}
})
}
@@ -425,23 +435,43 @@ function runApp() {
// FreeTube needs the following permissions:
// - "fullscreen": So that the video player can enter full screen
// - "clipboard-sanitized-write": To allow the user to copy video URLs and error messages
// - "fileSystem" Needed for the Web File System API (e.g. importing and exporting data)
session.defaultSession.setPermissionCheckHandler((webContents, permission, requestingOrigin) => {
session.defaultSession.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (!isFreeTubeUrl(requestingOrigin)) {
return false
}
return permission === 'fullscreen' || permission === 'clipboard-sanitized-write'
return (
permission === 'fullscreen' ||
permission === 'clipboard-sanitized-write' ||
(permission === 'fileSystem' && !details.isDirectory)
)
})
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
if (!isFreeTubeUrl(webContents.getURL())) {
// eslint-disable-next-line n/no-callback-literal
callback(false)
return
}
callback(permission === 'fullscreen' || permission === 'clipboard-sanitized-write')
callback(
permission === 'fullscreen' ||
permission === 'clipboard-sanitized-write' ||
(permission === 'fileSystem' && !details.isDirectory)
)
})
session.defaultSession.on('file-system-access-restricted', (event, details, callback) => {
if (!isFreeTubeUrl(details.origin)) {
// eslint-disable-next-line n/no-callback-literal
callback('deny')
return
}
// eslint-disable-next-line n/no-callback-literal
callback(details.isDirectory ? 'deny' : 'allow')
})
let docArray
@@ -690,14 +720,6 @@ function runApp() {
await createWindow()
if (process.env.NODE_ENV === 'development') {
try {
require('vue-devtools').install()
} catch (err) {
console.error(err)
}
}
if (isDebug) {
mainWindow.webContents.openDevTools()
}
@@ -726,6 +748,8 @@ function runApp() {
window.show()
}
}
if (trayWindows.length === BrowserWindow.getAllWindows().length) { mainWindow = window }
} else if (trayWindows.length > 0) {
window.close()
}
@@ -761,16 +785,29 @@ function runApp() {
{
type: 'separator'
},
{
label: 'Quit',
click: handleQuit
}
...defaultTrayMenu()
)
const menu = Menu.buildFromTemplate(menuItems)
tray.setContextMenu(menu)
}
function defaultTrayMenu() {
return [
{
label: 'New Window',
click: () => createWindow({
showWindowNow: true,
replaceMainWindow: trayWindows.some(item => item.id === mainWindow.id)
})
},
{
label: 'Quit',
click: handleQuit
}
]
}
function destroyTray() {
if (!tray) return
@@ -778,11 +815,7 @@ function runApp() {
tray.destroy()
tray = null
} else {
const quitItem = [{
label: 'Quit',
click: handleQuit
}]
const menu = Menu.buildFromTemplate(quitItem)
const menu = Menu.buildFromTemplate(defaultTrayMenu())
tray.setContextMenu(menu)
}
}
@@ -839,7 +872,7 @@ function runApp() {
if (process.env.NODE_ENV === 'development') {
return url_ !== null && url_.protocol === 'http:' && url_.host === 'localhost:9080' && (url_.pathname === '/' || url_.pathname === '/index.html')
} else {
return url_ !== null && url_.protocol === 'app:' && url_.host === 'bundle' && url_.pathname === '/index.html'
return url_ !== null && url_.protocol === 'app:' && url_.host === 'bundle' && (url_.pathname === '/' || url_.pathname === '/index.html')
}
}
@@ -897,6 +930,8 @@ function runApp() {
return '#fdf6e3'
case 'everforest-light-low':
return '#f3ead3'
case 'catppuccin-latte':
return '#eff1f5'
case 'system':
default:
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
@@ -907,10 +942,27 @@ function runApp() {
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
})
/**
* Initial window options
*/
const commonBrowserWindowOptions = {
let savedBounds, savedMaximized, savedFullScreen
const boundsDoc = await baseHandlers.settings._findOne('bounds')
if (typeof boundsDoc?.value === 'object') {
const { maximized, fullScreen, ...bounds } = boundsDoc.value
const windowVisible = screen.getAllDisplays().some(display => {
const { x, y, width, height } = display.bounds
return !(bounds.x > x + width || bounds.x + bounds.width < x || bounds.y > y + height || bounds.y + bounds.height < y)
})
if (windowVisible) {
savedBounds = bounds
}
savedMaximized = maximized
savedFullScreen = fullScreen
}
const newWindow = new BrowserWindow({
// It will be shown later when ready via `ready-to-show` event
show: showWindowNow,
backgroundColor: windowBackground,
darkTheme: nativeTheme.shouldUseDarkColors,
icon: process.env.NODE_ENV === 'development'
@@ -926,18 +978,19 @@ function runApp() {
: path.resolve(__dirname, 'preload.js')
},
minWidth: 340,
minHeight: 380
}
const newWindow = new BrowserWindow(
Object.assign(
{
// It will be shown later when ready via `ready-to-show` event
show: showWindowNow
},
commonBrowserWindowOptions
)
)
minHeight: 380,
...savedBounds
? {
x: savedBounds.x,
y: savedBounds.y,
width: savedBounds.width,
height: savedBounds.height
}
: {
width: 1200,
height: 800
}
})
// region Ensure child windows use same options since electron 14
@@ -1008,6 +1061,14 @@ function runApp() {
if (trayOnMinimize) {
newWindow.hide()
manageTray(newWindow)
if (newWindow === mainWindow) {
// A timer is needed because getFocusedWindow doesn't update until the minimize event ends
setTimeout(() => {
const newMainWindow = BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows().find(window => window.isVisible())
if (newMainWindow) { mainWindow = newMainWindow }
}, 100)
}
}
})
@@ -1024,35 +1085,12 @@ function runApp() {
mainWindow = newWindow
}
newWindow.setBounds({
width: 1200,
height: 800
})
if (savedMaximized) {
newWindow.maximize()
}
const boundsDoc = await baseHandlers.settings._findOne('bounds')
if (typeof boundsDoc?.value === 'object') {
const { maximized, fullScreen, ...bounds } = boundsDoc.value
const windowVisible = screen.getAllDisplays().some(display => {
const { x, y, width, height } = display.bounds
return !(bounds.x > x + width || bounds.x + bounds.width < x || bounds.y > y + height || bounds.y + bounds.height < y)
})
if (windowVisible) {
newWindow.setBounds({
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height
})
}
if (maximized) {
newWindow.maximize()
}
if (fullScreen) {
newWindow.setFullScreen(true)
}
if (savedFullScreen) {
newWindow.setFullScreen(true)
}
// If called multiple times
@@ -1069,13 +1107,21 @@ function runApp() {
}
if (typeof searchQueryText === 'string' && searchQueryText.length > 0) {
ipcMain.once(IpcChannels.SEARCH_INPUT_HANDLING_READY, () => {
newWindow.webContents.send(IpcChannels.UPDATE_SEARCH_INPUT_TEXT, searchQueryText)
})
/**
* @param {import('electron').IpcMainEvent} event
*/
const searchInputReadyHandler = (event) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
newWindow.webContents.ipc.off(IpcChannels.SEARCH_INPUT_HANDLING_READY, searchInputReadyHandler)
event.reply(IpcChannels.UPDATE_SEARCH_INPUT_TEXT, searchQueryText)
}
}
newWindow.webContents.ipc.on(IpcChannels.SEARCH_INPUT_HANDLING_READY, searchInputReadyHandler)
}
// Show when loaded
newWindow.once('ready-to-show', () => {
const showWindow = () => {
if (newWindow.isVisible()) {
// only open the dev tools if they aren't already open
if (process.env.NODE_ENV === 'development' && !newWindow.webContents.isDevToolsOpened()) {
@@ -1094,7 +1140,18 @@ function runApp() {
if (process.env.NODE_ENV === 'development') {
newWindow.webContents.openDevTools({ activate: false })
}
})
}
// The `ready-to-show` event doesn't always fire on wayland.
// Use the `did-finish-load` event on the web contents instead as that is similar enough
// https://github.com/electron/electron/issues/48859
if (process.platform === 'linux' && app.commandLine.getSwitchValue('ozone-platform') === 'wayland') {
newWindow.webContents.once('did-finish-load', showWindow)
} else {
// Show when loaded
newWindow.once('ready-to-show', showWindow)
}
newWindow.once('close', async () => {
if (BrowserWindow.getAllWindows().length !== 1) {
@@ -1124,11 +1181,13 @@ function runApp() {
return newWindow
}
ipcMain.on(IpcChannels.APP_READY, () => {
if (startupUrl) {
mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl)
ipcMain.on(IpcChannels.APP_READY, (event) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
if (startupUrl) {
mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl)
}
startupUrl = null
}
startupUrl = null
})
function relaunch() {
@@ -1172,15 +1231,23 @@ function runApp() {
const allWindows = BrowserWindow.getAllWindows()
allWindows.forEach((window) => {
window.webContents.send(IpcChannels.NATIVE_THEME_UPDATE, nativeTheme.shouldUseDarkColors)
if (isFreeTubeUrl(window.webContents.getURL())) {
window.webContents.send(IpcChannels.NATIVE_THEME_UPDATE, nativeTheme.shouldUseDarkColors)
}
})
})
ipcMain.handle(IpcChannels.GENERATE_PO_TOKENS, (_, videoId, visitorData, context) => {
return generatePoToken(videoId, visitorData, context, proxyUrl)
ipcMain.handle(IpcChannels.GENERATE_PO_TOKEN, (event, videoId, context) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
return generatePoToken(videoId, context, proxyUrl)
}
})
ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
ipcMain.on(IpcChannels.ENABLE_PROXY, (event, url) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
session.defaultSession.setProxy({
proxyRules: url
})
@@ -1188,7 +1255,11 @@ function runApp() {
session.defaultSession.closeAllConnections()
})
ipcMain.on(IpcChannels.DISABLE_PROXY, () => {
ipcMain.on(IpcChannels.DISABLE_PROXY, (event) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
session.defaultSession.setProxy({})
proxyUrl = undefined
session.defaultSession.closeAllConnections()
@@ -1200,7 +1271,11 @@ function runApp() {
// Math.trunc but with a bitwise OR so that it can be calcuated at build time and the number inlined
const HALF_OF_NAV_HISTORY_DISPLAY_LIMIT = (NAV_HISTORY_DISPLAY_LIMIT / 2) | 0
ipcMain.handle(IpcChannels.GET_NAVIGATION_HISTORY, ({ sender }) => {
ipcMain.handle(IpcChannels.GET_NAVIGATION_HISTORY, ({ senderFrame, sender }) => {
if (!isFreeTubeUrl(senderFrame.url)) {
return
}
const activeIndex = sender.navigationHistory.getActiveIndex()
const length = sender.navigationHistory.length()
@@ -1231,17 +1306,17 @@ function runApp() {
// #endregion navigation history
ipcMain.handle(IpcChannels.GET_SYSTEM_LOCALE, () => {
// we should switch to getPreferredSystemLanguages at some point and iterate through until we find a supported locale
return app.getSystemLocale()
ipcMain.handle(IpcChannels.GET_SYSTEM_LOCALE, (event) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
// we should switch to getPreferredSystemLanguages at some point and iterate through until we find a supported locale
return app.getSystemLocale()
}
})
ipcMain.handle(IpcChannels.GET_SCREENSHOT_FALLBACK_FOLDER, (event) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
if (isFreeTubeUrl(event.senderFrame.url)) {
return path.join(app.getPath('pictures'), 'Freetube')
}
return path.join(app.getPath('pictures'), 'Freetube')
})
ipcMain.on(IpcChannels.CHOOSE_DEFAULT_FOLDER, async (event, kind) => {
@@ -1286,7 +1361,9 @@ function runApp() {
}
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send(IpcChannels.SYNC_SETTINGS, syncPayload)
if (isFreeTubeUrl(window.webContents.getURL())) {
window.webContents.send(IpcChannels.SYNC_SETTINGS, syncPayload)
}
})
})
@@ -1403,16 +1480,24 @@ function runApp() {
})
})
ipcMain.on(IpcChannels.OPEN_IN_EXTERNAL_PLAYER, (_, executable, args) => {
const child = cp.spawn(executable, args, { detached: true, stdio: 'ignore' })
child.unref()
ipcMain.on(IpcChannels.OPEN_IN_EXTERNAL_PLAYER, (event, executable, args) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
const child = cp.spawn(executable, args, { detached: true, stdio: 'ignore' })
child.unref()
}
})
ipcMain.handle(IpcChannels.GET_REPLACE_HTTP_CACHE, () => {
return replaceHttpCache
ipcMain.handle(IpcChannels.GET_REPLACE_HTTP_CACHE, (event) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
return replaceHttpCache
}
})
ipcMain.once(IpcChannels.TOGGLE_REPLACE_HTTP_CACHE, async () => {
ipcMain.once(IpcChannels.TOGGLE_REPLACE_HTTP_CACHE, async (event) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
if (replaceHttpCache) {
await asyncFs.rm(REPLACE_HTTP_CACHE_PATH)
} else {
@@ -1433,7 +1518,11 @@ function runApp() {
return path.join(PLAYER_CACHE_PATH, sanitizedKey)
}
ipcMain.handle(IpcChannels.PLAYER_CACHE_GET, async (_, key) => {
ipcMain.handle(IpcChannels.PLAYER_CACHE_GET, async (event, key) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
const filePath = playerCachePathForKey(key)
try {
@@ -1451,7 +1540,11 @@ function runApp() {
}
})
ipcMain.handle(IpcChannels.PLAYER_CACHE_SET, async (_, key, value) => {
ipcMain.handle(IpcChannels.PLAYER_CACHE_SET, async (event, key, value) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
const filePath = playerCachePathForKey(key)
await asyncFs.mkdir(PLAYER_CACHE_PATH, { recursive: true })
@@ -1480,6 +1573,10 @@ function runApp() {
// Settings
ipcMain.handle(IpcChannels.DB_SETTINGS, async (event, { action, data }) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
try {
switch (action) {
case DBActions.GENERAL.FIND:
@@ -1538,6 +1635,10 @@ function runApp() {
// *********** //
// History
ipcMain.handle(IpcChannels.DB_HISTORY, async (event, { action, data }) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
try {
switch (action) {
case DBActions.GENERAL.FIND:
@@ -1552,12 +1653,12 @@ function runApp() {
)
return null
case DBActions.HISTORY.OVERWRITE:
case DBActions.GENERAL.OVERWRITE:
await baseHandlers.history.overwrite(data)
syncOtherWindows(
IpcChannels.SYNC_HISTORY,
event,
{ event: SyncEvents.HISTORY.OVERWRITE, data }
{ event: SyncEvents.GENERAL.OVERWRITE, data }
)
return null
@@ -1610,6 +1711,10 @@ function runApp() {
// *********** //
// Profiles
ipcMain.handle(IpcChannels.DB_PROFILES, async (event, { action, data }) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
try {
switch (action) {
case DBActions.GENERAL.CREATE: {
@@ -1678,6 +1783,10 @@ function runApp() {
// The remaining should have it implemented only when playlists
// get fully implemented into the app
ipcMain.handle(IpcChannels.DB_PLAYLISTS, async (event, { action, data }) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
try {
switch (action) {
case DBActions.GENERAL.CREATE:
@@ -1779,6 +1888,10 @@ function runApp() {
// ************** //
// Search History
ipcMain.handle(IpcChannels.DB_SEARCH_HISTORY, async (event, { action, data }) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
try {
switch (action) {
case DBActions.GENERAL.FIND:
@@ -1793,6 +1906,15 @@ function runApp() {
)
return null
case DBActions.GENERAL.OVERWRITE:
await baseHandlers.searchHistory.overwrite(data)
syncOtherWindows(
IpcChannels.SYNC_SEARCH_HISTORY,
event,
{ event: SyncEvents.GENERAL.OVERWRITE, data }
)
return null
case DBActions.GENERAL.DELETE:
await baseHandlers.searchHistory.delete(data)
syncOtherWindows(
@@ -1824,6 +1946,10 @@ function runApp() {
// *********** //
// Profiles
ipcMain.handle(IpcChannels.DB_SUBSCRIPTION_CACHE, async (event, { action, data }) => {
if (!isFreeTubeUrl(event.senderFrame.url)) {
return
}
try {
switch (action) {
case DBActions.GENERAL.FIND:
@@ -1906,7 +2032,7 @@ function runApp() {
function syncOtherWindows(channel, event, payload) {
const otherWindows = BrowserWindow.getAllWindows().filter((window) => {
return window.webContents.id !== event.sender.id
return window.webContents.id !== event.sender.id && isFreeTubeUrl(window.webContents.getURL())
})
for (const window of otherWindows) {
@@ -2016,9 +2142,19 @@ function runApp() {
replaceMainWindow: false,
showWindowNow: true,
})
ipcMain.once(IpcChannels.APP_READY, () => {
newWindow.webContents.send(IpcChannels.OPEN_URL, newStartupUrl)
})
/**
* @param {import('electron').IpcMainEvent} event
*/
const readyHandler = (event) => {
if (isFreeTubeUrl(event.senderFrame.url)) {
newWindow.webContents.ipc.off(IpcChannels.APP_READY, readyHandler)
event.reply(IpcChannels.OPEN_URL, newStartupUrl)
}
}
newWindow.webContents.ipc.on(IpcChannels.APP_READY, readyHandler)
})
app.on('web-contents-created', (_, webContents) => {
@@ -2077,7 +2213,7 @@ function runApp() {
*/
function navigateTo(path, browserWindow) {
if (browserWindow == null) {
if (browserWindow == null || !isFreeTubeUrl(browserWindow.webContents.getURL())) {
return
}
@@ -2158,11 +2294,6 @@ function runApp() {
{
label: 'View',
submenu: [
{ role: 'reload' },
{
role: 'forcereload',
accelerator: 'CmdOrCtrl+Shift+R'
},
{ role: 'toggledevtools' },
{ role: 'toggledevtools', accelerator: 'f12', visible: false },
{
@@ -2270,7 +2401,7 @@ function runApp() {
},
type: 'normal'
},
!hideTrendingVideos && {
(!hideTrendingVideos && (backendFallback || backendPreference === 'local')) && {
label: 'Trending',
click: (_menuItem, browserWindow, _event) => {
navigateTo('/trending', browserWindow)

View File

@@ -3,19 +3,18 @@ import { readFile } from 'fs/promises'
import { join } from 'path'
/**
* Generates a poToken (proof of origin token) using `bgutils-js`.
* Generates a content-bound poToken (proof of origin token) using `bgutils-js`.
* The script to generate it is `src/botGuardScript.js`
*
* This is intentionally split out into it's own thing, with it's own temporary in-memory session,
* as the BotGuard stuff accesses the global `document` and `window` objects and also requires making some requests.
* So we definitely don't want it running in the same places as the rest of the FreeTube code with the user data.
* @param {string} videoId
* @param {string} visitorData
* @param {string} context
* @param {string|undefined} proxyUrl
* @returns {Promise<{ contentPoToken: string, sessionPoToken: string }>}
* @returns {Promise<string>}
*/
export async function generatePoToken(videoId, visitorData, context, proxyUrl) {
export async function generatePoToken(videoId, context, proxyUrl) {
const sessionUuid = crypto.randomUUID()
const theSession = session.fromPartition(`potoken-${sessionUuid}`, { cache: false })
@@ -53,15 +52,31 @@ export async function generatePoToken(videoId, visitorData, context, proxyUrl) {
callback({ requestHeaders })
})
theSession.webRequest.onHeadersReceived({ urls: ['https://*/*'] }, ({ responseHeaders }, callback) => {
if (responseHeaders) {
callback({
responseHeaders: {
...responseHeaders,
'Access-Control-Allow-Origin': ['*'],
'Access-Control-Allow-Methods': ['GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH']
}
})
}
})
theSession.webRequest.onBeforeRequest({ urls: ['<all_urls>'], types: ['cspReport', 'ping'] }, (details, callback) => {
callback({ cancel: true })
})
const webContentsView = new WebContentsView({
webPreferences: {
backgroundThrottling: false,
safeDialogs: true,
sandbox: true,
contextIsolation: true,
v8CacheOptions: 'none',
session: theSession,
offscreen: true,
webSecurity: false,
disableBlinkFeatures: 'ElectronCSSCornerSmoothing'
}
})
@@ -97,7 +112,7 @@ export async function generatePoToken(videoId, visitorData, context, proxyUrl) {
}
})
const script = await getScript(videoId, visitorData, context)
const script = await getScript(videoId, context)
const response = await webContentsView.webContents.executeJavaScript(script)
@@ -111,10 +126,9 @@ let cachedScript
/**
* @param {string} videoId
* @param {string} visitorData
* @param {string} context
*/
async function getScript(videoId, visitorData, context) {
async function getScript(videoId, context) {
if (!cachedScript) {
const pathToScript = process.env.NODE_ENV === 'development'
? join(__dirname, '../../dist/botGuardScript.js')
@@ -129,5 +143,5 @@ async function getScript(videoId, visitorData, context) {
cachedScript = content.replace(match[0], `;${functionName}(FT_PARAMS)`)
}
return cachedScript.replace('FT_PARAMS', `"${videoId}","${visitorData}",${context}`)
return cachedScript.replace('FT_PARAMS', `"${videoId}",${context}`)
}

View File

@@ -6,7 +6,7 @@ import { IpcChannels } from '../constants.js'
* all systems running the electron app.
*/
ipcRenderer.on(IpcChannels.NATIVE_THEME_UPDATE, (_, shouldUseDarkColors) => {
webFrame.executeJavaScript(`document.body.dataset.systemTheme = "${shouldUseDarkColors ? 'dark' : 'light'}"`).catch()
document.body.dataset.systemTheme = shouldUseDarkColors ? 'dark' : 'light'
})
let currentUpdateSearchInputTextListener
@@ -100,12 +100,11 @@ export default {
/**
* @param {string} videoId
* @param {string} visitorData
* @param {string} context
* @returns {Promise<{ contentPoToken: string, sessionPoToken: string }>}
* @returns {Promise<string>}
*/
generatePoTokens: (videoId, visitorData, context) => {
return ipcRenderer.invoke(IpcChannels.GENERATE_PO_TOKENS, videoId, visitorData, context)
generatePoToken: (videoId, context) => {
return ipcRenderer.invoke(IpcChannels.GENERATE_PO_TOKEN, videoId, context)
},
/**

View File

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

View File

@@ -44,7 +44,6 @@ export default defineComponent({
latestBlogUrl: '',
updateChangelog: '',
changeLogTitle: '',
isPromptOpen: false,
lastExternalLinkToBeOpened: '',
showExternalLinkOpeningPrompt: false,
externalLinkOpeningPromptValues: [
@@ -154,6 +153,10 @@ export default defineComponent({
appTitle: function () {
return this.$store.getters.getAppTitle
},
isAnyPromptOpen: function () {
return this.$store.getters.isAnyPromptOpen
}
},
watch: {
windowTitle: 'setWindowTitle',
@@ -209,18 +212,16 @@ export default defineComponent({
}, 500)
})
this.$router.onReady(() => {
if (this.$router.currentRoute.path === '/') {
this.$router.replace({ path: this.landingPage })
}
if (this.$route.path === '/') {
this.$router.replace({ path: this.landingPage })
}
this.setWindowTitle()
})
this.setWindowTitle()
})
document.addEventListener('dragstart', this.handleDragStart)
},
beforeDestroy: function () {
beforeUnmount: function () {
document.removeEventListener('dragstart', this.handleDragStart)
},
methods: {
@@ -332,10 +333,6 @@ export default defineComponent({
this.showBlogBanner = false
},
handlePromptPortalUpdate: function(newVal) {
this.isPromptOpen = newVal
},
openDownloadsPage: function () {
const url = 'https://freetubeapp.io#download'
openExternalLink(url)
@@ -631,7 +628,6 @@ export default defineComponent({
transformed = true
break
case 'subscriptions':
case 'trending':
case 'history':
transformedURL.pathname = `/feed/${pathParts[1]}`
transformed = true

View File

@@ -1,7 +1,6 @@
<template>
<div
v-if="dataReady"
id="app"
class="app"
:class="{
hideOutlines: outlinesHidden,
@@ -10,11 +9,6 @@
hideLabelsSideBar: hideLabelsSideBar && !isSideNavOpen
}"
>
<portal-target
name="promptPortal"
multiple
@change="handlePromptPortalUpdate"
/>
<ft-prompt
v-if="showReleaseNotes"
theme="readable-width"
@@ -71,16 +65,16 @@
v-if="showProgressBar"
/>
<top-nav
:inert="isPromptOpen"
:inert="isAnyPromptOpen"
/>
<side-nav
ref="sideNav"
:inert="isPromptOpen"
:inert="isAnyPromptOpen"
/>
<ft-flex-box
class="flexBox routerView"
role="main"
:inert="isPromptOpen"
:inert="isAnyPromptOpen"
>
<div
v-if="showUpdatesBanner || showBlogBanner"
@@ -101,17 +95,18 @@
@click="handleNewBlogBannerClick"
/>
</div>
<transition
<RouterView
v-if="dataReady"
mode="out-in"
name="fade"
v-slot="{ Component }"
class="routerView"
>
<!-- <keep-alive> -->
<RouterView
class="routerView"
/>
<!-- </keep-alive> -->
</transition>
<Transition
mode="out-in"
name="fade"
>
<component :is="Component" />
</Transition>
</RouterView>
</ft-flex-box>
</div>
</template>

View File

@@ -42,7 +42,7 @@
v-if="subCount !== null && !hideChannelSubscriptions"
class="subCount"
>
{{ $tc('Global.Counts.Subscriber Count', subCount, { count: formattedSubCount }) }}
{{ $t('Global.Counts.Subscriber Count', { count: formattedSubCount }, subCount) }}
</p>
</div>
</div>
@@ -81,14 +81,14 @@
class="tab"
:class="{ selectedTab: currentTab === 'home' }"
role="tab"
:aria-selected="String(currentTab === 'home')"
:aria-selected="currentTab === 'home'"
aria-controls="homePanel"
:tabindex="(currentTab === 'home' || currentTab === 'search') ? 0 : -1"
@click="changeTab('home')"
@keydown.left.right="focusTab('home', $event)"
@keydown.enter.space.prevent="changeTab('home')"
>
{{ $t("Channel.Home.Home").toUpperCase() }}
{{ $t("Channel.Home.Home") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -97,14 +97,14 @@
class="tab"
:class="{ selectedTab: currentTab === 'videos' }"
role="tab"
:aria-selected="String(currentTab === 'videos')"
:aria-selected="currentTab === 'videos'"
aria-controls="videoPanel"
:tabindex="(currentTab === 'videos' || currentTab === 'search') ? 0 : -1"
@click="changeTab('videos')"
@keydown.left.right="focusTab('videos', $event)"
@keydown.enter.space.prevent="changeTab('videos')"
>
{{ $t("Channel.Videos.Videos").toUpperCase() }}
{{ $t("Channel.Videos.Videos") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -113,14 +113,14 @@
class="tab"
:class="{ selectedTab: currentTab === 'shorts' }"
role="tab"
:aria-selected="String(currentTab === 'shorts')"
:aria-selected="currentTab === 'shorts'"
aria-controls="shortPanel"
:tabindex="currentTab === 'shorts' ? 0 : -1"
@click="changeTab('shorts')"
@keydown.left.right="focusTab('shorts', $event)"
@keydown.enter.space.prevent="changeTab('shorts')"
>
{{ $t("Global.Shorts").toUpperCase() }}
{{ $t("Global.Shorts") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -129,14 +129,14 @@
class="tab"
:class="{ selectedTab: currentTab === 'live' }"
role="tab"
:aria-selected="String(currentTab === 'live')"
:aria-selected="currentTab === 'live'"
aria-controls="livePanel"
:tabindex="currentTab === 'live' ? 0 : -1"
@click="changeTab('live')"
@keydown.left.right="focusTab('live', $event)"
@keydown.enter.space.prevent="changeTab('live')"
>
{{ $t("Channel.Live.Live").toUpperCase() }}
{{ $t("Channel.Live.Live") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -144,7 +144,7 @@
id="releasesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'releases')"
:aria-selected="currentTab === 'releases'"
aria-controls="releasePanel"
:tabindex="currentTab === 'releases' ? 0 : -1"
:class="{ selectedTab: currentTab === 'releases' }"
@@ -152,7 +152,7 @@
@keydown.left.right="focusTab('releases', $event)"
@keydown.enter.space.prevent="changeTab('releases')"
>
{{ $t("Channel.Releases.Releases").toUpperCase() }}
{{ $t("Channel.Releases.Releases") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -160,7 +160,7 @@
id="podcastsTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'podcasts')"
:aria-selected="currentTab === 'podcasts'"
aria-controls="podcastPanel"
:tabindex="currentTab === 'podcasts' ? 0 : -1"
:class="{ selectedTab: currentTab === 'podcasts' }"
@@ -168,7 +168,7 @@
@keydown.left.right="focusTab('podcasts', $event)"
@keydown.enter.space.prevent="changeTab('podcasts')"
>
{{ $t("Channel.Podcasts.Podcasts").toUpperCase() }}
{{ $t("Channel.Podcasts.Podcasts") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -176,7 +176,7 @@
id="coursesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'courses')"
:aria-selected="currentTab === 'courses'"
aria-controls="coursesPanel"
:tabindex="currentTab === 'courses' ? 0 : -1"
:class="{ selectedTab: currentTab === 'courses' }"
@@ -184,7 +184,7 @@
@keydown.left.right="focusTab('courses', $event)"
@keydown.enter.space.prevent="changeTab('courses')"
>
{{ $t("Channel.Courses.Courses").toUpperCase() }}
{{ $t("Channel.Courses.Courses") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -192,7 +192,7 @@
id="playlistsTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'playlists')"
:aria-selected="currentTab === 'playlists'"
aria-controls="playlistPanel"
:tabindex="currentTab === 'playlists' ? 0 : -1"
:class="{ selectedTab: currentTab === 'playlists' }"
@@ -200,7 +200,7 @@
@keydown.left.right="focusTab('playlists', $event)"
@keydown.enter.space.prevent="changeTab('playlists')"
>
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
{{ $t("Channel.Playlists.Playlists") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
@@ -208,7 +208,7 @@
id="communityTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'community')"
:aria-selected="currentTab === 'community'"
aria-controls="communityPanel"
:tabindex="currentTab === 'community' ? 0 : -1"
:class="{ selectedTab: currentTab === 'community' }"
@@ -216,14 +216,14 @@
@keydown.left.right="focusTab('community', $event)"
@keydown.enter.space.prevent="changeTab('community')"
>
{{ $t("Global.Posts").toUpperCase() }}
{{ $t("Global.Posts") }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
id="aboutTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'about')"
:aria-selected="currentTab === 'about'"
aria-controls="aboutPanel"
:tabindex="currentTab === 'about' ? 0 : -1"
:class="{ selectedTab: currentTab === 'about' }"
@@ -231,7 +231,7 @@
@keydown.left.right="focusTab('about', $event)"
@keydown.enter.space.prevent="changeTab('about')"
>
{{ $t("Channel.About.About").toUpperCase() }}
{{ $t("Channel.About.About") }}
</div>
</div>
@@ -251,14 +251,14 @@
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, useTemplateRef } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import FtCard from '../ft-card/ft-card.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtShareButton from '../FtShareButton/FtShareButton.vue'
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import store from '../../store/index'
@@ -381,7 +381,7 @@ function search(query) {
emit('search', query)
}
const searchBar = ref(null)
const searchBar = useTemplateRef('searchBar')
/**
* @param {KeyboardEvent} event

View File

@@ -1,6 +1,14 @@
.shelfContainer {
max-inline-size: 85vw;
}
.shelfTitle {
font-size: 24px;
cursor: pointer;
/* Prevents overflow for long values */
max-inline-size: 100%;
overflow-wrap: anywhere;
}
.shelfTitle::marker {
@@ -32,4 +40,8 @@
.shelfSubtitle {
font-style: italic;
color: var(--tertiary-text-color);
/* Prevents overflow for long values */
max-inline-size: 100%;
overflow-wrap: anywhere;
}

View File

@@ -3,6 +3,7 @@
<div
v-for="(shelf, index) in filteredShelves"
:key="index"
class="shelfContainer"
>
<details
open

View File

@@ -65,7 +65,6 @@
block-size: 60px;
font-size: 20px;
line-height: 1em;
text-transform: capitalize;
color: rgb(0 0 0);
background-color: rgb(235 160 172);
border-radius: 50%;

View File

@@ -160,13 +160,9 @@
@keydown.space.prevent="toggleCommentReplies(index)"
@keydown.enter.prevent="toggleCommentReplies(index)"
>
<span v-if="!comment.showReplies">{{ $t("Comments.View") }}</span>
<span v-else>{{ $t("Comments.Hide") }}</span>
{{ comment.numReplies }}
<span v-if="comment.numReplies === 1">{{ $t("Comments.Reply").toLowerCase() }}</span>
<span v-else>{{ $t("Comments.Replies").toLowerCase() }}</span>
<span v-if="comment.hasOwnerReplied && !comment.showReplies"> {{ $t("Comments.From {channelName}", { channelName }) }}</span>
<span v-if="comment.numReplies > 1 && comment.hasOwnerReplied && !comment.showReplies"> {{ $t("Comments.And others") }}</span>
<span>
{{ toggleCommentRepliesLinkText(comment) }}
</span>
</span>
</p>
<div
@@ -264,7 +260,7 @@
v-if="reply.numReplies > 0"
class="commentMoreReplies"
>
{{ $t('Comments.View {replyCount} replies', { replyCount: reply.numReplies }) }}
{{ $t('Comments.View {replyCount} replies', { replyCount: reply.numReplies }, reply.numReplies) }}
</p>
</div>
<div
@@ -523,6 +519,26 @@ function getMoreComments() {
}
}
/** @typedef {import('../../helpers/api/local').LocalComment | import('../../helpers/api/invidious').InvidiousComment} Comment */
/**
* @param {Comment} comment
*/
function toggleCommentRepliesLinkText(comment) {
if (comment.showReplies) {
return t('Comments.Hide {replyCount} replies', { replyCount: comment.numReplies }, comment.numReplies)
}
if (comment.hasOwnerReplied) {
if (comment.numReplies > 1) {
return t('Comments.View {replyCount} replies from {channelName} and others', { replyCount: comment.numReplies, channelName: props.channelName })
}
return t('Comments.View 1 reply from {channelName}', { channelName: props.channelName })
}
return t('Comments.View {replyCount} replies', { replyCount: comment.numReplies }, comment.numReplies)
}
/**
* @param {number} index
*/

View File

@@ -0,0 +1,3 @@
.box {
justify-content: center;
}

View File

@@ -5,7 +5,7 @@
<h4 class="groupTitle">
{{ $t('Subscriptions.Subscriptions') }}
</h4>
<FtFlexBox class="dataSettingsBox">
<FtFlexBox class="box">
<FtButton
:label="$t('Settings.Data Settings.Import Subscriptions')"
@click="importSubscriptions"
@@ -29,20 +29,20 @@
<h4 class="groupTitle">
{{ $t('History.History') }}
</h4>
<FtFlexBox class="dataSettingsBox">
<FtFlexBox class="box">
<FtButton
:label="$t('Settings.Data Settings.Import History')"
@click="importHistory"
@click="importWatchHistory"
/>
<FtButton
:label="$t('Settings.Data Settings.Export History')"
@click="exportHistory"
@click="showExportWatchHistoryPrompt = true"
/>
</FtFlexBox>
<h4 class="groupTitle">
{{ $t('Playlists') }}
</h4>
<FtFlexBox class="dataSettingsBox">
<FtFlexBox class="box">
<FtButton
:label="$t('Settings.Data Settings.Import Playlists')"
@click="importPlaylists"
@@ -52,6 +52,19 @@
@click="exportPlaylists"
/>
</FtFlexBox>
<h4 class="groupTitle">
{{ t('Settings.Data Settings.Search history') }}
</h4>
<FtFlexBox class="box">
<FtButton
:label="t('Settings.Data Settings.Import search history')"
@click="importSearchHistory"
/>
<FtButton
:label="t('Settings.Data Settings.Export search history')"
@click="showExportSearchHistoryPrompt = true"
/>
</FtFlexBox>
<FtPrompt
v-if="showExportSubscriptionsPrompt"
:label="$t('Settings.Data Settings.Select Export Type')"
@@ -59,23 +72,37 @@
:option-values="SUBSCRIPTIONS_PROMPT_VALUES"
@click="exportSubscriptions"
/>
<FtPrompt
v-if="showExportWatchHistoryPrompt"
:label="t('Settings.Data Settings.Select Export Type')"
:option-names="exportWatchSearchHistoryPromptNames"
:option-values="WATCH_SEARCH_HISTORY_PROMPT_VALUES"
@click="exportWatchHistory"
/>
<FtPrompt
v-if="showExportSearchHistoryPrompt"
:label="t('Settings.Data Settings.Select Export Type')"
:option-names="exportWatchSearchHistoryPromptNames"
:option-values="WATCH_SEARCH_HISTORY_PROMPT_VALUES"
@click="exportSearchHistory"
/>
</FtSettingsSection>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useI18n } from '../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router/composables'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router'
import FtButton from './FtButton/FtButton.vue'
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'
import FtPrompt from './FtPrompt/FtPrompt.vue'
import FtSettingsSection from './FtSettingsSection/FtSettingsSection.vue'
import FtButton from '../FtButton/FtButton.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
import store from '../store/index'
import store from '../../store/index'
import { MAIN_PROFILE_ID } from '../../constants'
import { calculateColorLuminance, getRandomColor } from '../helpers/colors'
import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, getRandomColor } from '../../helpers/colors'
import {
deepCopy,
escapeHTML,
@@ -83,8 +110,8 @@ import {
readFileWithPicker,
showToast,
writeFileWithPicker,
} from '../helpers/utils'
import { processToBeAddedPlaylistVideo } from '../helpers/playlists'
} from '../../helpers/utils'
import { processToBeAddedPlaylistVideo } from '../../helpers/playlists'
const IMPORT_DIRECTORY_ID = 'data-settings-import'
const START_IN_DIRECTORY = 'downloads'
@@ -394,8 +421,6 @@ function importYouTubeSubscriptions(textDecode) {
const subscriptions = []
let count = 0
showToast(t('Settings.Data Settings.This might take a while, please wait'))
store.commit('setShowProgressBar', true)
store.commit('setProgressBarPercentage', 0)
@@ -732,7 +757,18 @@ async function exportNewPipeSubscriptions() {
// #endregion subscriptions export
// #region history
const WATCH_SEARCH_HISTORY_PROMPT_VALUES = [
'freetube',
'youtube'
]
const exportWatchSearchHistoryPromptNames = computed(() => [
`${t('Settings.Data Settings.Export FreeTube')} (.db)`,
`${t('Settings.Data Settings.Export YouTube')} (.json)`,
t('Close')
])
// #region watch history
const historyCacheById = computed(() => {
return store.getters.getHistoryCacheById
@@ -742,7 +778,7 @@ const historyCacheSorted = computed(() => {
return store.getters.getHistoryCacheSorted
})
async function importHistory() {
async function importWatchHistory() {
let response
try {
response = await readFileWithPicker(
@@ -767,16 +803,16 @@ async function importHistory() {
const { filename, content } = response
if (filename.endsWith('.db')) {
importFreeTubeHistory(content.split('\n'))
importFreeTubeWatchHistory(content.split('\n'))
} else if (filename.endsWith('.json')) {
importYouTubeHistory(JSON.parse(content))
importYouTubeWatchHistory(JSON.parse(content))
}
}
/**
* @param {string[]} textDecode
*/
async function importFreeTubeHistory(textDecode) {
async function importFreeTubeWatchHistory(textDecode) {
textDecode.pop()
const requiredKeys = [
@@ -806,7 +842,8 @@ async function importFreeTubeHistory(textDecode) {
'paid',
]
const historyItems = new Map(Object.entries(historyCacheById.value))
// deep copy so we don't get errors from Electron when we try to pass reactive objects through the IPC channels
const historyItems = new Map(deepCopy(Object.entries(historyCacheById.value)))
textDecode.forEach((history) => {
const historyData = JSON.parse(history)
@@ -843,7 +880,7 @@ async function importFreeTubeHistory(textDecode) {
/**
* @param {any[]} historyData
*/
async function importYouTubeHistory(historyData) {
async function importYouTubeWatchHistory(historyData) {
const filterPredicate = item =>
item.products.includes('YouTube') &&
item.titleUrl != null && // removed video doesnt contain url...
@@ -891,7 +928,8 @@ async function importYouTubeHistory(historyData) {
'activityControls',
].concat(Object.keys(keyMapping))
const historyItems = new Map(Object.entries(historyCacheById.value))
// deep copy so we don't get errors from Electron when we try to pass reactive objects through the IPC channels
const historyItems = new Map(deepCopy(Object.entries(historyCacheById.value)))
filteredHistoryData.forEach(element => {
const historyObject = {}
@@ -930,12 +968,30 @@ async function importYouTubeHistory(historyData) {
showToast(t('Settings.Data Settings.All watched history has been successfully imported'))
}
async function exportHistory() {
const showExportWatchHistoryPrompt = ref(false)
/**
* @param {'freetube' | 'youtube' | null} option
*/
async function exportWatchHistory(option) {
showExportWatchHistoryPrompt.value = false
switch (option) {
case 'freetube':
exportFreeTubeWatchHistory()
break
case 'youtube':
exportYouTubeWatchHistory()
break
}
}
async function exportFreeTubeWatchHistory() {
const historyDb = historyCacheSorted.value.map((historyEntry) => {
return JSON.stringify(historyEntry)
}).join('\n') + '\n'
const dateStr = getTodayDateStrLocalTimezone()
const exportFileName = 'freetube-history-' + dateStr + '.db'
const exportFileName = 'freetube-watch-history-' + dateStr + '.db'
await promptAndWriteToFile(
exportFileName,
@@ -947,7 +1003,40 @@ async function exportHistory() {
)
}
// #endregion history
async function exportYouTubeWatchHistory() {
const historyData = historyCacheSorted.value.map((entry) => {
return {
header: 'YouTube',
title: `Watched ${entry.title}`,
titleUrl: `https://www.youtube.com/watch?v=${entry.videoId}`,
subtitles: [{
name: entry.author,
url: `https://www.youtube.com/channel/${entry.authorId}`
}],
time: new Date(entry.timeWatched).toISOString(),
products: [
'YouTube'
],
activityControls: [
'YouTube watch history'
]
}
})
const dateStr = getTodayDateStrLocalTimezone()
const exportFileName = 'youtube-watch-history-' + dateStr + '.json'
await promptAndWriteToFile(
exportFileName,
JSON.stringify(historyData),
t('Settings.Data Settings.History File'),
'application/json',
'.json',
t('Settings.Data Settings.All watched history has been successfully exported')
)
}
// #endregion watch history
// #region playlists
@@ -1183,4 +1272,192 @@ async function exportPlaylists() {
}
// #endregion playlists
// #region search history
/** @type {import('vue').ComputedRef<{ _id: string, lastUpdatedAt: number }[]>} */
const searchHistoryEntries = computed(() => {
return store.getters.getSearchHistoryEntries
})
async function importSearchHistory() {
let response
try {
response = await readFileWithPicker(
t('Settings.Data Settings.Search history file'),
{
'application/x-freetube-db': '.db',
'application/json': '.json'
},
IMPORT_DIRECTORY_ID,
START_IN_DIRECTORY
)
} catch (err) {
const message = t('Settings.Data Settings.Unable to read file')
showToast(`${message}: ${err}`)
return
}
if (response === null) {
return
}
const { filename, content } = response
if (filename.endsWith('.db')) {
importFreeTubeSearchHistory(content.split('\n'))
} else if (filename.endsWith('.json')) {
importYouTubeSearchHistory(JSON.parse(content))
}
}
/**
* @param {string[]} textDecode
*/
async function importFreeTubeSearchHistory(textDecode) {
textDecode.pop()
// deep copy so we don't get errors from Electron when we try to pass reactive objects through the IPC channels
const historyItems = new Map(deepCopy(searchHistoryEntries.value).map(entry => [entry._id, entry]))
textDecode.forEach((rawEntry) => {
const entry = JSON.parse(rawEntry)
if (typeof entry._id !== 'string' || typeof entry.lastUpdatedAt !== 'number') {
showToast(t('Settings.Data Settings.History object has insufficient data, skipping item'))
console.error('Missing keys:', entry)
} else {
const existingEntry = historyItems.get(entry._id)
if (existingEntry == null || entry.lastUpdatedAt > existingEntry.lastUpdatedAt) {
let newEntry
if (Object.keys(entry) === 2) {
newEntry = entry
} else {
newEntry = { _id: entry._id, lastUpdatedAt: entry.lastUpdatedAt }
}
historyItems.set(entry._id, newEntry)
}
}
})
const newSearchHistoryEntries = Array.from(historyItems.values())
await store.dispatch('overwriteSearchHistory', newSearchHistoryEntries)
showToast(t('Settings.Data Settings.All search history has been successfully imported'))
}
/**
* @param {any[]} historyData
*/
async function importYouTubeSearchHistory(historyData) {
// deep copy so we don't get errors from Electron when we try to pass reactive objects through the IPC channels
const historyItems = new Map(deepCopy(searchHistoryEntries.value).map(entry => [entry._id, entry]))
for (const entry of historyData) {
if (
entry.products?.includes('YouTube') &&
entry.titleUrl?.includes('youtube.com/results?search_query') &&
entry.details == null // dont import ads
) {
try {
const url = new URL(entry.titleUrl)
const query = url.searchParams.get('search_query')
const lastUpdatedAt = Date.parse(entry.time)
if (!query || typeof query !== 'string' || query.length === 0 || isNaN(lastUpdatedAt)) {
showToast(t('Settings.Data Settings.History object has insufficient data, skipping item'))
console.error('Missing keys:', entry)
} else {
const existingEntry = historyItems.get(query)
if (existingEntry == null || lastUpdatedAt > existingEntry.lastUpdatedAt) {
historyItems.set(query, { _id: query, lastUpdatedAt })
}
}
} catch (error) {
console.error(error)
showToast(t('Settings.Data Settings.History object has insufficient data, skipping item'))
}
}
}
const newSearchHistoryEntries = Array.from(historyItems.values())
await store.dispatch('overwriteSearchHistory', newSearchHistoryEntries)
showToast(t('Settings.Data Settings.All search history has been successfully imported'))
}
const showExportSearchHistoryPrompt = ref(false)
/**
* @param {'freetube' | 'youtube' | null} option
*/
async function exportSearchHistory(option) {
showExportSearchHistoryPrompt.value = false
switch (option) {
case 'freetube':
exportFreeTubeSearchHistory()
break
case 'youtube':
exportYouTubeSearchHistory()
break
}
}
async function exportFreeTubeSearchHistory() {
const historyDb = searchHistoryEntries.value.map((entry) => {
return JSON.stringify(entry)
}).join('\n') + '\n'
const dateStr = getTodayDateStrLocalTimezone()
const exportFileName = 'freetube-search-history-' + dateStr + '.db'
await promptAndWriteToFile(
exportFileName,
historyDb,
t('Settings.Data Settings.Search history file'),
'application/x-freetube-db',
'.db',
t('Settings.Data Settings.All search history has been successfully exported')
)
}
async function exportYouTubeSearchHistory() {
const historyData = searchHistoryEntries.value.map((entry) => {
return {
header: 'YouTube',
title: `Searched for ${entry._id}`,
titleUrl: `https://www.youtube.com/results?search_query=${encodeURIComponent(entry._id)}`,
time: new Date(entry.lastUpdatedAt).toISOString(),
products: [
'YouTube'
],
activityControls: [
'YouTube search history'
]
}
})
const dateStr = getTodayDateStrLocalTimezone()
const exportFileName = 'youtube-search-history-' + dateStr + '.json'
await promptAndWriteToFile(
exportFileName,
JSON.stringify(historyData),
t('Settings.Data Settings.Search history file'),
'application/json',
'.json',
t('Settings.Data Settings.All search history has been successfully exported')
)
}
// #endregion search history
</script>
<style scoped src="./DataSettings.css" />

View File

@@ -78,12 +78,12 @@
</FtFlexBox>
<FtFlexBox class="containingTextFlexBox">
<FtInputTags
:label="t('Settings.Distraction Free Settings.Hide Videos and Playlists Containing Text')"
:tag-name-placeholder="t('Settings.Distraction Free Settings.Hide Videos and Playlists Containing Text Placeholder')"
:label="t('Settings.Distraction Free Settings.Hide Videos, Playlists and Channels Containing Text')"
:tag-name-placeholder="t('Settings.Distraction Free Settings.Hide Videos, Playlists and Channels Containing Text Placeholder')"
:show-tags="showAddedForbiddenTitles"
:tag-list="forbiddenTitles"
:min-input-length="3"
:tooltip="t('Tooltips.Distraction Free Settings.Hide Videos and Playlists Containing Text')"
:min-input-length="1"
:tooltip="t('Tooltips.Distraction Free Settings.Hide Videos, Playlists and Channels Containing Text')"
@change="handleForbiddenTitles"
@toggle-show-tags="handleAddedForbiddenTitles"
/>
@@ -96,8 +96,10 @@
<div class="switchColumnGrid">
<div class="switchColumn">
<FtToggleSwitch
v-if="SUPPORTS_LOCAL_API"
:label="t('Settings.Distraction Free Settings.Hide Trending Videos')"
:compact="true"
:disabled="disableHideTrendingVideos"
:default-value="hideTrendingVideos"
@change="updateHideTrendingVideos"
/>
@@ -298,6 +300,8 @@ import { checkYoutubeChannelId, findChannelTagInfo } from '../../helpers/channel
const { t } = useI18n()
const SUPPORTS_LOCAL_API = process.env.SUPPORTS_LOCAL_API
const channelHiderDisabled = ref(false)
/** @type {import('vue').ComputedRef<'local' | 'invidious'>} */
@@ -368,6 +372,7 @@ function handleHideRecommendedVideos(value) {
/** @type {import('vue').ComputedRef<boolean>} */
const hideTrendingVideos = computed(() => store.getters.getHideTrendingVideos)
const disableHideTrendingVideos = computed(() => backendPreference.value !== 'local' && !backendFallback.value)
/**
* @param {boolean} value
*/

View File

@@ -53,7 +53,7 @@ import { useI18n } from '../../composables/use-i18n-polyfill'
import FtButton from '../FtButton/FtButton.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtSelect from '../FtSelect/FtSelect.vue'
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
import FtToggleSwitch from '../FtToggleSwitch/FtToggleSwitch.vue'

View File

@@ -52,7 +52,9 @@
:tag-name-placeholder="$t('Settings.External Player Settings.Custom External Player Arguments')"
:tag-list="externalPlayerCustomArgs"
:tooltip="externalPlayerCustomArgsTooltip"
:show-tags="showAddedExternalPlayerCustomArgs"
@change="handleExternalPlayerCustomArgs"
@toggle-show-tags="handleAddedExternalPayerCustomArgs"
/>
</FtFlexBox>
</FtSettingsSection>
@@ -64,7 +66,7 @@ import { useI18n } from '../composables/use-i18n-polyfill'
import FtSettingsSection from './FtSettingsSection/FtSettingsSection.vue'
import FtSelect from './FtSelect/FtSelect.vue'
import FtInput from './ft-input/ft-input.vue'
import FtInput from './FtInput/FtInput.vue'
import FtToggleSwitch from './FtToggleSwitch/FtToggleSwitch.vue'
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'
import FtInputTags from './FtInputTags/FtInputTags.vue'
@@ -150,4 +152,11 @@ function updateExternalPlayerExecutable(value) {
function handleExternalPlayerCustomArgs(args) {
store.dispatch('updateExternalPlayerCustomArgs', JSON.stringify(args))
}
/** @type {import('vue').ComputedRef<boolean>} */
const showAddedExternalPlayerCustomArgs = computed(() => store.getters.getShowAddedExternalPlayerCustomArgs)
function handleAddedExternalPayerCustomArgs() {
store.dispatch('updateShowAddedExternalPlayerCustomArgs', !showAddedExternalPlayerCustomArgs.value)
}
</script>

View File

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

View File

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

View File

@@ -67,11 +67,11 @@
.bottomSection {
color: var(--tertiary-text-color);
display: block;
flex-direction: column;
display: flex;
align-items: center;
font-size: 15px;
margin-block-start: 4px;
max-inline-size: 210px;
max-inline-size: 100%;
text-align: start;
@media screen and (width <= 680px) {
@@ -88,6 +88,10 @@
.likeCount {
margin-inline: 5px 6px;
}
.shareButton {
margin-inline-start: 10px;
}
}
.playlistWrapper {

View File

@@ -117,8 +117,8 @@
>
<span
class="likeCount"
:title="$tc('Global.Counts.Like Count', voteCount, {count: formattedVoteCount})"
:aria-label="$tc('Global.Counts.Like Count', voteCount, {count: formattedVoteCount})"
:title="$t('Global.Counts.Like Count', {count: formattedVoteCount}, voteCount)"
:aria-label="$t('Global.Counts.Like Count', {count: formattedVoteCount}, voteCount)"
>
<FontAwesomeIcon
class="thumbs-up-icon"
@@ -136,8 +136,8 @@
>
<span
class="commentCount"
:title="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:aria-label="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:title="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
:aria-label="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
>
<FontAwesomeIcon
class="comment-count-icon"
@@ -148,13 +148,19 @@
<span
v-else-if="commentCount != null"
class="commentCount"
:title="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:aria-label="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:title="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
:aria-label="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
>
<FontAwesomeIcon
class="comment-count-icon"
:icon="['fas', 'comment']"
/> {{ commentCount }}</span>
<FtShareButton
:id="postId"
share-target-type="Post"
class="shareButton"
:size="18"
/>
</div>
</div>
</template>
@@ -163,11 +169,12 @@
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import autolinker from 'autolinker'
import { A11y, Navigation, Pagination } from 'swiper/modules'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, useTemplateRef } from 'vue'
import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListPlaylist from '../FtListPlaylist/FtListPlaylist.vue'
import FtCommunityPoll from '../FtCommunityPoll/FtCommunityPoll.vue'
import FtShareButton from '../FtShareButton/FtShareButton.vue'
import store from '../../store/index'
@@ -305,7 +312,7 @@ function getBestQualityImage(imageArray) {
return imageArrayCopy[0]?.url?.replace(/-c-fcrop64=[^-]+/i, '') ?? ''
}
const swiperContainerRef = ref(null)
const swiperContainerRef = useTemplateRef('swiperContainerRef')
if (postType === 'multiImage' && postContent.content.length > 0) {
onMounted(() => {

View File

@@ -43,12 +43,12 @@
</template>
<script setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtButton from '../FtButton/FtButton.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'
import store from '../../store/index'
@@ -83,7 +83,7 @@ const playlistPersistenceDisabled = computed(() => {
return playlistName.value === '' || playlistNameBlank.value || playlistWithNameExists.value
})
const playlistNameInput = ref(null)
const playlistNameInput = useTemplateRef('playlistNameInput')
onMounted(() => {
// Faster to input required playlist name

View File

@@ -44,8 +44,7 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp
opacity: 1;
}
.disabled label,
.disabled .ft-input {
.disabled {
opacity: 0.4;
cursor: not-allowed;
}
@@ -61,7 +60,11 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp
margin-inline: 3px;
padding: 10px;
border-radius: 100%;
border-style: none;
background-color: transparent;
color: var(--primary-text-color);
font-size: 1em;
line-height: 1em;
opacity: 0;
transition: background 0.2s ease-in;
}
@@ -130,6 +133,10 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp
position: relative;
}
.buttonIcon {
inline-size: 1em;
}
.inputAction {
position: absolute;
margin-block: 0;
@@ -137,8 +144,12 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp
padding: 10px;
inset-block-start: -8px;
inset-inline-end: 0;
border-style: none;
border-radius: 100%;
background-color: transparent;
color: var(--primary-text-color);
font-size: 1em;
line-height: 1em;
/* this should look disabled by default */
opacity: 0.5;

View File

@@ -0,0 +1,520 @@
<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->
<template>
<div
class="ft-input-component"
:class="{
search: isSearch,
forceTextColor,
showActionButton,
showClearTextButton,
clearTextButtonVisible: inputDataPresent || showOptions,
inputDataPresent,
showOptions
}"
>
<label
v-if="showLabel"
:for="id"
class="selectLabel"
:class="{ disabled }"
>
{{ label || placeholder }}
<FtTooltip
v-if="tooltip !== ''"
class="selectTooltip"
position="bottom"
:tooltip="tooltip"
/>
</label>
<button
v-if="showClearTextButton"
class="clearInputTextButton"
:class="{
visible: inputDataPresent || showOptions
}"
:aria-label="t('Search Bar.Clear Input')"
:title="t('Search Bar.Clear Input')"
@click="handleClearTextClick"
>
<FontAwesomeIcon
class="buttonIcon"
:icon="['fas', 'times-circle']"
/>
</button>
<span class="inputWrapper">
<input
:id="id"
ref="inputRef"
:value="inputDataDisplayed"
class="ft-input"
:class="{ disabled }"
:maxlength="maxlength"
:type="inputType"
:placeholder="placeholder"
:disabled="disabled"
:spellcheck="false"
:aria-label="showLabel ? null : placeholder"
@input="handleInput"
@focus="handleFocus"
@blur="handleInputBlur"
@keydown="handleKeyDown"
>
<button
v-if="showActionButton"
class="inputAction"
:class="{
enabled: inputDataPresent,
withLabel: showLabel
}"
@click="handleClick"
>
<FontAwesomeIcon
class="buttonIcon"
:icon="actionButtonIconName"
/>
</button>
</span>
<div class="options">
<ul
v-if="showOptions"
class="list"
@mouseenter="searchState.isPointerInList = true"
@mouseleave="searchState.isPointerInList = false"
>
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<li
v-for="(entry, index) in visibleDataList"
:key="index"
:class="{ hover: searchState.selectedOption === index }"
@click="handleOptionClick(index)"
@mouseenter="searchState.selectedOption = index"
@mouseleave="resetSelectedOption"
>
<div class="optionWrapper">
<FontAwesomeIcon
v-if="dataListProperties[index]?.iconName"
:icon="['fas', dataListProperties[index].iconName]"
class="searchResultIcon"
/>
<span>{{ entry }}</span>
</div>
<a
v-if="dataListProperties[index]?.isRemoveable"
class="removeButton"
:class="{ removeButtonSelected: removeButtonSelectedIndex === index }"
role="button"
:aria-label="t('Search Bar.Remove')"
href="javascript:void(0)"
@click.prevent.stop="handleRemoveClick(index)"
>
{{ t('Search Bar.Remove') }}
</a>
</li>
<!-- skipped -->
</ul>
</div>
</div>
</template>
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, reactive, ref, shallowRef, useId, useTemplateRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtTooltip from '../FtTooltip/FtTooltip.vue'
import store from '../../store/index'
import { isKeyboardEventKeyPrintableChar, isNullOrEmpty } from '../../helpers/strings'
const { t } = useI18n()
const props = defineProps({
inputType: {
type: String,
default: 'text'
},
placeholder: {
type: String,
required: true
},
label: {
type: String,
default: null
},
maxlength: {
type: Number,
default: null
},
value: {
type: String,
default: ''
},
showActionButton: {
type: Boolean,
default: true
},
forceActionButtonIconName: {
type: Array,
default: null
},
showClearTextButton: {
type: Boolean,
default: false
},
showLabel: {
type: Boolean,
default: false
},
isSearch: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
dataList: {
type: Array,
default: () => []
},
dataListProperties: {
type: Array,
default: () => []
},
searchResultIconNames: {
type: Array,
default: null
},
showDataWhenEmpty: {
type: Boolean,
default: false
},
tooltip: {
type: String,
default: ''
}
})
const emit = defineEmits(['clear', 'click', 'input', 'remove'])
const id = useId()
const inputRef = useTemplateRef('inputRef')
const inputData = ref(props.value)
const searchState = reactive({
showOptions: false,
selectedOption: -1,
isPointerInList: false,
keyboardSelectedOptionIndex: -1
})
const visibleDataList = ref(props.dataList)
const removeButtonSelectedIndex = ref(-1)
const removalMade = ref(false)
const actionButtonIconName = shallowRef(props.forceActionButtonIconName ?? ['fas', 'search'])
const showOptions = computed(() => {
return (inputData.value !== '' || props.showDataWhenEmpty) && visibleDataList.value.length > 0 && searchState.showOptions
})
const forceTextColor = computed(() => props.isSearch && store.getters.getBarColor)
const searchStateKeyboardSelectedOptionValue = computed(() => {
return searchState.keyboardSelectedOptionIndex === -1
? null
: visibleDataList.value[searchState.keyboardSelectedOptionIndex]
})
const inputDataDisplayed = computed(() => {
if (!props.isSearch) { return inputData.value }
/** @type {string | null | undefined} */
const selectedOptionValue = searchStateKeyboardSelectedOptionValue.value
if (selectedOptionValue != null && selectedOptionValue !== '') {
return selectedOptionValue
}
return inputData.value
})
const inputDataPresent = computed(() => inputDataDisplayed.value.length > 0)
watch(() => props.dataList, updateVisibleDataList, { deep: true })
watch(inputData, updateVisibleDataList)
watch(() => props.value, (value) => {
inputData.value = value
})
updateVisibleDataList()
/**
* @param {KeyboardEvent | MouseEvent} [event]
*/
function handleClick(event) {
const selectedValue = searchStateKeyboardSelectedOptionValue.value
const query = (selectedValue != null && selectedValue !== '') ? selectedValue : inputData.value
inputData.value = query
// No action if no input text
if (!inputDataPresent.value) {
return
}
searchState.showOptions = false
searchState.selectedOption = -1
searchState.keyboardSelectedOptionIndex = -1
removeButtonSelectedIndex.value = -1
emit('input', query)
emit('click', query, { event })
}
/**
* @param {string | InputEvent} data
*/
function handleInput(data) {
const text = typeof data === 'string' ? data : inputRef.value.value
inputData.value = text
if (
props.isSearch &&
searchState.selectedOption !== -1 &&
inputData.value === visibleDataList.value[searchState.selectedOption]
) {
return
}
handleActionIconChange()
emit('input', text)
}
function handleClearTextClick() {
// No action if no input text
if (!inputDataPresent.value) { return }
inputData.value = ''
handleActionIconChange()
updateVisibleDataList()
searchState.isPointerInList = false
inputRef.value.value = ''
// Focus on input element after text is clear for better UX
inputRef.value.focus()
emit('clear')
}
async function handleActionIconChange() {
// Only need to update icon if visible
if (!props.showActionButton) { return }
if (!inputDataPresent.value && props.forceActionButtonIconName === null) {
// Change back to default icon if text is blank
actionButtonIconName.value = ['fas', 'search']
return
}
// Update action button icon according to input
try {
const result = await store.dispatch('getYoutubeUrlInfo', inputData.value)
let isYoutubeLink = false
switch (result.urlType) {
case 'video':
case 'playlist':
case 'search':
case 'channel':
case 'hashtag':
case 'post':
case 'trending':
case 'subscriptions':
case 'history':
case 'userplaylists':
isYoutubeLink = true
break
case 'invalid_url':
default: {
// isYoutubeLink is already `false`
}
}
if (props.forceActionButtonIconName === null) {
if (isYoutubeLink) {
// Go to URL (i.e. Video/Playlist/Channel
actionButtonIconName.value = ['fas', 'arrow-right']
} else {
// Search with text
actionButtonIconName.value = ['fas', 'search']
}
}
} catch (ex) {
// On exception, consider text as invalid URL
if (props.forceActionButtonIconName === null) {
actionButtonIconName.value = ['fas', 'search']
}
// Rethrow exception
throw ex
}
}
/**
* @param {number} index
*/
function handleOptionClick(index) {
if (removeButtonSelectedIndex.value !== -1) {
handleRemoveClick(index)
return
}
searchState.showOptions = false
inputData.value = visibleDataList.value[index]
emit('input', inputData.value)
handleClick()
}
function resetSelectedOption() {
searchState.selectedOption = -1
removeButtonSelectedIndex.value = -1
}
/**
* @param {number} index
*/
function handleRemoveClick(index) {
if (!props.dataListProperties[index]?.isRemoveable) { return }
// keep input in focus even when the to-be-removed "Remove" button was clicked
inputRef.value.focus()
removalMade.value = true
emit('remove', visibleDataList.value[index])
}
/**
* @param {KeyboardEvent} event
*/
function handleKeyDown(event) {
// Update Input box value if enter key was pressed and option selected
if (event.key === 'Enter' && !event.isComposing) {
if (removeButtonSelectedIndex.value !== -1) {
handleRemoveClick(removeButtonSelectedIndex.value)
} else if (searchState.selectedOption !== -1) {
searchState.showOptions = false
event.preventDefault()
inputData.value = visibleDataList.value[searchState.selectedOption]
handleOptionClick(searchState.selectedOption)
} else {
handleClick(event)
}
return
}
if (visibleDataList.value.length === 0) { return }
searchState.showOptions = true
// "select" the Remove button through right arrow navigation, and unselect it with the left arrow
if (event.key === 'ArrowRight') {
removeButtonSelectedIndex.value = searchState.selectedOption
} else if (event.key === 'ArrowLeft') {
removeButtonSelectedIndex.value = -1
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault()
const newIndex = searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1)
updateSelectedOptionIndex(newIndex)
} else {
const selectedOptionValue = searchStateKeyboardSelectedOptionValue.value
// Keyboard selected & is char
if (!isNullOrEmpty(selectedOptionValue) && isKeyboardEventKeyPrintableChar(event.key)) {
// Update input based on KB selected suggestion value instead of current input value
event.preventDefault()
handleInput(`${selectedOptionValue}${event.key}`)
}
}
}
/**
* Updates the selected dropdown option index and handles the under/over-flow behavior
* @param {number} index
*/
function updateSelectedOptionIndex(index) {
searchState.selectedOption = index
// unset selection of "Remove" button
removeButtonSelectedIndex.value = -1
// Allow deselecting suggestion
if (searchState.selectedOption < -1) {
searchState.selectedOption = visibleDataList.value.length - 1
} else if (searchState.selectedOption > visibleDataList.value.length - 1) {
searchState.selectedOption = -1
}
// Update displayed value
searchState.keyboardSelectedOptionIndex = searchState.selectedOption
}
function handleInputBlur() {
if (!searchState.isPointerInList) {
searchState.showOptions = false
}
}
function handleFocus() {
searchState.showOptions = true
}
function updateVisibleDataList() {
// Reset selected option before it's updated
// Block resetting if it was just the "Remove" button that was pressed
if (!removalMade.value || searchState.selectedOption >= props.dataList.length) {
searchState.selectedOption = -1
searchState.keyboardSelectedOptionIndex = -1
removeButtonSelectedIndex.value = -1
}
removalMade.value = false
if (inputData.value.trim() === '') {
visibleDataList.value = props.dataList
return
}
// get list of items that match input
const lowerCaseInputData = inputData.value.toLowerCase()
visibleDataList.value = props.dataList.filter(x => {
return x.toLowerCase().includes(lowerCaseInputData)
})
}
defineExpose({
focus: () => {
inputRef.value?.focus()
},
blur: () => {
inputRef.value?.blur()
},
select: () => {
inputRef.value?.select()
},
/**
* @param {string} text
*/
setText: (text) => {
inputData.value = text
},
clear: () => {
handleClearTextClick()
}
})
</script>
<style scoped src="./FtInput.css" />

View File

@@ -55,7 +55,11 @@
}
.removeTagButton {
background-color: transparent;
border-style: none;
color: var(--primary-text-color);
font-size: 1em;
line-height: 1em;
opacity: 0.5;
padding: 10px;
padding-inline-start: 0;

View File

@@ -62,15 +62,15 @@
<span>{{ (tag.preferredName) ? tag.preferredName : tag.name }}</span>
</template>
<span v-else>{{ tag }}</span>
<FontAwesomeIcon
<button
v-if="!disabled"
:icon="['fas', 'fa-times']"
class="removeTagButton"
tabindex="0"
role="button"
@click="removeTag(tag)"
@keydown.enter.prevent="removeTag(tag)"
/>
>
<FontAwesomeIcon
:icon="['fas', 'fa-times']"
/>
</button>
</li>
</ul>
</div>
@@ -79,11 +79,10 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { ref } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { useId, useTemplateRef } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import { showToast } from '../../helpers/utils'
@@ -140,7 +139,7 @@ const { t } = useI18n()
const id = useId()
const tagNameInput = ref(null)
const tagNameInput = useTemplateRef('tagNameInput')
/**
* @param {string} text
@@ -168,7 +167,7 @@ async function updateTags(text) {
newList.push(trimmedText)
emit('change', newList)
// clear input box
tagNameInput.value.handleClearTextClick()
tagNameInput.value.clear()
}
/**
@@ -199,7 +198,7 @@ async function updateChannelTags(text) {
}
// clear input box
tagNameInput.value.handleClearTextClick()
tagNameInput.value.clear()
}
function removeTag(tag) {

View File

@@ -124,8 +124,6 @@ const localizedShortcutNameToShortcutsMappings = computed(() => {
[t('KeyboardShortcutPrompt.New Window'), ['NEW_WINDOW']],
[t('KeyboardShortcutPrompt.Minimize Window'), ['MINIMIZE_WINDOW']],
[t('KeyboardShortcutPrompt.Close Window'), ['CLOSE_WINDOW']],
[t('KeyboardShortcutPrompt.Restart Window'), ['RESTART_WINDOW']],
[t('KeyboardShortcutPrompt.Force Restart Window'), ['FORCE_RESTART_WINDOW']],
[t('KeyboardShortcutPrompt.Toggle Developer Tools'), ['TOGGLE_DEVTOOLS']],
[t('KeyboardShortcutPrompt.Reset Zoom'), ['RESET_ZOOM']],
[t('KeyboardShortcutPrompt.Zoom In'), ['ZOOM_IN']],

View File

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

View File

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

View File

@@ -172,7 +172,7 @@ const channelsHidden = computed(() => {
/** @type {string[]} */
const forbiddenTitles = computed(() => {
if (!props.hideForbiddenTitles) { return [] }
return JSON.parse(store.getters.getForbiddenTitles)
return JSON.parse(store.getters.getForbiddenTitles.toLowerCase())
})
const showResult = computed(() => {
@@ -203,13 +203,16 @@ const showResult = computed(() => {
return false
}
if (channelsHidden.value.some(ch => ch.name === props.data.authorId) || channelsHidden.value.some(ch => ch.name === props.data.author)) {
const lowerCaseAuthor = props.data.author?.toLowerCase()
if (channelsHidden.value.some(ch => ch.name === props.data.authorId) || channelsHidden.value.some(ch => ch.name === props.data.author) || (forbiddenTitles.value.some((text) => lowerCaseAuthor.includes(text)))) {
// hide videos by author
return false
}
const lowerCaseTitle = props.data.title?.toLowerCase()
if (forbiddenTitles.value.some((text) => lowerCaseTitle.includes(text.toLowerCase()))) {
if (forbiddenTitles.value.some((text) => lowerCaseTitle.includes(text))) {
return false
}
} else if (dataType === 'channel') {
@@ -223,14 +226,19 @@ const showResult = computed(() => {
props.data.authorId,
]
if (attrsToCheck.some(a => a != null && channelsHidden.value.some(ch => ch.name === a))) {
const lowerCaseName = props.data.name?.toLowerCase()
if ((attrsToCheck.some(a => a != null && channelsHidden.value.some(ch => ch.name === a))) ||
(forbiddenTitles.value.some((text) => lowerCaseName.includes(text)))) {
// hide channels by author
return false
}
} else if (dataType === 'playlist') {
const lowerCaseTitle = props.data.title?.toLowerCase()
const lowerCaseChannelName = props.data.channelName?.toLowerCase()
if (forbiddenTitles.value.some((text) => lowerCaseTitle.includes(text.toLowerCase()))) {
if ((forbiddenTitles.value.some((text) => lowerCaseTitle.includes(text))) ||
(forbiddenTitles.value.some((text) => lowerCaseChannelName.includes(text)))) {
return false
}

View File

@@ -130,15 +130,21 @@ const channelsHidden = computed(() => {
const forbiddenTitles = computed(() => {
if (!props.hideForbiddenTitles) { return [] }
return JSON.parse(store.getters.getForbiddenTitles)
return JSON.parse(store.getters.getForbiddenTitles.toLowerCase())
})
const hideChannelsBasedOnText = computed(() => {
return store.getters.getHideChannelsBasedOnText
})
const shouldBeVisible = computed(() => {
const lowerCaseTitle = props.data.title?.toLowerCase()
const lowerCaseAuthor = props.data.author?.toLowerCase()
return !(channelsHidden.value.some(ch => ch.name === props.data.authorId) ||
channelsHidden.value.some(ch => ch.name === props.data.author) ||
(lowerCaseTitle && forbiddenTitles.value.some((text) => lowerCaseTitle.includes(text.toLowerCase()))))
(lowerCaseTitle && forbiddenTitles.value.some((text) => lowerCaseTitle.includes(text))) ||
(hideChannelsBasedOnText.value && lowerCaseAuthor && forbiddenTitles.value.some((text) => lowerCaseAuthor.includes(text))))
})
/**

View File

@@ -7,12 +7,13 @@
color: var(--text-with-accent-color);
*/
margin: 4px;
padding: 16px;
padding-block: 3px 5px;
padding-inline: 16px;
box-shadow: 0 1px 2px rgb(0 0 0 / 10%);
position: relative;
cursor: pointer;
display: flex;
align-items: center;
}
.ftNotificationBanner:focus {
@@ -20,21 +21,25 @@
}
.message {
margin-inline-end: 25px;
cursor: pointer;
flex-grow: 1;
}
.bannerIcon {
position: absolute;
inset-block-start: 35%;
inset-inline-end: 10px;
.closeButton {
background-color: transparent;
border-style: none;
color: inherit;
cursor: pointer;
font-size: 1em;
line-height: 1em;
padding: 0.4em;
}
.closeIcon {
inline-size: 1em;
}
@media only screen and (width <= 680px) {
.bannerIcon {
inset-block-start: 27%;
block-size: 25px;
inline-size: 25px;
.closeButton {
font-size: 1.6em;
}
}

View File

@@ -9,27 +9,30 @@
@keydown.enter.prevent="handleClick"
@keydown.space.prevent="handleClick"
>
<div
<p
:id="id"
class="message"
>
<p :id="id">
{{ message }}
</p>
</div>
<FontAwesomeIcon
class="bannerIcon"
:icon="['fas', 'times']"
tabindex="0"
{{ message }}
</p>
<button
class="closeButton"
:aria-label="$t('Close Banner')"
:title="$t('Close Banner')"
@click.stop="handleClose"
@keydown.enter.space.stop.prevent="handleClose"
/>
@click="handleClose"
@keydown.enter.space.stop
>
<FontAwesomeIcon
class="closeIcon"
:icon="['fas', 'times']"
/>
</button>
</div>
</template>
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { useId } from '../../composables/use-id-polyfill'
import { useId } from 'vue'
defineProps({
message: {

View File

@@ -101,14 +101,14 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'
import FtButton from '../FtButton/FtButton.vue'
import FtPlaylistSelector from '../FtPlaylistSelector/FtPlaylistSelector.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtSelect from '../FtSelect/FtSelect.vue'
import FtToggleSwitch from '../FtToggleSwitch/FtToggleSwitch.vue'
@@ -282,7 +282,7 @@ const anyPlaylistContainsVideosToBeAdded = computed(() => {
return playlistIdsContainingVideosToBeAdded.value.size > 0
})
const searchBar = ref(null)
const searchBar = useTemplateRef('searchBar')
watch(allPlaylistsLength, (val, oldVal) => {
const allPlaylistIds = new Set()
@@ -395,19 +395,10 @@ function addSelectedToPlaylists() {
addedPlaylistIds.add(playlist._id)
})
let message
if (addedPlaylistIds.size === 1) {
message = t('User Playlists.AddVideoPrompt.Toast.{videoCount} video(s) added to 1 playlist', {
videoCount: toBeAddedToPlaylistVideoCount.value,
}, toBeAddedToPlaylistVideoCount.value)
} else {
message = t('User Playlists.AddVideoPrompt.Toast.{videoCount} video(s) added to {playlistCount} playlists', {
videoCount: toBeAddedToPlaylistVideoCount.value,
playlistCount: addedPlaylistIds.size,
}, toBeAddedToPlaylistVideoCount.value)
}
showToast(t('User Playlists.AddVideoPrompt.Toast.Video(s) added to {playlistCount} playlists', {
playlistCount: addedPlaylistIds.size,
}, addedPlaylistIds.size))
showToast(message)
hide()
}

View File

@@ -25,8 +25,7 @@
</template>
<script setup>
import { computed } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, useId } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { getFirstCharacter } from '../../helpers/strings'
@@ -60,7 +59,7 @@ const translatedProfileName = computed(() => {
const profileInitial = computed(() => {
return props.profileName
? getFirstCharacter(translatedProfileName.value, locale.value).toUpperCase()
? getFirstCharacter(translatedProfileName.value, locale.value)
: ''
})

View File

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

View File

@@ -48,7 +48,7 @@
:show-action-button="false"
:maxlength="100"
@input="profileName = $event"
@keydown.enter.native="saveProfile"
@keydown.enter="saveProfile"
/>
</div>
<div>
@@ -114,14 +114,14 @@ import { useI18n } from '../../composables/use-i18n-polyfill'
import FtCard from '../ft-card/ft-card.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtButton from '../FtButton/FtButton.vue'
import store from '../../store/index'
import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, colors } from '../../helpers/colors'
import { showToast } from '../../helpers/utils'
import { deepCopy, showToast } from '../../helpers/utils'
import { getFirstCharacter } from '../../helpers/strings'
/**
@@ -182,7 +182,7 @@ const translatedProfileName = computed(() => {
const profileInitial = computed(() => {
return profileName.value
? getFirstCharacter(translatedProfileName.value, locale.value).toUpperCase()
? getFirstCharacter(translatedProfileName.value, locale.value)
: ''
})
@@ -204,7 +204,7 @@ function saveProfile() {
name: profileName.value,
bgColor: profileBgColor.value,
textColor: profileTextColor.value,
subscriptions: props.profile.subscriptions
subscriptions: deepCopy(props.profile.subscriptions)
}
if (!props.isNew) {

View File

@@ -26,8 +26,8 @@
ref="profileListRef"
class="profileList"
tabindex="-1"
@focusout.native="handleProfileListFocusOut"
@keydown.native.esc.stop="handleProfileListEscape"
@focusout="handleProfileListFocusOut"
@keydown.esc.stop="handleProfileListEscape"
>
<h3
:id="id + 'title'"
@@ -80,10 +80,9 @@
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, nextTick, ref, useId, useTemplateRef } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router/composables'
import { useRouter } from 'vue-router'
import FtCard from '../ft-card/ft-card.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
@@ -120,7 +119,7 @@ const activeProfile = computed(() => store.getters.getActiveProfile)
const activeProfileInitial = computed(() => {
return activeProfile.value?.name
? getFirstCharacter(translateProfileName(activeProfile.value), locale.value).toUpperCase()
? getFirstCharacter(translateProfileName(activeProfile.value), locale.value)
: ''
})
@@ -130,7 +129,7 @@ const profileInitials = computed(() => {
return profileList.value.reduce((initials, profile) => {
initials[profile._id] = profile?.name
? getFirstCharacter(translateProfileName(profile), locale_).toUpperCase()
? getFirstCharacter(translateProfileName(profile), locale_)
: ''
return initials
@@ -144,7 +143,7 @@ function isActiveProfile(profile) {
return profile._id === activeProfile.value._id
}
const profileListRef = ref(null)
const profileListRef = useTemplateRef('profileListRef')
function toggleProfileList() {
profileListShown.value = !profileListShown.value
@@ -179,8 +178,7 @@ function handleProfileListFocusOut() {
}
}
/** @type {import('vue').Ref<HTMLDivElement | null>} */
const iconButton = ref(null)
const iconButton = useTemplateRef('iconButton')
function handleProfileListEscape() {
iconButton.value?.focus()

View File

@@ -1,5 +1,5 @@
<template>
<portal to="promptPortal">
<Teleport to=".app">
<div
class="prompt"
tabindex="-1"
@@ -52,12 +52,11 @@
</slot>
</FtCard>
</div>
</portal>
</Teleport>
</template>
<script setup>
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { nextTick, onBeforeUnmount, onMounted, useId, useTemplateRef } from 'vue'
import store from '../../store/index'
@@ -104,7 +103,7 @@ const emit = defineEmits(['click'])
const id = useId()
const promptCard = ref(null)
const promptCard = useTemplateRef('promptCard')
let promptButtons = []
let lastActiveElement = null
@@ -112,6 +111,7 @@ let lastActiveElement = null
onMounted(() => {
lastActiveElement = document.activeElement
document.addEventListener('keydown', handleEscape, true)
store.commit('addOpenPrompt', id)
nextTick(() => {
promptButtons = Array.from(promptCard.value.$el.querySelectorAll('.btn.ripple, .iconButton'))
@@ -121,6 +121,7 @@ onMounted(() => {
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleEscape, true)
store.commit('removeOpenPrompt', id)
nextTick(() => lastActiveElement?.focus())
})

View File

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

View File

@@ -1,5 +1,5 @@
.center {
margin-block: 5px 10px;
margin-block-start: 10px;
text-align: center;
user-select: none;
}
@@ -18,6 +18,42 @@
justify-content: center;
}
.titleContainer {
display: flex;
align-items: baseline;
justify-content: center;
gap: 0.5rem;
block-size: inherit;
}
.clearFilterButton {
border-radius: 50%;
border-style: none;
background-color: transparent;
color: var(--primary-text-color);
cursor: pointer;
font-size: 20px;
line-height: 1em;
padding: 10px;
transition: background 0.2s ease-out;
&:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
transition: background 0.2s ease-in;
}
&:active {
background-color: var(--tertiary-text-color);
color: var(--side-nav-active-text-color);
transition: background 0.2s ease-in;
}
}
.clearFilterIcon {
vertical-align: -0.25em;
}
@media only screen and (width <= 680px) {
.searchRadio {
border-inline-end: 0;

View File

@@ -4,12 +4,25 @@
@click="hideSearchFilters"
>
<template #label="{ labelId }">
<h2
:id="labelId"
class="center"
>
{{ title }}
</h2>
<div class="titleContainer">
<h2
:id="labelId"
class="center"
>
{{ title }}
</h2>
<button
class="clearFilterButton"
:title="$t('Search Filters.Clear Filters')"
:style="{visibility: (searchFilterValueChanged ? 'visible' : 'hidden')}"
@click="clearFilters"
>
<FontAwesomeIcon
class="clearFilterIcon"
:icon="['fas', 'filter-circle-xmark']"
/>
</button>
</div>
</template>
<FtFlexBox class="radioFlexBox">
@@ -50,12 +63,6 @@
/>
</FtFlexBox>
<div class="searchFilterCloseButtonContainer">
<FtButton
:label="$t('Search Filters.Clear Filters')"
background-color="var(--accent-color)"
text-color="var(--text-with-accent-color)"
@click="clearFilters"
/>
<FtButton
:label="$t('Close')"
background-color="null"

View File

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

View File

@@ -16,14 +16,14 @@
background-color: var(--card-bg-color);
padding-block: 10px;
> div {
> :deep(div) {
box-sizing: border-box;
padding-block: 0;
padding-inline: 20px;
inline-size: 100%;
}
> div:not(:last-child, .ft-flex-box) {
> :deep(div:not(:last-child, .ft-flex-box)) {
@media only screen and (width <= 800px) {
margin-block-end: 20px;
}
@@ -36,10 +36,6 @@
margin-block: 0.5em;
}
.dataSettingsBox {
justify-content: center;
}
:deep(.groupTitle) {
text-align: center;
margin-block: 0.5em;
@@ -90,10 +86,8 @@
@media only screen and (width <= 680px) {
.settingsSection {
> div {
:deep(.text.bottom) {
inset-inline-start: -85px;
}
> :deep(div .text.bottom) {
inset-inline-start: -85px;
}
:deep(.switch-ctn.containsTooltip) {
@@ -103,16 +97,14 @@
padding-inline: 10px;
}
:not(.select, .selectLabel) {
> :deep(.tooltip) {
display: inline-block;
position: absolute;
inset-inline-end: -25px;
inset-block-start: 12px;
}
:deep(:not(.select, .selectLabel) > .tooltip) {
display: inline-block;
position: absolute;
inset-inline-end: -25px;
inset-block-start: 12px;
}
.settingsFlexStart460px :deep(.tooltip) {
:deep(.settingsFlexStart460px .tooltip) {
inset-inline-end: 0;
inset-block-start: -2px;
}

View File

@@ -3,6 +3,7 @@
ref="iconButton"
:title="shareTitle"
theme="secondary"
:size="size"
:icon="['fas', 'share-alt']"
:dropdown-modal-on-mobile="true"
dropdown-position-x="left"
@@ -124,7 +125,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref } from 'vue'
import { computed, ref, useTemplateRef } from 'vue'
import { copyToClipboard, openExternalLink } from '../../helpers/utils'
import { useI18n } from '../../composables/use-i18n-polyfill'
@@ -161,11 +162,15 @@ const props = defineProps({
dropdownPositionY: {
type: String,
default: 'bottom'
},
size: {
type: Number,
default: 20
}
})
const includeTimestamp = ref(false)
const iconButton = ref(null)
const iconButton = useTemplateRef('iconButton')
const isChannel = computed(() => {
return props.shareTargetType === 'Channel'
@@ -175,6 +180,10 @@ const isPlaylist = computed(() => {
return props.shareTargetType === 'Playlist'
})
const isPost = computed(() => {
return props.shareTargetType === 'Post'
})
const isVideo = computed(() => {
return props.shareTargetType === 'Video'
})
@@ -186,6 +195,9 @@ const shareTitle = computed(() => {
if (isPlaylist.value) {
return t('Share.Share Playlist')
}
if (isPost.value) {
return t('Share.Share Post')
}
return t('Share.Share Video')
})
@@ -216,6 +228,9 @@ const invidiousURL = computed(() => {
if (isPlaylist.value) {
return `${currentInvidiousInstanceUrl.value}/playlist?list=${props.id}`
}
if (isPost.value) {
return `${currentInvidiousInstanceUrl.value}/post/${props.id}`
}
let videoUrl = `${currentInvidiousInstanceUrl.value}/watch?v=${props.id}`
// `playlistId` can be undefined
if (playlistSharable.value) {
@@ -247,6 +262,9 @@ const youtubeURL = computed(() => {
if (isPlaylist.value) {
return youtubePlaylistUrl.value
}
if (isPost.value) {
return `https://www.youtube.com/post/${props.id}`
}
let videoUrl = `https://www.youtube.com/watch?v=${props.id}`
if (playlistSharable.value) {
// `index` seems can be ignored
@@ -262,6 +280,9 @@ const youtubeShareURL = computed(() => {
if (isPlaylist.value) {
return youtubePlaylistUrl.value
}
if (isPost.value) {
return `https://www.youtube.com/post/${props.id}`
}
const videoUrl = `https://youtu.be/${props.id}`
if (playlistSharable.value) {
// `index` seems can be ignored
@@ -326,7 +347,7 @@ function updateIncludeTimestamp() {
}
function getFinalUrl(url) {
if (isChannel.value || isPlaylist.value) {
if (isChannel.value || isPlaylist.value || isPost.value) {
return url
}
if (url.indexOf('?') === -1) {

View File

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

View File

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

View File

@@ -90,8 +90,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref, shallowRef } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, ref, shallowRef, useId, useTemplateRef } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtButton from '../FtButton/FtButton.vue'
@@ -170,7 +169,7 @@ const profileInitials = computed(() => {
return profileList.value.reduce((accumulator, profile) => {
accumulator[profile._id] = profile.name
? getFirstCharacter(profile.name, locale_).toUpperCase()
? getFirstCharacter(profile.name, locale_)
: ''
return accumulator
@@ -183,7 +182,7 @@ const hideChannelSubscriptions = computed(() => {
})
const subscribedText = computed(() => {
let subscribedValue = (isProfileSubscribed(activeProfile.value) ? t('Channel.Unsubscribe') : t('Channel.Subscribe')).toUpperCase()
let subscribedValue = (isProfileSubscribed(activeProfile.value) ? t('Channel.Unsubscribe') : t('Channel.Subscribe'))
if (props.subscriptionCountText !== '' && !hideChannelSubscriptions.value) {
subscribedValue += ' ' + props.subscriptionCountText
}
@@ -243,10 +242,10 @@ function handleSubscription(profile) {
}
}
const subscribeButton = ref(null)
const subscribeButton = useTemplateRef('subscribeButton')
function handleProfileDropdownFocusOut() {
if (!subscribeButton.value.matches(':focus-within')) {
if (subscribeButton.value && !subscribeButton.value.matches(':focus-within')) {
isProfileDropdownOpen.value = false
}
}

View File

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

View File

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

View File

@@ -79,6 +79,10 @@
}
}
.containsTooltip .switch-label-text {
margin-inline-end: 5px;
}
.disabled {
.switch-label {
cursor: not-allowed;

View File

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

View File

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

View File

@@ -16,16 +16,16 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, useTemplateRef } from 'vue'
import FtCard from '../ft-card/ft-card.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import store from '../../store/index'
const emit = defineEmits(['unlocked'])
const password = ref(null)
const password = useTemplateRef('password')
onMounted(() => {
password.value.focus()

View File

@@ -22,7 +22,7 @@
input-type="password"
:value="password"
@input="e => password = e"
@keydown.enter.native="handleSetPassword"
@keydown.enter="handleSetPassword"
/>
<FtButton
class="centerButton"
@@ -37,7 +37,7 @@
import { computed, ref } from 'vue'
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtButton from '../FtButton/FtButton.vue'

View File

@@ -264,7 +264,7 @@ import FtToggleSwitch from '../FtToggleSwitch/FtToggleSwitch.vue'
import FtSlider from '../FtSlider/FtSlider.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtButton from '../FtButton/FtButton.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtTooltip from '../FtTooltip/FtTooltip.vue'
import store from '../../store/index'

View File

@@ -27,6 +27,18 @@
.playlistTitle {
margin-block-end: 0.1em;
/* Prevents overflow for long values */
overflow-wrap: anywhere;
@media only screen and (width <= 850px) {
// margin-inline-in routerView = 8px x2 = 16px
// For unknown reason class `routerView` is in 2 containers
// padding for `playlistInfo` is 10px x2 = 20px
// Also scrollbar got unknown width so using 95vw instead of 100vw
max-inline-size: calc(95vw - 32px - 20px);
text-overflow: ellipsis;
}
}
.playlistDescription {
@@ -99,7 +111,6 @@
.playlistDescription {
overflow-x: hidden;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
max-inline-size: 750px;
inline-size: 100%;

View File

@@ -45,7 +45,7 @@
:value="newTitle"
:maxlength="255"
@input="handlePlaylistNameInput"
@keydown.enter.native="savePlaylistInfo"
@keydown.enter="savePlaylistInfo"
/>
<FtFlexBox v-if="inputPlaylistNameBlank">
<p>
@@ -67,14 +67,14 @@
{{ title }}
</h2>
<p>
{{ $tc('Global.Counts.Video Count', videoCount, { count: parsedVideoCount }) }}
<span v-if="!hideViews && !isUserPlaylist">
- {{ $tc('Global.Counts.View Count', viewCount, { count: parsedViewCount }) }}
</span>
<span>- </span>
<span v-if="infoSource !== 'local'">
{{ t('Global.Counts.Video Count', { count: parsedVideoCount }, videoCount) }}
<template v-if="!hideViews && !isUserPlaylist">
- {{ t('Global.Counts.View Count', { count: parsedViewCount }, viewCount) }}
</template>
-
<template v-if="infoSource !== 'local'">
{{ $t("Playlist.Last Updated On") }}
</span>
</template>
{{ lastUpdated }}
<template v-if="durationFormatted !== ''">
<br>
@@ -92,7 +92,7 @@
:show-label="false"
:value="newDescription"
@input="(input) => newDescription = input"
@keydown.enter.native="savePlaylistInfo"
@keydown.enter="savePlaylistInfo"
/>
<p
v-else
@@ -263,13 +263,13 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router/composables'
import { useRouter } from 'vue-router'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'
import FtShareButton from '../FtShareButton/FtShareButton.vue'
@@ -282,6 +282,7 @@ import {
showToast,
getTodayDateStrLocalTimezone,
writeFileWithPicker,
deepCopy,
} from '../../helpers/utils'
import thumbnailPlaceholder from '../../assets/img/thumbnail_placeholder.svg'
@@ -481,7 +482,7 @@ const userPlaylistAnyVideoWatched = computed(() => {
const historyCacheById_ = historyCacheById.value
return selectedUserPlaylist.value.videos.some((video) => {
return Object.hasOwn(historyCacheById_, video.videoId)
return historyCacheById_[video.videoId] !== undefined
})
})
@@ -594,7 +595,7 @@ async function savePlaylistInfo() {
playlistName: newTitle.value,
protected: selectedUserPlaylist.value.protected,
description: newDescription.value,
videos: selectedUserPlaylist.value.videos,
videos: deepCopy(selectedUserPlaylist.value.videos),
_id: props.id,
}
try {
@@ -608,7 +609,7 @@ async function savePlaylistInfo() {
}
}
const playlistTitleInput = ref(null)
const playlistTitleInput = useTemplateRef('playlistTitleInput')
function enterEditMode() {
newTitle.value = props.title
@@ -740,7 +741,7 @@ const userPlaylistWatchedVideoCount = computed(() => {
const historyCacheById_ = historyCacheById.value
return selectedUserPlaylist.value.videos.reduce((count, video) => {
return Object.hasOwn(historyCacheById_, video.videoId) ? count + 1 : count
return historyCacheById_[video.videoId] !== undefined ? count + 1 : count
}, 0)
})
@@ -787,7 +788,7 @@ async function handleRemoveDuplicateVideosPromptAnswer(option) {
playlistName: props.title,
protected: selectedUserPlaylist.value.protected,
description: props.description,
videos: newVideoItems,
videos: deepCopy(newVideoItems),
_id: props.id,
}
try {
@@ -824,7 +825,7 @@ async function handleRemoveVideosOnWatchPromptAnswer(option) {
playlistName: props.title,
protected: selectedUserPlaylist.value.protected,
description: props.description,
videos: videosToWatch,
videos: deepCopy(videosToWatch),
_id: props.id
}
try {
@@ -892,7 +893,7 @@ const updateQueryDebounced = debounce((newQuery) => {
emit('search-video-query-change', newQuery)
}, 500)
const searchInput = ref(null)
const searchInput = useTemplateRef('searchInput')
/**
* @param {KeyboardEvent} event

View File

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

View File

@@ -55,7 +55,7 @@
</p>
</router-link>
<router-link
v-if="!hideTrendingVideos"
v-if="SUPPORTS_LOCAL_API && !hideTrendingVideos && (backendFallback || backendPreference === 'local')"
class="navOption mobileHidden"
role="button"
to="/trending"
@@ -200,6 +200,7 @@
<hr>
<div
v-if="!hideActiveSubscriptions"
class="mobileHidden"
>
<router-link
v-for="channel in activeSubscriptions"
@@ -255,6 +256,8 @@ import { KeyboardShortcuts } from '../../../constants'
const { locale, t } = useI18n()
const SUPPORTS_LOCAL_API = process.env.SUPPORTS_LOCAL_API
/** @type {import('vue').ComputedRef<boolean>} */
const isOpen = computed(() => {
return store.getters.getIsSideNavOpen

View File

@@ -56,7 +56,7 @@
</p>
</router-link>
<router-link
v-if="!hideTrendingVideos"
v-if=" SUPPORTS_LOCAL_API && trendingVisible"
class="navOption"
:title="$t('Trending.Trending')"
:aria-label="hideLabelsSideBar ? $t('Trending.Trending') : null"
@@ -197,18 +197,21 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import router from '../../router/index.js'
import { computed, ref, onMounted, onBeforeUnmount, useTemplateRef } from 'vue'
import { useRouter } from 'vue-router'
import store from '../../store/index'
const SUPPORTS_LOCAL_API = process.env.SUPPORTS_LOCAL_API
const openMoreOptions = ref(false)
const menuRef = ref(null)
const menuRef = useTemplateRef('menuRef')
/** @type {import('vue').ComputedRef<boolean>} */
const hideTrendingVideos = computed(() => {
return store.getters.getHideTrendingVideos
const trendingVisible = computed(() => {
return !store.getters.getHideTrendingVideos &&
(store.getters.getBackendFallback || store.getters.getBackendPreference === 'local')
})
/** @type {import('vue').ComputedRef<boolean>} */
@@ -238,6 +241,8 @@ function handleClickOutside(event) {
}
}
const router = useRouter()
onMounted(() => {
document.addEventListener('click', handleClickOutside)
router.afterEach(() => {

View File

@@ -74,7 +74,7 @@ import { computed } from 'vue'
import FtSettingsSection from './FtSettingsSection/FtSettingsSection.vue'
import FtToggleSwitch from './FtToggleSwitch/FtToggleSwitch.vue'
import FtInput from './ft-input/ft-input.vue'
import FtInput from './FtInput/FtInput.vue'
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'
import FtSponsorBlockCategory from './FtSponsorBlockCategory/FtSponsorBlockCategory.vue'

View File

@@ -155,6 +155,11 @@ function loadPostsFromCacheSometimes() {
isLoading.value = false
}
/** @type {import('vue').ComputedRef<string[]>} */
const forbiddenTitles = computed(() => {
return JSON.parse(store.getters.getForbiddenTitles.toLowerCase())
})
function loadPostsFromCacheForAllActiveProfileChannels() {
const postList_ = cacheEntriesForAllActiveProfileChannels.value.flatMap((cacheEntry) => {
return cacheEntry.posts
@@ -164,7 +169,7 @@ function loadPostsFromCacheForAllActiveProfileChannels() {
return b.publishedTime - a.publishedTime
})
postList.value = postList_
postList.value = postList_.filter(post => !forbiddenTitles.value.some(text => post.author.toLowerCase().includes(text)))
isLoading.value = false
}
@@ -224,6 +229,7 @@ async function loadPostsForSubscriptionsFromRemote() {
}
}
posts = posts.filter(post => !forbiddenTitles.value.some(text => post.author.toLowerCase().includes(text)))
return posts
}))).flat()

View File

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

View File

@@ -128,6 +128,7 @@ const BASE_THEME_VALUES = [
'pastelPink',
// Third group
'catppuccinFrappe',
'catppuccinLatte',
'catppuccinMocha',
'dracula',
'everforestDarkHard',
@@ -154,6 +155,7 @@ const baseThemeNames = computed(() => [
t('Settings.Theme Settings.Base Theme.Pastel Pink'),
// Third group
t('Settings.Theme Settings.Base Theme.Catppuccin Frappe'),
t('Settings.Theme Settings.Base Theme.Catppuccin Latte'),
t('Settings.Theme Settings.Base Theme.Catppuccin Mocha'),
t('Settings.Theme Settings.Base Theme.Dracula'),
t('Settings.Theme Settings.Base Theme.Everforest Dark Hard'),

View File

@@ -39,7 +39,7 @@
}
}
.menuIcon {
.menuButton {
@media only screen and (width <= 680px) {
display: none;
}
@@ -54,22 +54,24 @@
user-select: none;
}
.navIcon,
:deep(.ftIconButton:not(.arrowDisabled) .iconButton) {
.navButton {
border-radius: 50%;
border-style: none;
background-color: transparent;
color: var(--primary-text-color);
cursor: pointer;
font-size: 20px;
block-size: 1em;
line-height: 1em;
padding: 10px;
transition: background 0.2s ease-out;
inline-size: 1em;
}
.navButton,
:deep(.ftIconButton:not(.arrowDisabled) .iconButton) {
&:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
transition: background 0.2s ease-in;
}
&:active {
@@ -79,7 +81,11 @@
}
}
.topNavBarColor .navIcon,
.navIcon {
inline-size: 1em;
}
.topNavBarColor .navButton,
.topNavBarColor :deep(.ftIconButton:not(.arrowDisabled) .iconButton) {
color: var(--text-with-main-color);
@@ -93,7 +99,7 @@
}
.navFilterIcon {
.navFilterButton {
$effect-distance: 20px;
margin-inline-start: $effect-distance;
@@ -121,13 +127,13 @@
justify-content: flex-end;
}
.navSearchIcon {
.navSearchButton {
@media only screen and (width >= 681px) {
display: none;
}
}
.navNewWindowIcon {
.navNewWindowButton {
@media only screen and (width <= 680px) {
display: none;
}

View File

@@ -5,14 +5,15 @@
role="navigation"
>
<div class="side">
<FontAwesomeIcon
class="menuIcon navIcon"
:icon="['fas', 'bars']"
role="button"
tabindex="0"
<button
class="menuButton navButton"
@click="toggleSideNav"
@keydown.enter.prevent="toggleSideNav"
/>
>
<FontAwesomeIcon
class="navIcon"
:icon="['fas', 'bars']"
/>
</button>
<FtIconButton
class="navIconButton"
:disabled="isArrowBackwardDisabled"
@@ -26,7 +27,6 @@
open-on-right-or-long-click
:title="backwardText"
@click="historyBack"
@keydown.enter.prevent="historyBack"
/>
<FtIconButton
class="navIconButton"
@@ -41,26 +41,28 @@
open-on-right-or-long-click
:title="forwardText"
@click="historyForward"
@keydown.enter.prevent="historyForward"
/>
<FontAwesomeIcon
<button
v-if="!hideSearchBar"
class="navSearchIcon navIcon"
:icon="['fas', 'search']"
role="button"
tabindex="0"
class="navSearchButton navButton"
@click="toggleSearchContainer"
@keydown.enter.prevent="toggleSearchContainer"
/>
<FontAwesomeIcon
class="navNewWindowIcon navIcon"
:icon="['fas', 'clone']"
>
<FontAwesomeIcon
class="navIcon"
:icon="['fas', 'search']"
/>
</button>
<button
class="navNewWindowButton navButton"
:aria-label="t('Open New Window')"
:title="newWindowText"
role="button"
tabindex="0"
@click="createNewWindow"
@keydown.enter.prevent="createNewWindow"
/>
>
<FontAwesomeIcon
class="navIcon"
:icon="['fas', 'clone']"
/>
</button>
<div
v-if="!hideHeaderLogo"
class="logo"
@@ -89,7 +91,7 @@
>
<FtInput
ref="searchInput"
:placeholder="$t('Search / Go to URL')"
:placeholder="t('Search / Go to URL')"
class="searchInput"
is-search
:data-list="activeDataList"
@@ -101,16 +103,18 @@
@clear="clearLastSuggestionQuery"
@remove="removeSearchHistoryEntryInDbAndCache"
/>
<FontAwesomeIcon
class="navFilterIcon navIcon"
<button
class="navFilterButton navButton"
:class="{ filterChanged: searchFilterValueChanged }"
:icon="['fas', 'filter']"
:title="$t('Search Filters.Search Filters')"
role="button"
tabindex="0"
:aria-label="t('Search Filters.Search Filters')"
:title="t('Search Filters.Search Filters')"
@click="showSearchFilters"
@keydown.enter.prevent="showSearchFilters"
/>
>
<FontAwesomeIcon
class="navIcon"
:icon="['fas', 'filter']"
/>
</button>
</div>
</div>
<FtProfileSelector class="side profiles" />
@@ -119,11 +123,11 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRoute, useRouter } from 'vue-router/composables'
import { useRoute, useRouter } from 'vue-router'
import FtInput from '../ft-input/ft-input.vue'
import FtInput from '../FtInput/FtInput.vue'
import FtProfileSelector from '../FtProfileSelector/FtProfileSelector.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
@@ -365,10 +369,8 @@ function showSearchFilters() {
store.dispatch('showSearchFilters')
}
/** @type {import('vue').Ref<HTMLDivElement | null>} */
const searchContainer = ref(null)
/** @type {import('vue').Ref<InstanceType<typeof FtInput> | null>} */
const searchInput = ref(null)
const searchContainer = useTemplateRef('searchContainer')
const searchInput = useTemplateRef('searchInput')
/** @type {import('vue').ComputedRef<any>} */
const searchSettings = computed(() => store.getters.getSearchSettings)
@@ -493,7 +495,8 @@ function goToSearch(queryText, { event }) {
time: searchSettings.value.time,
type: searchSettings.value.type,
duration: searchSettings.value.duration,
features: searchSettings.value.features,
// Array proxy cannot be cloned during IPC call
features: [...searchSettings.value.features],
},
doCreateNewWindow,
searchQueryText: queryText,
@@ -516,7 +519,7 @@ function clearLastSuggestionQuery() {
* @param {string} text
*/
function updateSearchInputText(text) {
searchInput.value?.updateInputData(text)
searchInput.value?.setText(text)
}
/**

View File

@@ -32,7 +32,6 @@
<div
v-for="(chapter, index) in chapters"
:key="index"
:ref="index === currentIndex ? 'currentChaptersItem' : null"
class="chapter"
role="button"
tabindex="0"
@@ -65,7 +64,7 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, useTemplateRef, watch } from 'vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import FtCard from '../ft-card/ft-card.vue'
@@ -87,11 +86,7 @@ const props = defineProps({
const emit = defineEmits(['timestamp-event'])
/** @type {import('vue').Ref<HTMLDivElement | null>} */
const chaptersWrapper = ref(null)
/** @type {import('vue').Ref<HTMLDivElement[]>} */
const currentChaptersItem = ref([])
const chaptersWrapper = useTemplateRef('chaptersWrapper')
let chaptersVisible = false
const currentIndex = ref(props.currentChapterIndex)
@@ -181,9 +176,9 @@ function chaptersToggled(event) {
function scrollToCurrentChapter() {
const container = chaptersWrapper.value
const currentItem = currentChaptersItem.value[0]
const currentItem = container ? Array.from(container.children)[currentIndex.value] : null
if (container != null && currentItem != null) {
if (currentItem != null) {
container.scrollTop = currentItem.offsetTop - container.offsetTop
}
}

View File

@@ -20,7 +20,7 @@
:input-html="processedShownDescription"
:link-tab-index="linkTabIndex"
@timestamp-event="onTimestamp"
@click.native="expandDescriptionWithClick"
@click="expandDescriptionWithClick"
/>
<span
v-if="license && showFullDescription"
@@ -45,7 +45,7 @@
<script setup>
import autolinker from 'autolinker'
import { onMounted, ref, computed } from 'vue'
import { onMounted, ref, computed, useTemplateRef } from 'vue'
import FtCard from '../ft-card/ft-card.vue'
import FtTimestampCatcher from '../FtTimestampCatcher.vue'
@@ -67,7 +67,7 @@ const props = defineProps({
const emit = defineEmits(['timestamp-event'])
let shownDescription = ''
const descriptionContainer = ref()
const descriptionContainer = useTemplateRef('descriptionContainer')
const showFullDescription = ref(false)
const showControls = ref(false)

View File

@@ -0,0 +1,570 @@
<template>
<FtCard class="watchVideoInfo">
<div>
<h1
class="videoTitle"
>
{{ title }}
</h1>
<div
v-if="isUnlisted"
class="unlistedBadge"
>
{{ t('Video.Unlisted') }}
</div>
</div>
<div class="videoMetrics">
<div class="datePublishedAndViewCount">
{{ publishedString }} {{ dateString }}
<template
v-if="!hideVideoViews"
>
<span class="seperator"> </span><span class="videoViews">{{ parsedViewCount }}</span>
</template>
</div>
<div
v-if="!hideVideoLikesAndDislikes"
class="likeBarContainer"
>
<div
class="likeSection"
>
<span class="likeCount"><FontAwesomeIcon :icon="['fas', 'thumbs-up']" /> {{ parsedLikeCount }}</span>
</div>
</div>
</div>
<div class="videoButtons">
<div
class="profileRow"
>
<div
v-if="!hideUploader"
>
<RouterLink
:to="`/channel/${channelId}`"
>
<img
:src="channelThumbnail"
class="channelThumbnail"
alt=""
>
</RouterLink>
</div>
<div>
<div
v-if="!hideUploader"
>
<RouterLink
:to="`/channel/${channelId}`"
class="channelName"
>
{{ channelName }}
</RouterLink>
</div>
<FtSubscribeButton
v-if="!hideUnsubscribeButton"
:channel-id="channelId"
:channel-name="channelName"
:channel-thumbnail="channelThumbnail"
:subscription-count-text="subscriptionCountText"
/>
</div>
</div>
<div class="videoOptions">
<span class="videoOptionsMobileRow">
<FtIconButton
v-if="showPlaylists && !isUpcoming"
:title="t('User Playlists.Add to Playlist')"
:icon="['fas', 'plus']"
theme="base"
@click="togglePlaylistPrompt"
/>
<FtIconButton
v-if="isQuickBookmarkEnabled"
:title="quickBookmarkIconText"
:icon="isInQuickBookmarkPlaylist ? ['fas', 'check'] : ['fas', 'bookmark']"
class="quickBookmarkVideoIcon"
:class="{
bookmarked: isInQuickBookmarkPlaylist,
}"
:theme="quickBookmarkIconTheme"
@click="toggleQuickBookmarked"
/>
<FtIconButton
v-if="canSaveWatchedProgress && watchedProgressSavingInSemiAutoMode"
:title="t('Video.Save Watched Progress')"
:icon="['fas', 'bars-progress']"
@click="saveWatchedProgressManually"
/>
</span>
<span class="videoOptionsMobileRow">
<FtIconButton
v-if="USING_ELECTRON && externalPlayer !== ''"
:title="t('Video.External Player.OpenInTemplate', { externalPlayer })"
:icon="['fas', 'external-link-alt']"
theme="secondary"
@click="handleExternalPlayer"
/>
<FtIconButton
v-if="!isUpcoming && downloadLinks.length > 0"
ref="downloadButton"
:title="t('Video.Download Video')"
theme="secondary"
:icon="['fas', 'download']"
:return-index="true"
:dropdown-options="downloadLinks"
@click="handleDownload"
/>
<FtIconButton
v-if="!isUpcoming"
:title="t('Change Format.Change Media Formats')"
theme="secondary"
:icon="['fas', 'file-video']"
:dropdown-options="formatTypeOptions"
@click="changeFormat"
/>
<FtShareButton
v-if="!hideSharingActions"
:id="id"
:get-timestamp="getTimestamp"
:playlist-id="playlistId"
/>
</span>
</div>
</div>
</FtCard>
</template>
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, nextTick, onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtCard from '../ft-card/ft-card.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtShareButton from '../FtShareButton/FtShareButton.vue'
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
import store from '../../store'
import { formatNumber, openExternalLink, showToast } from '../../helpers/utils'
const props = defineProps({
id: {
type: String,
required: true
},
title: {
type: String,
required: true
},
channelId: {
type: String,
required: true
},
channelName: {
type: String,
required: true
},
channelThumbnail: {
type: String,
required: true
},
published: {
type: Number,
required: true
},
premiereDate: {
type: Date,
default: undefined
},
viewCount: {
type: Number,
default: null
},
subscriptionCountText: {
type: String,
required: true
},
likeCount: {
type: Number,
default: 0
},
dislikeCount: {
type: Number,
default: 0
},
getTimestamp: {
type: Function,
required: true
},
isLive: {
type: Boolean,
required: false
},
isLiveContent: {
type: Boolean,
required: true
},
isUpcoming: {
type: Boolean,
required: true
},
downloadLinks: {
type: Array,
required: true
},
playlistId: {
type: String,
default: null
},
getPlaylistIndex: {
type: Function,
required: true
},
getPlaylistReverse: {
type: Function,
required: true
},
getPlaylistShuffle: {
type: Function,
required: true
},
getPlaylistLoop: {
type: Function,
required: true
},
lengthSeconds: {
type: Number,
required: true
},
videoThumbnail: {
type: String,
required: true
},
inUserPlaylist: {
type: Boolean,
required: true
},
isUnlisted: {
type: Boolean,
required: false
},
canSaveWatchedProgress: {
type: Boolean,
required: true
},
})
const emit = defineEmits([
'change-format',
'pause-player',
'set-info-area-sticky',
'scroll-to-info-area',
'save-watched-progress',
])
const USING_ELECTRON = process.env.IS_ELECTRON
const { locale, t } = useI18n()
/** @type {import('vue').ComputedRef<boolean>} */
const hideSharingActions = computed(() => store.getters.getHideSharingActions)
/** @type {import('vue').ComputedRef<boolean>} */
const hideUnsubscribeButton = computed(() => store.getters.getHideUnsubscribeButton)
/** @type {import('vue').ComputedRef<boolean>} */
const hideUploader = computed(() => store.getters.getHideUploader)
/** @type {import('vue').ComputedRef<boolean>} */
const hideVideoLikesAndDislikes = computed(() => store.getters.getHideVideoLikesAndDislikes)
const parsedLikeCount = computed(() => {
if (hideVideoLikesAndDislikes.value || props.likeCount === null) {
return null
}
return formatNumber(props.likeCount)
})
/** @type {import('vue').ComputedRef<boolean>} */
const hideVideoViews = computed(() => store.getters.getHideVideoViews)
const parsedViewCount = computed(() => {
if (hideVideoViews.value || props.viewCount == null) {
return null
}
return t('Global.Counts.View Count', { count: formatNumber(props.viewCount) }, props.viewCount)
})
const dateString = computed(() => {
const formatter = new Intl.DateTimeFormat([locale.value, 'en'], { dateStyle: 'medium' })
const localeDateString = formatter.format(props.published)
// replace spaces with no break spaces to make the date act as a single entity while wrapping
return localeDateString.replaceAll(' ', '\u00A0')
})
const publishedString = computed(() => {
if (props.isLive) {
return t('Video.Started streaming on')
} else if (props.isLiveContent && !props.isLive) {
return t('Video.Streamed on')
} else {
return t('Video.Published on')
}
})
const formatTypeOptions = computed(() => [
{
label: t('Change Format.Use Dash Formats'),
value: 'dash'
},
{
label: t('Change Format.Use Legacy Formats'),
value: 'legacy'
},
{
label: t('Change Format.Use Audio Formats'),
value: 'audio'
}
])
/**
* @param {'dash' | 'legacy' | 'audio'} value
*/
function changeFormat(value) {
emit('change-format', value)
}
const watchedProgressSavingInSemiAutoMode = computed(() => {
return store.getters.getWatchedProgressSavingMode === 'semi-auto'
})
function saveWatchedProgressManually() {
emit('save-watched-progress')
}
/** @type {import('vue').ComputedRef<boolean>} */
const rememberHistory = computed(() => store.getters.getRememberHistory)
const historyEntryExists = computed(() => store.getters.getHistoryCacheById[props.id] !== undefined)
/** @type {import('vue').ComputedRef<string>} */
const externalPlayer = computed(() => store.getters.getExternalPlayer)
/** @type {import('vue').ComputedRef<number>} */
const defaultPlayback = computed(() => store.getters.getDefaultPlayback)
function handleExternalPlayer() {
emit('pause-player')
let payload
// Only play video in non playlist mode when user playlist detected
if (props.inUserPlaylist) {
payload = {
watchProgress: props.getTimestamp(),
playbackRate: defaultPlayback.value,
videoId: props.id,
videoLength: props.lengthSeconds
}
} else {
payload = {
watchProgress: props.getTimestamp(),
playbackRate: defaultPlayback.value,
videoId: props.id,
videoLength: props.lengthSeconds,
playlistId: props.playlistId,
playlistIndex: props.getPlaylistIndex(),
playlistReverse: props.getPlaylistReverse(),
playlistShuffle: props.getPlaylistShuffle(),
playlistLoop: props.getPlaylistLoop()
}
}
store.dispatch('openInExternalPlayer', payload)
if (rememberHistory.value) {
// Marking as watched
const videoData = {
videoId: props.id,
title: props.title,
author: props.channelName,
authorId: props.channelId,
published: props.published,
description: props.description,
viewCount: props.viewCount,
lengthSeconds: props.lengthSeconds,
watchProgress: 0,
timeWatched: Date.now(),
isLive: false,
type: 'video'
}
store.dispatch('updateHistory', videoData)
if (!historyEntryExists.value) {
showToast(t('Video.Video has been marked as watched'))
}
}
}
const downloadButton = useTemplateRef('downloadButton')
/** @type {import('vue').WatchHandle | undefined} */
let downloadDropdownWatcher
onMounted(() => {
if (process.env.IS_ELECTRON || 'mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: props.title,
artist: props.channelName,
artwork: [{
src: props.videoThumbnail,
sizes: '128x128',
type: 'img/png'
}]
})
}
// live and post-live DVR don't have a download button
if (downloadButton.value) {
downloadDropdownWatcher = watch(() => downloadButton.value.dropdownShown, (dropdownShown) => {
emit('set-info-area-sticky', !dropdownShown)
if (dropdownShown && window.innerWidth >= 901) {
// adds a slight delay so we know that the dropdown has shown up
// and won't mess up our scrolling
nextTick(() => {
emit('scroll-to-info-area')
})
}
})
}
})
onBeforeUnmount(() => {
if (downloadDropdownWatcher) {
downloadDropdownWatcher.stop()
downloadDropdownWatcher = undefined
}
})
/** @type {import('vue').ComputedRef<'download' | 'open'>} */
const downloadBehavior = computed(() => store.getters.getDownloadBehavior)
/**
* @param {number} index
*/
function handleDownload(index) {
const selectedDownloadLinkOption = props.downloadLinks[index]
const mimeTypeUrl = selectedDownloadLinkOption.value.split('||')
if (!process.env.IS_ELECTRON || downloadBehavior.value === 'open') {
openExternalLink(mimeTypeUrl[1])
} else {
store.dispatch('downloadMedia', {
url: mimeTypeUrl[1],
title: props.title,
mimeType: mimeTypeUrl[0]
})
}
}
const showPlaylists = computed(() => !store.getters.getHidePlaylists)
function togglePlaylistPrompt() {
const videoData = {
videoId: props.id,
title: props.title,
author: props.channelName,
authorId: props.channelId,
description: props.description,
viewCount: props.viewCount,
lengthSeconds: props.lengthSeconds,
published: props.published,
premiereDate: props.premiereDate
}
store.dispatch('showAddToPlaylistPromptForManyVideos', { videos: [videoData] })
}
const quickBookmarkPlaylist = computed(() => store.getters.getQuickBookmarkPlaylist)
const isQuickBookmarkEnabled = computed(() => quickBookmarkPlaylist.value != null)
const isInQuickBookmarkPlaylist = computed(() => {
if (!isQuickBookmarkEnabled.value) { return false }
// Accessing a reactive property has a negligible amount of overhead,
// however as we know that some users have playlists that have more than 10k items in them
// it adds up quickly. So create a temporary variable outside of the array, so we only have to do it once.
// Also the search is retriggered every time any playlist is modified.
const id = props.id
return quickBookmarkPlaylist.value.videos.some((video) => {
return video.videoId === id
})
})
const quickBookmarkIconText = computed(() => {
if (!isQuickBookmarkEnabled.value) { return '' }
const translationProperties = {
playlistName: quickBookmarkPlaylist.value.playlistName,
}
return isInQuickBookmarkPlaylist.value
? t('User Playlists.Remove from Favorites', translationProperties)
: t('User Playlists.Add to Favorites', translationProperties)
})
const quickBookmarkIconTheme = computed(() => isInQuickBookmarkPlaylist.value ? 'base favorite' : 'base')
function toggleQuickBookmarked() {
if (!isQuickBookmarkEnabled.value) {
// This should be prevented by UI
return
}
if (isInQuickBookmarkPlaylist.value) {
removeFromQuickBookmarkPlaylist()
} else {
addToQuickBookmarkPlaylist()
}
}
function addToQuickBookmarkPlaylist() {
const videoData = {
videoId: props.id,
title: props.title,
author: props.channelName,
authorId: props.channelId,
lengthSeconds: props.lengthSeconds,
published: props.published,
premiereDate: props.premiereDate
}
store.dispatch('addVideo', {
_id: quickBookmarkPlaylist.value._id,
videoData,
})
// TODO: Maybe show playlist name
showToast(t('Video.Video has been saved'))
}
function removeFromQuickBookmarkPlaylist() {
store.dispatch('removeVideo', {
_id: quickBookmarkPlaylist.value._id,
// Remove all playlist items with same videoId
videoId: props.id,
})
// TODO: Maybe show playlist name
showToast(t('Video.Video has been removed from your saved list'))
}
</script>
<style scoped src="./WatchVideoInfo.css" />

View File

@@ -29,7 +29,6 @@
</div>
<div
v-else-if="comments.length === 0"
ref="liveChatMessage"
class="messageContainer liveChatMessage"
>
<p
@@ -225,7 +224,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import autolinker from 'autolinker'
import { computed, nextTick, onBeforeUnmount, ref, shallowReactive } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, shallowReactive, useTemplateRef } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { YTNodes } from 'youtubei.js'
@@ -370,8 +369,7 @@ function startLiveChatLocal() {
liveChatInstance.start()
}
/** @type {import('vue').Ref<HTMLDivElement | null>} */
const commentsRef = ref(null)
const commentsRef = useTemplateRef('commentsRef')
/**
* @param {import ('youtubei.js/dist/src/parser/continuations').LiveChatContinuation} initialData

View File

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

View File

@@ -9,11 +9,13 @@
}
.iconButton {
background-color: transparent;
border-radius: 50%;
border-style: none;
color: inherit;
cursor: pointer;
block-size: 1em;
line-height: 1em;
transition: background 0.15s ease-out;
inline-size: 1em;
&.shadow {
box-shadow: 0 1px 2px rgb(0 0 0 / 50%);
@@ -115,6 +117,10 @@
}
}
.icon {
inline-size: 1em;
}
.disabled {
opacity: 0.5;
pointer-events: auto;

View File

@@ -4,10 +4,10 @@
class="ftIconButton"
@focusout="handleDropdownFocusOut"
>
<font-awesome-icon
<button
class="iconButton"
:aria-label="title"
:title="title"
:icon="icon"
:class="{
[theme]: true,
shadow: useShadow,
@@ -18,16 +18,17 @@
padding: padding + 'px',
fontSize: size + 'px'
}"
tabindex="0"
role="button"
:aria-disabled="disabled"
:aria-expanded="dropdownShown"
@pointerdown="handleIconPointerDown"
@contextmenu.prevent=""
@click="handleIconClick"
@keydown.enter.prevent="handleIconClick"
@keydown.space.prevent="handleIconClick"
/>
>
<font-awesome-icon
class="icon"
:icon="icon"
/>
</button>
<template
v-if="dropdownShown"
>

View File

@@ -1,391 +0,0 @@
import { defineComponent } from 'vue'
import { mapActions } from 'vuex'
import FtTooltip from '../FtTooltip/FtTooltip.vue'
import { isKeyboardEventKeyPrintableChar, isNullOrEmpty } from '../../helpers/strings'
export default defineComponent({
name: 'FtInput',
components: {
'ft-tooltip': FtTooltip
},
props: {
inputType: {
type: String,
required: false,
default: 'text'
},
placeholder: {
type: String,
required: true
},
label: {
type: String,
default: null
},
maxlength: {
type: Number,
default: null
},
value: {
type: String,
default: ''
},
showActionButton: {
type: Boolean,
default: true
},
forceActionButtonIconName: {
type: Array,
default: null
},
showClearTextButton: {
type: Boolean,
default: false
},
showLabel: {
type: Boolean,
default: false
},
isSearch: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
dataList: {
type: Array,
default: () => { return [] }
},
dataListProperties: {
type: Array,
default: () => { return [] }
},
searchResultIconNames: {
type: Array,
default: null
},
showDataWhenEmpty: {
type: Boolean,
default: false
},
tooltip: {
type: String,
default: ''
}
},
emits: ['clear', 'click', 'input', 'remove'],
data: function () {
let actionIcon = ['fas', 'search']
if (this.forceActionButtonIconName !== null) {
actionIcon = this.forceActionButtonIconName
}
return {
id: '',
inputData: '',
searchState: {
showOptions: false,
selectedOption: -1,
isPointerInList: false,
keyboardSelectedOptionIndex: -1,
},
visibleDataList: this.dataList,
// This button should be invisible on app start
// As the text input box should be empty
clearTextButtonExisting: false,
clearTextButtonVisible: false,
removeButtonSelectedIndex: -1,
removalMade: false,
actionButtonIconName: actionIcon
}
},
computed: {
showOptions: function () {
return (this.inputData !== '' || this.showDataWhenEmpty) && this.visibleDataList.length > 0 && this.searchState.showOptions
},
barColor: function () {
return this.$store.getters.getBarColor
},
forceTextColor: function () {
return this.isSearch && this.barColor
},
inputDataPresent: function () {
return this.inputDataDisplayed.length > 0
},
inputDataDisplayed() {
if (!this.isSearch) { return this.inputData }
const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue
if (selectedOptionValue != null && selectedOptionValue !== '') {
return selectedOptionValue
}
return this.inputData
},
searchStateKeyboardSelectedOptionValue() {
if (this.searchState.keyboardSelectedOptionIndex === -1) { return null }
return this.visibleDataList[this.searchState.keyboardSelectedOptionIndex]
},
},
watch: {
dataList(val, oldVal) {
if (val !== oldVal) {
this.updateVisibleDataList()
}
},
inputData(val, oldVal) {
if (val !== oldVal) {
this.updateVisibleDataList()
}
},
value(val, oldVal) {
if (val !== oldVal) {
this.inputData = val
}
}
},
created: function () {
this.id = this._uid
this.inputData = this.value
this.updateVisibleDataList()
},
methods: {
handleClick: function (e) {
const selectedValue = this.searchStateKeyboardSelectedOptionValue
const query = (selectedValue != null && selectedValue !== '') ? selectedValue : this.inputData
this.inputData = query
// No action if no input text
if (!this.inputDataPresent) {
return
}
this.searchState.showOptions = false
this.searchState.selectedOption = -1
this.searchState.keyboardSelectedOptionIndex = -1
this.removeButtonSelectedIndex = -1
this.$emit('input', query)
this.$emit('click', query, { event: e })
},
handleInput: function (val) {
this.inputData = val
if (this.isSearch &&
this.searchState.selectedOption !== -1 &&
this.inputData === this.visibleDataList[this.searchState.selectedOption]) { return }
this.handleActionIconChange()
this.$emit('input', val)
},
handleClearTextClick: function () {
// No action if no input text
if (!this.inputDataPresent) { return }
this.inputData = ''
this.handleActionIconChange()
this.updateVisibleDataList()
this.searchState.isPointerInList = false
this.$refs.input.value = ''
// Focus on input element after text is clear for better UX
this.$refs.input.focus()
this.$emit('clear')
},
handleActionIconChange: function() {
// Only need to update icon if visible
if (!this.showActionButton) { return }
if (!this.inputDataPresent && this.forceActionButtonIconName === null) {
// Change back to default icon if text is blank
this.actionButtonIconName = ['fas', 'search']
return
}
// Update action button icon according to input
try {
this.getYoutubeUrlInfo(this.inputData).then((result) => {
let isYoutubeLink = false
switch (result.urlType) {
case 'video':
case 'playlist':
case 'search':
case 'channel':
case 'hashtag':
case 'post':
case 'trending':
case 'subscriptions':
case 'history':
case 'userplaylists':
isYoutubeLink = true
break
case 'invalid_url':
default: {
// isYoutubeLink is already `false`
}
}
if (this.forceActionButtonIconName === null) {
if (isYoutubeLink) {
// Go to URL (i.e. Video/Playlist/Channel
this.actionButtonIconName = ['fas', 'arrow-right']
} else {
// Search with text
this.actionButtonIconName = ['fas', 'search']
}
}
})
} catch (ex) {
// On exception, consider text as invalid URL
if (this.forceActionButtonIconName === null) {
this.actionButtonIconName = ['fas', 'search']
}
// Rethrow exception
throw ex
}
},
handleOptionClick: function (index) {
if (this.removeButtonSelectedIndex !== -1) {
this.handleRemoveClick(index)
return
}
this.searchState.showOptions = false
this.inputData = this.visibleDataList[index]
this.$emit('input', this.inputData)
this.handleClick()
},
handleRemoveClick: function (index) {
if (!this.dataListProperties[index]?.isRemoveable) { return }
// keep input in focus even when the to-be-removed "Remove" button was clicked
this.$refs.input.focus()
this.removalMade = true
this.$emit('remove', this.visibleDataList[index])
},
/**
* @param {KeyboardEvent} event
*/
handleKeyDown: function (event) {
// Update Input box value if enter key was pressed and option selected
if (event.key === 'Enter' && !event.isComposing) {
if (this.removeButtonSelectedIndex !== -1) {
this.handleRemoveClick(this.removeButtonSelectedIndex)
} else if (this.searchState.selectedOption !== -1) {
this.searchState.showOptions = false
event.preventDefault()
this.inputData = this.visibleDataList[this.searchState.selectedOption]
this.handleOptionClick(this.searchState.selectedOption)
} else {
this.handleClick(event)
}
return
}
if (this.visibleDataList.length === 0) { return }
this.searchState.showOptions = true
// "select" the Remove button through right arrow navigation, and unselect it with the left arrow
if (event.key === 'ArrowRight') {
this.removeButtonSelectedIndex = this.searchState.selectedOption
} else if (event.key === 'ArrowLeft') {
this.removeButtonSelectedIndex = -1
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault()
const newIndex = this.searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1)
this.updateSelectedOptionIndex(newIndex)
} else {
const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue
// Keyboard selected & is char
if (!isNullOrEmpty(selectedOptionValue) && isKeyboardEventKeyPrintableChar(event.key)) {
// Update input based on KB selected suggestion value instead of current input value
event.preventDefault()
this.handleInput(`${selectedOptionValue}${event.key}`)
}
}
},
// Updates the selected dropdown option index and handles the under/over-flow behavior
updateSelectedOptionIndex: function (index) {
this.searchState.selectedOption = index
// unset selection of "Remove" button
this.removeButtonSelectedIndex = -1
// Allow deselecting suggestion
if (this.searchState.selectedOption < -1) {
this.searchState.selectedOption = this.visibleDataList.length - 1
} else if (this.searchState.selectedOption > this.visibleDataList.length - 1) {
this.searchState.selectedOption = -1
}
// Update displayed value
this.searchState.keyboardSelectedOptionIndex = this.searchState.selectedOption
},
handleInputBlur: function () {
if (!this.searchState.isPointerInList) { this.searchState.showOptions = false }
},
handleFocus: function () {
this.searchState.showOptions = true
},
updateVisibleDataList: function () {
// Reset selected option before it's updated
// Block resetting if it was just the "Remove" button that was pressed
if (!this.removalMade || this.searchState.selectedOption >= this.dataList.length) {
this.searchState.selectedOption = -1
this.searchState.keyboardSelectedOptionIndex = -1
this.removeButtonSelectedIndex = -1
}
this.removalMade = false
if (this.inputData.trim() === '') {
this.visibleDataList = this.dataList
return
}
// get list of items that match input
const lowerCaseInputData = this.inputData.toLowerCase()
this.visibleDataList = this.dataList.filter(x => {
return x.toLowerCase().indexOf(lowerCaseInputData) !== -1
})
},
updateInputData: function(text) {
this.inputData = text
},
focus() {
this.$refs.input.focus()
},
select() {
this.$refs.input.select()
},
blur() {
this.$refs.input.blur()
},
...mapActions([
'getYoutubeUrlInfo'
])
}
})

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