Compare commits

...

497 Commits

Author SHA1 Message Date
William Faray
1b98029ba0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2026-04-14 15:26:16 +00:00
Maxiligamer
5def949cff Translated using Weblate (Finnish)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2026-04-14 11:51:25 +00:00
Keylhan
6a89de66c3 Translated using Weblate (French)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2026-04-13 19:49:49 +00:00
renovate[bot]
7986ad4112 Update android.gradle to v9.1.1 2026-04-13 19:09:25 +02:00
Translation expert
2881600b57 Translated using Weblate (Arabic)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2026-04-12 20:32:19 +00:00
David Arist
c661b11c32 Translated using Weblate (Slovenian)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sl/
2026-04-12 18:28:46 +00:00
David Arist
1f9b50f66d Translated using Weblate (Slovenian)
Currently translated at 99.4% (586 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sl/
2026-04-12 16:48:17 +00:00
Niels van Velzen
b825b4cc8e Enable new AGP DSL 2026-04-12 11:10:09 +02:00
renovate[bot]
57f0075a66 Update aboutlibraries to v14 2026-04-12 11:10:09 +02:00
renovate[bot]
ece02d19fe Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.11.0 2026-04-12 10:56:03 +02:00
renovate[bot]
9e91e92ec4 Update actions/upload-artifact action to v7.0.1 2026-04-12 10:55:51 +02:00
renovate[bot]
22421d9f46 Update koin to v4.2.1 2026-04-12 10:55:39 +02:00
renovate[bot]
a3cfc160d6 Update dependency androidx.navigation3:navigation3-ui to v1.1.0 2026-04-12 10:55:20 +02:00
Mykolas Mickus
efeb432672 Translated using Weblate (Lithuanian)
Currently translated at 95.0% (560 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lt/
2026-04-11 08:54:35 +00:00
UncleNet41
403f1f24da Translated using Weblate (Italian)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2026-04-10 14:28:03 +00:00
Franco Castillo
cb6f1b3747 Translated using Weblate (Spanish (Argentina))
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_AR/
2026-04-10 02:29:10 +00:00
Alejandro Delgado Folgado
d301a675a4 Translated using Weblate (Spanish)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2026-04-08 13:26:16 +00:00
Andi Chandler
d87947266a Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2026-04-08 00:26:15 +00:00
Fjuro
d0f5974f86 Translated using Weblate (Czech)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2026-04-07 21:26:16 +00:00
Dzmitry Zubialevich
ccf3c85788 Translated using Weblate (Belarusian)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/be/
2026-04-07 08:26:17 +00:00
Dzmitry Zubialevich
e34647548b Translated using Weblate (Belarusian)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-04-07 08:26:16 +00:00
Niels van Velzen
eaf2411a99 Fix missing focus in new player UI (#5513)
* Fix missing focus in new player UI

* Update app/src/main/java/org/jellyfin/androidtv/ui/base/BaseScreen.kt

Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>

---------

Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2026-04-06 08:44:37 -04:00
renovate[bot]
25dd7515cb Update dependency org.jellyfin.sdk:jellyfin-core to v1.8.8 2026-04-06 12:29:24 +02:00
Gallyam Biktashev
afb55fc06c Translated using Weblate (Russian)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2026-04-06 10:27:22 +00:00
Gargotaire
bacbea0b59 Translated using Weblate (Catalan)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2026-04-06 09:33:58 +00:00
kscop-n1
cdcd528bcc Translated using Weblate (Ukrainian)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2026-04-06 09:33:57 +00:00
Lofuuzi
a8cd75aec3 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-06 07:50:10 +00:00
Pavel Miniutka
cac4958a0b Translated using Weblate (Belarusian)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-04-06 07:50:09 +00:00
Niels van Velzen
19cbfcda3e Fix interaction tracking in popover composable 2026-04-06 07:36:38 +02:00
Kityn
dd7f64d1e0 Translated using Weblate (Polish)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2026-04-06 00:54:39 +00:00
Daniel Aleksander Martinsen
d4c0af73c3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2026-04-05 23:26:15 +00:00
Lofuuzi
5ab2e91ae8 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 18:26:16 +00:00
Bas
62c7a65365 Translated using Weblate (Dutch)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2026-04-05 18:26:16 +00:00
Addi90
c2391f85e4 Translated using Weblate (German)
Currently translated at 100.0% (589 of 589 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2026-04-05 18:26:15 +00:00
renovate[bot]
d1691499a9 Update kotest to v6.1.11 2026-04-05 18:10:53 +02:00
Weblate
fa2fd94a6e Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/
2026-04-05 15:56:24 +00:00
Lofuuzi
59e918a144 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 15:56:23 +00:00
Akhil
de74f641ce Add ability to unblur backdrops (#5216)
* Add preference for enabling background blur on backdrops

* value for strings.xml for enabling backdrop blur feature

* Respect preference option for enabling backdrop blur feature

* update new customization screen

* Added Enum for backdrop state

* Added a new screen for CustomizationBackdrops

* Combined backdrop enabled and blur into a single behavior

modified type to enum , removed checkbox, instead added a subroute, updated BackgroundService to reflect new type values

* rename pref variable and run migration

* Misc fixes

---------

Co-authored-by: Niels van Velzen <git@ndat.nl>
2026-04-05 17:56:17 +02:00
Lofuuzi
48b5d262bc Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 97.7% (575 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 15:40:53 +00:00
Lofuuzi
6fe5a96045 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 97.2% (572 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 15:40:37 +00:00
Lofuuzi
9307bb8d81 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 93.5% (550 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 15:38:03 +00:00
Lofuuzi
0e0aa4bd26 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 48.9% (288 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 08:26:20 +00:00
Lofuuzi
7e2300e5fa Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 16.3% (96 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-05 05:37:27 +00:00
Lofuuzi
a50663a33c Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 16.1% (95 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-04 13:26:16 +00:00
Lofuuzi
1a3d8d8cb0 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 14.1% (83 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-04 10:50:16 +00:00
Lofuuzi
d6453b7224 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/yue_Hant/
2026-04-03 11:26:19 +00:00
Lofuuzi
1d2a0842a6 Translated using Weblate (Cantonese (Traditional Han script))
Currently translated at 5.6% (33 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/yue_Hant/
2026-04-03 11:26:18 +00:00
Dzmitry Zubialevich
cea16ea8d0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-04-03 11:26:17 +00:00
Niels van Velzen
8415802188 Move in-app screensaver cancellation into compose 2026-04-03 10:07:26 +02:00
Niels van Velzen
c7d8790e17 Use new interaction tracking API in MainActivity 2026-04-03 10:07:26 +02:00
Niels van Velzen
cafd225074 Support interaction tracking within dialogs 2026-04-03 10:07:26 +02:00
Niels van Velzen
74e24efcbf Add new InteractionTracker API to compose to allow for user interaction detection 2026-04-03 10:07:26 +02:00
Niels van Velzen
7600b4a27a Avoid nested composition local providers in JellyfinTheme 2026-04-03 10:07:26 +02:00
Lofuuzi
613ba0ef8b Added translation using Weblate (Cantonese (Traditional Han script)) 2026-04-02 12:37:49 +00:00
renovate[bot]
2242d15a7a Update dependency org.jellyfin.sdk:jellyfin-core to v1.8.7 2026-04-02 00:09:32 +02:00
renovate[bot]
193fa73799 Update kotest to v6.1.10 2026-04-01 11:46:18 +02:00
newton181
af80f41aac Translated using Weblate (Spanish (Latin America))
Currently translated at 93.0% (547 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2026-03-31 22:26:15 +00:00
Jens van Almsick
3e8e059ab2 Change heading from 'Refresh Switching' to 'Resume Preroll' 2026-03-31 16:46:27 +02:00
Niels van Velzen
0fca5d77fe Remove redundant JellyfinTheme usages 2026-03-29 22:08:50 +02:00
Niels van Velzen
b9f8757123 Refactor MainActivity to use compose as basis 2026-03-29 22:08:50 +02:00
Niels van Velzen
72ff0cfae8 Cleanup navigation destinations 2026-03-28 21:55:37 +01:00
Niels van Velzen
1f8cfa0f30 Fix dismissing in-app screensaver with remote 2026-03-28 15:32:38 +01:00
renovate[bot]
64f3bdac41 Update github/codeql-action action to v4.35.1 2026-03-27 19:53:10 +01:00
renovate[bot]
8eaaa39c24 Update androidx.media3 to v1.10.0 2026-03-27 19:48:03 +01:00
renovate[bot]
cbb230a588 Update github/codeql-action action to v4.35.0 2026-03-27 19:47:47 +01:00
renovate[bot]
489780f4ae Update kotest to v6.1.9 2026-03-27 19:47:36 +01:00
Gatis Rīders
b861f7372c Translated using Weblate (Latvian)
Currently translated at 99.1% (583 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lv/
2026-03-27 16:26:15 +00:00
gitlepus
f8aa323580 Translated using Weblate (Croatian)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/hr/
2026-03-27 13:26:15 +00:00
renovate[bot]
ab6c32825c Update androidx.compose to v1.10.6 2026-03-25 20:21:07 +01:00
renovate[bot]
b9aabaacec Update dependency androidx.work:work-runtime to v2.11.2 2026-03-25 20:20:49 +01:00
UncleNet41
b8c9b079ee Translated using Weblate (Italian)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2026-03-24 19:47:20 +00:00
renovate[bot]
9436e6966a Update kotest to v6.1.8 2026-03-24 19:43:38 +01:00
deepak r
eb6e931a22 Translated using Weblate (Tamil)
Currently translated at 94.7% (557 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ta/
2026-03-24 18:41:11 +00:00
Denislav Denev
a3f4b112b8 Translated using Weblate (Bulgarian)
Currently translated at 79.0% (465 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2026-03-20 23:17:19 +00:00
renovate[bot]
5bae1198dc Update Gradle to v9.4.1 2026-03-19 20:43:37 +01:00
renovate[bot]
38d0ee6bce Update koin to v4.2.0 2026-03-19 20:43:26 +01:00
Winoox
5d395978a7 Translated using Weblate (Bulgarian)
Currently translated at 78.7% (463 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2026-03-16 23:26:15 +00:00
renovate[bot]
02ec11135c Update androidx.media3 to v1.9.3 2026-03-16 21:02:50 +01:00
renovate[bot]
df0ff6eaa1 Update github/codeql-action action to v4.33.0 2026-03-16 15:26:52 +01:00
renovate[bot]
58a1449766 Update Kotlin to v2.3.20 2026-03-16 15:26:34 +01:00
Dimitar \"Topper\" Maznekov
d07a5d0fc4 Translated using Weblate (Bulgarian)
Currently translated at 76.1% (448 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2026-03-16 08:40:21 +00:00
renovate[bot]
924463bff9 Update kotest to v6.1.7 2026-03-15 14:11:42 +01:00
Andreas Olsson
67c7d7c0d0 Translated using Weblate (Swedish)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2026-03-15 10:26:14 +00:00
Andreas Olsson
db9695d702 Translated using Weblate (Swedish)
Currently translated at 98.8% (581 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2026-03-15 00:26:14 +00:00
lednurb
10d599d5df Translated using Weblate (Dutch)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2026-03-12 19:26:14 +00:00
jasoncoder666
aff07fcbd7 Translated using Weblate (Greek)
Currently translated at 96.0% (565 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2026-03-12 16:26:14 +00:00
renovate[bot]
d8b072383d Update dependency androidx.core:core-ktx to v1.18.0 2026-03-12 08:10:40 +01:00
aniara001
c5de74cf33 Translated using Weblate (Swedish)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/sv/
2026-03-11 22:26:14 +00:00
Bas
e7f51dba01 Translated using Weblate (Dutch)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2026-03-11 20:08:38 +00:00
lednurb
456bb619bc Translated using Weblate (Dutch)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2026-03-11 20:08:38 +00:00
renovate[bot]
2de7bd876a Update kotest to v6.1.6 2026-03-11 20:43:51 +01:00
renovate[bot]
7f0fbba0d2 Update androidx.compose to v1.10.5 2026-03-11 20:43:34 +01:00
renovate[bot]
829a88761f Update androidx.activity to v1.13.0 2026-03-11 20:43:24 +01:00
Antoni Encarnacion Marte
a054d9430e Translated using Weblate (Spanish (Dominican Republic))
Currently translated at 10.8% (64 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_DO/
2026-03-11 03:38:25 +00:00
nextlooper42
cab17b8bee Translated using Weblate (Slovak)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2026-03-10 11:26:14 +00:00
nextlooper42
b87d26d2ed Translated using Weblate (Slovak)
Currently translated at 96.4% (567 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2026-03-10 08:49:06 +00:00
Denislav Denev
e4952e6eba Translated using Weblate (Bulgarian)
Currently translated at 76.0% (447 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2026-03-10 02:26:21 +00:00
Weblate
26c78157d9 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/
2026-03-09 20:03:59 +00:00
Denislav Denev
8edf76c5dd Translated using Weblate (Bulgarian)
Currently translated at 53.2% (313 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2026-03-09 20:03:57 +00:00
Pavel Miniutka
83183892fd Translated using Weblate (Belarusian)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/be/
2026-03-09 08:26:13 +00:00
Bryan
5b8c76c531 Translated using Weblate (Afrikaans)
Currently translated at 10.3% (61 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/af/
2026-03-07 17:26:14 +00:00
queeup
56b16b37c5 Translated using Weblate (Turkish)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2026-03-07 00:26:13 +00:00
makoto
ba4b57ef93 Translated using Weblate (Japanese)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ja/
2026-03-06 11:26:14 +00:00
renovate[bot]
4ba1bbc946 Update github/codeql-action action to v4.32.6 2026-03-06 11:56:32 +01:00
Niels van Velzen
aa964647b5 Support Advanced SubStation Alpha direct play 2026-03-05 21:27:48 +01:00
renovate[bot]
e3062edc91 Update android.gradle to v9.1.0 2026-03-04 19:13:44 +01:00
renovate[bot]
2a54faee98 Update Gradle to v9.4.0 2026-03-04 13:39:42 +01:00
Niels van Velzen
3f0d4bdbfc Add next media preloading 2026-03-03 14:07:45 +01:00
Niels van Velzen
4308f46f4d Add timed event blocks and support for negative positions 2026-03-02 19:56:15 +01:00
renovate[bot]
861e00ad1a Update github/codeql-action action to v4.32.5 2026-03-02 19:55:54 +01:00
Niels van Velzen
947ab42e33 Refactor PlayerBackendEventListener to abstract class 2026-03-01 18:04:16 +01:00
renovate[bot]
22f3bafcab Update actions/upload-artifact action to v7 2026-02-27 20:55:01 +01:00
Niels van Velzen
62153f648a Add initial media segment support 2026-02-27 20:53:57 +01:00
Niels van Velzen
6a03b75eb2 Add timed event API 2026-02-27 20:53:57 +01:00
Niels van Velzen
333c8b9630 Move lyrics plugin into own package 2026-02-27 20:53:57 +01:00
Pavel Miniutka
3c9fe12ddc Translated using Weblate (Belarusian)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-02-27 13:03:05 +00:00
Pavel Miniutka
26a83a6bc4 Translated using Weblate (Belarusian)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/be/
2026-02-26 09:26:13 +00:00
Pavel Miniutka
978177c48b Translated using Weblate (Belarusian)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-02-26 09:26:13 +00:00
renovate[bot]
afa7d42fa4 Update kotest to v6.1.4 2026-02-25 21:59:06 +01:00
renovate[bot]
5c804a65d3 Update androidx.compose to v1.10.4 2026-02-25 21:58:45 +01:00
renovate[bot]
b15581c8a4 Update coil to v3.4.0 2026-02-24 22:22:38 +01:00
renovate[bot]
fcd3b2dcd6 Update android.gradle to v9.0.1 2026-02-24 13:18:02 +01:00
Dzmitry Zubialevich
c1d1b52fc0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-02-24 11:09:39 +00:00
renovate[bot]
d6e32b1075 Update gradle/actions action to v5.0.2 2026-02-24 10:32:50 +01:00
renovate[bot]
df610b2ec4 Update github/codeql-action action to v4.32.4 2026-02-20 16:51:07 +01:00
John Doe
c0c2a08572 Translated using Weblate (Finnish)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2026-02-18 14:31:09 +00:00
John Doe
fcff716b28 Translated using Weblate (Finnish)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2026-02-18 14:02:23 +00:00
John Doe
fce6998218 Translated using Weblate (Finnish)
Currently translated at 99.6% (586 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2026-02-18 13:28:23 +00:00
renovate[bot]
fc62714955 Update CI dependencies 2026-02-17 15:41:48 +01:00
Dzmitry Zubialevich
f0a8604acc Translated using Weblate (Belarusian)
Currently translated at 98.9% (582 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-02-16 10:25:23 +00:00
Gargotaire
bd5b10e040 Translated using Weblate (Catalan)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2026-02-14 21:53:54 +00:00
WizardOfYendor1
f6af0b8075 Fix recording from playback overlay by passing programId to timer defaults 2026-02-12 21:43:39 +01:00
Galaterro
ca715d4c10 Show additional parts for episodes 2026-02-12 21:41:15 +01:00
renovate[bot]
967d690139 Update dependency androidx.navigation3:navigation3-ui to v1.0.1 2026-02-11 21:43:26 +01:00
renovate[bot]
53e8a80592 Update androidx.compose to v1.10.3 2026-02-11 21:25:02 +01:00
renovate[bot]
8bb34428f8 Update androidx.activity to v1.12.4 2026-02-11 21:24:42 +01:00
skalthoff
3ab0ae4cf9 Fix non-enum preferences not persisting to server
The generic rememberPreference() overload for non-enum types (Int,
String, etc.) updates the local in-memory cache but never calls
commit() on the AsyncPreferenceStore. This means preferences like
skipForwardLength and skipBackLength are never sent to the server
via the DisplayPreferences API, silently reverting on app restart.

Add the same commit() call that the enum overload already has.
2026-02-11 09:10:50 +01:00
Pavel Miniutka
421ba3704c Translated using Weblate (Belarusian)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/be/
2026-02-10 11:05:56 +00:00
Pavel Miniutka
9f2ea8434f Translated using Weblate (Belarusian)
Currently translated at 95.0% (559 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2026-02-10 11:05:56 +00:00
無情天
ad46a348ad Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2026-02-06 21:58:34 +00:00
Niels van Velzen
02f9405c40 Remove unused NumberSpinnerView 2026-02-06 22:43:40 +01:00
Niels van Velzen
a368382262 Check for playAccess in BaseItemDto.canPlay extension 2026-02-06 22:43:35 +01:00
Niels van Velzen
6c7af50a34 Fix item names not showing when showCardInfoOverlay is set 2026-02-06 19:22:58 +01:00
renovate[bot]
8976dc302a Update github/codeql-action action to v4.32.2 2026-02-06 11:53:42 +01:00
renovate[bot]
c24ab17b10 Update androidx.media3 to v1.9.2 2026-02-06 11:53:18 +01:00
Jonas Engen Larsen
4ae28b1f2c Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2026-02-05 23:03:32 +00:00
renovate[bot]
c14e504d8b Update kotest to v6.1.3 2026-02-05 17:52:08 +01:00
renovate[bot]
cf507e8385 Update Kotlin to v2.3.10 2026-02-05 17:46:51 +01:00
Augusto
faaec6a999 Translated using Weblate (Spanish (Argentina))
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/es_AR/
2026-02-04 04:05:55 +00:00
JagerSprinkles
3d7f252664 Correct video start delay setting not showing decimal values in settings (#5376) 2026-02-03 13:02:21 +00:00
Niels van Velzen
d41f992b62 Add media type API to set proper content type in audio attributes 2026-02-03 09:00:05 +01:00
Niels van Velzen
7acfc41498 Specify audio attributes in VideoManager 2026-02-02 20:32:57 +01:00
renovate[bot]
067f227983 Update github/codeql-action action to v4.32.1 2026-02-02 20:23:11 +01:00
Flavian Kathurima
b844bf3165 Translated using Weblate (Swahili)
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-02-02 09:06:00 +00:00
Niels van Velzen
0935cc473b Specify JDK version for CI in .tools-versions file 2026-02-01 22:26:11 +01:00
Augusto
ebfa4bba7f Translated using Weblate (Spanish (Argentina))
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/es_AR/
2026-02-01 17:05:56 +00:00
Augusto
2b9280f3ef Translated using Weblate (Spanish (Argentina))
Currently translated at 100.0% (588 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_AR/
2026-02-01 17:05:56 +00:00
Flavian Kathurima
258e20f605 Translated using Weblate (Swahili)
Currently translated at 85.8% (505 of 588 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-02-01 09:05:57 +00:00
Weblate
5fa51d489d Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/
2026-01-31 16:08:00 +00:00
Niels van Velzen
ed0e470a40 Remove unused defaultRatingType setting 2026-01-31 17:07:56 +01:00
Flavian Kathurima
62d68a8485 Translated using Weblate (Swahili)
Currently translated at 68.6% (406 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-31 10:05:56 +00:00
KWeasley
d1d45906cf Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2026-01-30 01:05:55 +00:00
KWeasley
c83d7c62f1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2026-01-29 22:48:43 +00:00
KWeasley
8822076391 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2026-01-29 21:09:32 +00:00
KWeasley
fd9ab5d431 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2026-01-29 20:43:39 +00:00
millallo
d4ff5983a3 Translated using Weblate (Italian)
Currently translated at 98.6% (583 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2026-01-29 20:43:38 +00:00
renovate[bot]
f60a72909d Update Gradle to v9.3.1 2026-01-29 16:30:34 +01:00
Saman Miran
1ef40b614a Remove drop shadows from BrowseGridFragment (#5370) 2026-01-29 15:18:17 +00:00
renovate[bot]
e25acfadd6 Update kotest to v6.1.2 2026-01-29 16:17:30 +01:00
renovate[bot]
92148e9da8 Update dependency io.mockk:mockk to v1.14.9 2026-01-29 16:17:22 +01:00
renovate[bot]
4f700ba9d3 Update CI dependencies 2026-01-29 16:17:06 +01:00
renovate[bot]
69b3fa48be Update androidx.compose to v1.10.2 2026-01-29 16:16:56 +01:00
renovate[bot]
59c1ef00d5 Update dependency androidx.activity:activity to v1.12.3 2026-01-29 16:16:47 +01:00
renovate[bot]
860bfa529e Update dependency androidx.work:work-runtime to v2.11.1 2026-01-29 16:16:37 +01:00
Flavian Kathurima
51cec68bc9 Translated using Weblate (Swahili)
Currently translated at 60.9% (360 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-29 09:40:48 +00:00
Flavian Kathurima
a8d18f70ad Translated using Weblate (Swahili)
Currently translated at 53.4% (316 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-29 08:05:55 +00:00
Jonas Engen Larsen
da0ebbc5ce Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2026-01-29 08:05:54 +00:00
Niels van Velzen
a91e9f9ad3 Update badge styling in ItemCardBaseItemOverlay 2026-01-28 08:29:17 +01:00
Niels van Velzen
153ca1662f Update progress styling in NowPlayingView 2026-01-27 17:06:34 +01:00
renovate[bot]
eb1ef2f295 Update androidx.media3 to v1.9.1 2026-01-26 12:47:58 +01:00
renovate[bot]
aa54863603 Update CI dependencies 2026-01-25 21:46:32 +01:00
renovate[bot]
51ce9dece4 Update kotest to v6.1.1 2026-01-25 21:46:20 +01:00
Gabriel S
0bdcccf29b Translated using Weblate (Romanian)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2026-01-25 18:38:49 +00:00
queeup
7100b26c83 Translated using Weblate (Turkish)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2026-01-25 10:57:19 +00:00
Jógvan Olsen
da38f75a0f Added translation using Weblate (Faroese) 2026-01-25 09:38:32 +00:00
queeup
44682144c8 Translated using Weblate (Turkish)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2026-01-24 23:43:17 +00:00
Niels van Velzen
035a115dc9 Add media toasts to new video player 2026-01-24 18:55:47 +01:00
Andi Chandler
2480b05007 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2026-01-24 04:05:54 +00:00
Deniz ŞAHİN
9184f106a7 Translated using Weblate (Turkish)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2026-01-22 13:40:48 +00:00
rimasx
fc1f469661 Translated using Weblate (Estonian)
Currently translated at 99.8% (590 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2026-01-22 12:33:49 +00:00
renovate[bot]
e7ee94ab82 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.10.0 2026-01-21 21:36:08 +01:00
markrieder
24aac6a392 Translated using Weblate (Slovak)
Currently translated at 96.4% (570 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2026-01-21 18:05:54 +00:00
Flavian Kathurima
430f0ff689 Translated using Weblate (Swahili)
Currently translated at 51.4% (304 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-20 09:05:55 +00:00
renovate[bot]
3e7e9618c7 Update kotest to v6.1.0 2026-01-20 09:32:22 +01:00
renovate[bot]
5e9bfd4744 Update dependency org.jellyfin.sdk:jellyfin-core to v1.8.6 2026-01-20 09:31:55 +01:00
Flavian Kathurima
9a1a55160a Translated using Weblate (Swahili)
Currently translated at 44.5% (263 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-20 06:56:23 +00:00
Flavian Kathurima
6f0bc196ba Translated using Weblate (Swahili)
Currently translated at 34.5% (204 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-19 13:05:54 +00:00
Flavian Kathurima
96cd70d5a8 Translated using Weblate (Swahili)
Currently translated at 32.1% (190 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-19 10:05:54 +00:00
Niels van Velzen
6ad07f274f Recreate LegacyImageCardView with compose (#5343)
* Recreate LegacyImageCardView with compose

* Fix wrong aspect ratio used for Live TV cards

* Update app/src/main/java/org/jellyfin/androidtv/ui/presentation/CardPresenter.kt
2026-01-19 08:17:35 +00:00
Niels van Velzen
913beda916 Increase amount of cached MediaItems to 10 2026-01-17 17:57:57 +01:00
hoanghuy309
e39877b6f5 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2026-01-17 16:21:03 +00:00
Niels van Velzen
346c21c0b4 Migrate to AGP9 2026-01-17 13:27:28 +01:00
renovate[bot]
60e58e9fdb Update dependency com.android.tools.build:gradle to v9 2026-01-17 13:27:28 +01:00
renovate[bot]
99fb7d38aa Update Gradle to v9.3.0 2026-01-16 19:20:34 +01:00
Tal Sarid
8ac778d39f Translated using Weblate (Hebrew)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2026-01-16 16:05:55 +00:00
Flavian Kathurima
e77593265b Translated using Weblate (Swahili)
Currently translated at 24.3% (144 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-16 11:03:10 +00:00
Flavian Kathurima
eab61bd3a3 Translated using Weblate (Swahili)
Currently translated at 24.1% (143 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-16 08:05:54 +00:00
Flavian Kathurima
2c885a2ba7 Translated using Weblate (Swahili)
Currently translated at 17.2% (102 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-15 11:05:53 +00:00
Flavian Kathurima
9234073344 Translated using Weblate (Swahili)
Currently translated at 16.4% (97 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-15 08:43:44 +00:00
Flavian Kathurima
5d5ffc79ce Translated using Weblate (Swahili)
Currently translated at 8.4% (50 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-15 07:55:29 +00:00
Flavian Kathurima
63a6dca0e2 Translated using Weblate (Swahili)
Currently translated at 1.6% (10 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-15 06:52:32 +00:00
Flavian Kathurima
5189e2931a Translated using Weblate (Swahili)
Currently translated at 1.1% (7 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-15 06:43:03 +00:00
Flavian Kathurima
0eb848c2f2 Translated using Weblate (Swahili)
Currently translated at 0.5% (3 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2026-01-15 06:34:27 +00:00
renovate[bot]
e6a20cb47e Update github/codeql-action action to v4.31.10 2026-01-14 20:57:26 +01:00
renovate[bot]
84476439e7 Update androidx.compose to v1.10.1 2026-01-14 20:52:08 +01:00
Urbán Zoltán
a654d2af68 Translated using Weblate (Hungarian)
Currently translated at 94.4% (558 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2026-01-13 19:32:47 +00:00
Thadah D. Denyse
875ca123bb Translated using Weblate (Basque)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/eu/
2026-01-12 13:05:53 +00:00
Translation expert
341f031660 Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2026-01-12 13:05:53 +00:00
nenadsuperzmaj
14920a506e Translated using Weblate (Serbian)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sr/
2026-01-12 10:05:54 +00:00
Gallyam Biktashev
690977498c Translated using Weblate (Russian)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2026-01-12 10:05:53 +00:00
Translation expert
6998e6cdbb Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2026-01-12 01:14:00 +00:00
Marcos Soares
74535c7c03 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2026-01-10 23:05:53 +00:00
Harvey
25babf42c8 Fix issue playing multiple items remotely (#5017)
* Fix issue starting multiple items remotely

* Use separate code path for starting playback from SocketHandler.onPlayMessage

---------

Co-authored-by: Niels van Velzen <git@ndat.nl>
2026-01-10 11:13:02 +00:00
Tal Sarid
ce2023a4b1 Show placeholder thumbnail while Trickplay thumbnail loads (#4946) 2026-01-10 11:56:52 +01:00
Kityn
4466b2fd69 Translated using Weblate (Polish)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2026-01-09 21:50:47 +00:00
Dan Tsivinsky
1d6c9a81fc Translated using Weblate (Russian)
Currently translated at 99.6% (589 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2026-01-09 16:05:53 +00:00
vlatka-sinisa
a11ef51851 Translated using Weblate (Croatian)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hr/
2026-01-08 23:05:54 +00:00
Weblate
a056ac1e25 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/
2026-01-08 10:05:55 +00:00
John Doe
05de9bf84b Translated using Weblate (Finnish)
Currently translated at 99.4% (588 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2026-01-08 10:05:53 +00:00
Translation expert
dcd536cd19 Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2026-01-08 01:20:46 +00:00
Gallyam Biktashev
bb239f548c Translated using Weblate (Russian)
Currently translated at 98.8% (584 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2026-01-05 18:54:05 +00:00
mihlau
143be2f4f2 Translated using Weblate (Romanian)
Currently translated at 99.3% (587 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2026-01-05 18:54:04 +00:00
mihlau
5312958e3b Translated using Weblate (English (United Kingdom))
Currently translated at 99.3% (587 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2026-01-05 18:54:04 +00:00
Gargotaire
f1590c69be Translated using Weblate (Catalan)
Currently translated at 99.8% (590 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2026-01-05 14:26:06 +00:00
Thunderstrike116
3cb86e356b Translated using Weblate (Greek)
Currently translated at 93.0% (550 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2026-01-05 13:05:53 +00:00
kscop-n1
bb150ba8bf Translated using Weblate (Ukrainian)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2026-01-05 10:46:02 +00:00
takyaO
21d0072d7d Translated using Weblate (Japanese)
Currently translated at 94.9% (561 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ja/
2026-01-05 10:46:02 +00:00
Translation expert
df494a9392 Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2026-01-05 08:05:52 +00:00
Gabriel S
8d64f1ba63 Translated using Weblate (Romanian)
Currently translated at 96.2% (569 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2026-01-05 08:05:52 +00:00
Antonio Toledo
72ca9cdc66 Translated using Weblate (Spanish)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2026-01-04 22:05:53 +00:00
Fjuro
96d247c2b5 Translated using Weblate (Czech)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2026-01-04 22:05:52 +00:00
Enzo GIOIELLI
a45f5ce626 Translated using Weblate (French)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2026-01-04 00:49:19 +00:00
Niels van Velzen
c69c0ab23d Use space tokens in settings UI 2026-01-03 23:04:42 +01:00
Niels van Velzen
75b5f62e6b Use color tokens for settings UI 2026-01-03 18:52:43 +01:00
Niels van Velzen
5ae1347ec0 Update color tokens 2026-01-03 18:52:43 +01:00
Niels van Velzen
59a36bacf1 Support layout direction in ToolbarLayout 2026-01-03 18:52:29 +01:00
Niels van Velzen
9b321a1b17 Show progress and duration in video player 2026-01-03 16:51:29 +01:00
Niels van Velzen
638bd222fd Remove old preference UI 2026-01-03 14:33:03 +01:00
無情天
9c312645b3 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2026-01-03 05:05:53 +00:00
Niels van Velzen
84cdc00115 Migrate advanced playback settings to compose 2026-01-02 23:58:19 +01:00
Finn Möbius
40f4af82bc Translated using Weblate (German)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2026-01-02 17:05:52 +00:00
Niels van Velzen
a01d539b92 Fix 720 and 420 Kbit/s bitrate options not working 2026-01-02 14:49:28 +01:00
AfmanS
f87302d15f Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2026-01-02 13:05:53 +00:00
Niels van Velzen
7c8020ba30 Add shared transitions to RouterContent 2026-01-02 08:36:16 +01:00
Niels van Velzen
068880e780 Fix initial focus on visibility change in DialogBase 2026-01-01 18:29:43 +01:00
Niels van Velzen
2af5ea8c28 Use DialogBase for InAppScreensaver 2026-01-01 18:29:35 +01:00
Niels van Velzen
4f67eeb0d2 Animate settings dialog 2026-01-01 18:05:20 +01:00
Niels van Velzen
875f094ff2 Use sliding transition for settings navigation 2026-01-01 18:05:20 +01:00
Lionel Morin
0f5f1b23fc Translated using Weblate (French)
Currently translated at 99.6% (589 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2026-01-01 00:05:52 +00:00
Kityn
a3f5928349 Translated using Weblate (Polish)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-12-31 19:05:53 +00:00
Bas
105f86b16b Translated using Weblate (Dutch)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2025-12-31 16:06:00 +00:00
tan
b26b04bddc Translated using Weblate (Thai)
Currently translated at 36.7% (217 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/th/
2025-12-31 16:05:59 +00:00
Translation expert
6d1c48b696 Translated using Weblate (Arabic)
Currently translated at 100.0% (591 of 591 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-12-31 16:05:57 +00:00
Niels van Velzen
2912b830b5 Add explanation about branches to README 2025-12-31 14:56:15 +01:00
Niels van Velzen
ebec491592 Migrate live tv guide settings to compose 2025-12-31 14:56:05 +01:00
Niels van Velzen
00125664ac Migrate library display settings to compose 2025-12-31 11:50:38 +01:00
Niels van Velzen
d38627cb24 Support defaultRouteParameters in ProvideRouter 2025-12-31 11:50:38 +01:00
Niels van Velzen
540a3855b4 Fix rememberPreference not saving for AsyncPreferenceStore 2025-12-30 21:08:29 +01:00
Niels van Velzen
c1f78e5c66 Remove unused server/user preferences UI 2025-12-30 21:08:00 +01:00
Niels van Velzen
cff7ed0d74 Use ListMessage for errors 2025-12-30 21:08:00 +01:00
Niels van Velzen
07f89b2ac2 Don't overwrite displayPreferencesId in ItemRowAdapterHelper.retrieveUserViews 2025-12-30 20:59:12 +01:00
Niels van Velzen
c3ce0bdc4b Migrate home settings to compose 2025-12-30 15:50:09 +01:00
Niels van Velzen
139c8e6202 Migrate libraries settings to compose 2025-12-30 08:56:49 +01:00
Niels van Velzen
0e0f330bc0 Migrate customization settings to compose 2025-12-30 08:56:49 +01:00
Fjuro
022ca79fc8 Translated using Weblate (Czech)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2025-12-30 03:05:52 +00:00
l00d3r
52bde656d0 Translated using Weblate (Estonian)
Currently translated at 99.6% (588 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-29 19:05:52 +00:00
kscop-n1
9ce1487056 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2025-12-29 16:05:52 +00:00
Niels van Velzen
5d086b4ca3 Remove profile picture from ClockUserView 2025-12-29 15:45:09 +01:00
Niels van Velzen
184b966491 Add design tokens 2025-12-29 14:40:05 +01:00
renovate[bot]
695bf20d27 Update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.9.0+1 2025-12-29 12:14:27 +01:00
ilker
e41e2874b2 Translated using Weblate (Turkish)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/tr/
2025-12-29 11:05:54 +00:00
Gargotaire
df51fc0fea Translated using Weblate (Catalan)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2025-12-29 11:05:54 +00:00
ilker
42d7078e5c Translated using Weblate (Turkish)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2025-12-29 11:05:53 +00:00
Thunderstrike116
2c3066b624 Translated using Weblate (Greek)
Currently translated at 93.0% (549 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2025-12-29 11:05:52 +00:00
alxhu
76647acd33 Translated using Weblate (German)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2025-12-29 07:18:19 +00:00
Translation expert
88ac61fe0b Translated using Weblate (Arabic)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-12-28 23:05:54 +00:00
無情天
b0cd5db981 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2025-12-28 23:05:54 +00:00
Yofi-FR
d481842b13 Translated using Weblate (French)
Currently translated at 99.8% (589 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2025-12-28 23:05:53 +00:00
Spillutvikler
d0678c3982 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2025-12-28 15:05:53 +00:00
Niels van Velzen
81d84e47aa Run build workflow on release branches 2025-12-28 15:30:27 +01:00
Niels van Velzen
df94aadb19 Remove premieresEnabled and mediaManagementEnabled preferences 2025-12-28 15:16:21 +01:00
Niels van Velzen
d05836f755 Migrate screensaver customization settings to compose 2025-12-28 12:14:40 +01:00
Niels van Velzen
f0d4a97e12 Remove shortcut preferences 2025-12-28 12:13:11 +01:00
Kityn
45d0ee8b7d Translated using Weblate (Polish)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-12-28 09:05:52 +00:00
rimasx
ae22ecc1fd Translated using Weblate (Estonian)
Currently translated at 99.4% (587 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-28 06:05:52 +00:00
Niels van Velzen
7bf042f389 Update README build instructions 2025-12-27 20:19:35 +01:00
Bas
a91ae41bb6 Translated using Weblate (Dutch)
Currently translated at 100.0% (590 of 590 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2025-12-27 19:05:52 +00:00
Niels van Velzen
64763edf4d Allow selecting external video player app 2025-12-27 16:20:35 +01:00
Fjuro
eeb3e95283 Translated using Weblate (Czech)
Currently translated at 100.0% (586 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2025-12-27 13:05:53 +00:00
Translation expert
f012168d66 Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2025-12-27 00:05:52 +00:00
Translation expert
a29926b0c4 Translated using Weblate (Arabic)
Currently translated at 100.0% (586 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-12-27 00:05:52 +00:00
無情天
cae30bdacc Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (586 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2025-12-26 18:05:52 +00:00
rimasx
3768ac9728 Translated using Weblate (Estonian)
Currently translated at 99.1% (581 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-26 14:05:53 +00:00
koffevar
f2ec78235d Translated using Weblate (Russian)
Currently translated at 99.4% (583 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2025-12-26 11:05:52 +00:00
User X
fe04c050f1 Translated using Weblate (German)
Currently translated at 100.0% (586 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2025-12-25 17:05:52 +00:00
Kityn
3a8ca61cdc Translated using Weblate (Polish)
Currently translated at 100.0% (586 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-12-25 09:05:52 +00:00
Bas
30e43e12e7 Translated using Weblate (Dutch)
Currently translated at 100.0% (586 of 586 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2025-12-24 18:30:27 +00:00
DeDuplicate
b46dba52f8 Translated using Weblate (Hebrew)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2025-12-24 17:58:43 +00:00
Niels van Velzen
3fbc3a35ca Migrate subtitle customization settings to compose 2025-12-24 18:58:39 +01:00
Niels van Velzen
1c30542802 Add FMP4 support for transcodes 2025-12-24 15:18:47 +01:00
Niels van Velzen
9f328d0281 Fix AVC high 10 reporting as main 10 in device profile 2025-12-24 15:18:40 +01:00
Translation expert
c5c98b4513 Translated using Weblate (Arabic)
Currently translated at 99.4% (569 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-12-23 23:19:12 +00:00
Fjuro
0e0f6c601c Translated using Weblate (Czech)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2025-12-23 15:05:52 +00:00
takyaO
4bf92183d2 Translated using Weblate (Japanese)
Currently translated at 98.6% (564 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ja/
2025-12-23 08:05:51 +00:00
Niels van Velzen
15bd5a247c Remove value override from RangeControl 2025-12-23 07:27:29 +01:00
Gallyam Biktashev
29dacd490a Translated using Weblate (Russian)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2025-12-22 19:47:08 +00:00
Niels van Velzen
4158b701c6 Fix range control calculation when minimum is not 0 2025-12-22 15:50:55 +01:00
Gargotaire
4fc21f94bf Translated using Weblate (Catalan)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2025-12-22 13:05:52 +00:00
Weblate
8dd9545b47 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/
2025-12-22 10:05:55 +00:00
nenadsuperzmaj
3eea008374 Translated using Weblate (Swahili)
Currently translated at 0.3% (2 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sw/
2025-12-22 10:05:54 +00:00
nenadsuperzmaj
d600fcac38 Translated using Weblate (Serbian)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sr/
2025-12-22 10:05:53 +00:00
Maltavius
16f394afa6 Translated using Weblate (Swedish)
Currently translated at 98.9% (566 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2025-12-22 10:05:53 +00:00
Thunderstrike116
e6d8d8671c Translated using Weblate (Greek)
Currently translated at 92.6% (530 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2025-12-22 10:05:52 +00:00
alxhu
2d5f5966e6 Translated using Weblate (German)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2025-12-22 10:05:51 +00:00
Andi Chandler
69209bb842 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2025-12-21 21:05:52 +00:00
Niels van Velzen
af62319d11 Use dedicated screen for about section in settings 2025-12-21 15:31:32 +01:00
Bas
beb08e071b Translated using Weblate (Dutch)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2025-12-20 16:55:09 +00:00
renovate[bot]
629719d75a Update aboutlibraries to v13.2.1 2025-12-20 15:15:26 +01:00
Modac
a32f58d4e0 Translated using Weblate (German)
Currently translated at 99.3% (568 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2025-12-19 12:55:09 +00:00
jomi
096516b819 Translated using Weblate (Spanish)
Currently translated at 99.6% (570 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2025-12-19 06:55:09 +00:00
Gabriel S
8a20f864de Translated using Weblate (Romanian)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2025-12-18 14:55:09 +00:00
Kityn
d9fd231034 Translated using Weblate (Polish)
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-12-18 08:55:08 +00:00
無情天
022cc909f9 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2025-12-18 01:55:08 +00:00
無情天
fa97921c31 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (572 of 572 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2025-12-17 23:22:26 +00:00
renovate[bot]
133f189d5c Update Kotlin to v2.3.0 2025-12-17 22:18:53 +01:00
Niels van Velzen
ca57f2e103 Migrate playback settings to compose 2025-12-17 22:17:52 +01:00
renovate[bot]
036ca2bafa Update androidx.media3 to v1.9.0 2025-12-17 22:17:40 +01:00
renovate[bot]
7e0a1529f7 Update androidx.activity to v1.12.2 2025-12-17 22:16:48 +01:00
Niels van Velzen
d38d742cf5 Remove brief descriptions from settings screens 2025-12-17 13:10:47 +01:00
Niels van Velzen
ae52b479e0 Add RangeControl composable and use color tokens for Seekbar 2025-12-17 13:10:34 +01:00
Niels van Velzen
43b41ad4d5 Add footer to ListItemContent 2025-12-17 13:10:20 +01:00
Niels van Velzen
eee0942272 Add ListControl as base for ListButton 2025-12-17 13:10:20 +01:00
DeDuplicate
8bf581dae3 Translated using Weblate (Hebrew (Israel))
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/he_IL/
2025-12-17 09:55:08 +00:00
DeDuplicate
f7698e205c Translated using Weblate (Hebrew)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2025-12-17 09:55:08 +00:00
renovate[bot]
964e8eb065 Update github/codeql-action action to v4.31.9 2025-12-16 21:34:42 +01:00
Chris
700220bc07 Translated using Weblate (Swedish)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2025-12-16 10:55:08 +00:00
renovate[bot]
888bfaa05b Update dependency org.jellyfin.sdk:jellyfin-core to v1.8.5 2025-12-16 11:21:27 +01:00
rimasx
46dcc32a38 Translated using Weblate (Estonian)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-15 10:10:59 +00:00
alxhu
701d4b2826 Translated using Weblate (German)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2025-12-15 10:10:59 +00:00
Translation expert
9c64544a0e Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2025-12-14 23:55:09 +00:00
Translation expert
1247475cbf Translated using Weblate (Arabic)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-12-14 23:55:08 +00:00
Merlin Anne Garcia
cb8b690a69 Translated using Weblate (French)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2025-12-14 23:55:08 +00:00
Jesus Oziel Duran Gonzalez
1a69f77a14 Translated using Weblate (Spanish (Mexico))
Currently translated at 95.0% (535 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_MX/
2025-12-14 23:55:08 +00:00
tobster093
ac89159455 Fix Live TV direct play stability (#5229) 2025-12-14 22:27:33 +01:00
Niels van Velzen
24218f9717 Add ProfilePicture composable 2025-12-14 13:30:44 +01:00
Niels van Velzen
d714d58fef Add SettingsColumn composable and clean up formatting 2025-12-14 13:30:44 +01:00
Niels van Velzen
c6e0badbae Add and use checkbox 2025-12-14 13:30:44 +01:00
renovate[bot]
507127c842 Update actions/upload-artifact action to v6 2025-12-13 16:51:37 +01:00
renovate[bot]
72d5b209f3 Update github/codeql-action action to v4.31.8 2025-12-13 16:51:18 +01:00
queeup
dc781ea86f Translated using Weblate (Turkish)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2025-12-13 00:55:07 +00:00
Niels van Velzen
66cafdd3c8 Disallow seeking when duration is unknown 2025-12-12 09:21:44 +01:00
Niels van Velzen
25161f11db Use AAC over HLS for music transcoding 2025-12-12 09:21:36 +01:00
rimasx
3c97433a34 Translated using Weblate (Estonian)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-11 14:55:07 +00:00
renovate[bot]
d5783740f7 Update dependency com.android.tools.build:gradle to v8.13.2 2025-12-11 09:23:22 +01:00
Niels van Velzen
a145a437a8 Remove account manager migration 2025-12-11 08:30:28 +01:00
Niels van Velzen
385c6483f3 Use new settings from login screen 2025-12-11 08:30:06 +01:00
Andi Chandler
e339290b53 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2025-12-09 13:55:07 +00:00
Gargotaire
4dbad2ddc3 Translated using Weblate (Catalan)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2025-12-09 07:49:19 +00:00
Niels van Velzen
1b4d09a37d Migrate server/user settings to compose 2025-12-09 07:46:12 +01:00
takyaO
73a8945830 Translated using Weblate (Japanese)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ja/
2025-12-09 03:50:16 +00:00
無情天
58714986ae Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2025-12-08 23:55:07 +00:00
Gianluigi
e4a9b0dc78 Translated using Weblate (Italian)
Currently translated at 99.6% (561 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2025-12-08 16:55:07 +00:00
Resistive
7584e54ed6 Translated using Weblate (Russian)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2025-12-08 12:20:43 +00:00
Fabrizio Mansilla
b44ac842c9 Translated using Weblate (Spanish (Latin America))
Currently translated at 97.5% (549 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2025-12-08 09:55:11 +00:00
Fabrizio Mansilla
b96f934b49 Translated using Weblate (Spanish (Argentina))
Currently translated at 99.6% (561 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_AR/
2025-12-08 09:55:11 +00:00
kscop-n1
49f386f59d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2025-12-08 09:55:08 +00:00
Fabrizio Mansilla
74fde37643 Translated using Weblate (Spanish)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2025-12-08 09:55:07 +00:00
Thunderstrike116
dcc28101e1 Translated using Weblate (Greek)
Currently translated at 92.7% (522 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2025-12-08 09:55:07 +00:00
Kityn
b22b611c22 Translated using Weblate (Polish)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-12-08 06:32:08 +00:00
Bas
2b41879dfa Translated using Weblate (Dutch)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2025-12-07 23:55:08 +00:00
Fjuro
54ea9bf805 Translated using Weblate (Czech)
Currently translated at 100.0% (563 of 563 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2025-12-07 23:55:07 +00:00
Niels van Velzen
1868d8eb53 Migrate authentication settings to compose 2025-12-07 22:37:00 +01:00
renovate[bot]
759cc88e61 Update dependency io.mockk:mockk to v1.14.7 2025-12-07 22:16:15 +01:00
Niels van Velzen
a6811b462e Migrate licenses to compose 2025-12-07 16:50:21 +01:00
Niels van Velzen
a2fc550adc Add initial parameter support to router 2025-12-07 16:50:21 +01:00
Niels van Velzen
4461ff1776 Use mutableLongState in SettingsDeveloperScreen 2025-12-07 16:50:21 +01:00
Niels van Velzen
c3a2bb5120 Migrate developer settings to compose 2025-12-07 11:02:20 +01:00
renovate[bot]
5cac1a155e Update github/codeql-action action to v4.31.7 2025-12-06 17:48:56 +01:00
rimasx
ffcfacd893 Translated using Weblate (Estonian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-06 14:55:08 +00:00
Lionel Morin
98489f603a Translated using Weblate (French)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2025-12-04 19:56:13 +00:00
Gargotaire
72d67b5e03 Translated using Weblate (Catalan)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2025-12-04 16:01:16 +00:00
Niels van Velzen
d69d6a29bb Migrate telemetry settings to compose 2025-12-04 09:58:10 +01:00
renovate[bot]
e240769daf Update actions/setup-java action to v5.1.0 2025-12-04 07:34:32 +01:00
renovate[bot]
20ccff5514 Update androidx.compose to v1.10.0 2025-12-03 22:17:29 +01:00
Weblate
ff22b8d666 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/
2025-12-03 20:55:11 +00:00
Fabrizio Mansilla
08930ce9a9 Translated using Weblate (Spanish (Argentina))
Currently translated at 96.0% (540 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_AR/
2025-12-03 20:55:09 +00:00
Fabrizio Mansilla
740de57703 Translated using Weblate (Spanish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2025-12-03 20:55:07 +00:00
renovate[bot]
805bf966e2 Update androidx.activity to v1.12.1 2025-12-03 21:43:57 +01:00
renovate[bot]
fea336522d Update actions/stale action to v10.1.1 2025-12-03 09:13:25 +01:00
Damontecres
6674239bae Add VC-1 to device direct play profile
Adds direct play support for VC-1 video codec on devices with a builtin decoder
2025-12-02 22:48:49 +01:00
renovate[bot]
a0957534a9 Update actions/checkout action to v6.0.1 2025-12-02 21:00:14 +01:00
Niels van Velzen
1c75688935 Use android plugin for AboutLibraries generation 2025-12-02 16:33:17 +01:00
Niels van Velzen
1b0c5bf47b Use compose dialog for DialogBase 2025-12-02 16:26:04 +01:00
Niels van Velzen
ffb74649e6 Add navigation support to settings 2025-12-02 16:25:56 +01:00
Prasanth Baskar
357374d698 Translated using Weblate (Tamil)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ta/
2025-12-02 02:07:07 +00:00
Prasanth Baskar
d6a70bab64 Translated using Weblate (Tamil)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ta/
2025-12-02 02:07:06 +00:00
rimasx
0fbe21882f Translated using Weblate (Estonian)
Currently translated at 99.6% (560 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-01 22:08:57 +00:00
kscop-n1
3f4366c876 Translated using Weblate (Ukrainian)
Currently translated at 99.8% (561 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2025-12-01 19:46:37 +00:00
刘汉源
d35a1d328d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2025-12-01 19:46:37 +00:00
AfmanS
f3597cd8ce Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2025-12-01 19:46:36 +00:00
rimasx
f41f61ebb6 Translated using Weblate (Estonian)
Currently translated at 99.6% (560 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-01 15:55:07 +00:00
rimasx
dd3378b66e Translated using Weblate (Estonian)
Currently translated at 99.6% (560 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-12-01 12:46:22 +00:00
renovate[bot]
98838ed123 Update dependency org.jellyfin.sdk:jellyfin-core to v1.8.4 2025-12-01 12:36:44 +01:00
renovate[bot]
59047e11f8 Update github/codeql-action action to v4.31.6 2025-12-01 12:36:37 +01:00
renovate[bot]
c851c0bb71 Update kotest to v6.0.7 2025-12-01 12:36:27 +01:00
Gallyam Biktashev
47d45b4dc9 Translated using Weblate (Russian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2025-12-01 10:55:08 +00:00
Thunderstrike116
d62035bb58 Translated using Weblate (Greek)
Currently translated at 92.7% (521 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2025-12-01 10:55:08 +00:00
lasse_g
a8cf6310b4 Translated using Weblate (German)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2025-12-01 10:55:07 +00:00
nenadsuperzmaj
5272d851c1 Translated using Weblate (Serbian)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sr/
2025-12-01 07:54:58 +00:00
nextlooper42
b8a5525f14 Translated using Weblate (Slovak)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2025-12-01 07:54:57 +00:00
Gargotaire
de961044ff Translated using Weblate (Catalan)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2025-12-01 07:21:12 +00:00
takyaO
bd95c13f50 Translated using Weblate (Japanese)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ja/
2025-12-01 07:21:12 +00:00
Fabien
f4ed78a611 Translated using Weblate (French)
Currently translated at 99.8% (561 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2025-12-01 07:21:12 +00:00
Fjuro
04abc96882 Translated using Weblate (Czech)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2025-11-30 23:55:07 +00:00
Kityn
3bfff95465 Translated using Weblate (Polish)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-11-30 14:46:51 +00:00
Kityn
d98b8acf22 Translated using Weblate (Polish)
Currently translated at 99.8% (561 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2025-11-30 14:41:18 +00:00
Bas
81cb9f04ad Translated using Weblate (Dutch)
Currently translated at 100.0% (562 of 562 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2025-11-30 14:41:17 +00:00
Niels van Velzen
f8ee6de28f Add new settings UI 2025-11-30 15:32:24 +01:00
rimasx
d80032fb7f Translated using Weblate (Estonian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-11-30 14:15:35 +00:00
rimasx
f490064bec Translated using Weblate (Estonian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-11-30 06:19:51 +00:00
rimasx
4b3fe53bda Translated using Weblate (Estonian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-11-30 06:07:52 +00:00
rimasx
98128190d6 Translated using Weblate (Estonian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-11-29 21:46:45 +00:00
rimasx
248fe58381 Translated using Weblate (Estonian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2025-11-29 16:05:44 +00:00
Jens van Almsick
66573c399f utilize group id for track selection
group ids always start at 0 so we don't need the ```indexStartsAtOne```
parameter anymore
2025-11-29 15:48:28 +01:00
Jens van Almsick
a1e6b17faf clear audio stream index on item start 2025-11-29 15:48:28 +01:00
Niels van Velzen
07e3809227 Fix tinting in account switcher button fallback icon 2025-11-29 15:15:04 +01:00
renovate[bot]
f9e7e735e8 Update appleboy/ssh-action action to v1.2.4 2025-11-29 14:39:54 +01:00
renovate[bot]
f394d17f37 Update github/codeql-action action to v4.31.5 2025-11-27 21:59:13 +01:00
renovate[bot]
72d98710a9 Update kotest to v6.0.5 2025-11-27 21:58:32 +01:00
renovate[bot]
bb0096905f Update actions/checkout action to v6 2025-11-27 21:58:15 +01:00
clams4shoes
da71c6f6a0 Add AV1 detection support for older Android versions (#5186)
* Fix AV1 support on Fire OS

* Use API checks instead of reflection
2025-11-27 21:57:43 +01:00
Douglas Newman
7e95582f89 Fix Vimu Player Integration (#5187)
* fix activity result status code handling for vimu

* Fixup

---------

Co-authored-by: Niels van Velzen <git@ndat.nl>
2025-11-26 22:08:44 +01:00
Niels van Velzen
50e21994da Update issue templates 2025-11-23 11:08:32 +01:00
José Benigno
009fe70ed3 Translated using Weblate (Galician)
Currently translated at 13.3% (75 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/gl/
2025-11-23 00:55:07 +00:00
José Benigno
6c3d289169 Translated using Weblate (Galician)
Currently translated at 11.0% (62 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/gl/
2025-11-22 16:55:09 +00:00
Oatavandi
a5328eecb0 Translated using Weblate (Tamil)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ta/
2025-11-21 18:55:06 +00:00
Gabriel S
cd1e323616 Translated using Weblate (Romanian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2025-11-21 13:55:11 +00:00
Gabriel S
3929512331 Translated using Weblate (Romanian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2025-11-21 10:55:07 +00:00
Gabriel S
73cdfda7af Translated using Weblate (Romanian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2025-11-21 07:58:57 +00:00
Gabriel S
0337f27942 Translated using Weblate (Romanian)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2025-11-21 00:55:08 +00:00
renovate[bot]
44ff6ebbbf Update androidx.activity to v1.12.0 2025-11-20 09:10:23 +01:00
renovate[bot]
35df230463 Update androidx.lifecycle to v2.10.0 2025-11-20 09:10:10 +01:00
renovate[bot]
d706f937c1 Update dependency androidx.window:window to v1.5.1 2025-11-19 19:49:45 +01:00
renovate[bot]
da950a1e07 Update androidx.compose to v1.9.5 2025-11-19 19:49:30 +01:00
renovate[bot]
07fc2d614a Update Gradle to v9.2.1 2025-11-17 19:11:35 +01:00
renovate[bot]
48357a6f81 Update actions/checkout action to v5.0.1 2025-11-17 19:01:33 +01:00
renovate[bot]
c663982ba0 Update dependency org.jellyfin.sdk:jellyfin-core to v1.8.3 2025-11-17 19:01:26 +01:00
Niels van Velzen
9d9adab416 Fix search input not set to single line 2025-11-17 18:03:41 +01:00
Niels van Velzen
04da7eb69e Do not clear audio stream index when starting item 2025-11-16 21:36:20 +01:00
Niels van Velzen
fd5ff1cdd9 Increase log level for useful debug log messages 2025-11-16 19:51:37 +01:00
Niels van Velzen
7bc34dce91 Use media source instead of item in switchAudioStream 2025-11-16 14:46:41 +01:00
Niels van Velzen
ceaaa51812 Use MediaSource runtime when available 2025-11-16 14:22:46 +01:00
Ivan Noleto
2e2d9ec9b4 Fix play all and shuffle album/artist playback (#5135) 2025-11-16 12:57:59 +01:00
renovate[bot]
9471d4adf4 Update github/codeql-action action to v4.31.3 2025-11-14 20:47:24 +01:00
Ivan Noleto
d3c84df367 Fix music playback sort order to match web client 2025-11-14 20:40:48 +01:00
Niels van Velzen
936e33903c Add badge composable 2025-11-13 20:47:45 +01:00
Niels van Velzen
a377834e35 Remove redundant TargetApi annotations 2025-11-13 19:41:12 +01:00
Niels van Velzen
eb362165b1 Fix getting supportedSampleRates on Fire OS throwing NullPointerException 2025-11-13 19:40:58 +01:00
renovate[bot]
93287746a8 Update dependency com.android.tools.build:gradle to v8.13.1 2025-11-11 17:06:24 +01:00
Niels van Velzen
4d8e432c67 Apply API model changes 2025-11-09 19:21:10 +01:00
Niels van Velzen
5c9b6c625c Update SDK to 1.8.2 2025-11-09 19:21:10 +01:00
Niels van Velzen
0e7f52fd42 Bump upcomingMinimumServerVersion to 10.11 2025-11-09 19:21:10 +01:00
Niels van Velzen
0d9abb9f17 Fix crash in FullDetailsFragmentHelper.resumePlayback 2025-11-09 16:58:41 +01:00
Niels van Velzen
36da60a1e2 Update pull request template 2025-11-08 22:54:09 +01:00
Niels van Velzen
a330cdcc26 Remove display Dolby Vision checks 2025-11-08 22:53:58 +01:00
Niels van Velzen
56ac9ca8ff Disable visibilityThreshold in rememberPlayerProgress 2025-11-08 22:53:49 +01:00
Niels van Velzen
1dba22557d Get audio duration from player instead of item metadata in AudioNowPlayingFragment 2025-11-08 22:53:49 +01:00
Frederik Boster
f748a566dd Fix low quality card images
To avoid low quality card images the scaling is moved to the server-side by requesting images with appropriate dimensions in all cases.

Fixes #4955
2025-11-08 11:44:42 +01:00
renovate[bot]
f5dfa010f8 Update appleboy/ssh-action action to v1.2.3 2025-11-08 11:03:40 +01:00
Niels van Velzen
55f3fd7b05 Infer subtitle format from media stream path for external players 2025-11-07 18:16:54 +01:00
Niels van Velzen
ffc6765cfe Avoid divide by zero in Seekbar 2025-11-06 20:31:01 +01:00
Translation expert
3be558ce91 Translated using Weblate (Arabic)
Currently translated at 99.8% (559 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-11-06 13:45:30 +00:00
Translation expert
d3130e38d1 Translated using Weblate (Arabic)
Currently translated at 99.6% (558 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-11-05 00:01:49 +00:00
Fjuro
25ba439e9c Translated using Weblate (Czech)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/cs/
2025-11-04 19:06:49 +00:00
Fjuro
76d92c3361 Translated using Weblate (Czech)
Currently translated at 100.0% (560 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2025-11-04 19:06:49 +00:00
renovate[bot]
b07bc3062c Update dependency androidx.window:window to v1.5.0 2025-11-04 15:52:20 +01:00
renovate[bot]
f43dca4b76 Update dependency androidx.work:work-runtime to v2.11.0 2025-11-04 15:46:36 +01:00
Niels van Velzen
278cdb8e0f Require Android 6 at minimum (API 23) 2025-11-04 15:42:07 +01:00
Niels van Velzen
05180de3b4 Enable Proguard 2025-11-04 15:35:09 +01:00
renovate[bot]
d9c27d3849 Update aboutlibraries to v13 2025-11-04 15:34:26 +01:00
renovate[bot]
d5167884f0 Update Gradle to v9.2.0 2025-11-04 15:34:19 +01:00
renovate[bot]
fe8f180354 Update github/codeql-action action to v4.31.2 2025-11-04 15:34:11 +01:00
Jens van Almsick
a1dd6fcfd9 fix: clear only audio stream index on media item start
Before clearPlaybackSessionOptions() was called which also cleared the
subtitle index, but because baking in subs will restart the playback
this lead to an infinite loop.
2025-11-04 15:31:37 +01:00
Thadah D. Denyse
395db1ccf2 Translated using Weblate (Basque)
Currently translated at 50.0% (2 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/eu/
2025-11-03 14:51:53 +00:00
Translation expert
2d89bd4670 Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2025-11-02 20:32:12 +00:00
Translation expert
b3711228ab Translated using Weblate (Arabic)
Currently translated at 99.2% (556 of 560 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2025-11-02 20:32:10 +00:00
Niels van Velzen
3fe6ed6f7b Revert "fix: audio track selection on subsequent video media items"
This reverts commit 5c4ebcf7da.
2025-11-01 15:08:02 +01:00
390 changed files with 12118 additions and 6855 deletions

2
.gitattributes vendored
View File

@@ -5,3 +5,5 @@
CONTRIBUTORS.md merge=union
app/src/main/res/values*/strings.xml merge=union
design/src/main/kotlin/token/* linguist-generated

View File

@@ -10,6 +10,8 @@ body:
label: "This issue respects the following points:"
description: All conditions are **required**.
options:
- label: I have discussed this issue in the [Jellyfin forum](https://forum.jellyfin.org/) and/or [troubleshooting chat](https://jellyfin.org/contact) before opening this issue.
required: true
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-androidtv/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
required: true
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
@@ -46,6 +48,20 @@ body:
- Copy the media info from the web interface
placeholder: Paste media info…
render: shell
- type: textarea
id: mediacapabilitiesreport
attributes:
label: Media capabilities report
description: |
Please share the media capability report generated from within the app. To do some follow these steps:
1. Open the Jellyfin for Android TV app
2. Connect to a server **that you have admin access to**
3. Go to Preferences -> Playback -> Advanced playback preferences
4. On the bottom, press the "Report playback capabilities to server" button
5. Go to the logs in your Jellyfin dashboard (in your browser)
6. Open the newest log file, it should be named similar to `upload_Jellyfin Android TV[...].log`
7. Press the "Download" button and paste the file contents
placeholder: Paste capability report contents…
- type: markdown
attributes:
value: |
@@ -121,7 +137,7 @@ body:
id: other-sources
attributes:
label: Other sources
description: If this topic has been discussed outside of GitHub, please link it.
description: Please link the related forum topics or chat messages that prompted you open this issue.
placeholder: https://forum.jellyfin.org/…
validations:
required: false

View File

@@ -9,6 +9,8 @@ body:
label: "This issue respects the following points:"
description: All conditions are **required**.
options:
- label: I have discussed this issue in the [Jellyfin forum](https://forum.jellyfin.org/) and/or [troubleshooting chat](https://jellyfin.org/contact) before opening this issue.
required: true
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-androidtv/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
required: true
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
@@ -98,7 +100,7 @@ body:
id: other-sources
attributes:
label: Other sources
description: If this topic has been discussed outside of GitHub, please link it.
description: Please link the related forum topics or chat messages that prompted you open this issue.
placeholder: https://forum.jellyfin.org/…
validations:
required: false

View File

@@ -1,11 +1,15 @@
<!--
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://docs.jellyfin.org/general/contributing/issues.html page.
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.org/docs/general/contributing/issues/ page.
-->
**Changes**
<!-- Describe your changes here in 1-5 sentences. -->
**Code assistance**
<!-- If code assistance was used, describe how it contributed
e.g., code generated by LLM, explanation of code base, debugging guidance. -->
**Issues**
<!-- Tag any issues that this PR solves here.
ex. Fixes # -->

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- master
- release-*
pull_request:
permissions:
@@ -15,20 +16,20 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Java
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: 21
java-version-file: .tools-versions
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Assemble debug APKs
run: ./gradlew assembleDebug
- name: Create publish bundle
run: mkdir -p build/gh-app-publish/; find app/build/ -iname "*.apk" -exec mv "{}" build/gh-app-publish/ \;
- name: Upload artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-artifacts
retention-days: 14

View File

@@ -17,18 +17,18 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Java
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: 21
java-version-file: .tools-versions
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Run detekt and lint tasks
run: ./gradlew detekt lint
- name: Upload SARIF files
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
if: ${{ always() }}
with:
sarif_file: .

View File

@@ -12,14 +12,14 @@ jobs:
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Java
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: 21
java-version-file: .tools-versions
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Set JELLYFIN_VERSION
run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV
- name: Assemble release files
@@ -70,7 +70,7 @@ jobs:
remote_user: ${{ secrets.REPO_USER }}
remote_key: ${{ secrets.REPO_KEY }}
- name: Update repo.jellyfin.org symlinks
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
with:
host: ${{ secrets.REPO_HOST }}
username: ${{ secrets.REPO_USER }}

View File

@@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Java
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: 21
java-version-file: .tools-versions
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Run test task
run: ./gradlew test

View File

@@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-24.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120

1
.tools-versions Normal file
View File

@@ -0,0 +1 @@
java 21

View File

@@ -25,9 +25,6 @@
<a href="https://matrix.to/#/+jellyfin:matrix.org">
<img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/>
</a>
<a href="https://www.reddit.com/r/jellyfin">
<img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/>
</a>
<br/>
<a href="https://play.google.com/store/apps/details?id=org.jellyfin.androidtv">
<img width="153" alt="Jellyfin on Google Play" src="https://jellyfin.org/images/store-icons/google-play.png"/>
@@ -42,48 +39,39 @@
<a href="https://repo.jellyfin.org/releases/client/androidtv/">Download archive</a>
</p>
Jellyfin Android TV is a Jellyfin client for Android TV, Nvidia Shield, and Amazon Fire TV devices.
We welcome all contributions and pull requests! If you have a larger feature in mind please open an
issue so we can discuss the implementation before you start.
Jellyfin Android TV is a Jellyfin client for Android TV, Nvidia Shield, and Amazon Fire TV devices. We welcome all contributions and pull
requests! If you have a larger feature in mind please open an issue so we can discuss the implementation before you start.
## Building
The app uses Gradle and requires the Android SDK. We recommend using Android Studio, which includes all required dependencies, for
development and building. For manual building without Android Studio make sure a compatible JDK and Android SDK are installed and in your
PATH, then use the Gradle wrapper (`./gradlew`) to build the project with the `assembleDebug` Gradle task to generate an apk file:
```shell
./gradlew assembleDebug
```
The task will create an APK file in the `/app/build/outputs/apk/debug` directory. This APK file uses a different app-id from our stable
builds and can be manually installed to your device.
## Branching
The `master` branch is the primary development branch and the target for all pull requests. It is **unstable** and may contain breaking
changes or unresolved bugs. For production deployments and forks, always use the latest `release-x.y.z` branch. Do not base production work
or long-lived forks on `master`.
Release branches are created at the start of a beta cycle and are kept up to date with each published release. Maintainers will cherry-pick
selected changes into release branches as needed for backports. These branches are reused for subsequent patch releases.
## Translating
Translations can be improved very easily from our
[Weblate](https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv) instance.
Look through the following graphic to see if your native language could use some work!
Translations can be improved very easily from our [Weblate](https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv)
instance. Look through the following graphic to see if your native language could use some work! We cannot accept changes to translation
files via pull requests.
<p align="center">
<a href="https://translate.jellyfin.org/engage/jellyfin-android/">
<img alt="Detailed Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin-android/-/jellyfin-androidtv/multi-auto.svg"/>
</a>
## Build Process
### Dependencies
- Android Studio
### Build
1. Clone or download this repository
```sh
git clone https://github.com/jellyfin/jellyfin-androidtv.git
cd jellyfin-androidtv
```
2. Open the project in Android Studio and run it from there or build an APK directly through Gradle:
```sh
./gradlew assembleDebug
```
Add the Android SDK to your PATH environment variable or create the ANDROID_SDK_ROOT variable for
this to work.
### Deploy to device/emulator
```sh
./gradlew installDebug
```
*You can also replace the "Debug" with "Release" to get an optimized release binary.*
</p>

View File

@@ -1,9 +1,8 @@
plugins {
id("com.android.application")
kotlin("android")
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
android {
@@ -24,6 +23,7 @@ android {
buildConfig = true
viewBinding = true
compose = true
resValues = true
}
compileOptions {
@@ -32,7 +32,7 @@ android {
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
// Set package names used in various XML files
resValue("string", "app_id", namespace!!)
@@ -87,6 +87,7 @@ tasks.register("versionTxt") {
dependencies {
// Jellyfin
implementation(projects.design)
implementation(projects.playback.core)
implementation(projects.playback.jellyfin)
implementation(projects.playback.media3.exoplayer)
@@ -114,6 +115,7 @@ dependencies {
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.leanback.core)
implementation(libs.androidx.leanback.preference)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.preference)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.tvprovider)
@@ -136,6 +138,7 @@ dependencies {
implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.ui)
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
implementation(libs.libass.media3)
// Markdown
implementation(libs.bundles.markwon)

1
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1 @@
-dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough

View File

@@ -35,6 +35,11 @@
android:host="youtube.com"
android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="video/*" />
</intent>
</queries>
<application
@@ -47,22 +52,10 @@
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.Jellyfin"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<!-- Legacy service required for AccountManagerMigration -->
<service
android:name=".auth.service.AuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
android:usesCleartextTraffic="true">
<!-- Screensaver -->
<service
@@ -135,10 +128,6 @@
android:launchMode="singleTask"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name=".ui.preference.PreferencesActivity"
android:theme="@style/Theme.Jellyfin.Preferences" />
<activity android:name=".ui.playback.ExternalPlayerActivity" />
</application>
</manifest>

View File

@@ -1,48 +0,0 @@
package org.jellyfin.androidtv.auth
import android.accounts.AccountManager
import android.content.Context
import androidx.core.content.getSystemService
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.auth.model.AuthenticationStoreServer
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import java.util.UUID
class AccountManagerMigration(
context: Context,
) {
private val accountManager = requireNotNull(context.getSystemService<AccountManager>())
fun migrate(
servers: Map<UUID, AuthenticationStoreServer>,
) = servers.mapValues { (serverId, server) ->
Timber.i("Migrating server $serverId (${server.name})")
server.copy(
users = server.users.mapValues { (userId, user) ->
val accessToken = getAccessToken(serverId, userId)
Timber.i("Migrating user $userId (${user.name}): ${if (accessToken != null) "success" else "no token"}")
user.copy(accessToken = accessToken)
}
)
}
@Suppress("MissingPermission")
private fun getAccessToken(serverId: UUID, userId: UUID): String? = runCatching {
accountManager.getAccountsByType(ACCOUNT_TYPE)
.firstOrNull {
val validServerId = accountManager.getUserData(it, ACCOUNT_DATA_SERVER)?.toUUIDOrNull() == serverId
val validUserId = accountManager.getUserData(it, ACCOUNT_DATA_ID)?.toUUIDOrNull() == userId
validServerId && validUserId
}
?.let { account -> accountManager.peekAuthToken(account, ACCOUNT_ACCESS_TOKEN_TYPE) }
}.getOrNull()
companion object {
const val ACCOUNT_TYPE = BuildConfig.APPLICATION_ID
const val ACCOUNT_DATA_ID = "$ACCOUNT_TYPE.id"
const val ACCOUNT_DATA_SERVER = "$ACCOUNT_TYPE.server"
const val ACCOUNT_ACCESS_TOKEN_TYPE = "$ACCOUNT_TYPE.access_token"
}
}

View File

@@ -26,12 +26,12 @@ import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.discovery.RecommendedServerInfo
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.model.ServerVersion
import org.jellyfin.sdk.model.api.BrandingOptionsDto
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber
import java.time.Instant
import java.util.UUID
import org.jellyfin.sdk.model.api.BrandingOptions as BrandingOptionsDto
/**
* Repository to maintain servers.
@@ -55,7 +55,7 @@ interface ServerRepository {
val minimumServerVersion = Jellyfin.minimumVersion.copy(build = null)
val recommendedServerVersion = Jellyfin.apiVersion.copy(build = null)
val upcomingMinimumServerVersion = ServerVersion(10, 10, 0)
val upcomingMinimumServerVersion = ServerVersion(10, 11, 0)
}
}
@@ -123,7 +123,7 @@ class ServerRepositoryImpl(
}
}
Timber.d(buildString {
Timber.i(buildString {
append("Recommendations: ")
if (greatRecommendation == null) append(0)
else append(1)

View File

@@ -1,79 +0,0 @@
package org.jellyfin.androidtv.auth.service
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.core.os.bundleOf
import org.jellyfin.androidtv.ui.preference.PreferencesActivity
class AuthenticatorService : Service() {
private val authenticator by lazy {
Authenticator(this)
}
override fun onBind(intent: Intent): IBinder? = authenticator.iBinder
private inner class Authenticator(
private val service: AuthenticatorService
) : AbstractAccountAuthenticator(service) {
private val unsupportedOperationBundle = bundleOf(
AccountManager.KEY_ERROR_CODE to AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
AccountManager.KEY_ERROR_MESSAGE to "Unsupported operation"
)
override fun addAccount(
response: AccountAuthenticatorResponse,
accountType: String,
authTokenType: String?,
requiredFeatures: Array<String>?,
options: Bundle,
): Bundle = unsupportedOperationBundle
override fun confirmCredentials(
response: AccountAuthenticatorResponse,
account: Account,
options: Bundle?,
): Bundle = unsupportedOperationBundle
override fun editProperties(
response: AccountAuthenticatorResponse,
accountType: String,
): Bundle = bundleOf(
AccountManager.KEY_INTENT to Intent(service, PreferencesActivity::class.java)
)
override fun getAccountRemovalAllowed(
response: AccountAuthenticatorResponse,
account: Account,
): Bundle = bundleOf(
AccountManager.KEY_BOOLEAN_RESULT to true
)
override fun getAuthToken(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String,
options: Bundle,
): Bundle = unsupportedOperationBundle
override fun getAuthTokenLabel(authTokenType: String): String? = null
override fun hasFeatures(
response: AccountAuthenticatorResponse,
account: Account,
features: Array<String>,
): Bundle = unsupportedOperationBundle
override fun updateCredentials(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String?,
options: Bundle?,
): Bundle = unsupportedOperationBundle
}
}

View File

@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.auth.store
import android.content.Context
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
@@ -13,7 +12,6 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import org.jellyfin.androidtv.auth.AccountManagerMigration
import org.jellyfin.androidtv.auth.model.AuthenticationStoreServer
import org.jellyfin.androidtv.auth.model.AuthenticationStoreUser
import org.jellyfin.sdk.model.serializer.UUIDSerializer
@@ -28,7 +26,6 @@ import java.util.UUID
*/
class AuthenticationStore(
private val context: Context,
private val accountManagerMigration: AccountManagerMigration,
) {
private val storePath
get() = context.filesDir.resolve("authentication_store.json")
@@ -59,11 +56,13 @@ class AuthenticationStore(
// Check for version
return when (root["version"]?.jsonPrimitive?.intOrNull) {
1 -> json.decodeFromJsonElement<Map<UUID, AuthenticationStoreServer>>(root["servers"]!!)
// Add access tokens from account manager to stored users and save the migrated data
.let { servers -> accountManagerMigration.migrate(servers) }
.also { servers -> write(servers) }
// Migration was removed, clear stored servers
1 -> {
Timber.e("Migrating from version 1 is no longer possible")
emptyMap()
}
// Current version, return as-is
2 -> json.decodeFromJsonElement<Map<UUID, AuthenticationStoreServer>>(root["servers"]!!)
null -> {

View File

@@ -66,6 +66,7 @@ object Codec {
const val VP8 = "vp8"
const val VP9 = "vp9"
const val AV1 = "av1"
const val VC1 = "vc1"
}
object Subtitle {

View File

@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.data.eventhandling
import android.content.Context
import android.media.AudioManager
import android.os.Build
import android.widget.Toast
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
@@ -79,7 +78,7 @@ class SocketHandler(
add(GeneralCommandType.SEND_STRING)
// Note: These are used in the PlaySessionSocketService
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !audioManager.isVolumeFixed) {
if (!audioManager.isVolumeFixed) {
add(GeneralCommandType.VOLUME_UP)
add(GeneralCommandType.VOLUME_DOWN)
add(GeneralCommandType.SET_VOLUME)
@@ -172,13 +171,14 @@ class SocketHandler(
}
private fun onPlayMessage(message: PlayMessage) {
val itemId = message.data?.itemIds?.firstOrNull() ?: return
val itemIds = message.data?.itemIds ?: return
runCatching {
playbackHelper.retrieveAndPlay(
itemId,
itemIds,
false,
message.data?.startPositionTicks,
message.data?.startIndex,
context
)
}.onFailure { Timber.w(it, "Failed to start playback") }

View File

@@ -1,10 +1,11 @@
package org.jellyfin.androidtv.data.model
import org.jellyfin.androidtv.util.apiclient.JellyfinImage
import java.util.UUID
data class ChapterItemInfo(
val itemId: UUID,
val name: String?,
val startPositionTicks: Long,
val imagePath: String?,
val image: JellyfinImage?,
)

View File

@@ -0,0 +1,62 @@
package org.jellyfin.androidtv.data.repository
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ResolveInfo
import androidx.core.net.toUri
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.util.componentName
class ExternalAppRepository(
private val userPreferences: UserPreferences,
) {
companion object {
const val SAMPLE_VIDEO_URL = "http://jellyfin.local/query.mp4"
const val MEDIA_TYPE_VIDEO = "video/*"
}
private val externalPlayerAppIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndTypeAndNormalize(SAMPLE_VIDEO_URL.toUri(), MEDIA_TYPE_VIDEO)
}
fun getExternalPlayerApps(context: Context): List<ResolveInfo> = context.packageManager
.queryIntentActivities(externalPlayerAppIntent, 0)
// Hide apps with priority below zero (system stubs)
.filter { it.priority >= 0 }
fun getCurrentExternalPlayerApp(context: Context): ActivityInfo? {
// Validate if external app should be used at all
val useExternalPlayer = userPreferences[UserPreferences.useExternalPlayer]
if (!useExternalPlayer) return null
// Resolve external app information
val resolvedInfo = userPreferences[UserPreferences.externalPlayerComponentName]
.takeIf { it.isNotEmpty() }
?.let(ComponentName::unflattenFromString)
?.runCatching { context.packageManager.getActivityInfo(this, 0) }
?.getOrNull()
if (resolvedInfo != null) return resolvedInfo
// Fallback in case the app is uninstalled or unavailable for some other reason
val externalApps = getExternalPlayerApps(context)
// System default
val systemDefault = externalApps.find { it.isDefault }?.activityInfo
if (systemDefault != null) return systemDefault
// First compatible, or none
return externalApps.firstOrNull()?.activityInfo
}
fun setExternalPlayerapp(activityInfo: ActivityInfo?) {
if (activityInfo == null) {
userPreferences[UserPreferences.useExternalPlayer] = false
userPreferences[UserPreferences.externalPlayerComponentName] = ""
} else {
userPreferences[UserPreferences.useExternalPlayer] = true
userPreferences[UserPreferences.externalPlayerComponentName] = activityInfo.componentName.flattenToShortString()
}
}
}

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.auth.model.Server
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.BackdropBehavior
import org.jellyfin.androidtv.util.apiclient.getUrl
import org.jellyfin.androidtv.util.apiclient.itemBackdropImages
import org.jellyfin.androidtv.util.apiclient.parentBackdropImages
@@ -59,12 +60,14 @@ class BackgroundService(
* Use all available backdrops from [baseItem] as background.
*/
fun setBackground(baseItem: BaseItemDto?) {
val backdropBehavior = userPreferences[UserPreferences.backdropBehavior]
// Check if item is set and backgrounds are enabled
if (baseItem == null || !userPreferences[UserPreferences.backdropEnabled])
if (baseItem == null || backdropBehavior == BackdropBehavior.DISABLED)
return clearBackgrounds()
// Enable blur for backdrops
_blurBackground.value = true
_blurBackground.value = backdropBehavior == BackdropBehavior.BACKDROP_WITH_BLUR
// Get all backdrop urls
val backdropUrls = (baseItem.itemBackdropImages + baseItem.parentBackdropImages)
@@ -79,7 +82,7 @@ class BackgroundService(
*/
fun setBackground(server: Server) {
// Check if item is set and backgrounds are enabled
if (!userPreferences[UserPreferences.backdropEnabled])
if (userPreferences[UserPreferences.backdropBehavior] == BackdropBehavior.DISABLED)
return clearBackgrounds()
// Check if splashscreen is enabled in (cached) branding options

View File

@@ -20,6 +20,7 @@ import org.jellyfin.androidtv.data.eventhandling.SocketHandler
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.androidtv.data.repository.CustomMessageRepository
import org.jellyfin.androidtv.data.repository.CustomMessageRepositoryImpl
import org.jellyfin.androidtv.data.repository.ExternalAppRepository
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
import org.jellyfin.androidtv.data.repository.ItemMutationRepositoryImpl
import org.jellyfin.androidtv.data.repository.NotificationsRepository
@@ -43,6 +44,7 @@ import org.jellyfin.androidtv.ui.search.SearchFragmentDelegate
import org.jellyfin.androidtv.ui.search.SearchRepository
import org.jellyfin.androidtv.ui.search.SearchRepositoryImpl
import org.jellyfin.androidtv.ui.search.SearchViewModel
import org.jellyfin.androidtv.ui.settings.compat.SettingsViewModel
import org.jellyfin.androidtv.ui.startup.ServerAddViewModel
import org.jellyfin.androidtv.ui.startup.StartupViewModel
import org.jellyfin.androidtv.ui.startup.UserLoginViewModel
@@ -139,6 +141,7 @@ val appModule = module {
single<NavigationRepository> { NavigationRepositoryImpl(Destinations.home) }
single<SearchRepository> { SearchRepositoryImpl(get()) }
single<MediaSegmentRepository> { MediaSegmentRepositoryImpl(get(), get()) }
single<ExternalAppRepository> { ExternalAppRepository(get()) }
viewModel { StartupViewModel(get(), get(), get(), get()) }
viewModel { UserLoginViewModel(get(), get(), get(), get(defaultDeviceInfo)) }
@@ -148,6 +151,7 @@ val appModule = module {
viewModel { PhotoPlayerViewModel(get()) }
viewModel { SearchViewModel(get()) }
viewModel { DreamViewModel(get(), get(), get(), get(), get()) }
viewModel { SettingsViewModel() }
single { BackgroundService(get(), get(), get(), get(), get()) }

View File

@@ -1,6 +1,5 @@
package org.jellyfin.androidtv.di
import org.jellyfin.androidtv.auth.AccountManagerMigration
import org.jellyfin.androidtv.auth.repository.AuthenticationRepository
import org.jellyfin.androidtv.auth.repository.AuthenticationRepositoryImpl
import org.jellyfin.androidtv.auth.repository.ServerRepository
@@ -14,8 +13,7 @@ import org.jellyfin.androidtv.auth.store.AuthenticationStore
import org.koin.dsl.module
val authModule = module {
single { AccountManagerMigration(get()) }
single { AuthenticationStore(get(), get()) }
single { AuthenticationStore(get()) }
single { AuthenticationPreferences(get()) }
single<AuthenticationRepository> {

View File

@@ -26,9 +26,11 @@ import org.jellyfin.playback.media3.session.MediaSessionOptions
import org.jellyfin.playback.media3.session.media3SessionPlugin
import org.jellyfin.sdk.api.client.HttpClientOptions
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
import org.jellyfin.sdk.model.api.MediaSegmentType
import org.koin.android.ext.koin.androidContext
import org.koin.core.scope.Scope
import org.koin.dsl.module
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import org.jellyfin.androidtv.ui.playback.PlaybackManager as LegacyPlaybackManager
@@ -39,10 +41,12 @@ val playbackModule = module {
single { PlaybackLauncher(get(), get(), get(), get()) }
// OkHttp data source using OkHttpFactory from SDK
single<HttpDataSource.Factory> {
val okHttpFactory = get<OkHttpFactory>()
val httpClientOptions = get<HttpClientOptions>()
val httpClientOptions = get<HttpClientOptions>().copy(
// Disable request timeout for media playback as this causes issues with Live TV
requestTimeout = Duration.ZERO
)
OkHttpDataSource.Factory(okHttpFactory.createClient(httpClientOptions))
}
@@ -68,6 +72,7 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
val userPreferences = get<UserPreferences>()
val exoPlayerOptions = ExoPlayerOptions(
preferFfmpeg = userPreferences[UserPreferences.preferExoPlayerFfmpeg],
enableLibass = userPreferences[UserPreferences.assDirectPlay],
enableDebugLogging = userPreferences[UserPreferences.debuggingEnabled],
baseDataSourceFactory = get<HttpDataSource.Factory>(),
)
@@ -82,7 +87,7 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
install(media3SessionPlugin(get(), mediaSessionOptions))
val deviceProfileBuilder = { createDeviceProfile(androidContext(), userPreferences, get()) }
install(jellyfinPlugin(get(), deviceProfileBuilder, ProcessLifecycleOwner.get().lifecycle))
install(jellyfinPlugin(get(), deviceProfileBuilder, setOf(MediaSegmentType.INTRO), ProcessLifecycleOwner.get().lifecycle))
// Options
val userSettingPreferences = get<UserSettingPreferences>()

View File

@@ -42,8 +42,8 @@ import org.jellyfin.androidtv.util.apiclient.itemImages
import org.jellyfin.androidtv.util.apiclient.parentImages
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.jellyfin.lyrics
import org.jellyfin.playback.jellyfin.lyricsFlow
import org.jellyfin.playback.jellyfin.lyrics.lyrics
import org.jellyfin.playback.jellyfin.lyrics.lyricsFlow
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.ImageType
import org.koin.compose.koinInject

View File

@@ -1,14 +1,13 @@
package org.jellyfin.androidtv.preference
import android.content.Context
import android.view.KeyEvent
import androidx.preference.PreferenceManager
import org.jellyfin.androidtv.preference.UserPreferences.Companion.screensaverInAppEnabled
import org.jellyfin.androidtv.preference.constant.AppTheme
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.preference.constant.BackdropBehavior
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.RatingType
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.preference.constant.StillWatchingBehavior
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
@@ -42,19 +41,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
var appTheme = enumPreference("app_theme", AppTheme.DARK)
/**
* Enable background images while browsing
* Behavior of app background while browsing
*/
var backdropEnabled = booleanPreference("pref_show_backdrop", true)
/**
* Show premieres on home screen
*/
var premieresEnabled = booleanPreference("pref_enable_premieres", false)
/**
* Enable management of media like deleting items when the user has sufficient permissions.
*/
var mediaManagementEnabled = booleanPreference("enable_media_management", false)
var backdropBehavior = enumPreference("backdrop_behavior", BackdropBehavior.BACKDROP_WITH_BLUR)
/* Playback - General*/
/**
@@ -99,6 +88,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var useExternalPlayer = booleanPreference("external_player", false)
/**
* Component name for the external playback application.
*/
var externalPlayerComponentName = stringPreference("external_player_component", "")
/**
* Change refresh rate to match media when device supports it
*/
@@ -131,16 +125,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var liveTvDirectPlayEnabled = booleanPreference("pref_live_direct", true)
/**
* Shortcut used for changing the audio track
*/
var shortcutAudioTrack = intPreference("shortcut_audio_track", KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK)
/**
* Shortcut used for changing the subtitle track
*/
var shortcutSubtitleTrack = intPreference("shortcut_subtitle_track", KeyEvent.KEYCODE_CAPTIONS)
/* Developer options */
/**
* Show additional debug information
@@ -157,11 +141,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var clockBehavior = enumPreference("pref_clock_behavior", ClockBehavior.ALWAYS)
/**
* Set which ratings provider should show on MyImageCardViews
*/
var defaultRatingType = enumPreference("pref_rating_type", RatingType.RATING_TOMATOES)
/**
* Set when watched indicators should show on MyImageCardViews
*/
@@ -195,7 +174,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
/**
* Subtitles font size
*/
var subtitlesTextSize = floatPreference("subtitles_text_size", 1f)
var subtitlesTextSize = floatPreference("subtitles_text_size", 24f)
/**
* Subtitles offset
@@ -249,7 +228,12 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
var trickPlayEnabled = booleanPreference("trick_play_enabled", false)
/**
* Enable PGS subtitle direct-play.
* Enable libass.
*/
var assDirectPlay = booleanPreference("libass_enabled", false)
/**
* Enable PGS subtitle direct-play.
*/
var pgsDirectPlay = booleanPreference("pgs_enabled", true)
}
@@ -275,6 +259,19 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
val subtitleStrokeSize = it.getInt("subtitles_stroke_size", 0)
putLong("subtitles_text_stroke_color", if (subtitleStrokeSize > 0) 0XFF000000L else 0X00FFFFFFL)
}
// v0.19.0 to v0.20.0
migration(toVersion = 9) {
// Reset subtitle text size as we changed from fractional sizing to absolute sizing
remove("subtitles_text_size")
// Set the BackdropBehavior if it was enabled in a previous version
val backdropEnabled = it.getBoolean("pref_show_backdrop", true)
putString(
"backdrop_behavior",
if (backdropEnabled) BackdropBehavior.BACKDROP_WITH_BLUR.name else BackdropBehavior.DISABLED.name
)
}
}
}
}

View File

@@ -0,0 +1,20 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
enum class BackdropBehavior(
override val nameRes: Int,
) : PreferenceEnum {
DISABLED(R.string.state_disabled),
/**
* When selected, backdrop is shown with a blur effect.
*/
BACKDROP_WITH_BLUR(R.string.backdrop_behavior_with_blur),
/**
* When selected, backdrop is shown without any blur effect.
*/
BACKDROP_WITHOUT_BLUR(R.string.backdrop_behavior_without_blur),
}

View File

@@ -1,23 +0,0 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
enum class RatingType(
override val nameRes: Int,
) : PreferenceEnum {
/**
* Sets default rating type to tomatoes.
*/
RATING_TOMATOES(R.string.lbl_tomatoes),
/**
* Sets the default rating type to stars.
*/
RATING_STARS(R.string.lbl_stars),
/**
* Sets the default rating type to hidden.
*/
RATING_HIDDEN(R.string.lbl_hidden),
}

View File

@@ -4,16 +4,12 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.databinding.ClockUserBugBinding
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.util.ImageHelper
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -25,9 +21,7 @@ class ClockUserView @JvmOverloads constructor(
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes), KoinComponent {
private val binding: ClockUserBugBinding = ClockUserBugBinding.inflate(LayoutInflater.from(context), this, true)
private val userPreferences by inject<UserPreferences>()
private val userRepository by inject<UserRepository>()
private val navigationRepository by inject<NavigationRepository>()
private val imageHelper by inject<ImageHelper>()
var isVideoPlayer = false
set(value) {
@@ -40,15 +34,6 @@ class ClockUserView @JvmOverloads constructor(
init {
updateClockVisibility()
val currentUser = userRepository.currentUser.value
binding.clockUserImage.load(
url = currentUser?.let(imageHelper::getPrimaryImageUrl),
placeholder = ContextCompat.getDrawable(context, R.drawable.ic_user)
)
binding.clockUserImage.isVisible = currentUser != null
binding.home.setOnClickListener {
navigationRepository.reset(Destinations.home, clearHistory = true)
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.LocalContext
@@ -33,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.ProvideTextStyle
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.base.button.ButtonBase
@@ -90,6 +92,7 @@ fun NowPlayingComposable(
.padding(3.dp)
) {
val image = item.itemImages[ImageType.PRIMARY] ?: item.albumPrimaryImage ?: item.parentImages[ImageType.PRIMARY]
val progressFillColor = JellyfinTheme.colorScheme.rangeControlFill
AsyncImage(
url = image?.getUrl(api),
@@ -102,16 +105,18 @@ fun NowPlayingComposable(
.drawWithContent {
drawContent()
val width = 3.dp.toPx()
// Background
drawCircle(
style = Stroke(width = 3.dp.toPx()),
style = Stroke(width),
color = Color.Black,
alpha = 0.4f,
)
// Foreground
drawArc(
style = Stroke(width = 3.dp.toPx()),
color = Color.White,
style = Stroke(width, cap = StrokeCap.Round),
color = progressFillColor,
useCenter = false,
startAngle = -90f,
sweepAngle = 360f * progress,

View File

@@ -1,31 +0,0 @@
package org.jellyfin.androidtv.ui
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import org.jellyfin.androidtv.databinding.ViewNumberSpinnerBinding
class NumberSpinnerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
val binding = ViewNumberSpinnerBinding.inflate(LayoutInflater.from(context), this, true)
var increment = 10L
var valueChangedListener: ValueChangedListener<Long>? = null
var value = 0L
set(value) {
field = value
binding.value.text = value.toString()
valueChangedListener?.onValueChanged(value)
}
init {
binding.increase.setOnClickListener { value += increment }
binding.decrease.setOnClickListener { value -= increment }
}
}

View File

@@ -1,5 +0,0 @@
package org.jellyfin.androidtv.ui
fun interface ValueChangedListener<T> {
fun onValueChanged(value: T)
}

View File

@@ -0,0 +1,36 @@
package org.jellyfin.androidtv.ui.base
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import org.jellyfin.design.Tokens
@Composable
fun Badge(
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
containerColor: Color = JellyfinTheme.colorScheme.badge,
contentColor: Color = JellyfinTheme.colorScheme.onBadge,
content: @Composable BoxScope.() -> Unit,
) {
ProvideTextStyle(JellyfinTheme.typography.badge.copy(color = contentColor)) {
Box(
modifier = modifier
.defaultMinSize(minWidth = 24.dp, minHeight = 24.dp)
.background(containerColor, shape)
.padding(Tokens.Space.spaceXs),
contentAlignment = Alignment.Center,
) {
content()
}
}
}

View File

@@ -0,0 +1,29 @@
package org.jellyfin.androidtv.ui.base
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@Composable
@Stable
fun BaseScreen(
content: @Composable () -> Unit,
) {
// Work around a focus issue with our current fragment based navigation approach
// by always focusing the screen contents whenever the screen is created
val focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.focusRequester(focusRequester)
.focusGroup()
) {
content()
}
LaunchedEffect(focusRequester) { focusRequester.requestFocus() }
}

View File

@@ -17,8 +17,8 @@ import androidx.compose.ui.graphics.drawscope.Stroke
@Composable
fun CircularProgressIndicator(
color: Color = LocalTextStyle.current.color,
modifier: Modifier = Modifier,
color: Color = LocalTextStyle.current.color,
) {
val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(

View File

@@ -16,10 +16,11 @@ fun JellyfinTheme(
LocalColorScheme provides colorScheme,
LocalShapes provides shapes,
LocalTypography provides typography,
LocalTextStyle provides LocalTextStyle.current.merge(typography.default),
// Disable overscroll
LocalOverscrollFactory provides null,
) {
ProvideTextStyle(value = typography.default, content = content)
content()
}
}

View File

@@ -0,0 +1,54 @@
package org.jellyfin.androidtv.ui.base
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.vectorResource
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import org.jellyfin.androidtv.R
@Composable
fun ProfilePicture(
url: String?,
modifier: Modifier = Modifier,
contentDescription: String? = null,
iconPadding: PaddingValues = PaddingValues.Zero,
) {
Box(
modifier = modifier
) {
val userImagePainter = rememberAsyncImagePainter(url)
val userImageState by userImagePainter.state.collectAsState()
val userImageVisible = userImageState is AsyncImagePainter.State.Success
if (!userImageVisible) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_user),
contentDescription = contentDescription,
modifier = Modifier
.align(Alignment.Center)
.padding(iconPadding)
.fillMaxSize()
)
} else {
Image(
painter = userImagePainter,
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
modifier = Modifier
.aspectRatio(1f)
)
}
}
}

View File

@@ -41,10 +41,10 @@ object SeekbarDefaults {
@ReadOnlyComposable
@Composable
fun colors(
backgroundColor: Color = Color(0xCC4B4B4B),
bufferColor: Color = Color(0x40FFFFFF),
progressColor: Color = Color(0xFF00A4DC),
knobColor: Color = Color.White,
backgroundColor: Color = JellyfinTheme.colorScheme.rangeControlBackground,
bufferColor: Color = JellyfinTheme.colorScheme.seekbarBuffer,
progressColor: Color = JellyfinTheme.colorScheme.rangeControlFill,
knobColor: Color = JellyfinTheme.colorScheme.rangeControlKnob,
) = SeekbarColors(
backgroundColor = backgroundColor,
bufferColor = bufferColor,
@@ -67,7 +67,7 @@ fun Seekbar(
enabled: Boolean = true,
colors: SeekbarColors = SeekbarDefaults.colors(),
) {
val durationMs = duration.inWholeMilliseconds.toFloat()
val durationMs = duration.inWholeMilliseconds.toFloat().coerceAtLeast(1f)
val progressPercentage = progress.inWholeMilliseconds.toFloat() / durationMs
val bufferPercentage = buffer.inWholeMilliseconds.toFloat() / durationMs
val seekForwardPercentage = seekForwardAmount.inWholeMilliseconds.toFloat() / durationMs

View File

@@ -3,10 +3,11 @@ package org.jellyfin.androidtv.ui.base
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import org.jellyfin.design.Tokens
fun colorScheme(): ColorScheme = ColorScheme(
background = Color(0xFF101010),
onBackground = Color(0xFFFFFFFF),
background = Tokens.Color.colorGrey975,
onBackground = Tokens.Color.colorBluegrey25,
button = Color(0xB3747474),
onButton = Color(0xFFDDDDDD),
buttonFocused = Color(0xE6CCCCCC),
@@ -19,9 +20,22 @@ fun colorScheme(): ColorScheme = ColorScheme(
onInput = Color(0xE6CCCCCC),
inputFocused = Color(0xE6CCCCCC),
onInputFocused = Color(0xFFDDDDDD),
recording = Color(0xB3FF7474),
onRecording = Color(0xFFDDDDDD),
popover = Color(0xFF212225),
rangeControlBackground = Tokens.Color.colorBluegrey700,
rangeControlFill = Tokens.Color.colorCyan500,
rangeControlKnob = Tokens.Color.colorBluegrey100,
seekbarBuffer = Tokens.Color.colorBluegrey300,
recording = Tokens.Color.colorRed300,
onRecording = Tokens.Color.colorRed25,
badge = Tokens.Color.colorCyan500,
onBadge = Tokens.Color.colorBluegrey100,
listHeader = Tokens.Color.colorGrey50,
listOverline = Tokens.Color.colorGrey500,
listHeadline = Tokens.Color.colorGrey25,
listCaption = Tokens.Color.colorGrey200,
listButton = Color.Transparent,
listButtonFocused = Tokens.Color.colorBluegrey800,
surface = Tokens.Color.colorBluegrey900,
scrim = Tokens.Color.colorBlack.copy(alpha = 0.67f),
)
@Immutable
@@ -43,10 +57,26 @@ data class ColorScheme(
val inputFocused: Color,
val onInputFocused: Color,
val rangeControlBackground: Color,
val rangeControlFill: Color,
val rangeControlKnob: Color,
val seekbarBuffer: Color,
val recording: Color,
val onRecording: Color,
val popover: Color,
val badge: Color,
val onBadge: Color,
val listHeader: Color,
val listOverline: Color,
val listHeadline: Color,
val listCaption: Color,
val listButton: Color,
val listButtonFocused: Color,
val surface: Color,
val scrim: Color,
)
val LocalColorScheme = staticCompositionLocalOf { colorScheme() }

View File

@@ -0,0 +1,84 @@
package org.jellyfin.androidtv.ui.base.dialog
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.interactionTracker
@Composable
fun DialogBase(
visible: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
scrimColor: Color = JellyfinTheme.colorScheme.scrim,
enterTransition: EnterTransition = fadeIn(tween(300)),
exitTransition: ExitTransition = fadeOut(tween(300)),
contentAlignment: Alignment = Alignment.Center,
content: @Composable BoxScope.() -> Unit,
) {
val transition = updateTransition(visible)
val alpha by transition.animateFloat(
transitionSpec = { tween(1000) },
targetValueByState = { visible -> if (visible) 1f else 0f }
)
if (transition.currentState || transition.isRunning) {
val focusRequester = remember { FocusRequester() }
var contentVisible by remember { mutableStateOf(false) }
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false
),
) {
Box(
modifier = modifier
.fillMaxSize()
.background(scrimColor.copy(alpha = scrimColor.alpha * alpha))
.focusRequester(focusRequester)
.focusGroup()
.interactionTracker(),
contentAlignment = contentAlignment,
) {
transition.AnimatedVisibility(
visible = { it && contentVisible },
enter = enterTransition,
exit = exitTransition,
) {
content()
LaunchedEffect(focusRequester) { focusRequester.requestFocus() }
}
}
}
LaunchedEffect(contentVisible) { contentVisible = true }
}
}

View File

@@ -0,0 +1,49 @@
package org.jellyfin.androidtv.ui.base.form
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.base.Icon
import org.jellyfin.androidtv.ui.base.JellyfinTheme
@Composable
fun Checkbox(
checked: Boolean,
modifier: Modifier = Modifier,
shape: Shape = JellyfinTheme.shapes.extraSmall,
containerColor: Color = JellyfinTheme.colorScheme.button,
contentColor: Color = JellyfinTheme.colorScheme.onButton
) {
Box(
modifier = modifier
.defaultMinSize(minWidth = 18.dp, minHeight = 18.dp)
.background(if (checked) containerColor else Color.Unspecified, shape)
.border(if (checked) 0.dp else 2.dp, containerColor, shape),
contentAlignment = Alignment.Center,
) {
AnimatedVisibility(
visible = checked,
modifier = Modifier
.matchParentSize()
) {
Icon(
painterResource(R.drawable.ic_check),
tint = contentColor,
contentDescription = null,
modifier = Modifier
.padding(3.dp)
)
}
}
}

View File

@@ -0,0 +1,51 @@
package org.jellyfin.androidtv.ui.base.form
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
@Composable
fun ColorSwatch(
color: Color,
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
) {
Box(
modifier = modifier
.defaultMinSize(minWidth = 18.dp, minHeight = 18.dp)
.clip(shape)
) {
Canvas(
modifier = Modifier
.matchParentSize()
) {
if (color.alpha < 1f) {
val squareSize = 6.dp.toPx()
repeat((size.height / squareSize).toInt()) { row ->
repeat((size.width / squareSize).toInt()) { col ->
val isLight = (row + col) % 2 == 0
drawRect(
color = if (isLight) Color.LightGray else Color.DarkGray,
topLeft = Offset(col * squareSize, row * squareSize),
size = Size(squareSize, squareSize),
)
}
}
}
drawRect(
color = color,
)
}
}
}

View File

@@ -0,0 +1,50 @@
package org.jellyfin.androidtv.ui.base.form
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.base.Icon
import org.jellyfin.androidtv.ui.base.JellyfinTheme
@Composable
fun RadioButton(
checked: Boolean,
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
containerColor: Color = JellyfinTheme.colorScheme.button,
contentColor: Color = JellyfinTheme.colorScheme.onButton
) {
Box(
modifier = modifier
.defaultMinSize(minWidth = 18.dp, minHeight = 18.dp)
.background(if (checked) containerColor else Color.Unspecified, shape)
.border(if (checked) 0.dp else 2.dp, containerColor, shape),
contentAlignment = Alignment.Center,
) {
AnimatedVisibility(
visible = checked,
modifier = Modifier
.matchParentSize()
) {
Icon(
painterResource(R.drawable.ic_check),
tint = contentColor,
contentDescription = null,
modifier = Modifier
.padding(3.dp)
)
}
}
}

View File

@@ -0,0 +1,115 @@
package org.jellyfin.androidtv.ui.base.form
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import org.jellyfin.androidtv.ui.base.JellyfinTheme
@Immutable
data class RangeControlColors(
val backgroundColor: Color,
val fillColor: Color,
val knobColor: Color,
)
object RangeControlDefaults {
@ReadOnlyComposable
@Composable
fun colors(
backgroundColor: Color = JellyfinTheme.colorScheme.rangeControlBackground,
fillColor: Color = JellyfinTheme.colorScheme.rangeControlFill,
knobColor: Color = JellyfinTheme.colorScheme.rangeControlKnob,
) = RangeControlColors(
backgroundColor = backgroundColor,
fillColor = fillColor,
knobColor = knobColor,
)
}
@Composable
fun RangeControl(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
min: Float = 0f,
max: Float = 1f,
value: Float = 0f,
stepForward: Float = 0.01f,
stepBackward: Float = stepForward,
onValueChange: ((vakye: Float) -> Unit)? = null,
enabled: Boolean = true,
colors: RangeControlColors = RangeControlDefaults.colors(),
) {
val focused by interactionSource.collectIsFocusedAsState()
val knobSize by animateFloatAsState(if (focused) 1.75f else 1f)
Box(
modifier = modifier
.onKeyEvent {
if (!enabled) return@onKeyEvent false
val isForward = it.key == Key.DirectionRight
val isRewind = it.key == Key.DirectionLeft
val isScrubbing = isForward || isRewind
val isKeyDown = it.type == KeyEventType.KeyDown
val newValue = when {
isKeyDown && isForward -> (value + stepForward).coerceAtMost(max)
isKeyDown && isRewind -> (value - stepBackward).coerceAtLeast(min)
else -> value
}
if (value != newValue && onValueChange != null) onValueChange(newValue)
return@onKeyEvent isScrubbing
}
.focusable(interactionSource = interactionSource, enabled = enabled)
.drawWithContent {
val barCornerRadius = CornerRadius(size.minDimension, size.minDimension)
// Background bar
drawRoundRect(
color = colors.backgroundColor,
cornerRadius = barCornerRadius,
)
// Get percental value based on min/max options
val valuePercentage = (value - min) / (max - min)
// Value fill bar
if (valuePercentage > 0) {
drawRoundRect(
color = colors.fillColor,
size = size.copy(
width = valuePercentage * size.width,
),
cornerRadius = barCornerRadius,
)
}
// Progress knob
drawCircle(
color = colors.knobColor,
center = center.copy(
x = valuePercentage * size.width,
),
radius = size.minDimension * knobSize,
)
}
)
}

View File

@@ -0,0 +1,38 @@
package org.jellyfin.androidtv.ui.base
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onPreviewKeyEvent
typealias InteractionTracker = () -> Unit
private val DefaultInteractionTracker: InteractionTracker = {}
val LocalInteractionTracker = staticCompositionLocalOf<InteractionTracker> { DefaultInteractionTracker }
@Composable
fun ProvideLocalInteractionTracker(
interactionTracker: InteractionTracker,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalInteractionTracker provides interactionTracker
) {
Box(
modifier = Modifier.interactionTracker(interactionTracker),
) {
content()
}
}
}
@Composable
fun Modifier.interactionTracker(
interactionTracker: InteractionTracker = LocalInteractionTracker.current,
): Modifier = onPreviewKeyEvent {
interactionTracker()
false
}

View File

@@ -0,0 +1,45 @@
package org.jellyfin.androidtv.ui.base.list
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
@Composable
fun ListButton(
onClick: () -> Unit,
headingContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
colors: ListControlColors = ListControlDefaults.colors(),
overlineContent: (@Composable () -> Unit)? = null,
captionContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable () -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
ListControl(
headingContent = headingContent,
modifier = modifier.combinedClickable(
interactionSource = interactionSource,
indication = null,
enabled = enabled,
role = Role.Button,
onClick = onClick,
),
enabled = enabled,
interactionSource = interactionSource,
colors = colors,
overlineContent = overlineContent,
captionContent = captionContent,
leadingContent = leadingContent,
trailingContent = trailingContent,
footerContent = footerContent,
)
}

View File

@@ -0,0 +1,76 @@
package org.jellyfin.androidtv.ui.base.list
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.LocalShapes
object ListControlDefaults {
@ReadOnlyComposable
@Composable
fun colors(
containerColor: Color = JellyfinTheme.colorScheme.listButton,
focusedContainerColor: Color = JellyfinTheme.colorScheme.listButtonFocused,
) = ListControlColors(
containerColor = containerColor,
focusedContainerColor = focusedContainerColor,
)
}
@Composable
fun ListControl(
headingContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: InteractionSource? = null,
colors: ListControlColors = ListControlDefaults.colors(),
overlineContent: (@Composable () -> Unit)? = null,
captionContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable () -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
val focused by interactionSource.collectIsFocusedAsState()
val pressed by interactionSource.collectIsPressedAsState()
val backgroundColor = when {
!enabled -> colors.containerColor
pressed -> colors.focusedContainerColor
focused -> colors.focusedContainerColor
else -> colors.containerColor
}
Box(
modifier = modifier
.fillMaxWidth()
.background(backgroundColor, LocalShapes.current.large)
.clip(LocalShapes.current.large)
.alpha(if (enabled) 1f else 0.4f),
) {
ListItemContent(
headingContent = headingContent,
overlineContent = overlineContent,
captionContent = captionContent,
leadingContent = leadingContent,
trailingContent = trailingContent,
footerContent = footerContent,
headingStyle = JellyfinTheme.typography.listHeadline
.copy(color = JellyfinTheme.colorScheme.listHeadline),
)
}
}

View File

@@ -0,0 +1,10 @@
package org.jellyfin.androidtv.ui.base.list
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
data class ListControlColors(
val containerColor: Color,
val focusedContainerColor: Color,
)

View File

@@ -0,0 +1,97 @@
package org.jellyfin.androidtv.ui.base.list
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.LocalTextStyle
import org.jellyfin.androidtv.ui.base.ProvideTextStyle
import org.jellyfin.design.Tokens
@Composable
fun ListItemContent(
modifier: Modifier = Modifier,
headingContent: @Composable () -> Unit,
overlineContent: (@Composable () -> Unit)? = null,
captionContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable () -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
headingStyle: TextStyle,
) {
Column(
modifier = modifier
// TODO: Add suitable space token for this padding
.padding(12.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
leadingContent?.let { content ->
Box(
modifier = Modifier
.sizeIn(minWidth = 24.dp),
contentAlignment = Alignment.Center,
content = {
ProvideTextStyle(LocalTextStyle.current.copy(color = JellyfinTheme.colorScheme.listCaption)) {
content()
}
}
)
Spacer(Modifier.width(Tokens.Space.spaceMd))
}
Column(
modifier = Modifier
.weight(1f),
) {
overlineContent?.let { content ->
ProvideTextStyle(JellyfinTheme.typography.listOverline.copy(color = JellyfinTheme.colorScheme.listOverline)) {
content()
}
Spacer(Modifier.height(Tokens.Space.space2xs))
}
ProvideTextStyle(headingStyle) {
headingContent()
}
captionContent?.let { content ->
Spacer(Modifier.height(Tokens.Space.spaceXs))
ProvideTextStyle(JellyfinTheme.typography.listCaption.copy(color = JellyfinTheme.colorScheme.listCaption)) {
content()
}
}
}
trailingContent?.let { content ->
Spacer(Modifier.width(Tokens.Space.spaceMd))
Box(
modifier = Modifier
.sizeIn(minWidth = 24.dp),
contentAlignment = Alignment.Center,
content = { content() }
)
}
}
footerContent?.let { content ->
Spacer(Modifier.height(Tokens.Space.spaceXs))
ProvideTextStyle(LocalTextStyle.current.copy(color = JellyfinTheme.colorScheme.listCaption)) {
content()
}
}
}
}

View File

@@ -0,0 +1,26 @@
package org.jellyfin.androidtv.ui.base.list
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.LocalTextStyle
import org.jellyfin.androidtv.ui.base.ProvideTextStyle
@Composable
fun ListMessage(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = modifier
// TODO: Add suitable space token for this padding
.padding(12.dp),
) {
ProvideTextStyle(LocalTextStyle.current.copy(color = JellyfinTheme.colorScheme.listCaption)) {
content()
}
}
}

View File

@@ -0,0 +1,28 @@
package org.jellyfin.androidtv.ui.base.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jellyfin.androidtv.ui.base.JellyfinTheme
@Composable
fun ListSection(
modifier: Modifier = Modifier,
headingContent: @Composable () -> Unit,
overlineContent: (@Composable () -> Unit)? = null,
captionContent: (@Composable () -> Unit)? = null,
leadingContent: (@Composable () -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
footerContent: (@Composable () -> Unit)? = null,
) {
ListItemContent(
headingContent = headingContent,
overlineContent = overlineContent,
captionContent = captionContent,
leadingContent = leadingContent,
trailingContent = trailingContent,
footerContent = footerContent,
headingStyle = JellyfinTheme.typography.listHeader
.copy(color = JellyfinTheme.colorScheme.listHeader),
modifier = modifier,
)
}

View File

@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.interactionTracker
object PopoverDefaults {
val Shape: Shape = RoundedCornerShape(4.dp)
@@ -39,11 +40,10 @@ fun Popover(
alignment: Alignment = Alignment.TopStart,
offset: DpOffset = DpOffset.Zero,
shape: Shape = PopoverDefaults.Shape,
backgroundColor: Color = JellyfinTheme.colorScheme.popover,
backgroundColor: Color = JellyfinTheme.colorScheme.surface,
content: @Composable BoxScope.() -> Unit,
) {
val density = LocalDensity.current
val focusRequester = remember { FocusRequester() }
val popupPositionProvider = remember(alignment, density, offset) {
PopoverMenuPositionProvider(
alignment = alignment,
@@ -60,6 +60,8 @@ fun Popover(
)
if (alpha != 0f) {
val focusRequester = remember { FocusRequester() }
Popup(
onDismissRequest = onDismissRequest,
properties = PopupProperties(
@@ -87,6 +89,7 @@ fun Popover(
.wrapContentSize()
.focusRequester(focusRequester)
.focusGroup()
.interactionTracker()
) {
content()
}

View File

@@ -3,14 +3,51 @@ package org.jellyfin.androidtv.ui.base
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
object TypographyDefaults {
val Default: TextStyle = TextStyle.Default
val ListHeader: TextStyle = Default.copy(
fontSize = 15.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.W700,
)
val ListOverline: TextStyle = Default.copy(
fontSize = 10.sp,
lineHeight = 12.sp,
fontWeight = FontWeight.W600,
letterSpacing = 0.65.sp,
)
val ListHeadline: TextStyle = Default.copy(
fontSize = 14.sp,
lineHeight = 28.sp,
fontWeight = FontWeight.W600,
)
val ListCaption: TextStyle = Default.copy(
fontSize = 11.sp,
lineHeight = 14.sp,
fontWeight = FontWeight.W500,
letterSpacing = 0.1.sp,
)
val Badge: TextStyle = Default.copy(
fontSize = 11.sp,
fontWeight = FontWeight.W700,
textAlign = TextAlign.Center,
)
}
@Immutable
data class Typography(
val default: TextStyle = TypographyDefaults.Default,
val listHeader: TextStyle = TypographyDefaults.ListHeader,
val listOverline: TextStyle = TypographyDefaults.ListOverline,
val listHeadline: TextStyle = TypographyDefaults.ListHeadline,
val listCaption: TextStyle = TypographyDefaults.ListCaption,
val badge: TextStyle = TypographyDefaults.Badge
)
val LocalTypography = staticCompositionLocalOf { Typography() }

View File

@@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.widget.BaseGridView;
import androidx.leanback.widget.FocusHighlight;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
import androidx.leanback.widget.Presenter;
@@ -39,7 +40,6 @@ import org.jellyfin.androidtv.constant.QueryType;
import org.jellyfin.androidtv.data.model.FilterOptions;
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
import org.jellyfin.androidtv.data.repository.UserViewsRepository;
import org.jellyfin.androidtv.data.service.BackgroundService;
import org.jellyfin.androidtv.databinding.HorizontalGridBrowseBinding;
import org.jellyfin.androidtv.databinding.PopupEmptyBinding;
@@ -50,8 +50,6 @@ import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapterHelperKt;
import org.jellyfin.androidtv.ui.navigation.ActivityDestinations;
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
import org.jellyfin.androidtv.ui.presentation.HorizontalGridPresenter;
import org.jellyfin.androidtv.util.CoroutineUtils;
@@ -74,6 +72,7 @@ import java.util.Objects;
import java.util.UUID;
import kotlin.Lazy;
import kotlinx.coroutines.flow.MutableStateFlow;
import kotlinx.serialization.json.Json;
import timber.log.Timber;
@@ -115,9 +114,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private final Lazy<BackgroundService> backgroundService = inject(BackgroundService.class);
private final Lazy<PreferencesRepository> preferencesRepository = inject(PreferencesRepository.class);
private final Lazy<UserViewsRepository> userViewsRepository = inject(UserViewsRepository.class);
private final Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
private final Lazy<NavigationRepository> navigationRepository = inject(NavigationRepository.class);
private final Lazy<ItemLauncher> itemLauncher = inject(ItemLauncher.class);
private final Lazy<KeyProcessor> keyProcessor = inject(KeyProcessor.class);
private final Lazy<ApiClient> api = inject(ApiClient.class);
@@ -155,7 +152,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
mCardFocusScale = getResources().getFraction(R.fraction.card_scale_focus, 1, 1);
if (mGridDirection.equals(GridDirection.VERTICAL))
setGridPresenter(new VerticalGridPresenter());
setGridPresenter(new VerticalGridPresenter(FocusHighlight.ZOOM_FACTOR_LARGE, false));
else
setGridPresenter(new HorizontalGridPresenter());
@@ -255,6 +252,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
}
gridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
gridPresenter.setOnItemViewClickedListener(mClickedListener);
gridPresenter.setShadowEnabled(false);
mGridPresenter = gridPresenter;
}
@@ -267,6 +265,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
}
gridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
gridPresenter.setOnItemViewClickedListener(mClickedListener);
gridPresenter.setShadowEnabled(false);
mGridPresenter = gridPresenter;
}
@@ -577,7 +576,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
mGridDirection = gridDirection;
if (mGridDirection.equals(GridDirection.VERTICAL) && (mGridPresenter == null || !(mGridPresenter instanceof VerticalGridPresenter))) {
setGridPresenter(new VerticalGridPresenter());
setGridPresenter(new VerticalGridPresenter(FocusHighlight.ZOOM_FACTOR_LARGE, false));
} else if (mGridDirection.equals(GridDirection.HORIZONTAL) && (mGridPresenter == null || !(mGridPresenter instanceof HorizontalGridPresenter))) {
setGridPresenter(new HorizontalGridPresenter());
}
@@ -608,8 +607,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
}
private void buildAdapter() {
mCardPresenter = new CardPresenter(false, mImageType, mCardHeight);
mCardPresenter.setUniformAspect(true); // make grid layouts always uniform
mCardPresenter = new CardPresenter(false, mImageType, mCardHeight, true);
Timber.d("buildAdapter cardHeight <%s> getCardWidthBy <%s> chunks <%s> type <%s>", mCardHeight, (int) getCardWidthBy(mCardHeight, mImageType, mFolder), mRowDef.getChunkSize(), mRowDef.getQueryType().toString());
@@ -807,19 +805,22 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
mLetterButton.setContentDescription(getString(R.string.lbl_by_letter));
binding.toolBar.addView(mLetterButton);
mSettingsButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mSettingsButton.setImageResource(R.drawable.ic_settings);
mSettingsButton.setMaxHeight(size);
mSettingsButton.setAdjustViewBounds(true);
mSettingsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean allowViewSelection = userViewsRepository.getValue().allowViewSelection(mFolder.getCollectionType());
startActivity(ActivityDestinations.INSTANCE.displayPreferences(getContext(), mFolder.getDisplayPreferencesId(), allowViewSelection));
}
});
mSettingsButton.setContentDescription(getString(R.string.lbl_settings));
binding.toolBar.addView(mSettingsButton);
if (mFolder.getDisplayPreferencesId() != null) {
MutableStateFlow<Boolean> settingsVisible = BrowseGridFragmentHelperKt.createSettingsVisibility(BrowseGridFragment.this);
mSettingsButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mSettingsButton.setImageResource(R.drawable.ic_settings);
mSettingsButton.setMaxHeight(size);
mSettingsButton.setAdjustViewBounds(true);
mSettingsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
settingsVisible.setValue(true);
}
});
mSettingsButton.setContentDescription(getString(R.string.lbl_settings));
binding.toolBar.addView(mSettingsButton);
BrowseGridFragmentHelperKt.addSettings(BrowseGridFragment.this, binding.settings, mFolder.getId(), mFolder.getDisplayPreferencesId(), settingsVisible);
}
}
class JumplistPopup {
@@ -878,7 +879,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private void refreshCurrentItem() {
if (mCurrentItem == null) return;
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
Timber.i("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
ItemRowAdapterHelperKt.refreshItem(mAdapter, api.getValue(), this, mCurrentItem, () -> {
//Now - if filtered make sure we still pass
if (mAdapter.getFilters() == null) return null;

View File

@@ -0,0 +1,41 @@
package org.jellyfin.androidtv.ui.browsing
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.androidtv.ui.navigation.ProvideRouter
import org.jellyfin.androidtv.ui.settings.Routes
import org.jellyfin.androidtv.ui.settings.composable.SettingsDialog
import org.jellyfin.androidtv.ui.settings.composable.SettingsRouterContent
import org.jellyfin.androidtv.ui.settings.routes
import java.util.UUID
fun BrowseGridFragment.createSettingsVisibility() = MutableStateFlow(false)
fun BrowseGridFragment.addSettings(
view: ComposeView,
itemId: UUID,
displayPreferencesId: String,
visible: MutableStateFlow<Boolean>,
) {
view.setContent {
val isVisible by visible.collectAsState(false)
ProvideRouter(
routes,
Routes.LIBRARIES_DISPLAY,
mapOf("itemId" to itemId.toString(), "displayPreferencesId" to displayPreferencesId)
) {
SettingsDialog(
visible = isVisible,
onDismissRequest = {
visible.value = false
onResume()
}
) {
SettingsRouterContent()
}
}
}
}

View File

@@ -12,7 +12,6 @@ import org.jellyfin.androidtv.constant.Extras;
import org.jellyfin.androidtv.constant.LiveTvOption;
import org.jellyfin.androidtv.constant.QueryType;
import org.jellyfin.androidtv.data.querying.GetSeriesTimersRequest;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.ui.GridButton;
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
@@ -65,11 +64,6 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
GetNextUpRequest getNextUpRequest = BrowsingUtils.createGetNextUpRequest(mFolder.getId());
mRows.add(new BrowseRowDef(getString(R.string.lbl_next_up), getNextUpRequest, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
//Premieres
if (KoinJavaComponent.<UserPreferences>get(UserPreferences.class).get(UserPreferences.Companion.getPremieresEnabled())) {
mRows.add(new BrowseRowDef(getString(R.string.lbl_new_premieres), BrowsingUtils.createPremieresRequest(mFolder.getId()), 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
}
//Latest content added
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.EPISODE, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));

View File

@@ -248,22 +248,6 @@ object BrowsingUtils {
sortBy = setOf(ItemSortBy.SORT_NAME),
)
@JvmStatic
fun createPremieresRequest(parentId: UUID) = GetItemsRequest(
fields = ItemRepository.itemFields,
includeItemTypes = setOf(BaseItemKind.EPISODE),
parentId = parentId,
indexNumber = 1,
recursive = true,
isMissing = false,
imageTypeLimit = 1,
filters = setOf(ItemFilter.IS_UNPLAYED),
sortBy = setOf(ItemSortBy.DATE_CREATED),
sortOrder = setOf(SortOrder.DESCENDING),
enableTotalRecordCount = false,
limit = 300,
)
@JvmStatic
fun createLastPlayedRequest(parentId: UUID) = GetItemsRequest(
fields = ItemRepository.itemFields,

View File

@@ -1,61 +0,0 @@
package org.jellyfin.androidtv.ui.browsing
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.GridDirection
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.constant.PosterSize
import org.jellyfin.androidtv.preference.LibraryPreferences
import org.jellyfin.androidtv.preference.PreferencesRepository
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.enum
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.jellyfin.preference.store.PreferenceStore
import org.koin.android.ext.android.inject
class DisplayPreferencesScreen : OptionsFragment() {
private val preferencesRepository: PreferencesRepository by inject()
private val libraryPreferences: LibraryPreferences by lazy {
preferencesRepository.getLibraryPreferences(preferencesId!!)
}
private val preferencesId by lazy { requireArguments().getString(ARG_PREFERENCES_ID) }
private val allowViewSelection by lazy { requireArguments().getBoolean(ARG_ALLOW_VIEW_SELECTION) }
override val stores: Array<PreferenceStore<*, *>>
get() = arrayOf(libraryPreferences)
override val screen by optionsScreen {
setTitle(R.string.lbl_display_preferences)
category {
enum<PosterSize> {
setTitle(R.string.lbl_image_size)
bind(libraryPreferences, LibraryPreferences.posterSize)
}
enum<ImageType> {
setTitle(R.string.lbl_image_type)
bind(libraryPreferences, LibraryPreferences.imageType)
}
enum<GridDirection> {
setTitle(R.string.grid_direction)
bind(libraryPreferences, LibraryPreferences.gridDirection)
}
if (allowViewSelection) {
checkbox {
setTitle(R.string.enable_smart_view)
contentOn = requireContext().getString(R.string.enable_smart_view_description)
contentOff = contentOn
bind(libraryPreferences, LibraryPreferences.enableSmartScreen)
}
}
}
}
companion object {
const val ARG_ALLOW_VIEW_SELECTION = "allow_view_selection"
const val ARG_PREFERENCES_ID = "preferences_id"
}
}

View File

@@ -357,13 +357,13 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
if (item instanceof GridButton) {
switch (((GridButton) item).getId()) {
case GRID:
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder));
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, null));
break;
case ALBUMS:
mFolder = JavaCompat.copyWithDisplayPreferencesId(mFolder, mFolder.getId() + "AL");
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, BaseItemKind.MUSIC_ALBUM.getSerialName()));
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, null));
break;
case ALBUM_ARTISTS:

View File

@@ -3,10 +3,9 @@ package org.jellyfin.androidtv.ui.browsing
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
@@ -20,16 +19,17 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.databinding.ActivityMainBinding
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
import org.jellyfin.androidtv.ui.InteractionTrackerViewModel
import org.jellyfin.androidtv.ui.background.AppBackground
import org.jellyfin.androidtv.ui.navigation.NavigationAction
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.ProvideLocalInteractionTracker
import org.jellyfin.androidtv.ui.composable.compat.AppNavigationHost
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.screensaver.InAppScreensaver
import org.jellyfin.androidtv.ui.settings.compat.MainActivitySettings
import org.jellyfin.androidtv.ui.startup.StartupActivity
import org.jellyfin.androidtv.util.applyTheme
import org.jellyfin.androidtv.util.isMediaSessionKeyEvent
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
@@ -41,14 +41,6 @@ class MainActivity : FragmentActivity() {
private val interactionTrackerViewModel by viewModel<InteractionTrackerViewModel>()
private val workManager by inject<WorkManager>()
private lateinit var binding: ActivityMainBinding
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
if (navigationRepository.canGoBack) navigationRepository.goBack()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
applyTheme()
@@ -62,21 +54,28 @@ class MainActivity : FragmentActivity() {
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}.launchIn(lifecycleScope)
onBackPressedDispatcher.addCallback(this, backPressedCallback)
if (savedInstanceState == null && navigationRepository.canGoBack) navigationRepository.reset(clearHistory = true)
navigationRepository.currentAction
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { action ->
handleNavigationAction(action)
backPressedCallback.isEnabled = navigationRepository.canGoBack
.onEach {
interactionTrackerViewModel.notifyInteraction(canCancel = false, userInitiated = false)
}.launchIn(lifecycleScope)
binding = ActivityMainBinding.inflate(layoutInflater)
binding.background.setContent { AppBackground() }
binding.screensaver.setContent { InAppScreensaver() }
setContentView(binding.root)
setContent {
JellyfinTheme {
ProvideLocalInteractionTracker(
interactionTracker = { interactionTrackerViewModel.notifyInteraction(false, userInitiated = true) }
) {
AppBackground()
AppNavigationHost(
navigationRepository = navigationRepository,
)
InAppScreensaver()
MainActivitySettings()
}
}
}
}
override fun onResume() {
@@ -112,39 +111,21 @@ class MainActivity : FragmentActivity() {
workManager.enqueue(OneTimeWorkRequestBuilder<LeanbackChannelWorker>().build())
lifecycleScope.launch(Dispatchers.IO) {
Timber.d("MainActivity stopped")
Timber.i("MainActivity stopped")
sessionRepository.restoreSession(destroyOnly = true)
}
}
private fun handleNavigationAction(action: NavigationAction) {
interactionTrackerViewModel.notifyInteraction(canCancel = true, userInitiated = false)
when (action) {
is NavigationAction.NavigateFragment -> binding.contentView.navigate(action)
NavigationAction.GoBack -> binding.contentView.goBack()
NavigationAction.Nothing -> Unit
}
}
// Forward key events to fragments
private fun Fragment.onKeyEvent(keyCode: Int, event: KeyEvent?): Boolean {
var result = childFragmentManager.fragments.any { it.onKeyEvent(keyCode, event) }
if (!result && this is View.OnKeyListener) result = onKey(currentFocus, keyCode, event)
return result
}
private fun onKeyEvent(keyCode: Int, event: KeyEvent?): Boolean {
// Ignore the key event that closes the screensaver
if (interactionTrackerViewModel.visible.value) {
interactionTrackerViewModel.notifyInteraction(canCancel = event?.action == KeyEvent.ACTION_UP, userInitiated = true)
return true
}
return supportFragmentManager.fragments
.any { it.onKeyEvent(keyCode, event) }
}
private fun onKeyEvent(keyCode: Int, event: KeyEvent?): Boolean = supportFragmentManager.fragments
.any { it.onKeyEvent(keyCode, event) }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean =
onKeyEvent(keyCode, event) || super.onKeyDown(keyCode, event)
@@ -154,44 +135,4 @@ class MainActivity : FragmentActivity() {
override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean =
onKeyEvent(keyCode, event) || super.onKeyUp(keyCode, event)
override fun onUserInteraction() {
super.onUserInteraction()
interactionTrackerViewModel.notifyInteraction(false, userInitiated = true)
}
@Suppress("RestrictedApi") // False positive
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// Ignore the key event that closes the screensaver
if (!event.isMediaSessionKeyEvent() && interactionTrackerViewModel.visible.value) {
interactionTrackerViewModel.notifyInteraction(canCancel = event.action == KeyEvent.ACTION_UP, userInitiated = true)
return true
}
@Suppress("RestrictedApi") // False positive
return super.dispatchKeyEvent(event)
}
@Suppress("RestrictedApi") // False positive
override fun dispatchKeyShortcutEvent(event: KeyEvent): Boolean {
// Ignore the key event that closes the screensaver
if (!event.isMediaSessionKeyEvent() && interactionTrackerViewModel.visible.value) {
interactionTrackerViewModel.notifyInteraction(canCancel = event.action == KeyEvent.ACTION_UP, userInitiated = true)
return true
}
@Suppress("RestrictedApi") // False positive
return super.dispatchKeyShortcutEvent(event)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
// Ignore the touch event that closes the screensaver
if (interactionTrackerViewModel.visible.value) {
interactionTrackerViewModel.notifyInteraction(canCancel = true, userInitiated = true)
return true
}
return super.dispatchTouchEvent(ev)
}
}

View File

@@ -18,7 +18,6 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.RatingType
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.composable.getResolutionName
import org.jellyfin.androidtv.util.TimeUtils
@@ -246,16 +245,13 @@ fun BaseItemInfoRow(
includeRuntime: Boolean,
) {
val userPreferences = koinInject<UserPreferences>()
val ratingType = userPreferences[UserPreferences.defaultRatingType]
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (ratingType != RatingType.RATING_HIDDEN) {
item.communityRating?.let { InfoRowCommunityRating(it / 10f) }
item.criticRating?.let { InfoRowCriticRating(it / 100f) }
}
item.communityRating?.let { InfoRowCommunityRating(it / 10f) }
item.criticRating?.let { InfoRowCriticRating(it / 100f) }
when (item.type) {
BaseItemKind.EPISODE -> {

View File

@@ -1,261 +0,0 @@
package org.jellyfin.androidtv.ui.card;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.leanback.widget.BaseCardView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.databinding.ViewCardLegacyImageBinding;
import org.jellyfin.androidtv.ui.AsyncImageView;
import org.jellyfin.androidtv.ui.itemhandling.BaseItemDtoBaseRowItem;
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.util.ContextExtensionsKt;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
import org.jellyfin.androidtv.util.Utils;
import java.text.NumberFormat;
/**
* Modified ImageCard with no fade on the badge
* A card view with an {@link ImageView} as its main region.
*/
public class LegacyImageCardView extends BaseCardView {
private ViewCardLegacyImageBinding binding = ViewCardLegacyImageBinding.inflate(LayoutInflater.from(getContext()), this);
private ImageView mBanner;
private int BANNER_SIZE = Utils.convertDpToPixel(getContext(), 50);
private int noIconMargin = Utils.convertDpToPixel(getContext(), 5);
private NumberFormat nf = NumberFormat.getInstance();
public LegacyImageCardView(Context context, boolean showInfo) {
super(context, null, androidx.leanback.R.attr.imageCardViewStyle);
if (!showInfo) {
setCardType(CARD_TYPE_MAIN_ONLY);
}
binding.mainImage.setClipToOutline(true);
// "hack" to trigger KeyProcessor to open the menu for this item on long press
setOnLongClickListener(v -> {
Activity activity = ContextExtensionsKt.getActivity(getContext());
if (activity == null) return false;
// Make sure the view is focused so the created menu uses it as anchor
if (!v.requestFocus()) return false;
return activity.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU));
});
setForeground(null);
}
public void setBanner(int bannerResource) {
if (mBanner == null) {
mBanner = new ImageView(getContext());
mBanner.setLayoutParams(new ViewGroup.LayoutParams(BANNER_SIZE, BANNER_SIZE));
((ViewGroup) getRootView()).addView(mBanner);
}
mBanner.setImageResource(bannerResource);
mBanner.setVisibility(VISIBLE);
}
public final AsyncImageView getMainImageView() {
return binding.mainImage;
}
public void setPlayingIndicator(boolean playing) {
if (playing) {
// TODO use decent animation for equalizer icon
binding.extraBadge.setBackgroundResource(R.drawable.ic_play);
binding.extraBadge.setVisibility(VISIBLE);
} else {
binding.extraBadge.setBackgroundResource(R.drawable.blank10x10);
}
}
public void setMainImageDimensions(int width, int height) {
setMainImageDimensions(width, height, ImageView.ScaleType.CENTER_CROP);
}
public void setMainImageDimensions(int width, int height, ImageView.ScaleType scaleType) {
ViewGroup.LayoutParams lp = binding.mainImage.getLayoutParams();
lp.width = Math.round(width * getResources().getDisplayMetrics().density);
lp.height = Math.round(height * getResources().getDisplayMetrics().density);
binding.mainImage.setLayoutParams(lp);
binding.mainImage.setScaleType(scaleType);
if (mBanner != null) mBanner.setX(lp.width - BANNER_SIZE);
ViewGroup.LayoutParams lp2 = binding.resumeProgress.getLayoutParams();
lp2.width = lp.width;
binding.resumeProgress.setLayoutParams(lp2);
}
public void setTitleText(CharSequence text) {
if (binding.title == null) {
return;
}
binding.title.setText(text);
setTextMaxLines();
}
public void setOverlayText(String text) {
if (getCardType() == BaseCardView.CARD_TYPE_MAIN_ONLY) {
binding.overlayText.setText(text);
binding.nameOverlay.setVisibility(VISIBLE);
} else {
binding.nameOverlay.setVisibility(GONE);
}
}
public void setOverlayInfo(BaseRowItem item) {
if (binding.overlayText == null) return;
if (getCardType() == BaseCardView.CARD_TYPE_MAIN_ONLY && item.getShowCardInfoOverlay()) {
switch (item.getBaseItem().getType()) {
case PHOTO:
insertCardData(item.getBaseItem().getPremiereDate() != null ? DateTimeExtensionsKt.getDateFormatter(getContext()).format(item.getBaseItem().getPremiereDate()) : item.getFullName(getContext()), R.drawable.ic_camera, true);
break;
case PHOTO_ALBUM:
insertCardData(item.getFullName(getContext()), R.drawable.ic_photos, true);
break;
case VIDEO:
insertCardData(item.getFullName(getContext()), R.drawable.ic_movie, true);
break;
case FOLDER:
insertCardData(item.getFullName(getContext()), R.drawable.ic_folder, true);
break;
case PLAYLIST:
case MUSIC_ARTIST:
case PERSON:
default:
binding.overlayText.setText(item.getFullName(getContext()));
break;
}
if (item instanceof BaseItemDtoBaseRowItem) {
binding.overlayCount.setText(((BaseItemDtoBaseRowItem) item).getChildCountStr());
} else {
binding.overlayCount.setText(null);
}
binding.nameOverlay.setVisibility(VISIBLE);
}
}
public void insertCardData (@Nullable String fullName, @NonNull int icon, @NonNull boolean iconVisible) {
binding.overlayText.setText(fullName);
if (iconVisible) {
binding.iconImage.setImageResource(icon);
binding.icon.setVisibility(VISIBLE);
}
}
public CharSequence getTitle() {
if (binding.title == null) {
return null;
}
return binding.title.getText();
}
public void setContentText(CharSequence text) {
if (binding.contentText == null) {
return;
}
binding.contentText.setText(text);
setTextMaxLines();
}
public CharSequence getContentText() {
if (binding.contentText == null) {
return null;
}
return binding.contentText.getText();
}
public void setRating(String rating) {
if (rating != null) {
binding.badgeText.setText(rating);
binding.badgeText.setVisibility(VISIBLE);
} else {
binding.badgeText.setText("");
binding.badgeText.setVisibility(GONE);
}
}
public void setBadgeImage(Drawable drawable) {
if (binding.extraBadge == null) {
return;
}
if (drawable != null) {
binding.extraBadge.setImageDrawable(drawable);
binding.extraBadge.setVisibility(View.VISIBLE);
} else {
binding.extraBadge.setVisibility(View.GONE);
}
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
private void setTextMaxLines() {
if (TextUtils.isEmpty(getTitle())) {
binding.contentText.setMaxLines(2);
} else {
binding.contentText.setMaxLines(1);
}
if (TextUtils.isEmpty(getContentText())) {
binding.title.setMaxLines(2);
} else {
binding.title.setMaxLines(1);
}
}
public void clearBanner() {
if (mBanner != null) {
mBanner.setVisibility(GONE);
}
}
public void setUnwatchedCount(int count) {
if (count > 0) {
binding.unwatchedCount.setText(count > 99 ? getContext().getString(R.string.watch_count_overflow) : nf.format(count));
binding.unwatchedCount.setVisibility(VISIBLE);
binding.checkMark.setVisibility(INVISIBLE);
binding.watchedIndicator.setVisibility(VISIBLE);
} else if (count == 0) {
binding.checkMark.setVisibility(VISIBLE);
binding.unwatchedCount.setVisibility(INVISIBLE);
binding.watchedIndicator.setVisibility(VISIBLE);
} else {
binding.watchedIndicator.setVisibility(GONE);
}
}
public void setProgress(int pct) {
if (pct > 0) {
binding.resumeProgress.setProgress(pct);
binding.resumeProgress.setVisibility(VISIBLE);
} else {
binding.resumeProgress.setVisibility(GONE);
}
}
public void showFavIcon(boolean show) {
binding.favIcon.setVisibility(show ? VISIBLE : GONE);
}
}

View File

@@ -5,7 +5,6 @@ import android.graphics.Rect
import android.os.Build
import android.util.AttributeSet
import android.view.KeyEvent
import android.widget.ImageView
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
@@ -16,13 +15,13 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
@@ -36,17 +35,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.base.Icon
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.LocalTextStyle
import org.jellyfin.androidtv.ui.base.ProfilePicture
import org.jellyfin.androidtv.ui.base.ProvideTextStyle
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.composable.AsyncImage
import org.jellyfin.androidtv.util.MenuBuilder
import org.jellyfin.androidtv.util.popupMenu
import org.jellyfin.androidtv.util.showIfNotEmpty
@@ -156,26 +151,17 @@ class UserCardView @JvmOverloads constructor(
UserCard(
image = {
if (image != null) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
scaleType = ImageView.ScaleType.CENTER_CROP,
url = image,
)
} else {
Box(modifier = Modifier.fillMaxSize()) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_user),
contentDescription = name,
modifier = Modifier
.size(48.dp)
.align(Alignment.Center)
)
}
}
ProfilePicture(
url = image,
iconPadding = PaddingValues(24.dp),
modifier = Modifier.fillMaxSize()
)
},
name = {
Text(name.orEmpty(), maxLines = 1)
Text(
text = name.orEmpty(),
maxLines = 1
)
},
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 8.dp)

View File

@@ -0,0 +1,59 @@
package org.jellyfin.androidtv.ui.composable.compat
import android.content.Context
import android.view.View
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.jellyfin.androidtv.ui.browsing.DestinationFragmentView
import org.jellyfin.androidtv.ui.navigation.NavigationAction
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.koin.compose.koinInject
@Composable
fun AppNavigationHost(
modifier: Modifier = Modifier,
navigationRepository: NavigationRepository = koinInject(),
) {
val factory = remember { AppNavigationHostViewFactory() }
val currentAction by navigationRepository.currentAction.collectAsState(NavigationAction.Nothing)
val canGoBack by remember {
navigationRepository.currentAction.map { navigationRepository.canGoBack }.distinctUntilChanged()
}.collectAsState(navigationRepository.canGoBack)
BackHandler(canGoBack) { navigationRepository.goBack() }
AndroidView(
factory = factory,
modifier = modifier,
)
LaunchedEffect(currentAction) {
when (val currentAction = currentAction) {
is NavigationAction.NavigateFragment -> factory.view.navigate(currentAction)
NavigationAction.GoBack -> factory.view.goBack()
NavigationAction.Nothing -> Unit
}
}
}
private class AppNavigationHostViewFactory : (Context) -> View {
private var _view: DestinationFragmentView? = null
val view get() = requireNotNull(_view)
override operator fun invoke(
context: Context
): View = DestinationFragmentView(context).also { view ->
_view = view
}
}

View File

@@ -0,0 +1,36 @@
package org.jellyfin.androidtv.ui.composable.item
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import org.jellyfin.androidtv.ui.base.JellyfinTheme
@Composable
@Stable
fun ItemCard(
modifier: Modifier = Modifier,
image: @Composable BoxScope.() -> Unit,
overlay: (@Composable BoxScope.() -> Unit)? = null,
shape: Shape = JellyfinTheme.shapes.medium,
) {
Box(
modifier = modifier
.clip(shape)
.background(JellyfinTheme.colorScheme.surface, shape)
) {
image()
if (overlay != null) {
Box(
modifier = Modifier.fillMaxSize(),
content = overlay
)
}
}
}

View File

@@ -0,0 +1,171 @@
package org.jellyfin.androidtv.ui.composable.item
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
import org.jellyfin.androidtv.ui.base.Badge
import org.jellyfin.androidtv.ui.base.Icon
import org.jellyfin.androidtv.ui.base.Seekbar
import org.jellyfin.androidtv.ui.base.Text
import org.jellyfin.androidtv.ui.composable.rememberPlayerProgress
import org.jellyfin.androidtv.ui.composable.rememberQueueEntry
import org.jellyfin.design.Tokens
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.jellyfin.queue.baseItem
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.koin.compose.koinInject
@Composable
@Stable
fun ItemCardBaseItemOverlay(
item: BaseItemDto,
footer: (@Composable () -> Unit)? = null,
) = Box(
modifier = Modifier
.fillMaxSize()
.padding(Tokens.Space.spaceXs)
) {
StateIndicator(
item = item,
modifier = Modifier.align(Alignment.TopStart),
)
WatchIndicator(
item = item,
modifier = Modifier.align(Alignment.TopEnd)
)
Column(
modifier = Modifier.align(Alignment.BottomCenter),
verticalArrangement = Arrangement.spacedBy(Tokens.Space.spaceXs)
) {
ProgressIndicator(
item = item,
)
if (footer != null) footer()
}
}
@Composable
@Stable
private fun StateIndicator(
item: BaseItemDto,
modifier: Modifier = Modifier,
) {
val isFavorited = item.userData?.isFavorite == true
val isRecording = item.timerId?.takeIf { item.type == BaseItemKind.LIVE_TV_PROGRAM || item.type == BaseItemKind.PROGRAM } != null
val isRecordingActive = item.seriesTimerId != null && isRecording
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(Tokens.Space.spaceXs)
) {
if (isRecording) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_record_series),
contentDescription = null,
tint = if (isRecordingActive) Tokens.Color.colorRed600 else Tokens.Color.colorGrey100,
modifier = modifier
.size(24.dp)
)
}
if (isFavorited) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_heart),
contentDescription = null,
tint = Tokens.Color.colorRed500,
modifier = modifier
.size(24.dp)
)
}
}
}
@Composable
@Stable
private fun WatchIndicator(
item: BaseItemDto,
modifier: Modifier = Modifier,
) {
val userPreferences = koinInject<UserPreferences>()
val watchedIndicatorBehavior = userPreferences[UserPreferences.watchedIndicatorBehavior]
if (watchedIndicatorBehavior == WatchedIndicatorBehavior.NEVER) return
if (watchedIndicatorBehavior == WatchedIndicatorBehavior.EPISODES_ONLY && item.type != BaseItemKind.EPISODE) return
val isPlayed = item.userData?.played == true
val unplayedItems = item.userData?.unplayedItemCount?.takeIf { it > 0 }
if (isPlayed) {
Badge(
modifier = modifier
.size(24.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_watch),
contentDescription = null,
modifier = Modifier.size(12.dp)
)
}
} else if (unplayedItems != null) {
if (watchedIndicatorBehavior == WatchedIndicatorBehavior.HIDE_UNWATCHED) return
Badge(
modifier = modifier
.sizeIn(minWidth = 24.dp, minHeight = 24.dp),
) {
Text(
text = unplayedItems.toString(),
)
}
}
}
@Composable
private fun ProgressIndicator(
item: BaseItemDto,
modifier: Modifier = Modifier,
) {
val playbackManager = koinInject<org.jellyfin.playback.core.PlaybackManager>()
val playState by playbackManager.state.playState.collectAsState()
val currentQueueEntry by rememberQueueEntry(playbackManager)
val playedPercentage = if (playState == PlayState.PLAYING && currentQueueEntry?.baseItem?.id == item.id) {
rememberPlayerProgress(playbackManager)
} else {
item.userData?.playedPercentage?.toFloat()?.div(100f)?.coerceIn(0f, 1f)?.takeIf { it > 0f && it < 1f }
}
if (playedPercentage != null) {
Box(modifier = modifier.padding(Tokens.Space.spaceXs)) {
Seekbar(
progress = playedPercentage,
enabled = false,
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
)
}
}
}

View File

@@ -0,0 +1,99 @@
package org.jellyfin.androidtv.ui.composable.item
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.sp
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.base.ProvideTextStyle
import org.jellyfin.design.Tokens
@Composable
@Stable
fun ItemPreview(
card: @Composable () -> Unit,
modifier: Modifier = Modifier,
title: (@Composable () -> Unit)? = null,
subtitle: (@Composable () -> Unit)? = null,
spacing: Dp = Tokens.Space.spaceXs,
) {
ItemPreviewLayout(
card = card,
metadata = {
ItemPreviewMetadata(
title = title,
subtitle = subtitle,
spacing = spacing,
)
},
spacing = spacing,
modifier = modifier
)
}
@Composable
@Stable
private fun ItemPreviewMetadata(
title: (@Composable () -> Unit)?,
subtitle: (@Composable () -> Unit)?,
spacing: Dp,
) {
Column(
modifier = Modifier.padding(spacing),
verticalArrangement = Arrangement.spacedBy(spacing),
) {
title?.let { content ->
ProvideTextStyle(
value = JellyfinTheme.typography.default.copy(
color = Tokens.Color.colorGrey100,
fontSize = 12.sp,
),
content = content,
)
}
subtitle?.let { content ->
ProvideTextStyle(
value = JellyfinTheme.typography.default.copy(
color = Tokens.Color.colorGrey300,
fontSize = 10.sp,
),
content = content,
)
}
}
}
@Composable
@Stable
fun ItemPreviewLayout(
card: @Composable () -> Unit,
metadata: @Composable () -> Unit,
spacing: Dp = Tokens.Space.spaceXs,
modifier: Modifier,
) {
SubcomposeLayout(
modifier = modifier,
) { constraints ->
val cardPlaceables = subcompose("card", card).map { it.measure(constraints) }
val cardWidth = cardPlaceables.maxOf { it.width }
val cardHeight = cardPlaceables.maxOf { it.height }
val childConstraints = constraints.copy(minWidth = cardWidth, maxWidth = cardWidth)
val metadataPlaceables = subcompose("metadata", metadata).map { it.measure(childConstraints) }
val metadataHeight = metadataPlaceables.maxOf { it.height }
val spacingPx = spacing.roundToPx()
val height = cardHeight + spacingPx + metadataHeight
layout(cardWidth, height) {
cardPlaceables.forEach { it.place(0, 0) }
metadataPlaceables.forEach { it.place(0, cardHeight + spacingPx) }
}
}
}

View File

@@ -5,15 +5,20 @@ import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import kotlinx.coroutines.delay
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.core.model.PositionInfo
import org.jellyfin.playback.core.queue.queue
import org.koin.compose.koinInject
import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Composable
fun rememberQueueEntry(
@@ -22,6 +27,29 @@ fun rememberQueueEntry(
playbackManager.queue.entry
}.collectAsState()
@Composable
fun rememberPlayerPositionInfo(
playbackManager: PlaybackManager = koinInject(),
precision: Duration = 1.seconds
): MutableState<PositionInfo> {
val playState by playbackManager.state.playState.collectAsState()
val playing = playState == PlayState.PLAYING
val positionInfo = remember { mutableStateOf(playbackManager.state.positionInfo) }
LaunchedEffect(playing, precision) {
val precisionMs = precision.inWholeMilliseconds
while (playing) {
positionInfo.value = playbackManager.state.positionInfo
delay(precisionMs - (positionInfo.value.active.inWholeMilliseconds % precisionMs))
}
}
return positionInfo
}
@Composable
fun rememberPlayerProgress(
playbackManager: PlaybackManager = koinInject(),
@@ -43,7 +71,7 @@ fun rememberPlayerProgress(
active: Duration,
duration: Duration,
): Float {
val animatable = remember { Animatable(0f) }
val animatable = remember { Animatable(0f, 0f) }
LaunchedEffect(playing, duration) {
val activeMs = active.inWholeMilliseconds.toFloat()

View File

@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.onEach
import org.jellyfin.androidtv.auth.repository.ServerRepository
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.data.repository.NotificationsRepository
import org.jellyfin.androidtv.ui.base.JellyfinTheme
import org.jellyfin.androidtv.ui.shared.toolbar.MainToolbar
import org.jellyfin.androidtv.ui.shared.toolbar.MainToolbarActiveButton
import org.koin.android.ext.android.inject
@@ -47,35 +46,33 @@ class HomeFragment : Fragment() {
val rowsFocusRequester = remember { FocusRequester() }
LaunchedEffect(rowsFocusRequester) { rowsFocusRequester.requestFocus() }
JellyfinTheme {
Column {
MainToolbar(MainToolbarActiveButton.Home)
Column {
MainToolbar(MainToolbarActiveButton.Home)
// The leanback code has its own awful focus handling that doesn't work properly with Compose view inteop to workaround this
// issue we add custom behavior that only allows focus exit when the current selected row is the first one. Additionally when
// we do switch the focus, we reset the leanback state so it won't cause weird behavior when focus is regained
var rowsSupportFragment by remember { mutableStateOf<HomeRowsFragment?>(null) }
AndroidFragment<HomeRowsFragment>(
modifier = Modifier
.focusGroup()
.focusRequester(rowsFocusRequester)
.focusProperties {
onExit = {
val isFirstRowSelected = rowsSupportFragment?.selectedPosition?.let { it <= 0 } ?: false
if (requestedFocusDirection != FocusDirection.Up || !isFirstRowSelected) {
cancelFocusChange()
} else {
rowsSupportFragment?.selectedPosition = 0
rowsSupportFragment?.verticalGridView?.clearFocus()
}
// The leanback code has its own awful focus handling that doesn't work properly with Compose view inteop to workaround this
// issue we add custom behavior that only allows focus exit when the current selected row is the first one. Additionally when
// we do switch the focus, we reset the leanback state so it won't cause weird behavior when focus is regained
var rowsSupportFragment by remember { mutableStateOf<HomeRowsFragment?>(null) }
AndroidFragment<HomeRowsFragment>(
modifier = Modifier
.focusGroup()
.focusRequester(rowsFocusRequester)
.focusProperties {
onExit = {
val isFirstRowSelected = rowsSupportFragment?.selectedPosition?.let { it <= 0 } ?: false
if (requestedFocusDirection != FocusDirection.Up || !isFirstRowSelected) {
cancelFocusChange()
} else {
rowsSupportFragment?.selectedPosition = 0
rowsSupportFragment?.verticalGridView?.clearFocus()
}
}
.fillMaxSize(),
onUpdate = { fragment ->
rowsSupportFragment = fragment
}
)
}
.fillMaxSize(),
onUpdate = { fragment ->
rowsSupportFragment = fragment
}
)
}
}

View File

@@ -9,14 +9,13 @@ import org.jellyfin.androidtv.data.querying.GetUserViewsRequest
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter
import org.jellyfin.androidtv.ui.presentation.CardPresenter
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter
import org.jellyfin.androidtv.ui.presentation.UserViewCardPresenter
class HomeFragmentViewsRow(
val small: Boolean,
) : HomeFragmentRow {
private companion object {
val smallCardPresenter = UserViewCardPresenter(true)
val largeCardPresenter = UserViewCardPresenter(false)
val smallCardPresenter = CardPresenter(true, 75)
val largeCardPresenter = CardPresenter(true, 126)
}
override fun addToRowsAdapter(context: Context, cardPresenter: CardPresenter, rowsAdapter: MutableObjectAdapter<Row>) {

View File

@@ -231,7 +231,7 @@ class HomeRowsFragment : RowsSupportFragment(), AudioEventListener, View.OnKeyLi
val adapter = currentRow?.adapter as? ItemRowAdapter ?: return
val item = currentItem ?: return
Timber.d("Refresh item ${item.getFullName(requireContext())}")
Timber.i("Refresh item ${item.getFullName(requireContext())}")
adapter.refreshItem(api, this, item)
}

View File

@@ -250,7 +250,7 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
loadItem(lastPlayedItem.getId());
dataRefreshService.getValue().setLastPlayedItem(null); //blank this out so a detail screen we back up to doesn't also do this
} else {
Timber.d("Updating info after playback");
Timber.i("Updating info after playback");
FullDetailsFragmentHelperKt.getItem(FullDetailsFragment.this, mBaseItem.getId(), item -> {
if (item == null) return null;
@@ -547,7 +547,7 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
//Chapters
if (mBaseItem.getChapters() != null && !mBaseItem.getChapters().isEmpty()) {
List<ChapterItemInfo> chapters = BaseItemExtensionsKt.buildChapterItems(mBaseItem, api.getValue());
List<ChapterItemInfo> chapters = BaseItemExtensionsKt.buildChapterItems(mBaseItem);
ItemRowAdapter chapterAdapter = new ItemRowAdapter(requireContext(), chapters, new CardPresenter(true, 120), adapter);
addItemRow(adapter, chapterAdapter, 2, getString(R.string.lbl_chapters));
}
@@ -612,6 +612,12 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
break;
case EPISODE:
//Additional Parts
if (mBaseItem.getPartCount() != null && mBaseItem.getPartCount() > 0) {
ItemRowAdapter additionalPartsAdapter = new ItemRowAdapter(requireContext(), new GetAdditionalPartsRequest(mBaseItem.getId()), new CardPresenter(), adapter);
addItemRow(adapter, additionalPartsAdapter, 0, getString(R.string.lbl_additional_parts));
}
if (mBaseItem.getSeasonId() != null && mBaseItem.getIndexNumber() != null) {
// query index is zero-based but episode no is not
ItemRowAdapter nextAdapter = new ItemRowAdapter(requireContext(), BrowsingUtils.createNextEpisodesRequest(mBaseItem.getSeasonId(), mBaseItem.getIndexNumber()), 0, false, true, new CardPresenter(true, 120), adapter);
@@ -632,7 +638,7 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
//Chapters
if (mBaseItem.getChapters() != null && !mBaseItem.getChapters().isEmpty()) {
List<ChapterItemInfo> chapters = BaseItemExtensionsKt.buildChapterItems(mBaseItem, api.getValue());
List<ChapterItemInfo> chapters = BaseItemExtensionsKt.buildChapterItems(mBaseItem);
ItemRowAdapter chapterAdapter = new ItemRowAdapter(requireContext(), chapters, new CardPresenter(true, 120), adapter);
addItemRow(adapter, chapterAdapter, 1, getString(R.string.lbl_chapters));
}
@@ -996,22 +1002,20 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
mDetailsOverviewRow.addAction(goToSeriesButton);
}
if (userPreferences.getValue().get(UserPreferences.Companion.getMediaManagementEnabled())) {
boolean deletableItem = false;
UserDto currentUser = KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue();
if (mBaseItem.getType() == BaseItemKind.RECORDING && currentUser.getPolicy().getEnableLiveTvManagement() && mBaseItem.getCanDelete() != null)
deletableItem = mBaseItem.getCanDelete();
else if (mBaseItem.getCanDelete() != null) deletableItem = mBaseItem.getCanDelete();
boolean deletableItem = false;
UserDto currentUser = KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue();
if (mBaseItem.getType() == BaseItemKind.RECORDING && currentUser.getPolicy().getEnableLiveTvManagement() && mBaseItem.getCanDelete() != null)
deletableItem = mBaseItem.getCanDelete();
else if (mBaseItem.getCanDelete() != null) deletableItem = mBaseItem.getCanDelete();
if (deletableItem) {
deleteButton = TextUnderButton.create(requireContext(), R.drawable.ic_delete, buttonSize, 0, getString(R.string.lbl_delete), new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteItem();
}
});
mDetailsOverviewRow.addAction(deleteButton);
}
if (deletableItem) {
deleteButton = TextUnderButton.create(requireContext(), R.drawable.ic_delete, buttonSize, 0, getString(R.string.lbl_delete), new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteItem();
}
});
mDetailsOverviewRow.addAction(deleteButton);
}
if (mSeriesTimerInfo != null) {

View File

@@ -270,12 +270,10 @@ fun FullDetailsFragment.resumePlayback(v: View) {
getString(R.string.msg_video_playback_error),
Toast.LENGTH_LONG
).show()
}
if (nextUpEpisode?.userData?.playbackPositionTicks == 0L) {
} else if (nextUpEpisode.userData?.playbackPositionTicks == 0L) {
play(nextUpEpisode, 0, false)
} else {
showResumeMenu(v, nextUpEpisode!!)
showResumeMenu(v, nextUpEpisode)
}
}
}

View File

@@ -230,7 +230,7 @@ public class ItemListFragment extends Fragment implements View.OnKeyListener {
}
@Override
public void onProgress(long pos) {
public void onProgress(long pos, long duration) {
if (mCurrentlyPlayingRow != null) {
mCurrentlyPlayingRow.updateCurrentTime(pos);
}
@@ -351,7 +351,7 @@ public class ItemListFragment extends Fragment implements View.OnKeyListener {
}
private void play(List<BaseItemDto> items, int ndx, boolean shuffle) {
Timber.d("play items: %d, ndx: %d, shuffle: %b", items.size(), ndx, shuffle);
Timber.i("play items: %d, ndx: %d, shuffle: %b", items.size(), ndx, shuffle);
int pos = 0;
BaseItemDto item = items.size() > 0 ? items.get(ndx) : null;

View File

@@ -197,7 +197,7 @@ public class MusicFavoritesListFragment extends Fragment implements View.OnKeyLi
}
@Override
public void onProgress(long pos) {
public void onProgress(long pos, long duration) {
if (mCurrentlyPlayingRow != null) {
mCurrentlyPlayingRow.updateCurrentTime(pos);
}
@@ -283,7 +283,7 @@ public class MusicFavoritesListFragment extends Fragment implements View.OnKeyLi
};
private void play(List<BaseItemDto> items, int ndx, boolean shuffle) {
Timber.d("play items: %d, ndx: %d, shuffle: %b", items.size(), ndx, shuffle);
Timber.i("play items: %d, ndx: %d, shuffle: %b", items.size(), ndx, shuffle);
playbackLauncher.getValue().launch(requireContext(), items, 0, false, ndx, shuffle);
}

View File

@@ -1,20 +1,21 @@
package org.jellyfin.androidtv.ui.itemhandling
import android.content.Context
import androidx.core.content.ContextCompat
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.TimeUtils
import org.jellyfin.androidtv.util.apiclient.JellyfinImage
import org.jellyfin.androidtv.util.apiclient.albumPrimaryImage
import org.jellyfin.androidtv.util.apiclient.itemImages
import org.jellyfin.androidtv.util.apiclient.parentImages
import org.jellyfin.androidtv.util.apiclient.seriesPrimaryImage
import org.jellyfin.androidtv.util.apiclient.seriesThumbImage
import org.jellyfin.androidtv.util.getTimeFormatter
import org.jellyfin.androidtv.util.locale
import org.jellyfin.androidtv.util.sdk.getFullName
import org.jellyfin.androidtv.util.sdk.getSubName
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.api.ImageType
import java.time.format.DateTimeFormatter
import org.jellyfin.androidtv.constant.ImageType as BaseRowImageType
open class BaseItemDtoBaseRowItem @JvmOverloads constructor(
item: BaseItemDto,
@@ -59,26 +60,6 @@ open class BaseItemDtoBaseRowItem @JvmOverloads constructor(
override val isFavorite get() = baseItem?.userData?.isFavorite == true
override val isPlayed get() = baseItem?.userData?.played == true
val childCountStr: String?
get() {
// Playlist
if (baseItem?.type == BaseItemKind.PLAYLIST) {
val childCount = baseItem.cumulativeRunTimeTicks?.ticks?.let { duration ->
TimeUtils.formatMillis(duration.inWholeMilliseconds)
}
if (childCount != null) return childCount
}
// Folder
if (baseItem?.isFolder == true && baseItem.type != BaseItemKind.MUSIC_ARTIST) {
val childCount = baseItem.childCount
if (childCount != null && childCount > 0) return childCount.toString()
}
// Default
return null
}
override fun getCardName(context: Context) = when {
baseItem?.type == BaseItemKind.AUDIO && baseItem.artists != null -> baseItem.artists?.joinToString(", ")
baseItem?.type == BaseItemKind.AUDIO && baseItem.albumArtists != null -> baseItem.albumArtists?.joinToString(", ")
@@ -120,58 +101,29 @@ open class BaseItemDtoBaseRowItem @JvmOverloads constructor(
else -> baseItem?.getSubName(context)
}
override fun getImageUrl(
context: Context,
imageHelper: ImageHelper,
imageType: ImageType,
fillWidth: Int,
fillHeight: Int
): String? {
val seriesPrimaryImage = baseItem?.seriesPrimaryImage
override fun getImage(imageType: BaseRowImageType): JellyfinImage? {
val primaryImage = when {
preferSeriesPoster && baseItem?.type == BaseItemKind.EPISODE -> baseItem.parentImages[ImageType.PRIMARY]
?: baseItem.seriesPrimaryImage
return when {
preferSeriesPoster && seriesPrimaryImage != null -> imageHelper.getImageUrl(seriesPrimaryImage)
preferParentThumb && baseItem?.type == BaseItemKind.EPISODE -> baseItem.parentImages[ImageType.THUMB]
?: baseItem.seriesThumbImage
imageType == ImageType.BANNER -> imageHelper.getBannerImageUrl(
requireNotNull(
baseItem
), fillWidth, fillHeight
)
baseItem?.type == BaseItemKind.SEASON -> baseItem.seriesPrimaryImage
baseItem?.type == BaseItemKind.PROGRAM -> baseItem.itemImages[ImageType.THUMB]
baseItem?.type == BaseItemKind.AUDIO -> baseItem.albumPrimaryImage
else -> null
} ?: baseItem?.itemImages[ImageType.PRIMARY]
imageType == ImageType.THUMB -> imageHelper.getThumbImageUrl(
requireNotNull(
baseItem
), fillWidth, fillHeight
)
else -> imageHelper.getPrimaryImageUrl(
baseItem!!,
preferParentThumb,
null,
fillHeight
)
return when (imageType) {
BaseRowImageType.BANNER -> baseItem?.itemImages[ImageType.BANNER] ?: primaryImage
BaseRowImageType.THUMB -> baseItem?.itemImages[ImageType.THUMB] ?: primaryImage
else -> primaryImage
}
}
override fun getBadgeImage(
context: Context,
imageHelper: ImageHelper,
) = when (baseItem?.type) {
BaseItemKind.LIVE_TV_PROGRAM,
BaseItemKind.PROGRAM -> when {
baseItem.seriesTimerId != null -> R.drawable.ic_record_series_red
baseItem.timerId != null -> R.drawable.ic_record_red
else -> null
}
else -> when {
baseItem?.criticRating != null -> when {
baseItem.criticRating!! > 59f -> R.drawable.ic_rt_fresh
else -> R.drawable.ic_rt_rotten
}
else -> null
}
}?.let { ContextCompat.getDrawable(context, it) }
override fun equals(other: Any?): Boolean {
if (other is BaseItemDtoBaseRowItem) return other.baseItem == baseItem
return super.equals(other)
}
}

View File

@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.ui.itemhandling
import android.content.Context
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.apiclient.primaryImage
import org.jellyfin.sdk.model.api.BaseItemPerson
class BaseItemPersonBaseRowItem(
@@ -11,14 +11,7 @@ class BaseItemPersonBaseRowItem(
baseRowType = BaseRowType.Person,
staticHeight = true,
) {
override fun getImageUrl(
context: Context,
imageHelper: ImageHelper,
imageType: ImageType,
fillWidth: Int,
fillHeight: Int
) = imageHelper.getPrimaryImageUrl(person, fillHeight)
override fun getImage(imageType: ImageType) = person.primaryImage
override val itemId get() = person.id
override fun getFullName(context: Context) = person.name
override fun getName(context: Context) = person.name

View File

@@ -1,9 +1,8 @@
package org.jellyfin.androidtv.ui.itemhandling
import android.content.Context
import android.graphics.drawable.Drawable
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.apiclient.JellyfinImage
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.UUID
@@ -21,19 +20,12 @@ abstract class BaseRowItem protected constructor(
open fun getCardName(context: Context): String? = getFullName(context)
open fun getImageUrl(
context: Context,
imageHelper: ImageHelper,
imageType: ImageType,
fillWidth: Int,
fillHeight: Int,
): String? = null
open fun getImage(imageType: ImageType): JellyfinImage? = null
open fun getFullName(context: Context): String? = null
open fun getName(context: Context): String? = null
open fun getSubText(context: Context): String? = null
open fun getSummary(context: Context): String? = null
open fun getBadgeImage(context: Context, imageHelper: ImageHelper): Drawable? = null
override fun equals(other: Any?): Boolean {
if (other is BaseRowItem) return other.itemId == itemId

View File

@@ -3,7 +3,6 @@ package org.jellyfin.androidtv.ui.itemhandling
import android.content.Context
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.data.model.ChapterItemInfo
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.TimeUtils
import org.jellyfin.sdk.model.extensions.ticks
@@ -13,14 +12,7 @@ class ChapterItemInfoBaseRowItem(
baseRowType = BaseRowType.Chapter,
staticHeight = true,
) {
override fun getImageUrl(
context: Context,
imageHelper: ImageHelper,
imageType: ImageType,
fillWidth: Int,
fillHeight: Int
) = chapterInfo.imagePath
override fun getImage(imageType: ImageType) = chapterInfo.image
override val itemId get() = chapterInfo.itemId
override fun getFullName(context: Context) = chapterInfo.name
override fun getName(context: Context) = chapterInfo.name

View File

@@ -3,7 +3,6 @@ package org.jellyfin.androidtv.ui.itemhandling
import android.content.Context
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.ui.GridButton
import org.jellyfin.androidtv.util.ImageHelper
class GridButtonBaseRowItem(
val gridButton: GridButton,
@@ -11,14 +10,7 @@ class GridButtonBaseRowItem(
baseRowType = BaseRowType.GridButton,
staticHeight = true,
) {
override fun getImageUrl(
context: Context,
imageHelper: ImageHelper,
imageType: ImageType,
fillWidth: Int,
fillHeight: Int
) = gridButton.imageRes?.let { imageHelper.getResourceUrl(context, it) }
override fun getImage(imageType: ImageType) = null
override fun getFullName(context: Context) = gridButton.text
override fun getName(context: Context) = gridButton.text
}

View File

@@ -56,13 +56,13 @@ public class ItemLauncher {
LibraryPreferences displayPreferences = preferencesRepository.getValue().getLibraryPreferences(baseItem.getDisplayPreferencesId());
boolean enableSmartScreen = displayPreferences.get(LibraryPreferences.Companion.getEnableSmartScreen());
if (!enableSmartScreen) return Destinations.INSTANCE.libraryBrowser(baseItem);
if (!enableSmartScreen) return Destinations.INSTANCE.libraryBrowser(baseItem, null);
else return Destinations.INSTANCE.librarySmartScreen(baseItem);
case MUSIC:
case LIVETV:
return Destinations.INSTANCE.librarySmartScreen(baseItem);
default:
return Destinations.INSTANCE.libraryBrowser(baseItem);
return Destinations.INSTANCE.libraryBrowser(baseItem, null);
}
}
@@ -71,7 +71,7 @@ public class ItemLauncher {
case BaseItem:
BaseItemDto baseItem = rowItem.getBaseItem();
try {
Timber.d("Item selected: %s (%s)", baseItem.getName(), baseItem.getType().toString());
Timber.i("Item selected: %s (%s)", baseItem.getName(), baseItem.getType().toString());
} catch (Exception e) {
//swallow it
}
@@ -147,7 +147,7 @@ public class ItemLauncher {
baseItem = JavaCompat.copyWithDisplayPreferencesId(baseItem, baseItem.getId().toString());
}
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(baseItem));
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(baseItem, null));
} else {
switch (rowItem.getSelectAction()) {

View File

@@ -468,18 +468,18 @@ public class ItemRowAdapter extends MutableObjectAdapter<Object> {
return;
}
if (isCurrentlyRetrieving()) {
Timber.d("Not loading more because currently retrieving");
Timber.i("Not loading more because currently retrieving");
return;
}
// This needs tobe based on the actual estimated cards on screen via type of presenter and WindowAlignmentOffsetPercent
if (chunkSize > 0) {
// we can use chunkSize as indicator on when to load
if (pos >= (itemsLoaded - (chunkSize / 1.7))) {
Timber.d("Loading more items trigger pos <%s> itemsLoaded <%s> from total <%s> with chunkSize <%s>", pos, itemsLoaded, totalItems, chunkSize);
Timber.i("Loading more items trigger pos <%s> itemsLoaded <%s> from total <%s> with chunkSize <%s>", pos, itemsLoaded, totalItems, chunkSize);
retrieveNext();
}
} else if (pos >= itemsLoaded - 20) {
Timber.d("Loading more items trigger pos <%s> itemsLoaded <%s> from total <%s>", pos, itemsLoaded, totalItems);
Timber.i("Loading more items trigger pos <%s> itemsLoaded <%s> from total <%s>", pos, itemsLoaded, totalItems);
retrieveNext();
}
}

View File

@@ -47,7 +47,7 @@ fun <T : Any> ItemRowAdapter.setItems(
items: Collection<T>,
transform: (T, Int) -> BaseRowItem?,
) {
Timber.d("Creating items from $itemsLoaded existing and ${items.size} new, adapter size is ${size()}")
Timber.i("Creating items from $itemsLoaded existing and ${items.size} new, adapter size is ${size()}")
val allItems = buildList {
// Add current items before loaded items
@@ -235,11 +235,10 @@ fun ItemRowAdapter.retrieveUserViews(api: ApiClient, userViewsRepository: UserVi
val filteredItems = response.items
.filter { userViewsRepository.isSupported(it.collectionType) }
.map { it.copy(displayPreferencesId = it.id.toString()) }
setItems(
items = filteredItems,
transform = { item, _ -> BaseItemDtoBaseRowItem(item) }
transform = { item, _ -> BaseItemDtoBaseRowItem(item, staticHeight = true) }
)
if (filteredItems.isEmpty()) removeRow()

View File

@@ -3,7 +3,6 @@ package org.jellyfin.androidtv.ui.itemhandling
import android.content.Context
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.apiclient.getSeriesOverview
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@@ -13,17 +12,7 @@ class SeriesTimerInfoDtoBaseRowItem(
) : BaseRowItem(
baseRowType = BaseRowType.SeriesTimer,
) {
override fun getImageUrl(
context: Context,
imageHelper: ImageHelper,
imageType: ImageType,
fillWidth: Int,
fillHeight: Int
) = imageHelper.getResourceUrl(
context,
R.drawable.tile_land_series_timer
)
override fun getImage(imageType: ImageType) = null
override fun getFullName(context: Context) = seriesTimerInfo.name
override fun getName(context: Context) = seriesTimerInfo.name
override val itemId get() = seriesTimerInfo.id?.toUUIDOrNull()

View File

@@ -1,34 +0,0 @@
package org.jellyfin.androidtv.ui.livetv
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.SystemPreferences
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.OptionsItemCheckbox
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.koin.android.ext.android.inject
class GuideFiltersScreen : OptionsFragment() {
private val systemPreferences: SystemPreferences by inject()
override val screen by optionsScreen {
setTitle(R.string.lbl_filters)
category {
setOf(
SystemPreferences.liveTvGuideFilterMovies to getString(R.string.lbl_movies),
SystemPreferences.liveTvGuideFilterSeries to getString(R.string.lbl_series),
SystemPreferences.liveTvGuideFilterNews to getString(R.string.lbl_news),
SystemPreferences.liveTvGuideFilterKids to getString(R.string.lbl_kids),
SystemPreferences.liveTvGuideFilterSports to getString(R.string.lbl_sports),
SystemPreferences.liveTvGuideFilterPremiere to getString(R.string.lbl_new_only),
).forEach { (preference, label) ->
checkbox {
title = label
type = OptionsItemCheckbox.Type.SWITCH
bind(systemPreferences, preference)
}
}
}
}
}

View File

@@ -1,63 +0,0 @@
package org.jellyfin.androidtv.ui.livetv
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.preference.LiveTvPreferences
import org.jellyfin.androidtv.preference.constant.LiveTvChannelOrder
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.OptionsItemCheckbox
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
import org.jellyfin.androidtv.ui.preference.dsl.enum
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
import org.jellyfin.preference.store.PreferenceStore
import org.koin.android.ext.android.inject
class GuideOptionsScreen : OptionsFragment() {
private val liveTvPreferences: LiveTvPreferences by inject()
override val stores: Array<PreferenceStore<*, *>>
get() = arrayOf(liveTvPreferences)
override val screen by optionsScreen {
setTitle(R.string.live_tv_preferences)
category {
enum<LiveTvChannelOrder> {
title = getString(R.string.lbl_sort_by)
bind {
get { LiveTvChannelOrder.fromString(liveTvPreferences[LiveTvPreferences.channelOrder]) }
set { liveTvPreferences[LiveTvPreferences.channelOrder] = it.stringValue }
default { LiveTvChannelOrder.fromString(liveTvPreferences.getDefaultValue(LiveTvPreferences.channelOrder)) }
}
}
checkbox {
title = getString(R.string.lbl_start_favorites)
bind(liveTvPreferences, LiveTvPreferences.favsAtTop)
}
checkbox {
title = getString(R.string.lbl_colored_backgrounds)
bind(liveTvPreferences, LiveTvPreferences.colorCodeGuide)
}
}
category {
title = getString(R.string.lbl_show_indicators)
setOf(
LiveTvPreferences.showHDIndicator to getString(R.string.lbl_hd_programs),
LiveTvPreferences.showLiveIndicator to getString(R.string.lbl_live_broadcasts),
LiveTvPreferences.showNewIndicator to getString(R.string.lbl_new_episodes),
LiveTvPreferences.showPremiereIndicator to getString(R.string.lbl_premieres),
LiveTvPreferences.showRepeatIndicator to getString(R.string.lbl_repeat_episodes),
).forEach { (preference, label) ->
checkbox {
title = label
type = OptionsItemCheckbox.Type.SWITCH
bind(liveTvPreferences, preference)
}
}
}
}
}

View File

@@ -39,7 +39,6 @@ import org.jellyfin.androidtv.ui.ObservableHorizontalScrollView;
import org.jellyfin.androidtv.ui.ObservableScrollView;
import org.jellyfin.androidtv.ui.ProgramGridCell;
import org.jellyfin.androidtv.ui.ScrollViewListener;
import org.jellyfin.androidtv.ui.navigation.ActivityDestinations;
import org.jellyfin.androidtv.ui.playback.PlaybackLauncher;
import org.jellyfin.androidtv.util.CoroutineUtils;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
@@ -59,6 +58,7 @@ import java.util.List;
import java.util.UUID;
import kotlin.Lazy;
import kotlinx.coroutines.flow.MutableStateFlow;
import timber.log.Timber;
public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.OnKeyListener {
@@ -105,6 +105,8 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
private final Lazy<PlaybackHelper> playbackHelper = inject(PlaybackHelper.class);
private final Lazy<ImageHelper> imageHelper = inject(ImageHelper.class);
private final Lazy<PlaybackLauncher> playbackLauncher = inject(PlaybackLauncher.class);
private MutableStateFlow<Boolean> showOptions;
private MutableStateFlow<Boolean> showFilterOptions;
@Nullable
@Override
@@ -129,20 +131,22 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
mSpinner = binding.spinner;
mSpinner.setVisibility(View.VISIBLE);
showFilterOptions = LiveTvGuideFragmentHelperKt.addSettingsFilters(this, binding);
View mFilterButton = binding.filterButton;
mFilterButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showFilterOptions();
showFilterOptions.setValue(true);
}
});
mFilterButton.setContentDescription(getString(R.string.lbl_filters));
showOptions = LiveTvGuideFragmentHelperKt.addSettingsOptions(this, binding);
View mOptionsButton = binding.optionsButton;
mOptionsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showOptions();
showOptions.setValue(true);
}
});
mOptionsButton.setContentDescription(getString(R.string.lbl_other_options));
@@ -317,7 +321,7 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
switch (keyCode) {
case KeyEvent.KEYCODE_MENU:
// bring up filter selection
showFilterOptions();
showFilterOptions.setValue(true);
break;
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
@@ -451,16 +455,6 @@ public class LiveTvGuideFragment extends Fragment implements LiveTvGuide, View.O
mDetailPopup.show(mImage, mTitle.getLeft(), mTitle.getTop() - 10);
}
public void showFilterOptions() {
startActivity(ActivityDestinations.INSTANCE.liveTvGuideFilterPreferences(getContext()));
TvManager.forceReload();
}
public void showOptions() {
startActivity(ActivityDestinations.INSTANCE.liveTvGuideOptionPreferences(getContext()));
TvManager.forceReload();
}
public void displayChannels(int start, int max) {
int end = start + max;
if (end > mAllChannels.size()) {

View File

@@ -2,15 +2,24 @@ package org.jellyfin.androidtv.ui.livetv
import android.content.Context
import android.view.View
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
import org.jellyfin.androidtv.databinding.LiveTvGuideBinding
import org.jellyfin.androidtv.ui.GuideChannelHeader
import org.jellyfin.androidtv.ui.navigation.ProvideRouter
import org.jellyfin.androidtv.ui.settings.Routes
import org.jellyfin.androidtv.ui.settings.composable.SettingsDialog
import org.jellyfin.androidtv.ui.settings.composable.SettingsRouterContent
import org.jellyfin.androidtv.ui.settings.routes
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemDto
@@ -74,3 +83,55 @@ fun LiveTvGuideFragment.refreshSelectedProgram() {
detailUpdateInternal();
}
}
fun LiveTvGuideFragment.addSettingsOptions(binding: LiveTvGuideBinding): MutableStateFlow<Boolean> {
val visible = MutableStateFlow(false)
binding.settingsOptions.setContent {
val isVisible by visible.collectAsState(false)
ProvideRouter(
routes,
Routes.LIVETV_GUIDE_OPTIONS
) {
SettingsDialog(
visible = isVisible,
onDismissRequest = {
visible.value = false
TvManager.forceReload()
doLoad()
}
) {
SettingsRouterContent()
}
}
}
return visible
}
fun LiveTvGuideFragment.addSettingsFilters(binding: LiveTvGuideBinding): MutableStateFlow<Boolean> {
val visible = MutableStateFlow(false)
binding.settingsFilters.setContent {
val isVisible by visible.collectAsState(false)
ProvideRouter(
routes,
Routes.LIVETV_GUIDE_FILTERS
) {
SettingsDialog(
visible = isVisible,
onDismissRequest = {
visible.value = false
TvManager.forceReload()
doLoad()
}
) {
SettingsRouterContent()
}
}
}
return visible
}

View File

@@ -3,40 +3,11 @@ package org.jellyfin.androidtv.ui.navigation
import android.content.Context
import android.content.Intent
import androidx.core.os.bundleOf
import org.jellyfin.androidtv.ui.browsing.DisplayPreferencesScreen
import org.jellyfin.androidtv.ui.livetv.GuideFiltersScreen
import org.jellyfin.androidtv.ui.livetv.GuideOptionsScreen
import org.jellyfin.androidtv.ui.playback.ExternalPlayerActivity
import org.jellyfin.androidtv.ui.preference.PreferencesActivity
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.screen.UserPreferencesScreen
import org.jellyfin.androidtv.ui.startup.StartupActivity
import kotlin.time.Duration
object ActivityDestinations {
private inline fun <reified T : OptionsFragment> preferenceIntent(
context: Context,
vararg screenArguments: Pair<String, Any?>
) = Intent(context, PreferencesActivity::class.java).apply {
putExtras(
bundleOf(
PreferencesActivity.EXTRA_SCREEN to T::class.qualifiedName,
PreferencesActivity.EXTRA_SCREEN_ARGS to bundleOf(*screenArguments),
)
)
}
fun userPreferences(context: Context) = preferenceIntent<UserPreferencesScreen>(context)
fun displayPreferences(context: Context, displayPreferencesId: String, allowViewSelection: Boolean) =
preferenceIntent<DisplayPreferencesScreen>(
context,
DisplayPreferencesScreen.ARG_PREFERENCES_ID to displayPreferencesId,
DisplayPreferencesScreen.ARG_ALLOW_VIEW_SELECTION to allowViewSelection,
)
fun liveTvGuideFilterPreferences(context: Context) = preferenceIntent<GuideFiltersScreen>(context)
fun liveTvGuideOptionPreferences(context: Context) = preferenceIntent<GuideOptionsScreen>(context)
fun externalPlayer(context: Context, position: Duration = Duration.ZERO) = Intent(context, ExternalPlayerActivity::class.java).apply {
putExtras(
bundleOf(

View File

@@ -39,50 +39,45 @@ object Destinations {
// Browsing
// TODO only pass item id instead of complete JSON to browsing destinations
fun libraryBrowser(item: BaseItemDto) = fragmentDestination<BrowseGridFragment>(
Extras.Folder to Json.Default.encodeToString(item),
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun libraryBrowser(item: BaseItemDto, includeType: String) =
fun libraryBrowser(item: BaseItemDto, includeType: String? = null) =
fragmentDestination<BrowseGridFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
Extras.IncludeType to includeType,
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun librarySmartScreen(item: BaseItemDto) = fragmentDestination<BrowseViewFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun collectionBrowser(item: BaseItemDto) = fragmentDestination<CollectionFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun folderBrowser(item: BaseItemDto) = fragmentDestination<GenericFolderFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun libraryByGenres(item: BaseItemDto, includeType: String) =
fragmentDestination<ByGenreFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
Extras.IncludeType to includeType,
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun libraryByLetter(item: BaseItemDto, includeType: String) =
fragmentDestination<ByLetterFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
Extras.IncludeType to includeType,
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun librarySuggestions(item: BaseItemDto) =
fragmentDestination<SuggestedMoviesFragment>(
Extras.Folder to Json.Default.encodeToString(item),
Extras.Folder to Json.encodeToString(item),
)
// Item details
@@ -95,14 +90,14 @@ object Destinations {
fragmentDestination<FullDetailsFragment>(
"ItemId" to item.toString(),
"ChannelId" to channel.toString(),
"ProgramInfo" to Json.Default.encodeToString(programInfo),
"ProgramInfo" to Json.encodeToString(programInfo),
)
// TODO only pass item id instead of complete JSON to browsing destinations
fun seriesTimerDetails(item: UUID, seriesTimer: SeriesTimerInfoDto) =
fragmentDestination<FullDetailsFragment>(
"ItemId" to item.toString(),
"SeriesTimer" to Json.Default.encodeToString(seriesTimer),
"SeriesTimer" to Json.encodeToString(seriesTimer),
)
fun itemList(item: UUID) = fragmentDestination<ItemListFragment>(

View File

@@ -68,7 +68,7 @@ class NavigationRepositoryImpl(
override val currentAction = _currentAction.asSharedFlow()
override fun navigate(destination: Destination, replace: Boolean) {
Timber.d("Navigating to $destination (via navigate function)")
Timber.i("Navigating to $destination (via navigate function)")
val action = when (destination) {
is Destination.Fragment -> NavigationAction.NavigateFragment(destination, true, replace, false)
}
@@ -84,7 +84,7 @@ class NavigationRepositoryImpl(
override fun goBack(): Boolean {
if (fragmentHistory.empty()) return false
Timber.d("Navigating back")
Timber.i("Navigating back")
fragmentHistory.pop()
_currentAction.tryEmit(NavigationAction.GoBack)
return true
@@ -94,7 +94,7 @@ class NavigationRepositoryImpl(
fragmentHistory.clear()
val actualDestination = destination ?: defaultDestination
_currentAction.tryEmit(NavigationAction.NavigateFragment(actualDestination, true, false, clearHistory))
Timber.d("Navigating to $actualDestination (via reset, clearHistory=$clearHistory)")
Timber.i("Navigating to $actualDestination (via reset, clearHistory=$clearHistory)")
}
}

View File

@@ -0,0 +1,120 @@
package org.jellyfin.androidtv.ui.navigation
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.scene.Scene
import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.ui.defaultPopTransitionSpec
import androidx.navigation3.ui.defaultTransitionSpec
import androidx.navigationevent.NavigationEvent
typealias RouteParameters = Map<String, String>
typealias RouteComposable = @Composable ((context: RouteContext) -> Unit)
data class RouteContext(
val route: String,
val parameters: RouteParameters,
)
class Router(
val routes: Map<String, RouteComposable>,
val backStack: SnapshotStateList<RouteContext>,
) {
// Route resolving
fun resolve(route: String): RouteComposable? = routes[route]
fun verifyRoute(route: String, parameters: RouteParameters = emptyMap()) = require(resolve(route) != null) { "Invalid route $route" }
// Route manipulation
fun push(route: String, parameters: RouteParameters = emptyMap()) {
verifyRoute(route, parameters)
val context = RouteContext(route, parameters)
backStack.add(context)
}
fun replace(route: String, parameters: RouteParameters = emptyMap()) {
verifyRoute(route, parameters)
val context = RouteContext(route, parameters)
backStack.removeLastOrNull()
backStack.add(context)
}
fun back() {
backStack.removeLastOrNull()
}
}
val LocalRouter = compositionLocalOf<Router> { error("No router provided") }
val LocalRouterTransitionScope = compositionLocalOf<SharedTransitionScope> { error("No router transition scope provided") }
@Composable
fun ProvideRouter(
routes: Map<String, RouteComposable>,
defaultRoute: String,
defaultRouteParameters: RouteParameters = emptyMap(),
content: @Composable () -> Unit,
) {
val backStack = remember { mutableStateListOf(RouteContext(defaultRoute, defaultRouteParameters)) }
val router = remember(routes, backStack) {
Router(
routes = routes,
backStack = backStack,
)
}
CompositionLocalProvider(
LocalRouter provides router,
content = content
)
}
@Composable
fun RouterContent(
router: Router = LocalRouter.current,
fallbackRoute: String = "/",
transitionSpec: AnimatedContentTransitionScope<Scene<RouteContext>>.() -> ContentTransform = defaultTransitionSpec(),
popTransitionSpec: AnimatedContentTransitionScope<Scene<RouteContext>>.() -> ContentTransform = defaultPopTransitionSpec(),
predictivePopTransitionSpec: AnimatedContentTransitionScope<Scene<RouteContext>>.(@NavigationEvent.SwipeEdge Int) -> ContentTransform = { popTransitionSpec() },
) {
SharedTransitionLayout {
CompositionLocalProvider(LocalRouterTransitionScope provides this@SharedTransitionLayout) {
NavDisplay(
backStack = router.backStack,
onBack = { router.back() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
),
transitionSpec = transitionSpec,
popTransitionSpec = popTransitionSpec,
predictivePopTransitionSpec = predictivePopTransitionSpec,
entryProvider = { backStackEntry ->
NavEntry(backStackEntry) {
val route = backStackEntry.route
val composable = router.resolve(route)
if (composable == null) {
val fallbackComposable = router.resolve(fallbackRoute)
?: error("Unknown route $route, fallback $fallbackRoute is invalid")
val context = backStackEntry.copy(route = fallbackRoute)
fallbackComposable(context)
} else {
composable(backStackEntry)
}
}
}
)
}
}
}

View File

@@ -4,7 +4,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
interface AudioEventListener {
fun onPlaybackStateChange(newState: PlaybackController.PlaybackState, currentItem: BaseItemDto?) = Unit
fun onProgress(pos: Long) = Unit
fun onProgress(pos: Long, duration: Long) = Unit
fun onQueueStatusChanged(hasQueue: Boolean) = Unit
fun onQueueReplaced() = Unit
}

View File

@@ -65,7 +65,6 @@ public class AudioNowPlayingFragment extends Fragment {
private TextView mCurrentNdx;
private TextView mCurrentPos;
private TextView mRemainingTime;
private int mCurrentDuration;
private RowsSupportFragment mRowsFragment;
private ArrayObjectAdapter mRowsAdapter;
private PositionableListRowPresenter mAudioQueuePresenter;
@@ -239,7 +238,7 @@ public class AudioNowPlayingFragment extends Fragment {
// load the item duration and set the position to 0 since it won't be set elsewhere until playback is initialized
if (!mediaManager.getValue().isAudioPlayerInitialized())
setCurrentTime(0);
setCurrentTime(0, 0);
}
@Override
@@ -258,8 +257,8 @@ public class AudioNowPlayingFragment extends Fragment {
}
@Override
public void onProgress(long pos) {
setCurrentTime(pos);
public void onProgress(long pos, long duration) {
setCurrentTime(pos, duration);
}
@Override
@@ -354,17 +353,19 @@ public class AudioNowPlayingFragment extends Fragment {
mAlbumTitle.setText(null);
}
mCurrentNdx.setText(getString(R.string.lbl_now_playing_track, mediaManager.getValue().getCurrentAudioQueueDisplayPosition(), mediaManager.getValue().getCurrentAudioQueueDisplaySize()));
mCurrentDuration = ((Long) ((item.getRunTimeTicks() != null ? item.getRunTimeTicks() : 0) / 10000)).intValue();
addGenres(mGenreRow);
backgroundService.getValue().setBackground(item);
}
}
public void setCurrentTime(long time) {
// Round the current time as otherwise the time played and time remaining will not be in sync
public void setCurrentTime(long time, long duration) {
// Round the time to seconds so both the position and remaining times are in sync
time = Math.round(time / 1000L) * 1000L;
mCurrentPos.setText(TimeUtils.formatMillis(time));
mRemainingTime.setText(mCurrentDuration > 0 ? "-" + TimeUtils.formatMillis(mCurrentDuration - time) : "");
if (duration == 0L) mRemainingTime.setText(null);
else mRemainingTime.setText("-" + TimeUtils.formatMillis(duration - time));
}
private void addGenres(TextView textView) {

View File

@@ -37,8 +37,8 @@ import org.jellyfin.androidtv.util.apiclient.itemImages
import org.jellyfin.androidtv.util.apiclient.parentImages
import org.jellyfin.playback.core.PlaybackManager
import org.jellyfin.playback.core.model.PlayState
import org.jellyfin.playback.jellyfin.lyrics
import org.jellyfin.playback.jellyfin.lyricsFlow
import org.jellyfin.playback.jellyfin.lyrics.lyrics
import org.jellyfin.playback.jellyfin.lyrics.lyricsFlow
import org.jellyfin.playback.jellyfin.queue.baseItem
import org.jellyfin.playback.jellyfin.queue.baseItemFlow
import org.jellyfin.sdk.api.client.ApiClient

View File

@@ -7,7 +7,6 @@ import android.content.Context;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.KeyEvent;
@@ -698,7 +697,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
@Override
public void onStop() {
super.onStop();
Timber.d("Stopping!");
Timber.i("Stopping!");
if (leanbackOverlayFragment != null)
leanbackOverlayFragment.setOnKeyInterceptListener(null);
@@ -706,7 +705,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
// end playback from here if this fragment belongs to the current session.
// if it doesn't, playback has already been stopped elsewhere, and the references to this have been replaced
if (playbackControllerContainer.getValue().getPlaybackController() != null && playbackControllerContainer.getValue().getPlaybackController().getFragment() == this) {
Timber.d("this fragment belongs to the current session, ending it");
Timber.i("this fragment belongs to the current session, ending it");
playbackControllerContainer.getValue().getPlaybackController().endPlayback();
}
@@ -1283,7 +1282,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
if (chapters != null && !chapters.isEmpty()) {
// create chapter row for later use
ItemRowAdapter chapterAdapter = new ItemRowAdapter(requireContext(), BaseItemExtensionsKt.buildChapterItems(item, api.getValue()), new CardPresenter(true, 110), new MutableObjectAdapter<Row>());
ItemRowAdapter chapterAdapter = new ItemRowAdapter(requireContext(), BaseItemExtensionsKt.buildChapterItems(item), new CardPresenter(true, 110), new MutableObjectAdapter<Row>());
chapterAdapter.Retrieve();
if (mChapterRow != null) mPopupRowAdapter.remove(mChapterRow);
mChapterRow = new ListRow(new HeaderItem(requireContext().getString(R.string.chapters)), chapterAdapter);
@@ -1340,10 +1339,8 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
WindowCompat.getInsetsController(requireActivity().getWindow(), requireActivity().getWindow().getDecorView()).show(WindowInsetsCompat.Type.systemBars());
// Reset display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
WindowManager.LayoutParams params = getActivity().getWindow().getAttributes();
params.preferredDisplayModeId = 0;
getActivity().getWindow().setAttributes(params);
}
WindowManager.LayoutParams params = getActivity().getWindow().getAttributes();
params.preferredDisplayModeId = 0;
getActivity().getWindow().setAttributes(params);
}
}

View File

@@ -159,12 +159,12 @@ fun CustomPlaybackOverlayFragment.recordProgram(program: BaseItemDto, isSeries:
lifecycleScope.launch {
runCatching {
withContext(Dispatchers.IO) {
val defaultTimer by api.liveTvApi.getDefaultTimer()
val defaultTimer by api.liveTvApi.getDefaultTimer(program.id.toString())
if (isSeries) {
api.liveTvApi.createSeriesTimer(defaultTimer.copy(programId = program.id.toString()))
api.liveTvApi.createSeriesTimer(defaultTimer)
} else {
api.liveTvApi.createTimer(defaultTimer.asTimerInfoDto().copy(programId = program.id.toString()))
api.liveTvApi.createTimer(defaultTimer.asTimerInfoDto())
}
}
}.fold(

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