Compare commits

...

775 Commits

Author SHA1 Message Date
Niels van Velzen
ef899f98bc Fix SelectQualityAction crashing when opening menu
(cherry picked from commit 044ce67110)
2024-08-06 15:20:34 +02:00
Niels van Velzen
de6c7904f0 Use "direct play" as default audio behavior
(cherry picked from commit f6c536880d)
2024-08-06 15:20:34 +02:00
Marc Vollmer
0a79d5b60f Fix NullPointerException in LeanbackOverlayFragment.onPause
(cherry picked from commit 3b852a031e)
2024-08-06 15:20:33 +02:00
Niels van Velzen
36e0bc1edc Fix NullPointerException in CustomPlaybackOverlayFragment.onResume
(cherry picked from commit 7bc99d2e66)
2024-08-06 15:20:33 +02:00
Niels van Velzen
8faeb3aac1 Remove context requirement from ExoPlayerProfile
(cherry picked from commit 9ecf952351)
2024-08-06 15:20:11 +02:00
majmongoose
160ef4914f Add Google Chromecast HD limitation (#3820)
* Add identification for Google Chromecast HD

Possibly an upstream bug is preventing this from working, as the chromecast is being idenitified correctly in debugging.

* Update DeviceUtils.kt

Added a note for the reason of the change.

* Removed unintentional imports

* Update app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt

Make changes suggested by maintainer.

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>

* Update DeviceUtils.kt

Removed unneeded isChromecastHD

---------

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
(cherry picked from commit a8c9f054ab)
2024-08-06 15:20:10 +02:00
Niels van Velzen
474c0774f1 Fix AudioNowPlayingFragment showing wrong item in extremely rare cases
(cherry picked from commit 61b4178dec)
2024-08-06 15:20:10 +02:00
Niels van Velzen
0c9ba1ccec Fix race condition in PictureViewerViewModel
Using the showNext/showPrevious functions while the album is still loading can cause an IndexOutOfBoundsException. Check if the album is empty (default value) first before trying to read from it to avoid these crashes.

(cherry picked from commit 9aa1759ae7)
2024-08-06 15:20:10 +02:00
Niels van Velzen
0c667ab615 Strip HTML from descriptions in leanback integration
(cherry picked from commit 2e55987939)
2024-08-06 15:20:09 +02:00
Micka149
fa231f7b9b Translated using Weblate (Russian)
Currently translated at 96.7% (502 of 519 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2024-07-30 15:41:31 -04:00
Lukas Šlenys
21dd49d1d3 Translated using Weblate (Lithuanian)
Currently translated at 78.9% (410 of 519 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lt/
2024-07-26 10:41:31 -04:00
renovate[bot]
b12e4a6978 fix(deps): update dependency org.jellyfin.sdk:jellyfin-core to v1.5.2 2024-07-25 21:36:46 +02:00
Niels van Velzen
27a3cda6a9 Fix Live TV program popup not populating when opening early 2024-07-25 20:52:46 +02:00
Niels van Velzen
de49e106c5 Fix record buttons in LiveProgramDetailPopup swapped 2024-07-25 20:35:41 +02:00
Niels van Velzen
2d451e51a0 Update targetSdk to 34 2024-07-25 19:16:30 +02:00
renovate[bot]
a9ab6c86ea chore(deps): update github/codeql-action action to v3.25.14 2024-07-25 18:38:17 +02:00
renovate[bot]
444e4e79b3 fix(deps): update androidx.media3 to v1.4.0 2024-07-25 18:38:04 +02:00
Niels van Velzen
fa4ccd3c0f Fix nullability in copyWith[Series]TimerId functions 2024-07-25 15:35:41 +02:00
Niels van Velzen
cc7df2483a Fix "other" items in ByLetterFragment 2024-07-24 21:29:48 +02:00
renovate[bot]
4b933c247a fix(deps): update dependency androidx.activity:activity to v1.9.1 2024-07-24 21:16:59 +02:00
renovate[bot]
01fecadac3 fix(deps): update androidx.fragment to v1.8.2 2024-07-24 21:12:58 +02:00
renovate[bot]
108d24f55f fix(deps): update androidx.lifecycle to v2.8.4 2024-07-24 21:12:47 +02:00
Justin
4488a679c9 Translated using Weblate (German)
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/de/
2024-07-24 10:54:33 -04:00
Justin
3c2839462a Translated using Weblate (German)
Currently translated at 100.0% (519 of 519 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-07-24 10:54:33 -04:00
Niels van Velzen
dcbf87488c Fix NPE when opening Live TV guide for the first time 2024-07-23 17:53:56 +02:00
Bond_009
da8e5e8396 Add PCM_S16LE, PCM_S20LE and PCM_S24LE to supported audio codecs
These formats can be found on DVD and Blu-Ray
2024-07-23 17:39:53 +02:00
Weblate
63ab22f31d 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/
2024-07-23 09:19:29 -04:00
Niels van Velzen
38589ef0c7 Remove unused dtsEnabled preference 2024-07-23 15:19:25 +02:00
Niels van Velzen
a54a5945ee Disable tunneling 2024-07-23 15:19:12 +02:00
Niels van Velzen
6390a95314 Update MediaInfoCardView design 2024-07-23 07:35:42 +02:00
Niels van Velzen
bf60489403 Always show debug media info when available 2024-07-23 07:35:42 +02:00
Niels van Velzen
07eddcd559 Add some audio related information to MediaInfoCardView 2024-07-23 07:35:42 +02:00
bummer2000
3165459b8c Translated using Weblate (Bulgarian)
Currently translated at 32.8% (171 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2024-07-22 18:41:30 -04:00
Imjustjokingwithya
1edf91aa7c Translated using Weblate (Finnish)
Currently translated at 92.3% (480 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2024-07-22 11:41:30 -04:00
Niels van Velzen
b2939bfe38 Split "playback/exoplayer" Gradle module into two (#3781)
* Split media3-session to own Gradle module

* Rename playback.exoplayer to playback.media3-exoplayer

* Use submodule for media3
2024-07-22 07:52:31 +02:00
Giuseppe Sacco
0a4b4c1fd6 Translated using Weblate (Italian)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2024-07-21 08:41:30 -04:00
renovate[bot]
263b485ba4 fix(deps): update dependency org.jellyfin.sdk:jellyfin-core to v1.5.1 2024-07-21 13:31:44 +02:00
Niels van Velzen
a9b035016f Fix FullDetailsFragment duplicating buttons on resume 2024-07-20 14:07:44 +02:00
renovate[bot]
cbcaea6d2d chore(deps): update github/codeql-action action to v3.25.13 2024-07-19 20:16:46 +02:00
Adorján
cb7054f4c2 Translated using Weblate (Hungarian)
Currently translated at 99.8% (519 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2024-07-19 06:41:30 -04:00
Niels van Velzen
3170670c23 Replace all left-over Date usages with java.time 2024-07-18 20:55:38 +02:00
renovate[bot]
ad18e5b0d6 fix(deps): update dependency io.coil-kt:coil-base to v2.7.0 2024-07-17 20:32:31 +02:00
Troja
631096433c Translated using Weblate (Belarusian)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2024-07-17 06:41:30 -04:00
Troja
08345f2b07 Translated using Weblate (Belarusian)
Currently translated at 95.5% (497 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2024-07-16 17:41:30 -04:00
Troja
80cb7092af Translated using Weblate (Belarusian)
Currently translated at 71.3% (371 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2024-07-16 14:41:30 -04:00
Weblate
5900197af6 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/
2024-07-16 11:41:31 -04:00
Troja
85791fc7d6 Translated using Weblate (Filipino)
Currently translated at 3.2% (17 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fil/
2024-07-16 11:41:31 -04:00
Troja
8ce0f7a420 Translated using Weblate (Belarusian)
Currently translated at 69.8% (363 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2024-07-16 11:41:30 -04:00
Niels van Velzen
1c80f6e7a4 Remove unused compat streaminfo code 2024-07-16 15:37:15 +02:00
Niels van Velzen
5891264476 Fix timezone issues in Live TV guide 2024-07-16 14:46:44 +02:00
Troja
f5e1b18543 Translated using Weblate (Filipino)
Currently translated at 6.9% (36 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fil/
2024-07-16 08:41:30 -04:00
Troja
7e1d94795c Translated using Weblate (Belarusian)
Currently translated at 42.3% (220 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2024-07-16 08:41:30 -04:00
Niels van Velzen
181bb62adb Set HTTP timeouts in VideoManager ExoPlayer 2024-07-16 14:26:26 +02:00
Niels van Velzen
9a9fc6752b Re-use HTTP timeouts from ApiClient for ExoPlayer 2024-07-16 14:26:26 +02:00
renovate[bot]
93272f4614 Update dependency io.mockk:mockk to v1.13.12 2024-07-16 13:57:03 +02:00
renovate[bot]
9e3a81df19 Update gradle/actions action to v3.5.0 2024-07-16 13:56:51 +02:00
Niels van Velzen
801c043807 Tweak formatting 2024-07-16 13:54:37 +02:00
Christophe Beyls
644a4d5112 fix: apply suggested changes after review 2024-07-16 13:54:37 +02:00
Christophe Beyls
352a3af46e Replace Kotlin Multiplatform BlurHash library with faster local implementation 2024-07-16 13:54:37 +02:00
Niels van Velzen
96174e5d00 Update editor config
- Increase max line length (120->140)
- Fix wildcard for Kotlin files
2024-07-16 13:37:55 +02:00
Niels van Velzen
aafebfc546 Improve intro loading in SdkPlaybackHelper 2024-07-15 16:25:38 +02:00
rdma
540b5e619e Translated using Weblate (Czech)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-07-14 12:41:29 -04:00
Niels van Velzen
4a2115f093 Fix AudioNowPlayingFragment showing "From null" when there is no album name 2024-07-14 15:20:35 +02:00
Niels van Velzen
9e41a8b114 Fix race condition in player backdrop 2024-07-14 14:57:56 +02:00
Niels van Velzen
b22095a460 Fix "Next episode(s)" row not showing 2024-07-13 18:06:57 +02:00
renovate[bot]
d2dd5fdb4d Update dependency org.jellyfin.sdk:jellyfin-core to v1.5.0 2024-07-13 14:23:49 +02:00
Niels van Velzen
223348365a Fix PlaybackController spam refreshing Live TV channels 2024-07-13 14:13:10 +02:00
Niels van Velzen
fd5ca3ffea Fix cast exception in ChannelCardView 2024-07-13 14:13:10 +02:00
Niels van Velzen
b1356ac6a0 Fix NPE in ProgramGridCell 2024-07-13 14:13:10 +02:00
Niels van Velzen
5febfaa1a8 Fix NPE in FullDetailsFragment 2024-07-13 14:13:10 +02:00
Niels van Velzen
f6dfe02a3d Fix NPE in EnhancedBrowseFragment 2024-07-13 14:13:10 +02:00
Niels van Velzen
d6ab991b27 Fix program card sizes 2024-07-13 14:13:10 +02:00
renovate[bot]
b4defa717f Update CI dependencies 2024-07-13 13:25:44 +02:00
renovate[bot]
748fd2f4aa Update dependency gradle to v8.9 2024-07-13 13:25:23 +02:00
renovate[bot]
e9bbdc4085 Update dependency com.android.tools.build:gradle to v8.5.1 2024-07-13 13:25:14 +02:00
Filan Fisteku
cd40860ca5 Translated using Weblate (Albanian)
Currently translated at 23.4% (122 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sq/
2024-07-12 14:41:30 -04:00
renovate[bot]
24191b3698 Update dependency androidx.tv:tv-material to v1.0.0-rc01 2024-07-10 22:09:37 +02:00
Niels van Velzen
c69e9bf292 Add direct play support for WAV audio 2024-07-10 19:05:16 +02:00
Niels van Velzen
0d82a786e1 Fix authorization for transcoding 2024-07-10 08:40:21 +02:00
Niels van Velzen
8e0a18414e Fix lifecycle for LiveProgramDetailPopup 2024-07-08 22:38:35 +02:00
Niels van Velzen
f94468c2a9 Use context in Live TV popups 2024-07-08 22:38:35 +02:00
Niels van Velzen
08cb004b99 Fix crash when item has no media sources 2024-07-07 22:46:54 +02:00
Niels van Velzen
eacbfe7005 Rewrite details row with constraint layout 2024-07-07 15:55:59 +02:00
Niels van Velzen
19bfaa620e Shrink BaseItemInfoRowRuntime size 2024-07-06 19:05:36 +02:00
Niels van Velzen
fda20e07ac Fix crash when refreshing current item 2024-07-06 10:04:31 +02:00
Niels van Velzen
f6f81c290d Fix isPlayingAudio behavior 2024-07-05 07:27:46 +02:00
Mirronth
50fcdb44c7 Translated using Weblate (Hungarian)
Currently translated at 98.8% (514 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2024-07-04 16:41:29 -04:00
Niels van Velzen
9ead8ddc34 Catch errors in ExoPlayerAudioPipeline 2024-07-04 14:53:18 +02:00
Niels van Velzen
9d3626ccdd Fix refreshing current playing item hiding play icon 2024-07-04 14:52:59 +02:00
Niels van Velzen
f8b7087f16 Fix crash when re-opening app during music playback 2024-07-03 14:39:23 +02:00
newton181
862607ee10 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2024-07-02 20:41:29 -04:00
Niels van Velzen
3bd33cc0ec Fix PlaySessionService no longer reporting to server 2024-07-02 18:55:39 +02:00
Niels van Velzen
483cfc8cb8 Fix header position in BrowseFolderFragment 2024-07-02 14:55:28 +02:00
Niels van Velzen
16ceaf642d Remove hasPassword support 2024-07-02 14:55:16 +02:00
renovate[bot]
52d69f33fe Update dependency com.android.tools.build:gradle to v8.5.0 2024-07-02 12:38:21 +02:00
renovate[bot]
3feb8798c9 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.1 2024-07-02 12:38:08 +02:00
renovate[bot]
191a528143 Update CI dependencies 2024-07-02 12:37:41 +02:00
renovate[bot]
3a800663ee Update aboutlibraries to v11.2.2 2024-07-02 12:37:33 +02:00
renovate[bot]
ec5bd66a74 Update androidx.lifecycle to v2.8.3 2024-07-02 12:37:11 +02:00
renovate[bot]
95c6ed3dcd Update androidx.fragment to v1.8.1 2024-07-02 12:37:05 +02:00
Pablo Reinoso
3ce4e3d50d Translated using Weblate (Spanish)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-07-02 01:41:29 -04:00
Thomas Schwery
64131c1784 Translated using Weblate (French)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-07-01 16:41:29 -04:00
Andi Chandler
ae879b296f Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-07-01 16:41:28 -04:00
Niels van Velzen
73a96256eb Fix skip to previous/next playback actions 2024-07-01 20:15:16 +02:00
Niels van Velzen
501e362dae Refactor sort option code in BrowseGridFragment 2024-07-01 20:15:09 +02:00
Niels van Velzen
a17922cfdb Use Enum.name instead of Enum.toString in preference stores 2024-07-01 20:15:09 +02:00
Marc
107890a1e0 Translated using Weblate (Catalan)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-06-30 11:41:28 -04:00
Justin
ff9d659976 Translated using Weblate (German)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-06-26 07:41:28 -04:00
nextlooper42
2241cf43fa Translated using Weblate (Slovak)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-06-24 07:41:28 -04:00
nexia58
eb32016d86 Translated using Weblate (Swedish)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-06-23 07:41:28 -04:00
無情天
1c75bd2d10 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-06-22 11:41:28 -04:00
AfmanS
d82d09c12e Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-06-22 11:41:28 -04:00
queeup
c24cf01853 Translated using Weblate (Turkish)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-06-22 08:41:28 -04:00
Niels van Velzen
f3a89d2051 Move dream service logic to view model 2024-06-21 23:00:04 +02:00
Niels van Velzen
1ca2e1b956 Add view model support to DreamServiceCompat 2024-06-21 23:00:04 +02:00
stanol
5dc68367b1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-06-21 08:41:28 -04:00
Javier Crespo
a61f51b1d3 Translated using Weblate (Spanish)
Currently translated at 99.2% (516 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-06-21 08:41:28 -04:00
Kityn
052dccaac7 Translated using Weblate (Polish)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-06-20 21:41:28 -04:00
Bas
d9f6d1a411 Translated using Weblate (Dutch)
Currently translated at 100.0% (520 of 520 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-06-20 16:41:28 -04:00
Hayden
a82fcf89b6 Add media logo to screensaver (#3680)
* Show media logo on screensaver

* Fallback to title text if logo is null

* Preload logo and backdrop

* set logo description to item name

* Add correct tag to logo.

* Show media logo on screensaver

* Fallback to title text if logo is null

* Preload logo and backdrop

* set logo description to item name

* Add correct tag to logo.

* contentDescription nullable.

* contentDescription nullable.

---------

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
2024-06-20 21:22:07 +02:00
Niels van Velzen
72e4bf82b7 Remove unused ModelCompat extensions 2024-06-20 20:18:39 +02:00
Niels van Velzen
9a918db958 Migrate all other Live TV code to SDK 2024-06-20 20:18:39 +02:00
Niels van Velzen
f6dd2a1a87 Migrate Live TV code in ChannelCardView to SDK 2024-06-20 20:18:39 +02:00
Niels van Velzen
5e08f1f8a7 Migrate Live TV code in LiveProgramDetailPopupHelper to SDK 2024-06-20 20:18:39 +02:00
Niels van Velzen
d2ab816f40 Migrate Live TV code in RecordPopup to SDK 2024-06-20 20:18:39 +02:00
Niels van Velzen
6a5dc7b8ae Migrate Live TV code in FullDetailsFragment to SDK 2024-06-20 20:18:39 +02:00
Niels van Velzen
1db2da98aa Migrate Live TV code in BrowseRecordingsFragment to SDK 2024-06-20 20:18:39 +02:00
Niels van Velzen
97b61189cc Migrate Live TV code in BrowseViewFragment to SDK 2024-06-20 20:18:39 +02:00
bene toffix
c58f3cda6a Translated using Weblate (Catalan)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-06-19 13:41:28 -04:00
Hayden
4e33b0fa28 Translated using Weblate (Russian)
Currently translated at 96.5% (499 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2024-06-19 13:41:28 -04:00
Andi Chandler
6e631adad3 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-06-18 19:41:28 -04:00
Niels van Velzen
a47c97d9e9 Fix Live TV not playing video streams 2024-06-18 22:21:03 +02:00
Hayden
bb4af4a2a5 Translated using Weblate (Russian)
Currently translated at 93.2% (482 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2024-06-18 12:41:28 -04:00
DJSweder
63c01d0493 Translated using Weblate (Czech)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-06-18 12:41:28 -04:00
AfmanS
c643113cfc Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-06-18 04:41:28 -04:00
queeup
a1895a755f Translated using Weblate (Turkish)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-06-17 20:41:28 -04:00
Kityn
14d4963050 Translated using Weblate (Polish)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-06-17 20:41:28 -04:00
Nir Israel Hen
a6c7446fc8 Translated using Weblate (Hebrew)
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/
2024-06-17 13:41:28 -04:00
Nir Israel Hen
0dbfa38822 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/
2024-06-17 13:41:28 -04:00
Nir Israel Hen
e69c0d57f9 Translated using Weblate (Hebrew)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2024-06-17 13:41:28 -04:00
Bas
f0f4e853e6 Translated using Weblate (Dutch)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-06-17 13:41:28 -04:00
Nir Israel Hen
391de8329b Translated using Weblate (Hebrew)
Currently translated at 96.5% (498 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2024-06-17 10:52:47 -04:00
Niels van Velzen
fcfee4a942 Improve "continue listening" row 2024-06-17 16:52:44 +02:00
AfmanS
5f1d39ab06 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-06-16 08:41:28 -04:00
Hayden
ddc9a01949 Add a blur to backdrop images (#3674)
* Feature added - blur backdrop

* Removed unnecessary _blur usages

* Blur by default

* Update UserPreferences.kt

* Update BackgroundService.kt

* Disable blur on splashscreen

* Update AppBackground.kt Formatting

* Update app/src/main/java/org/jellyfin/androidtv/ui/background/AppBackground.kt

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>

* Update AppBackground.kt

---------

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
2024-06-16 08:44:13 +00:00
AfmanS
3c3126f776 Translated using Weblate (Portuguese (Portugal))
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/pt_PT/
2024-06-16 04:41:27 -04:00
Blackspirits
a9908a8db9 Translated using Weblate (Portuguese (Portugal))
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/pt_PT/
2024-06-16 01:41:28 -04:00
stanol
ccad1fe4fd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-06-15 17:41:27 -04:00
AfmanS
31eed48c36 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-06-15 14:41:28 -04:00
Joseph Price
d709ca8805 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-06-14 20:41:27 -04:00
renovate[bot]
538c4132ff Update dependency androidx.compose.foundation:foundation to v1.6.8 2024-06-14 19:35:17 +02:00
renovate[bot]
27583592a5 Update CI dependencies 2024-06-14 19:31:14 +02:00
renovate[bot]
e4bf303e21 Update dependency androidx.compose.ui:ui-tooling to v1.6.8 2024-06-14 19:30:48 +02:00
renovate[bot]
3674eb78b8 Update androidx.lifecycle to v2.8.2 2024-06-14 19:30:30 +02:00
renovate[bot]
97d5a907b8 Update androidx.fragment to v1.8.0 2024-06-14 19:30:16 +02:00
newton181
c312256a46 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2024-06-13 18:41:28 -04:00
newton181
fa8055eba3 Translated using Weblate (Spanish)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-06-13 18:41:27 -04:00
Niels van Velzen
48d9acea15 Fix PlaybackController not respecting selected media source 2024-06-13 22:35:51 +02:00
Niels van Velzen
3d4d77928f Fix version menu not showing current version as selected 2024-06-13 22:35:51 +02:00
nextlooper42
39730f6407 Translated using Weblate (Slovak)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-06-13 09:41:27 -04:00
fourtakig
efa0e0d63b Translated using Weblate (Thai)
Currently translated at 39.1% (202 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/th/
2024-06-13 05:18:22 -04:00
無情天
d7aefa5f5e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-06-12 17:41:27 -04:00
Niels van Velzen
2b5f72eb0b Drop unused playback-ui module 2024-06-12 22:20:53 +02:00
Niels van Velzen
5f3dd47243 Move media session implementation to exoplayer module 2024-06-12 22:20:53 +02:00
renovate[bot]
d08e57c287 Update github/codeql-action action to v3.25.9 2024-06-12 19:58:43 +02:00
queeup
5e7b76b334 Translated using Weblate (Turkish)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-06-12 10:41:27 -04:00
VXsz
05485a4765 Translated using Weblate (Arabic)
Currently translated at 98.8% (510 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2024-06-12 04:40:35 -04:00
millallo
5eee574f41 Translated using Weblate (Italian)
Currently translated at 99.2% (512 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2024-06-12 04:40:35 -04:00
Kityn
af366b90af Translated using Weblate (Polish)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-06-11 19:41:27 -04:00
Niels van Velzen
c65a16e350 Fix usage of removed androidx.legacy.widget.Space class in picture viewer 2024-06-11 22:13:39 +02:00
Niels van Velzen
3ce1059f27 Fix "shuffle all" not working for albums 2024-06-11 22:13:39 +02:00
Niels van Velzen
c7fb82ef1f Fix NPE in FullDetailsFragment when opening (album) artists 2024-06-11 22:13:39 +02:00
Niels van Velzen
91c2dc79a1 Use SDK for FilterOptions 2024-06-11 21:56:12 +02:00
Bas
4617bc787c Translated using Weblate (Dutch)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-06-11 14:59:10 -04:00
Justin
862f8f2313 Translated using Weblate (German)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-06-11 14:59:10 -04:00
Weblate
48128f20d9 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/
2024-06-11 12:10:19 -04:00
無情天
b5ffc8562b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (522 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-06-11 12:10:17 -04:00
Niels van Velzen
071bdac6aa Simplify video player preference 2024-06-11 18:10:13 +02:00
renovate[bot]
dc06409618 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.0 2024-06-11 13:44:57 +02:00
renovate[bot]
20454ed2f6 Update dependency com.android.tools.build:gradle to v8.4.2 2024-06-11 13:40:28 +02:00
Niels van Velzen
87c8ae525b Use Kotlin compose compiler 2024-06-11 13:32:35 +02:00
Niels van Velzen
50919a123d Add .kotlin directory to .gitignore 2024-06-11 13:32:35 +02:00
renovate[bot]
4e958db404 Update Kotlin to v2 2024-06-11 13:32:35 +02:00
liolio6
cadbc559d0 Translated using Weblate (French)
Currently translated at 100.0% (522 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-06-10 19:41:27 -04:00
Niels van Velzen
62c32d00d1 Fix play after pause in MediaManager implementation 2024-06-10 22:02:25 +02:00
stanol
7c4b91a3d2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (522 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-06-10 15:41:27 -04:00
Niels van Velzen
472705614a Implement normalization gain for music tracks 2024-06-10 19:41:05 +02:00
Niels van Velzen
f0554b26c1 Send QueueEntry to playback backend 2024-06-10 19:22:04 +02:00
Kityn
b6a73f7088 Translated using Weblate (Polish)
Currently translated at 100.0% (522 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-06-10 11:41:27 -04:00
Kristian Haveland
6438236e49 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.2% (518 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2024-06-10 11:41:27 -04:00
nextlooper42
6f9b4780d4 Translated using Weblate (Slovak)
Currently translated at 100.0% (522 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-06-10 06:41:27 -04:00
DJSweder
8cf76a463b Translated using Weblate (Czech)
Currently translated at 100.0% (522 of 522 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-06-10 01:41:27 -04:00
Weblate
ce4389ce3d 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/
2024-06-09 15:30:58 -04:00
Connor Smith
e5299d72c0 Change 'Queue all subsequent episodes' setting to queue all episodes in a series 2024-06-09 21:30:54 +02:00
Niels van Velzen
b2a6411860 Remove unused AlbumArtistsQuery 2024-06-09 09:20:16 +02:00
Niels van Velzen
1217fbf4f1 Migrate "Play first unwatched" to SDK 2024-06-09 09:20:16 +02:00
renovate[bot]
7dfc8bc337 Update github/codeql-action action to v3.25.8 2024-06-09 09:16:20 +02:00
renovate[bot]
d02cb0ef9f Update kotest to v5.9.1 2024-06-09 09:16:03 +02:00
Niels van Velzen
8d125424d9 Remove StdItemQuery 2024-06-09 09:12:31 +02:00
Niels van Velzen
f43916f04f Fix crash when opening Live TV recordings 2024-06-09 09:12:31 +02:00
Niels van Velzen
ab97adb80a Use Collection<T> instead of Array<T> in ItemRowAdapterHelper.setItems 2024-06-09 09:12:31 +02:00
Niels van Velzen
3afedc8c1a Remove row clearing 2024-06-09 09:12:31 +02:00
Niels van Velzen
093167a99c Replace ItemQuery with GetItemsRequest 2024-06-09 09:12:31 +02:00
Pascal Weidenhammer
5aa3c1eb63 Fix QuickConnect sticky state 2024-06-09 09:12:13 +02:00
Niels van Velzen
0e3a3c21f9 Fix "favorites" playlist not showing 2024-06-08 10:00:57 +02:00
Niels van Velzen
e0e4f51c09 Fix ItemRowAdapter.getStartLetter for (album) artists 2024-06-08 10:00:57 +02:00
efreet7
af68d6cd9d Reorder actionsList 2024-06-06 22:49:42 +02:00
Romain Veillerot
5ce9cd13db Translated using Weblate (French)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-06-06 13:41:27 -04:00
Svetoslav Popov
6728ba3dc0 Translated using Weblate (Bulgarian)
Currently translated at 28.6% (150 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2024-06-05 16:43:33 -04:00
paaff
ffe3cc9b8e Translated using Weblate (Danish)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/da/
2024-06-05 15:41:27 -04:00
dharmik2319
f293bf1361 Translated using Weblate (Hindi)
Currently translated at 13.5% (71 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hi/
2024-06-05 12:00:50 -04:00
Weblate
b2e5afc7d5 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/
2024-06-05 10:41:28 -04:00
alien75
2034073bc1 Translated using Weblate (Italian)
Currently translated at 99.0% (518 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2024-06-05 10:41:27 -04:00
paaff
e38ff37e5f Translated using Weblate (Danish)
Currently translated at 97.3% (509 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/da/
2024-06-05 10:41:27 -04:00
queeup
196c262a71 Translated using Weblate (Turkish)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-06-04 18:41:27 -04:00
Niels van Velzen
99146e5a91 Fix paged item rows not always resetting 2024-06-04 22:37:08 +02:00
Niels van Velzen
732b8b706e Migrate "(Album) Artists" to SDK 2024-06-04 22:37:08 +02:00
Niels van Velzen
992bbe0f6b Migrate "Live TV Channels" to SDK 2024-06-04 22:37:08 +02:00
Niels van Velzen
4751c7fd69 Rename query -> request for custom request types 2024-06-04 22:37:08 +02:00
Niels van Velzen
5ec3b9a1dd Migrate Live TV rows to SDK 2024-06-04 16:12:16 +02:00
Niels van Velzen
0a9c8c83ff Migrate "Trailers" to SDK 2024-06-04 15:17:49 +02:00
Niels van Velzen
a0c9640018 Migrate "More like this" to SDK 2024-06-04 15:17:49 +02:00
Niels van Velzen
d65118d56e Migrate "Upcoming episodes" to SDK 2024-06-04 15:17:49 +02:00
Niels van Velzen
4d7546d765 Migrate "Seasons" to SDK 2024-06-04 15:17:49 +02:00
Niels van Velzen
fcfa8aa988 Migrate "user views" to SDK 2024-06-04 15:17:49 +02:00
Niels van Velzen
903e30c91e Migrate "Additional parts" to SDK 2024-06-04 15:17:49 +02:00
Niels van Velzen
dfd03c3099 Migrate "Specials" to SDK 2024-06-04 15:17:49 +02:00
queeup
a13b06df0f Translated using Weblate (Turkish)
Currently translated at 99.4% (520 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-06-03 18:52:42 -04:00
Justin
5473c5295d Translated using Weblate (German)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-06-03 18:52:41 -04:00
無情天
900beb8275 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-06-03 16:41:27 -04:00
bene toffix
f93fa8703e Translated using Weblate (Catalan)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-06-03 12:30:15 -04:00
Alejandro M
d996598b69 Translated using Weblate (Spanish)
Currently translated at 99.8% (522 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-06-03 09:41:27 -04:00
nextlooper42
369a285b9a Translated using Weblate (Slovak)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-06-03 06:41:27 -04:00
Niels van Velzen
48fbe8ae46 Migrate "Recently added" to SDK 2024-06-03 07:33:19 +02:00
stanol
5007deb408 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-06-02 14:41:27 -04:00
Kityn
2fadedd165 Translated using Weblate (Polish)
Currently translated at 100.0% (523 of 523 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-06-02 14:41:27 -04:00
Weblate
e54ee5a8ff 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/
2024-06-02 11:11:02 -04:00
Bas
58ce8a1dfe Translated using Weblate (Dutch)
Currently translated at 100.0% (528 of 528 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-06-02 11:11:00 -04:00
Niels van Velzen
c1b651d6c6 Remove LibVLC 2024-06-02 17:10:57 +02:00
Niels van Velzen
afa5c4aec9 Remove some unused code related to ItemRowAdapter 2024-06-02 17:10:45 +02:00
Niels van Velzen
bdc9255187 Fix now playing fragment not using string resource for current queue title 2024-06-02 15:44:48 +02:00
renovate[bot]
e287e05403 Update dependency gradle to v8.8 2024-06-02 15:00:27 +02:00
renovate[bot]
0013d418f0 Update CI dependencies 2024-06-02 15:00:06 +02:00
renovate[bot]
7ed15332da Update aboutlibraries to v11.2.1 2024-06-02 14:59:57 +02:00
renovate[bot]
867484c616 Update dependency org.jellyfin.sdk:jellyfin-core to v1.5.0-beta.3 2024-06-02 14:08:08 +02:00
Niels van Velzen
d9eb4fbd0a USe ApiClient.update in UserLoginViewModel 2024-06-01 14:49:02 +02:00
Niels van Velzen
f03e74978c Remove redundant if statement in UserRepository 2024-06-01 14:49:02 +02:00
Niels van Velzen
aae0f27a75 Fix StartupActivity preventing invalid login from opening login fragment 2024-06-01 14:49:02 +02:00
Niels van Velzen
1fb8342b0c Fix SessionRepository applying crash reporting information before validating token 2024-06-01 14:49:02 +02:00
Niels van Velzen
3d797cf08a Fix AuthenticationRepository leaking access token to logs 2024-06-01 14:49:02 +02:00
Niels van Velzen
bfa26bd2f9 Fix UserLoginFragment not using lifecycle correctly 2024-06-01 14:49:02 +02:00
Niels van Velzen
8bf7380056 Start series from first unwatched in "play next up" 2024-06-01 14:48:44 +02:00
Niels van Velzen
6eb97d6178 Fix unchecked warning in FullDetailsFragment 2024-05-31 22:20:20 +02:00
Pascal Weidenhammer
f31a8095a0 Fix JSON decode crash in PictureViewerFragment
The arguments are provided as a String, not as JSON. Use the SDK provided methods to convert to an enum instead.
2024-05-31 19:49:34 +02:00
Niels van Velzen
29da028fe6 Migrate "next up" row to SDK 2024-05-30 22:45:04 +02:00
Niels van Velzen
0e94140c18 Fix resumable episodes showing up twice in "Play next" integration 2024-05-30 22:41:37 +02:00
renovate[bot]
aa8a2fbdc8 Update dependency androidx.window:window to v1.3.0 2024-05-29 21:26:12 +02:00
renovate[bot]
f10f8042af Update androidx.lifecycle to v2.8.1 2024-05-29 21:22:26 +02:00
renovate[bot]
505669445f Update dependency androidx.appcompat:appcompat to v1.7.0 2024-05-29 21:22:14 +02:00
stanol
07f34695ff Translated using Weblate (Ukrainian)
Currently translated at 100.0% (527 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-05-29 14:41:27 -04:00
foXaCe
cc6d0dfd06 Translated using Weblate (French)
Currently translated at 99.6% (525 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-05-29 14:41:27 -04:00
Pascal Weidenhammer
f36c40cddb Fix app crash in EditServerScreen
The java.text.DateFormat can't handle java.time.Instant resulting in a crash when loading the screen.
2024-05-28 20:12:21 +02:00
Niels van Velzen
3f71142ab2 Rewrite artist name code in DreamContentNowPlaying 2024-05-28 14:10:30 +02:00
Niels van Velzen
17fa838a98 Migrate to androidx.tv.material 2024-05-28 14:10:30 +02:00
Niels van Velzen
d584ee788f Update RecommendedServerIssueExtensions for removed exceptions 2024-05-28 08:56:53 +02:00
renovate[bot]
3a84b16e9a Update dependency org.jellyfin.sdk:jellyfin-core to v1.5.0-beta.2 2024-05-28 08:56:53 +02:00
Niels van Velzen
44a4cfec0c Use singleTask launch mode for StartupActivity 2024-05-28 08:43:05 +02:00
Niels van Velzen
879831d601 Support new intent in StartupActivity 2024-05-28 08:43:05 +02:00
無情天
04ed562334 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (527 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-05-27 13:41:26 -04:00
hoanghuy309
1b4e787f3b Translated using Weblate (Vietnamese)
Currently translated at 95.6% (504 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2024-05-27 09:41:26 -04:00
hoanghuy309
60be450620 Translated using Weblate (Vietnamese)
Currently translated at 95.0% (501 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2024-05-27 06:41:27 -04:00
stanol
d992cc6a70 Translated using Weblate (Ukrainian)
Currently translated at 99.8% (526 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-05-27 06:41:27 -04:00
nextlooper42
8ed0896e4e Translated using Weblate (Slovak)
Currently translated at 100.0% (527 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-05-27 06:41:27 -04:00
Blackspirits
4870e6c5f0 Translated using Weblate (Portuguese (Portugal))
Currently translated at 85.7% (452 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-05-27 06:41:26 -04:00
Kityn
c91dfdd539 Translated using Weblate (Polish)
Currently translated at 100.0% (527 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-05-27 03:41:26 -04:00
Bas
162233b8be Translated using Weblate (Dutch)
Currently translated at 100.0% (527 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-05-26 18:41:26 -04:00
Anonymous
c2e1847d0c Translated using Weblate (Spanish (Latin America))
Currently translated at 98.1% (517 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2024-05-26 16:12:00 -04:00
Anonymous
0581afc2ab Translated using Weblate (Chinese (Traditional))
Currently translated at 96.7% (510 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hant/
2024-05-26 16:11:59 -04:00
Bas
f32cf16a4d Translated using Weblate (Dutch)
Currently translated at 99.8% (526 of 527 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-05-26 16:11:59 -04:00
Niels van Velzen
dd5d6e7b3e Add app notification to notify about upcoming server requirement 2024-05-26 22:05:03 +02:00
Niels van Velzen
ead9df84df Fix ModelCompat crash for media type 2024-05-26 16:59:41 +02:00
stanol
f49b5af185 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (526 of 526 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-05-26 10:41:26 -04:00
無情天
b1c953050d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (526 of 526 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-05-26 10:41:26 -04:00
Kityn
6085f31d85 Translated using Weblate (Polish)
Currently translated at 100.0% (526 of 526 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-05-26 03:41:26 -04:00
foXaCe
17519ed827 Translated using Weblate (French)
Currently translated at 100.0% (526 of 526 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-05-26 03:41:26 -04:00
Bas
861cc723e9 Translated using Weblate (Dutch)
Currently translated at 100.0% (526 of 526 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-05-25 13:41:26 -04:00
Niels van Velzen
feafdf2162 Refactor BaseRowItem to use inheritance (#3598)
* Only use SDK types in BaseRowItem

This also cleans the constructor hell a bit (although still a mess)

* Refactor BaseRowItem constructors into subclasses

* Move function implementations to BaseRowItem subclasses

* Move properties to BaseRowItem subclasses

* Add missing BaseItemKinds in BaseItemDtoBaseRowItem

* Remove refresh function from BaseRowItem

* Optimize MutableObjectAdapter.remove(element: T)

* Make BaseRowItem immutable

* Remove dependency injection from BaseRowItem

* Use properties when possible in BaseRowItem

* Remove index property from BaseRowItem

* Move even more BaseRowItem properties

* Remove index from AudioQueueBaseRowItem

* Fix ItemRowAdapter counts after index removal

* Remove unused ChangeTriggerTypes
2024-05-25 17:19:03 +02:00
renovate[bot]
7926aafae7 Update aboutlibraries to v11.2.0 2024-05-25 11:29:09 +02:00
renovate[bot]
e9d0f856d2 Update github/codeql-action action to v3.25.6 2024-05-25 11:28:45 +02:00
renovate[bot]
8f9e6cf58b Update dependency com.android.tools.build:gradle to v8.4.1 2024-05-25 11:28:35 +02:00
DJSweder
76ba6bc528 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/
2024-05-25 04:41:26 -04:00
DJSweder
e9ba530272 Translated using Weblate (Czech)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-05-25 04:41:26 -04:00
Niels van Velzen
50b70a5269 Remove unused "Offline" item location 2024-05-22 20:52:44 +02:00
Jack Govier
50e812f946 Translated using Weblate (Welsh)
Currently translated at 27.8% (146 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cy/
2024-05-21 15:41:26 -04:00
Blackspirits
78f6c98edd Translated using Weblate (Portuguese (Portugal))
Currently translated at 83.8% (440 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-05-21 15:41:26 -04:00
Blackspirits
92552fcbdb Translated using Weblate (Portuguese (Portugal))
Currently translated at 81.9% (430 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-05-21 12:41:26 -04:00
Jorge
759d56b7f1 Translated using Weblate (Portuguese (Portugal))
Currently translated at 76.9% (404 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-05-21 09:41:26 -04:00
Niels van Velzen
135ccac2bf Use SDK for API calls in FullDetailsFragment 2024-05-20 15:24:16 +02:00
Niels van Velzen
88e8a28f9e Use SDK types in FullDetailsFragment 2024-05-20 15:24:16 +02:00
Niels van Velzen
a953860d64 Remove ApiClientPlaybackHelper 2024-05-19 16:42:30 +02:00
Niels van Velzen
03ac5b61cb Add context to PlaybackHelper.getItemsToPlay 2024-05-19 16:42:30 +02:00
Niels van Velzen
a46aa958a1 Add SdkPlaybackHelper 2024-05-19 16:42:30 +02:00
Niels van Velzen
faddfd15a0 Remove unused code branch in BrowseViewFragment 2024-05-19 16:27:50 +02:00
Niels van Velzen
0971d8918e Remove ImageUtils 2024-05-19 16:27:29 +02:00
Niels van Velzen
53f2c6681d Move constants and remaining functions to ImageHelper 2024-05-19 16:27:29 +02:00
Niels van Velzen
2df9e3dda4 Update remaining ImageUtils functions to use SDK for image URL construction 2024-05-19 16:27:29 +02:00
renovate[bot]
70b07d1ceb Update kotest to v5.9.0 2024-05-18 22:08:41 +02:00
renovate[bot]
2d1dba654b Update CI dependencies 2024-05-18 22:08:27 +02:00
renovate[bot]
a4cf654d6b Update dependency io.mockk:mockk to v1.13.11 2024-05-18 22:08:18 +02:00
Niels van Velzen
419c01babe Add interface for PlaybackHelper 2024-05-18 18:52:04 +02:00
Niels van Velzen
7bf5998f6c Refactor PlaybackHelper to use dependency injection 2024-05-18 18:52:04 +02:00
Niels van Velzen
5d0a677265 Fix "Add server" button not visible when there are too many servers 2024-05-16 22:15:13 +02:00
Niels van Velzen
dc8f97c71b Remove FakeBaseItem.SERIES_TIMERS 2024-05-16 22:07:36 +02:00
Niels van Velzen
c8850147a2 Formatting fixes 2024-05-16 22:07:36 +02:00
Niels van Velzen
a22f976d30 Remove FakeBaseItem.FAV_SONGS 2024-05-16 22:07:36 +02:00
Niels van Velzen
725b5d3dd8 Split MusicFavoritesListFragment from ItemListFragment 2024-05-16 22:07:36 +02:00
Niels van Velzen
e8bb6811b7 Make FakeBaseItem ids private (access via BaseItemDto) 2024-05-16 22:07:36 +02:00
Niels van Velzen
f7722584c5 Fully migrate ItemListFragment to use SDK 2024-05-16 20:49:45 +02:00
renovate[bot]
b6022bc133 Update dependency androidx.compose.compiler:compiler to v1.5.14 2024-05-15 19:09:13 +02:00
renovate[bot]
aee6f26af6 Update Kotlin 2024-05-15 19:09:04 +02:00
Niels van Velzen
9249c91321 Use non-ktx versions for lifecycle dependencies 2024-05-15 19:08:44 +02:00
renovate[bot]
2fc5cecb76 Update androidx.lifecycle to v2.8.0 2024-05-15 19:08:44 +02:00
renovate[bot]
d212572df8 Update androidx.fragment to v1.7.1 2024-05-15 19:02:13 +02:00
Juho Han
1b7f4de4c9 Translated using Weblate (Korean)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ko/
2024-05-14 11:41:27 -04:00
Niels van Velzen
5db06f1390 Remove unused methods from StreamHelper 2024-05-14 16:39:38 +02:00
Niels van Velzen
51d1dc9a3e Use dependency injection for ReportingHelper 2024-05-14 16:39:38 +02:00
Niels van Velzen
d3a35875df Use dependency injection for KeyProcessor 2024-05-14 16:39:38 +02:00
Niels van Velzen
128a212027 Move addBlockText to ProgramGridCell 2024-05-14 16:39:38 +02:00
Niels van Velzen
bda1514b8f Use dependency injection for ItemLauncher 2024-05-14 16:39:38 +02:00
triwira
8b8ae207ee Translated using Weblate (Indonesian)
Currently translated at 68.0% (357 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/id/
2024-05-13 06:41:25 -04:00
Niels van Velzen
25a24801d6 Update to Kotlin SDK 1.5.0 (Jellyfin 10.9) 2024-05-12 22:03:34 +02:00
Oatavandi
369f183a2d Translated using Weblate (Tamil)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ta/
2024-05-11 12:41:25 -04:00
Niels van Velzen
45d0d18223 Add media source selection 2024-05-11 17:31:45 +02:00
Niels van Velzen
d48c1e6517 Add initial video codec support code 2024-05-11 17:24:11 +02:00
Niels van Velzen
4c95258fdf Add initial subtitle implementation 2024-05-10 19:15:00 +02:00
Niels van Velzen
54f575d838 Use screensaverLock in PlaybackRewriteFragment 2024-05-10 19:15:00 +02:00
Niels van Velzen
2d0369a5f4 Add basic lifecycle events to PlaybackRewriteFragment 2024-05-10 19:15:00 +02:00
Niels van Velzen
050702716a Remove interceptPlayRequest 2024-05-10 19:15:00 +02:00
Niels van Velzen
79ce9e3ca4 Read items to play from legacy video queue 2024-05-10 19:15:00 +02:00
Niels van Velzen
0b47d5d7c8 Replace PlaybackForwardingActivity with PlaybackRewriteFragment 2024-05-07 21:21:23 +02:00
Niels van Velzen
6942d92590 Fix screensaver and media manager displaying video entries 2024-05-07 21:17:57 +02:00
Niels van Velzen
59153d47fe Add VideoMediaStreamResolver 2024-05-07 15:41:55 +02:00
Niels van Velzen
84a16804fa Update PlaybackForwardingActivity to show video output 2024-05-07 15:41:55 +02:00
Niels van Velzen
359a9b8899 Fix typo in AudioMediaStreamResolver 2024-05-07 15:41:55 +02:00
Niels van Velzen
50212a1a00 Add hls dependency to media3 module 2024-05-07 15:41:55 +02:00
Niels van Velzen
7d0c7c441e Fix PlaybackLauncher not converting item id to string 2024-05-07 15:41:55 +02:00
Niels van Velzen
1da4a8e69b Check for mediaType in AudioMediaStreamResolver 2024-05-07 15:41:55 +02:00
Niels van Velzen
a0f8ed343d Add PlayerSurfaceView to display video output 2024-05-07 15:41:55 +02:00
musenq
fc5940ec1d Translated using Weblate (Japanese)
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/ja/
2024-05-06 10:41:26 -04:00
musenq
4b8d7a0680 Translated using Weblate (Japanese)
Currently translated at 72.9% (383 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ja/
2024-05-06 10:41:25 -04:00
Vincent_lok
9844882ef1 Translated using Weblate (Chinese (Traditional))
Currently translated at 97.3% (511 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hant/
2024-05-06 03:21:26 -04:00
Weblate
d514e55e11 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/
2024-05-06 01:23:13 -04:00
Vincent_lok
60bab91aab Translated using Weblate (Slovenian)
Currently translated at 94.6% (497 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sl/
2024-05-06 01:23:13 -04:00
Vincent_lok
32d49e7167 Translated using Weblate (Chinese (Traditional))
Currently translated at 96.3% (506 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hant/
2024-05-06 01:23:12 -04:00
Vincent_lok
02a8050528 Translated using Weblate (Chinese (Traditional))
Currently translated at 93.3% (490 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hant/
2024-05-06 00:20:11 -04:00
Niels van Velzen
8ed8ddbde3 Cache computed PlayableMediaStream in QueueEntry element 2024-05-05 22:34:07 +02:00
Tommy
e1a3c50adc Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2024-05-05 13:38:06 -04:00
Niels van Velzen
01642e644c Refactor QueueEntry design 2024-05-05 16:30:43 +02:00
Niels van Velzen
73626e185e Force Quick Connect code to use LTR text direction 2024-05-05 11:15:50 +02:00
Niels van Velzen
e596cf80a0 Use LocalDateTime in TimeUtils 2024-05-04 16:22:31 +02:00
Niels van Velzen
5d17b3cf33 Remove unused TimeUtils 2024-05-04 16:22:31 +02:00
Niels van Velzen
f5b3d5f20c Replace java.util.Calendar in some places 2024-05-04 16:22:31 +02:00
Niels van Velzen
b4440300af Use Instant in DataRefreshService 2024-05-04 16:22:31 +02:00
Niels van Velzen
070b9b2cc3 Prefer Instant.now().toEpochMilli() over System.getTimeMillis() 2024-05-04 16:22:31 +02:00
Niels van Velzen
912f4808e2 Migrate from java.util.Date to java.time 2024-05-04 16:22:31 +02:00
Niels van Velzen
723e83887d Set view composition strategy in DreamServiceCompat 2024-05-04 16:22:09 +02:00
Niels van Velzen
5ef7bbc573 Use androidx fragment-compose 2024-05-04 16:22:09 +02:00
renovate[bot]
c539b1da17 fix(deps): update dependency com.android.tools.build:gradle to v8.4.0 2024-05-04 13:36:18 +02:00
Szilágyi Kristóf
5f4ee497a5 Translated using Weblate (Hungarian)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2024-05-04 06:41:25 -04:00
renovate[bot]
07e833d8df fix(deps): update dependency androidx.fragment:fragment-ktx to v1.7.0 2024-05-04 12:18:25 +02:00
renovate[bot]
cd13187cf4 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.7 2024-05-04 12:16:08 +02:00
renovate[bot]
190310a24d fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.7 2024-05-04 12:16:00 +02:00
renovate[bot]
6d19682e78 chore(deps): update aboutlibraries to v11.1.4 2024-05-04 12:12:51 +02:00
renovate[bot]
af77ccbad0 fix(deps): update dependency androidx.core:core-ktx to v1.13.1 2024-05-04 12:12:13 +02:00
renovate[bot]
67c654ab4e fix(deps): update dependency androidx.compose.material:material to v1.6.7 2024-05-04 12:12:05 +02:00
renovate[bot]
2a91691476 fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.13 2024-05-04 12:11:50 +02:00
Andi Chandler
ce7397a7c4 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-05-03 18:41:25 -04:00
Kityn
3cb22b0c8f Translated using Weblate (Polish)
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/pl/
2024-05-01 09:14:41 -04:00
cfsima
758fbcb207 Translated using Weblate (Swedish)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-04-29 21:15:34 -04:00
cfsima
20ea9e59f1 Translated using Weblate (Swedish)
Currently translated at 98.2% (516 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-04-29 19:25:46 -04:00
renovate[bot]
902c826844 chore(deps): update ci dependencies 2024-04-29 20:04:40 +02:00
Gian Klug
675ad8d536 Translated using Weblate (German)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-04-28 11:52:12 -04:00
miguelsvieira
80107acf2c Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2024-04-28 00:07:02 -04:00
無情天
7fded02c76 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-04-26 19:38:04 -04:00
DJSweder
5915d5c956 Translated using Weblate (Czech)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-04-26 10:36:29 -04:00
nextlooper42
7bab2a064e Translated using Weblate (Slovak)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-04-26 07:53:07 -04:00
GeorgeAL78
a86e1feb98 Translated using Weblate (Albanian)
Currently translated at 10.8% (57 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sq/
2024-04-25 09:23:24 -04:00
renovate[bot]
c059a13cf3 fix(deps): update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.3.1+2 2024-04-25 07:38:34 +02:00
FrankJeager
5748e35905 Translated using Weblate (French)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-04-24 17:38:04 -04:00
Troja
589d5e951f Translated using Weblate (Belarusian)
Currently translated at 24.1% (127 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/be/
2024-04-23 09:05:16 -04:00
ligang945
bc4fcb088f Translated using Weblate (Chinese (Simplified))
Currently translated at 98.4% (517 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-04-23 05:25:40 -04:00
queeup
1c9952f414 Translated using Weblate (Turkish)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-04-21 13:19:39 -04:00
queeup
0416997e15 Translated using Weblate (Turkish)
Currently translated at 99.8% (524 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-04-21 13:00:42 -04:00
Blackspirits
d6e7c48b65 Translated using Weblate (Portuguese (Portugal))
Currently translated at 76.9% (404 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-04-21 12:38:04 -04:00
Víctor Vázquez
be3bda90e2 Translated using Weblate (Spanish)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-04-21 08:28:08 -04:00
bene toffix
f1f6c9aacd Translated using Weblate (Catalan)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-04-20 04:56:23 -04:00
KarlderHeinz
940d41322e Translated using Weblate (German)
Currently translated at 99.6% (523 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-04-19 17:46:41 -04:00
stanol
47c96fd189 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-04-19 16:38:04 -04:00
Bas
6d6a76e4f5 Translated using Weblate (Dutch)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-04-19 12:54:40 -04:00
nextlooper42
31f8176aad Translated using Weblate (Slovak)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-04-19 09:38:04 -04:00
vic991
3e863bbe02 Translated using Weblate (Swedish)
Currently translated at 97.1% (510 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-04-18 22:38:03 -04:00
Filip Bryłka
b031041048 Translated using Weblate (Polish)
Currently translated at 100.0% (525 of 525 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-04-18 18:38:04 -04:00
renovate[bot]
c108df7e05 chore(deps): update gradle/actions action to v3.3.1 2024-04-18 22:20:26 +02:00
Niels van Velzen
a086a2d241 Fix getAllChannelsIndex always returning -1 2024-04-18 22:12:03 +02:00
Niels van Velzen
9c068290bb Fix channel adapter not using string resource 2024-04-18 22:12:03 +02:00
Niels van Velzen
17f16208fd Fix Live TV channels refreshing to infinity 2024-04-18 22:12:03 +02:00
Niels van Velzen
e6b990cd42 Remove leakcanary 2024-04-18 22:09:09 +02:00
Niels van Velzen
a9928fb51e Add preferences for screensaver age rating 2024-04-18 21:49:15 +02:00
renovate[bot]
3b524ccd4b fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.12 2024-04-18 21:48:58 +02:00
renovate[bot]
40d2f81cac fix(deps): update dependency androidx.compose.material:material to v1.6.6 2024-04-18 21:48:45 +02:00
renovate[bot]
e5ce29eb62 chore(deps): update ci dependencies 2024-04-18 21:48:11 +02:00
renovate[bot]
6ef67c8961 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.6 2024-04-18 21:44:12 +02:00
renovate[bot]
314af7200d fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.6 2024-04-18 21:44:02 +02:00
Niels van Velzen
424259aa4d Migrate from androidx.activity-ktx to androidx.activity 2024-04-18 21:43:29 +02:00
renovate[bot]
dc0962c2ed fix(deps): update dependency androidx.activity:activity-ktx to v1.9.0 2024-04-18 21:43:29 +02:00
renovate[bot]
ec32ab4e6f fix(deps): update dependency androidx.core:core-ktx to v1.13.0 2024-04-18 21:38:33 +02:00
Alan Azar
0e142ce33b Translated using Weblate (Lithuanian)
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/lt/
2024-04-18 15:38:04 -04:00
Alan Azar
d0191dac89 Translated using Weblate (Indonesian)
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/id/
2024-04-18 15:38:04 -04:00
Alan Azar
272a12cadb Translated using Weblate (Indonesian)
Currently translated at 68.5% (355 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/id/
2024-04-18 15:38:04 -04:00
Blackspirits
3460aca9d9 Translated using Weblate (Portuguese (Portugal))
Currently translated at 76.6% (397 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-04-17 15:00:29 -04:00
annorberg98
c58e44a940 Translated using Weblate (Swedish)
Currently translated at 97.2% (504 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-04-16 13:38:04 -04:00
renovate[bot]
94ca41bae0 fix(deps): update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.3.1+1 2024-04-16 08:47:09 +02:00
nextlooper42
58f6a9f295 Translated using Weblate (Slovak)
Currently translated at 100.0% (518 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-04-15 08:16:13 -04:00
Niels van Velzen
3ae6ff2709 Remove PlayAccess checks 2024-04-14 16:33:43 +02:00
Niels van Velzen
d6a4586dfb Migrate to gradle/actions/wrapper-validation 2024-04-14 10:04:43 +02:00
renovate[bot]
6d9ee9ff74 chore(deps): update gradle/wrapper-validation-action action to v3 2024-04-14 10:04:43 +02:00
renovate[bot]
837123b11a chore(deps): update gradle/actions action to v3.3.0 2024-04-14 10:03:06 +02:00
renovate[bot]
c7b55fbb19 fix(deps): update koin to v3.5.6 2024-04-14 10:01:34 +02:00
Bas
408526c9e6 Translated using Weblate (Dutch)
Currently translated at 100.0% (518 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-04-13 08:38:03 -04:00
newton181
ebf0c72c8b Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (518 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2024-04-12 16:38:04 -04:00
newton181
664a86dd2c Translated using Weblate (Spanish)
Currently translated at 100.0% (518 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-04-12 16:38:03 -04:00
stanol
aa6c81df26 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (518 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-04-12 11:38:03 -04:00
Kityn
cae496d60c Translated using Weblate (Polish)
Currently translated at 100.0% (518 of 518 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-04-11 19:11:18 -04:00
renovate[bot]
e0abc4fe0a fix(deps): update koin to v3.5.5 2024-04-11 21:40:17 +02:00
Thomas Erbe
d7b5689057 Add home button when away from home view (#3262)
* Add home button when away from home view

* * Create a custom title view to replace Android Leanback's default
* Move clock view rendering to the new view instead of in the Browse Folder Fragment

* * Fix the now playing screen scrolling down when focusing the home button
* Add some spacing between the folder view title and the clock user view
* Add missing line at end of TitleView

* * Add a new GoHome NavigationAction so that the back stack is properly reset and to allow for the back button to quit the app properly after navigating home.
* Fix the artist title overlapping the home button on the music now playing screen.

* * Merge the clear feature of the GoHome action into the NavigateFragment action and remove the GoHome action

* Clean up and small refactoring

---------

Co-authored-by: Thomas Erbe <vizuaalog@gmail.com>
Co-authored-by: Niels van Velzen <git@ndat.nl>
2024-04-11 21:39:25 +02:00
stanol
857a24ea72 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-04-11 10:38:03 -04:00
Kityn
01d8b3ce33 Translated using Weblate (Polish)
Currently translated at 100.0% (517 of 517 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-04-11 01:46:50 -04:00
renovate[bot]
12b07baabf chore(deps): update aboutlibraries to v11.1.3 2024-04-10 21:50:35 +02:00
renovate[bot]
e6eb0adc34 chore(deps): update gradle/wrapper-validation-action action to v2.1.3 2024-04-10 21:50:12 +02:00
renovate[bot]
2cdfc55bd6 fix(deps): update dependency com.android.tools.build:gradle to v8.3.2 2024-04-10 21:49:55 +02:00
Niels van Velzen
1675bb648d Split playback preferences into normal and advanced screens 2024-04-10 21:49:36 +02:00
Awildidiot
f277ccf1f7 Translated using Weblate (German)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-04-09 15:38:03 -04:00
renovate[bot]
e36268712b fix(deps): update androidx.media3 to v1.3.1 2024-04-09 19:12:22 +02:00
Blackspirits
673f50440d Translated using Weblate (Portuguese (Portugal))
Currently translated at 76.5% (395 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-04-09 09:38:03 -04:00
zeskeertwee
d1233b9423 Translated using Weblate (Dutch)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-04-09 09:38:03 -04:00
stanol
fa57cbf1d5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-04-09 06:38:03 -04:00
ROYJohan08
6c314b8b30 Translated using Weblate (French)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-04-09 06:38:03 -04:00
renovate[bot]
529d17d62d chore(deps): update gradle/actions action to v3.2.1 2024-04-09 08:52:26 +02:00
Niels van Velzen
00059d544f Safely convert string to uuid in java files 2024-04-09 08:51:55 +02:00
Niels van Velzen
d09af32f6b Use UUID for other parameters 2024-04-09 08:51:55 +02:00
Niels van Velzen
d8f2e9f070 Use UUID for "id" parameters 2024-04-09 08:51:55 +02:00
Niels van Velzen
9fd84b97db Use UUID for "itemId" parameters 2024-04-09 08:51:55 +02:00
Niels van Velzen
9d8e23d7a9 Remove non existent "SeriesTimers" collection type 2024-04-09 08:44:21 +02:00
Butter
c4d096cd2a Translated using Weblate (Thai)
Currently translated at 25.0% (1 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/th/
2024-04-08 20:38:02 -04:00
bene toffix
63bce6f0c2 Translated using Weblate (Catalan)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-04-08 15:38:03 -04:00
Johnny Finglish
1cb0757a8e Translated using Weblate (Finnish)
Currently translated at 95.9% (495 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2024-04-08 15:38:03 -04:00
queeup
796830ed7d Translated using Weblate (Turkish)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-04-08 08:38:03 -04:00
nextlooper42
30cc171309 Translated using Weblate (Slovak)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-04-07 21:38:03 -04:00
Kityn
eb0e429cf4 Translated using Weblate (Polish)
Currently translated at 100.0% (516 of 516 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-04-07 18:38:02 -04:00
Niels van Velzen
9f90bf7c33 USe Timber.e instead of Exception.printStackTrace 2024-04-07 21:25:18 +02:00
queeup
bb5ea9be8b Translated using Weblate (Turkish)
Currently translated at 100.0% (515 of 515 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-04-07 14:35:05 -04:00
Niels van Velzen
138ae58555 Update SDK to 1.4.7 (10.8 with enum backports) 2024-04-07 20:34:57 +02:00
Kityn
e1d98dfbc0 Translated using Weblate (Polish)
Currently translated at 100.0% (515 of 515 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-04-07 08:38:03 -04:00
stanol
07364b950b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (515 of 515 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-04-06 19:38:03 -04:00
throny
c4ca734842 Translated using Weblate (German)
Currently translated at 100.0% (515 of 515 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-04-06 19:38:02 -04:00
Niels van Velzen
84e262fe2c Replace viewModel() with koinViewModel() 2024-04-06 21:35:37 +02:00
renovate[bot]
05b2453df1 fix(deps): update koin to v3.5.4 2024-04-06 21:35:37 +02:00
renovate[bot]
50e5f24464 fix(deps): update dependency androidx.compose.material:material to v1.6.5 2024-04-06 21:27:43 +02:00
renovate[bot]
0b3b843b23 chore(deps): update ci dependencies 2024-04-06 21:21:22 +02:00
renovate[bot]
20177183ae chore(deps): update dependency io.gitlab.arturbosch.detekt to v1.23.6 2024-04-06 21:21:07 +02:00
renovate[bot]
e0b1e60def fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.5 2024-04-06 21:20:42 +02:00
renovate[bot]
1d6610eaa7 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.5 2024-04-06 21:20:30 +02:00
Niels van Velzen
dbde98ac50 Fix app_logo cutoff in some cases 2024-04-06 21:19:57 +02:00
Niels van Velzen
a13babee94 Add button to pick a random item to the smart screen
Works for movies, tv series and music (random album)
2024-04-06 17:21:06 +02:00
alison2033
971ad74f62 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2024-04-05 19:38:03 -04:00
Stefan Kolevski
cec0a64dbc Translated using Weblate (Macedonian)
Currently translated at 99.6% (512 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/mk/
2024-04-03 10:38:02 -04:00
John Lester Saclot
87e6c78564 Translated using Weblate (Filipino)
Currently translated at 3.3% (17 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fil/
2024-04-03 02:36:55 -04:00
Stefan Kolevski
4656f8cf53 Translated using Weblate (Macedonian)
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/mk/
2024-04-01 12:38:03 -04:00
Stefan Kolevski
ede43bdfb2 Translated using Weblate (Macedonian)
Currently translated at 99.6% (512 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/mk/
2024-04-01 12:38:03 -04:00
Samuel Kouba
d56acedd08 Translated using Weblate (Czech)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-03-31 20:38:02 -04:00
Oatavandi
a2e8386f39 Translated using Weblate (Tamil)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ta/
2024-03-31 17:38:02 -04:00
nextlooper42
f91f6b92c9 Translated using Weblate (Slovak)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-03-31 11:38:02 -04:00
Niels van Velzen
d22ab694e2 Check if fragment is null in PlaybackController.play (#3475)
* Check if fragment is null in PlaybackController.play

* Update logging levels in PlaybackController
2024-03-30 21:24:11 +01:00
nextlooper42
f89343e8b0 Translated using Weblate (Slovak)
Currently translated at 96.8% (498 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-03-30 13:38:02 -04:00
Stjepan Mrgnaić
6dac3ed0e1 Translated using Weblate (Croatian)
Currently translated at 80.3% (413 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hr/
2024-03-28 13:38:02 -04:00
Bas
be78e7685c Translated using Weblate (Dutch)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-03-24 16:38:01 -04:00
Kityn
517cd5e0f3 Translated using Weblate (Polish)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-03-24 11:38:01 -04:00
renovate[bot]
1a1c932b05 chore(deps): update eps1lon/actions-label-merge-conflict action to v3 2024-03-23 19:07:59 +01:00
renovate[bot]
090f53f917 chore(deps): update aboutlibraries to v11 2024-03-23 16:26:44 +01:00
renovate[bot]
320778732e fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.4 2024-03-23 16:25:18 +01:00
renovate[bot]
68acbb52e5 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.4 2024-03-23 16:24:41 +01:00
MichaelR416
4ba9e18cfa Enable Vorbis audio in video direct play 2024-03-23 16:19:53 +01:00
renovate[bot]
82ef53785b fix(deps): update dependency com.vanniktech:blurhash to v0.3.0 2024-03-23 16:18:56 +01:00
renovate[bot]
09a43d9ec3 chore(deps): update ci dependencies 2024-03-23 16:17:36 +01:00
renovate[bot]
cfeb5f37cd fix(deps): update dependency androidx.compose.material:material to v1.6.4 2024-03-23 16:14:15 +01:00
renovate[bot]
3381689b13 chore(deps): update dependency gradle to v8.7 2024-03-23 16:10:32 +01:00
renovate[bot]
65725657f2 fix(deps): update dependency com.android.tools.build:gradle to v8.3.1 2024-03-23 16:10:23 +01:00
renovate[bot]
8309968440 fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.11 2024-03-23 16:09:56 +01:00
renovate[bot]
9edca009ac chore(deps): update kotlin to v1.9.23 2024-03-23 16:09:50 +01:00
bene toffix
580b62c8c0 Translated using Weblate (Catalan)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-03-22 12:38:01 -04:00
John MacJelly
327e76094e Translated using Weblate (Spanish)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-03-22 01:38:01 -04:00
皇甫朝云
74e5189509 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-03-21 04:38:01 -04:00
Dino Martan
5870a3d74a Translated using Weblate (Croatian)
Currently translated at 77.4% (398 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hr/
2024-03-18 22:38:02 -04:00
foXaCe
be826c3a47 Translated using Weblate (French)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-03-16 14:38:01 -04:00
stanol
6fa02af542 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-03-16 10:38:01 -04:00
Fra146
9b1939811f Translated using Weblate (Italian)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2024-03-15 11:38:01 -04:00
stanol
f8f49dab35 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-03-14 11:38:01 -04:00
Niels van Velzen
f906998fdc colors 2024-03-13 08:07:28 +01:00
Niels van Velzen
2784a1bcbb Fix search action ignoring received query 2024-03-12 15:19:46 +01:00
Niels van Velzen
b903bc0897 Update logo sizing in splash screen and screensaver 2024-03-11 19:07:33 +01:00
Niels van Velzen
47282c09b0 Update app_logo to use new logo font 2024-03-11 19:07:33 +01:00
Niels van Velzen
dd5002beac Fix background color for preference list views 2024-03-11 17:58:54 +01:00
Niels van Velzen
07692d83b2 Recreate icon/banner resources 2024-03-11 17:28:34 +01:00
renovate[bot]
9b889556d3 fix(deps): update kotest to v5.8.1 2024-03-11 08:49:59 +01:00
Gnkalk
991a139d70 Translated using Weblate (Persian)
Currently translated at 69.6% (358 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fa/
2024-03-10 13:38:01 -04:00
foXaCe
e1cdbb4f81 Translated using Weblate (French)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-03-10 13:38:00 -04:00
renovate[bot]
f2e802a300 fix(deps): update androidx.media3 to v1.3.0 2024-03-10 17:46:52 +01:00
renovate[bot]
3cbf34c7d8 fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.3 2024-03-10 09:41:53 +01:00
renovate[bot]
1591f89fe3 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.3 2024-03-10 09:41:36 +01:00
renovate[bot]
7bffb6b97a fix(deps): update dependency androidx.compose.material:material to v1.6.3 2024-03-10 09:37:13 +01:00
Niels van Velzen
60e694959c Use fully transparent background during video playback 2024-03-10 09:35:06 +01:00
Zan
a2ec0098ee Translated using Weblate (Hungarian)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2024-03-09 18:38:04 -05:00
Joshua M. Boniface
07a3ef431a Remove a target dir if it exists
Helps with potential tag overwrites should they occur.
2024-03-09 19:52:13 +01:00
Joshua M. Boniface
eba5ef0e03 Fix paths to use incoming and sudo move
Aligns with new permissions model on new repo server.
2024-03-09 19:52:13 +01:00
Joshua M. Boniface
6556c4c02e Update CI to new repository secrets and layout 2024-03-09 18:38:37 +01:00
Blackspirits
9ba83d141f Translated using Weblate (Portuguese (Portugal))
Currently translated at 76.4% (393 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-03-08 23:38:04 -05:00
Blackspirits
a40a2b5cc8 Translated using Weblate (Portuguese (Portugal))
Currently translated at 74.7% (384 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-03-08 20:38:04 -05:00
Bas
a71c2c8cba Translated using Weblate (Dutch)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-03-06 18:38:04 -05:00
foXaCe
2615fa017f Translated using Weblate (French)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-03-05 15:38:04 -05:00
foXaCe
53a045f141 Translated using Weblate (French)
Currently translated at 99.8% (513 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-03-04 09:38:04 -05:00
foXaCe
ac24d88e82 Translated using Weblate (French)
Currently translated at 99.0% (509 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-03-04 03:38:04 -05:00
Niels van Velzen
35f274ff58 Remove non-profile usages of DeviceUtils 2024-03-03 19:55:55 +01:00
renovate[bot]
94c560f19a chore(deps): update ci dependencies 2024-03-02 11:25:43 +01:00
renovate[bot]
d85492aacd fix(deps): update dependency io.mockk:mockk to v1.13.10 2024-03-02 11:25:30 +01:00
queeup
5674a93390 Translated using Weblate (Turkish)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-02-29 18:38:03 -05:00
nalipiaoxiang
06132531b0 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.6% (507 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-02-29 09:37:14 -05:00
mathmahan
2cea38cea9 Translated using Weblate (Swedish)
Currently translated at 97.4% (501 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-02-29 09:37:13 -05:00
Fabian Magard
84569ef55d Translated using Weblate (German)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-02-29 09:37:13 -05:00
Ronan Fitzgerald
79f748b235 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-02-28 18:37:13 -05:00
Kityn
8dde71c91b Translated using Weblate (Polish)
Currently translated at 100.0% (514 of 514 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-02-27 13:37:12 -05:00
Niels van Velzen
3ad5375db4 Add fallthrough to media3 MimeTypes in codec->mime type mapping 2024-02-27 17:13:28 +01:00
Niels van Velzen
e76c56b55d Add "clear image cache" button to developer preferences 2024-02-27 15:30:13 +01:00
Weblate
27de820faf 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/
2024-02-27 09:26:03 -05:00
Niels van Velzen
d892addefd Remove "automatically choose" option from video player preference 2024-02-27 15:25:57 +01:00
Niels van Velzen
a31c84685e Replace deprecated rememberKoinInject with koinInject 2024-02-27 15:25:47 +01:00
DJSweder
bb13a7454a Translated using Weblate (Czech)
Currently translated at 100.0% (513 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2024-02-26 11:37:12 -05:00
queeup
07122dd48c Translated using Weblate (Turkish)
Currently translated at 100.0% (513 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-02-25 07:37:11 -05:00
renovate[bot]
8b7564ec82 chore(deps): update github/codeql-action action to v3.24.5 2024-02-25 13:17:53 +01:00
iNc
f9aeb20458 Translated using Weblate (German)
Currently translated at 75.0% (3 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/de/
2024-02-24 22:37:11 -05:00
Benediktisda
bf6085e235 Translated using Weblate (German)
Currently translated at 100.0% (513 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-02-24 17:37:11 -05:00
renovate[bot]
79d200c27b fix(deps): update coil to v2.6.0 2024-02-24 10:04:45 +01:00
VB2007
6cfdf7fe80 Translated using Weblate (Hungarian)
Currently translated at 97.2% (499 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2024-02-23 14:37:10 -05:00
Awildidiot
180cfa3e5c Translated using Weblate (German)
Currently translated at 98.6% (506 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-02-23 05:37:10 -05:00
Niels van Velzen
efc6be8d3d Fix crash when checking whether a person is a folder or not 2024-02-22 20:19:39 +01:00
stanol
1e058c997a Translated using Weblate (Ukrainian)
Currently translated at 98.0% (503 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-02-22 11:37:10 -05:00
Bas
0a11f9cdd5 Translated using Weblate (Dutch)
Currently translated at 100.0% (513 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-02-22 11:37:10 -05:00
renovate[bot]
e3b0ca36aa fix(deps): update dependency androidx.compose.material:material to v1.6.2 2024-02-22 09:39:47 +01:00
renovate[bot]
4fedf0f13d fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.10 2024-02-22 09:39:37 +01:00
renovate[bot]
d8755c395c fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.2 2024-02-22 09:33:51 +01:00
renovate[bot]
9f2901ecd5 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.2 2024-02-22 09:32:54 +01:00
Niels van Velzen
8b87dafb26 Fix crash in SocketHandler.onPlayMessage when user id is not set 2024-02-21 22:28:39 +01:00
DutchKarma
f23bb7c2be Translated using Weblate (Dutch)
Currently translated at 97.4% (500 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-02-21 10:37:09 -05:00
Kityn
c2c11d40e9 Translated using Weblate (Polish)
Currently translated at 100.0% (513 of 513 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-02-20 16:37:09 -05:00
Niels van Velzen
004d4e06de Rewrite BaseItemInfoRow with Compose (#3379)
* Rewrite BaseItemInfoRow with Compose

* Fix typo

Co-authored-by: Cody Robibero <cody@robibe.ro>

---------

Co-authored-by: Cody Robibero <cody@robibe.ro>
2024-02-20 18:57:12 +01:00
Niels van Velzen
fb25e79233 Fix DPAD_CENTER on screensaver not handled 2024-02-20 18:50:49 +01:00
Niels van Velzen
bd704c4ac9 Replace CoroutineUtils.runBlocking with new CoroutineUtils.runOnLifecycle
This will prevent the main thread from blocking, which in extreme cases can kill the app
2024-02-20 18:50:35 +01:00
Dotun Olatodje
a433920179 Translated using Weblate (French)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-02-20 04:37:09 -05:00
Pedro Lima
e7a43c295b Translated using Weblate (Portuguese (Portugal))
Currently translated at 77.1% (385 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2024-02-19 10:37:08 -05:00
SirH00D
0950a80509 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2024-02-19 06:37:08 -05:00
Lucas Fogolin
0e395353ba Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
2024-02-18 20:37:08 -05:00
Lucas Fogolin
af1d83bd05 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2024-02-18 20:37:08 -05:00
renovate[bot]
80ee21ab1c chore(deps): update ci dependencies 2024-02-17 17:38:21 +01:00
renovate[bot]
fb34fdf0f0 fix(deps): update kotlin 2024-02-17 17:37:53 +01:00
Alexander Welsing
7611fcc188 Translated using Weblate (German)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-02-16 08:37:07 -05:00
Weblate
3da43bfa50 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/
2024-02-14 16:15:07 -05:00
Niels van Velzen
0476a66796 Remove "auto" max bit rate and add additional values 2024-02-14 22:15:02 +01:00
Niels van Velzen
68b578057d Fix crash when updating clock in FullDetailsFragment while item information is unavailable 2024-02-14 22:07:26 +01:00
Cat Tom
c33186509d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-02-13 15:30:39 -05:00
Niels van Velzen
345e20afd9 Specify BUILD_TOOLS_VERSION for r0adkll/sign-android-release step in publish workflow 2024-02-13 13:49:36 +01:00
bene toffix
0a0a3fa0f6 Translated using Weblate (Catalan)
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-02-11 11:30:38 -05:00
Niels van Velzen
5ff02c7e5d Only show PG-13 content in screensaver 2024-02-11 16:09:21 +01:00
BlueToxic
6b317e8360 Translated using Weblate (Swedish)
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-02-11 07:30:38 -05:00
renovate[bot]
25cfd427c9 chore(deps): update gradle/wrapper-validation-action action to v2.1.1 2024-02-11 10:34:12 +01:00
MichaelR416
d5c5bcc41c Add if else statement for audio downmixing support 2024-02-11 10:33:48 +01:00
hatsiko panado
6c14b9d0b2 Translated using Weblate (Filipino)
Currently translated at 2.6% (13 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fil/
2024-02-09 04:30:38 -05:00
hatsiko panado
43e058e01e Added translation using Weblate (Filipino) 2024-02-09 02:01:14 -05:00
renovate[bot]
88b1d39bc1 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.1 2024-02-08 20:50:07 +01:00
renovate[bot]
82ab40517b fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.1 2024-02-08 20:50:00 +01:00
renovate[bot]
1532db7a5d fix(deps): update dependency androidx.compose.material:material to v1.6.1 2024-02-08 20:43:21 +01:00
renovate[bot]
d2f0967a27 fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.9 2024-02-08 20:43:11 +01:00
Bas
c28700db9d Translated using Weblate (Dutch)
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-02-07 16:30:37 -05:00
renovate[bot]
49c573d381 chore(deps): update ci dependencies 2024-02-07 19:16:29 +01:00
renovate[bot]
60eac66d90 fix(deps): update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.2.1+1 2024-02-07 19:16:11 +01:00
renovate[bot]
abf46d42f5 fix(deps): update androidx.media3 to v1.2.1 2024-02-07 19:15:53 +01:00
antti202
d206e82837 Translated using Weblate (Estonian)
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2024-02-06 13:30:37 -05:00
queeup
9af6dda60f Translated using Weblate (Turkish)
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-02-06 13:30:37 -05:00
Niels van Velzen
c482d488d4 Use data object when applicable 2024-02-06 07:28:50 +01:00
Andi Chandler
b03cd05bc5 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-02-05 23:30:37 -05:00
mnsrulz
4214e364d9 android tv hls playback direct play fix (#1) 2024-02-05 19:06:25 +01:00
renovate[bot]
667325fec6 chore(deps): update dependency gradle to v8.6 2024-02-04 12:24:27 +01:00
Kityn
dfe37a38ee Translated using Weblate (Polish)
Currently translated at 100.0% (500 of 500 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-02-03 14:30:36 -05:00
Niels van Velzen
1fdaa87d97 Apply review comments 2024-02-03 16:06:12 +01:00
ben
c581047733 Sort by runtime only shows in movie libraries 2024-02-03 16:06:12 +01:00
ben
7ca6ae92d0 Added ability to sort by runtime 2024-02-03 16:06:12 +01:00
Niels van Velzen
b5a3238d02 Migrate to gradle/actions/setup-gradle 2024-02-03 15:09:24 +01:00
renovate[bot]
f65290744f chore(deps): update gradle/gradle-build-action action to v3 2024-02-03 15:09:24 +01:00
Niels van Velzen
b8f74fa116 Fix formatting in InfoLayoutHelper.java 2024-02-03 15:02:31 +01:00
MichaelR416
05cea3bbc4 Merge duplicate methods after parameter removal. 2024-02-03 15:02:31 +01:00
MichaelR416
9ec9de0ba1 Fix media details tag for multiple versions support 2024-02-03 15:02:31 +01:00
renovate[bot]
89c075fc94 chore(deps): update dependency io.gitlab.arturbosch.detekt to v1.23.5 2024-02-03 12:53:10 +01:00
renovate[bot]
62aace820f chore(deps): update github/codeql-action action to v3.24.0 2024-02-03 12:52:48 +01:00
renovate[bot]
3849bb421a chore(deps): update gradle/wrapper-validation-action action to v2 2024-02-03 12:51:15 +01:00
LesDomen
b46f1acc99 Translated using Weblate (Slovenian)
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/sl/
2024-01-29 07:30:34 -05:00
LesDomen
2d4273773a Translated using Weblate (Slovenian)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sl/
2024-01-29 07:30:34 -05:00
Kévin LEBLOND
dae3137ada Translated using Weblate (French)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-01-27 17:30:33 -05:00
Mor
ddcf561aef Translated using Weblate (Hebrew)
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/
2024-01-27 09:30:38 -05:00
antti202
7ff4734be5 Translated using Weblate (Estonian)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2024-01-27 09:30:38 -05:00
Mor
cb8692fb9c Translated using Weblate (Hebrew)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2024-01-27 09:30:33 -05:00
Niels van Velzen
4e203323e5 Remove baseline workaround 2024-01-27 14:04:02 +01:00
renovate[bot]
c644cb2449 fix(deps): update dependency androidx.compose.material:material to v1.6.0 2024-01-27 12:16:46 +01:00
renovate[bot]
16ec3c0e26 fix(deps): update dependency androidx.compose.ui:ui-tooling to v1.6.0 2024-01-27 12:11:59 +01:00
renovate[bot]
c92b806c03 fix(deps): update dependency androidx.compose.foundation:foundation to v1.6.0 2024-01-27 12:11:51 +01:00
renovate[bot]
3392078bc8 chore(deps): update ci dependencies 2024-01-27 12:11:31 +01:00
renovate[bot]
66b5b93b98 fix(deps): update dependency com.android.tools.build:gradle to v8.2.2 2024-01-27 12:11:05 +01:00
Niels van Velzen
d466420858 Update app/src/main/java/org/jellyfin/androidtv/util/profile/MediaCodecCapabilitiesTest.kt 2024-01-27 12:08:13 +01:00
lawadr
f5125e937a Fix up 2024-01-27 12:08:13 +01:00
lawadr
1fb3e0399c Widen support for HEVC Main 10
Previously the HEVC codec profile only flagged support for HEVC Main 10
when MediaCodec advertised its support for level 5 (main tier) or above.
This meant devices that support only levels 4 or 4.1 (such as the
Chromecast with Google TV HD) would transcode HEVC Main 10 content even
when supported.

This change lowers the requirement from level 5 to level 4 and also adds
additional profiles specifically for HEVC Main and Main 10 with
conditions on the level so that levels beyond what is supported by
MediaCodec get transcoded.

The result is that HEVC Main 10 level 5 videos get transcoded on devices
supporting only 4.1, but HEVC Main 10 level 4.1 content plays back
directly.
2024-01-27 12:08:13 +01:00
MichaelR416
17b76e1536 Use LTR for default subtitle direction 2024-01-27 12:05:37 +01:00
antti202
476e991deb Translated using Weblate (Estonian)
Currently translated at 57.5% (287 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/et/
2024-01-26 17:30:37 -05:00
hoanghuy309
a3bbe84558 Translated using Weblate (Vietnamese)
Currently translated at 96.7% (483 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2024-01-26 00:30:33 -05:00
Ahmad Mujahid
701ff6afb5 Translated using Weblate (Arabic)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2024-01-25 09:30:33 -05:00
newton181
9f2a772fa3 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2024-01-24 14:30:32 -05:00
newton181
2e879197b8 Translated using Weblate (Spanish)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2024-01-24 14:30:32 -05:00
queeup
924ae07535 Translated using Weblate (Turkish)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-01-22 15:30:32 -05:00
Abid Hasan
8060a9d9d0 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 24.6% (123 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bn_BD/
2024-01-22 15:30:32 -05:00
Weblate
eac2e14afd 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/
2024-01-21 11:17:16 -05:00
Niels van Velzen
f4f02528ec Simplify MediaManager interface 2024-01-21 17:17:11 +01:00
Weblate
766db51dd7 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/
2024-01-21 09:46:05 -05:00
Niels van Velzen
81b4fcdf78 Remove legacy media manager implementation 2024-01-21 15:45:59 +01:00
renovate[bot]
5497d070a3 chore(deps): update ci dependencies 2024-01-20 17:36:08 +01:00
bene toffix
11fba9274e Translated using Weblate (Catalan)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2024-01-19 10:30:30 -05:00
Kegelcizer
6ee6130f9b Translated using Weblate (Romanian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2024-01-18 10:30:30 -05:00
Andi Chandler
440d1e48f8 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-01-16 22:30:29 -05:00
stanol
a72de76df1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-16 17:30:29 -05:00
Xiyue Deng
a6d3e42672 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-01-16 04:30:29 -05:00
Sebastiano Picchi
df86b9445c Translated using Weblate (Italian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2024-01-15 12:30:29 -05:00
Mikael Fredriksson
e5136fdc55 Translated using Weblate (Swedish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2024-01-14 23:30:29 -05:00
Bas
ccd92845c0 Translated using Weblate (Dutch)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2024-01-14 17:30:29 -05:00
Kityn
8db1db029d Translated using Weblate (Polish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-01-14 13:30:29 -05:00
Weblate
3532bb0c49 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/
2024-01-14 09:44:44 -05:00
Niels van Velzen
cdd9dd6842 Additional clarification 2024-01-14 15:44:39 +01:00
DoggoOfSpeed
63946b06c9 Watched indicator setting string clarity improvements 2024-01-14 15:44:39 +01:00
Niels van Velzen
7ed6a07fd1 Small tweaks to getting the index from messages 2024-01-14 15:39:01 +01:00
Gwilherm Folliot
d1a36f6f22 Impl SET_AUDIO_STREAM_INDEX & SET_SUBTITLE_STREAM_INDEX commands 2024-01-14 15:39:01 +01:00
renovate[bot]
d2d026eec0 chore(deps): update ci dependencies 2024-01-14 15:38:07 +01:00
renovate[bot]
bb9d503b19 fix(deps): update androidx.lifecycle to v2.7.0 2024-01-14 15:37:47 +01:00
renovate[bot]
beb5987948 fix(deps): update dependency io.mockk:mockk to v1.13.9 2024-01-14 15:37:40 +01:00
Mohit Pardeshi
2a77b47fa8 Translated using Weblate (Hindi)
Currently translated at 12.5% (63 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hi/
2024-01-13 12:30:29 -05:00
Vasyl Belynets
f3ddf14de9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-12 20:30:28 -05:00
Vasyl Belynets
5b80360a59 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-12 13:30:28 -05:00
queeup
75981f36e7 Translated using Weblate (Turkish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2024-01-11 16:30:28 -05:00
renovate[bot]
93f6c7cebb fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.8 2024-01-10 21:26:42 +01:00
renovate[bot]
fac11d92d1 chore(deps): update kotlin to v1.9.22 2024-01-10 21:26:30 +01:00
Guy Porat
b7b4de967e Translated using Weblate (Hebrew)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2024-01-10 11:30:27 -05:00
Kevin Fernando Oliva Hernández
ee786a67b5 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2024-01-10 00:30:27 -05:00
Vasyl Belynets
b1d66e930d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-09 11:30:27 -05:00
nextlooper42
a57386677a Translated using Weblate (Slovak)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2024-01-09 11:30:27 -05:00
Paun Luca
06b0209b15 Translated using Weblate (Romanian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ro/
2024-01-08 16:30:27 -05:00
Niels van Velzen
50985bfad8 Show specials for series 2024-01-08 17:59:58 +01:00
Vasyl Belynets
7256152db8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-07 20:30:27 -05:00
Vasyl Belynets
3578ac264c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-07 15:30:26 -05:00
F F
0a304fd350 Translated using Weblate (Danish)
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/da/
2024-01-07 12:30:27 -05:00
Vasyl Belynets
9a5dbc052f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-07 12:30:27 -05:00
F F
1edd5c3127 Translated using Weblate (Danish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/da/
2024-01-07 12:30:26 -05:00
renovate[bot]
492c6060a8 chore(deps): update appleboy/ssh-action action to v1.0.3 2024-01-07 17:18:17 +01:00
Niels van Velzen
706a05eea0 Show specials for seasons 2024-01-07 17:17:55 +01:00
Leo
8ee9b43e8d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2024-01-06 23:30:26 -05:00
Vasyl Belynets
1a04779a05 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-06 19:30:27 -05:00
K0RR
43860a4d37 Translated using Weblate (Polish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2024-01-06 13:30:26 -05:00
Niels van Velzen
d721482127 Defer home initialization until user info is available 2024-01-06 13:49:40 +01:00
Vasyl Belynets
b4af6eb98f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-06 06:30:26 -05:00
Gwilherm Folliot
6867c319bb Fix crash when quitting player after two plays
If you play a title from a device and play another without quitting the player and then quit the player the app crash.

Its because the NavigationRepository stacks two players and when coming to the last one, the app crash because the last player is terminated.

My fix just replace the last destination if an fragment is active

java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.activity.OnBackPressedDispatcher.addCallback, parameter onBackPressedCallback
	at androidx.activity.OnBackPressedDispatcher.addCallback(Unknown Source:3)
	at org.jellyfin.androidtv.ui.playback.CustomPlaybackOverlayFragment.onActivityCreated(CustomPlaybackOverlayFragment.java:346)
	at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:3156)
	at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:619)
	at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:275)
	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1934)
	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1839)
	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1782)
	at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:565)
	at android.os.Handler.handleCallback(Handler.java:942)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loopOnce(Looper.java:201)
	at android.os.Looper.loop(Looper.java:288)
	at android.app.ActivityThread.main(ActivityThread.java:7884)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
2024-01-05 21:21:40 +01:00
Niels van Velzen
1fbc19740b Disable crossfading for CardLegacyImageView to fix scaling issue 2024-01-05 19:43:34 +01:00
renovate[bot]
11b5affdd9 chore(deps): update aboutlibraries to v10.10.0 2024-01-05 19:41:39 +01:00
renovate[bot]
41256f77e2 chore(deps): update appleboy/ssh-action action to v1.0.2 2024-01-05 19:41:21 +01:00
renovate[bot]
ea916c23e4 fix(deps): update dependency com.android.tools.build:gradle to v8.2.1 2024-01-05 19:41:04 +01:00
renovate[bot]
2e8b4db01b fix(deps): update dependency com.squareup.leakcanary:leakcanary-android to v2.13 2024-01-05 19:40:51 +01:00
Gwilherm Folliot
0116530794 Fix crash when DISPLAY_CONTENT while playing
When a video is playing and on another device you browse libraries on a casting device it cause the app to crash

The onDisplayContent method is trying to read ExoPlayer state but from the socket thread and not the main thread

java.lang.IllegalStateException: Player is accessed on the wrong thread.
Current thread: 'DefaultDispatcher-worker-4'
Expected thread: 'main'
See https://developer.android.com/guide/topics/media/issues/player-accessed-on-wrong-thread
	at androidx.media3.exoplayer.ExoPlayerImpl.verifyApplicationThread(ExoPlayerImpl.java:2842)
	at androidx.media3.exoplayer.ExoPlayerImpl.getPlaybackState(ExoPlayerImpl.java:514)
	at androidx.media3.common.BasePlayer.isPlaying(BasePlayer.java:124)
	at org.jellyfin.androidtv.ui.playback.VideoManager.isPlaying(VideoManager.java:341)
	at org.jellyfin.androidtv.ui.playback.PlaybackController.isPlaying(PlaybackController.java:262)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler.onDisplayContent(SocketHandler.kt:184)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler.lambda$13$lambda$8(SocketHandler.kt:103)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler$$ExternalSyntheticLambda3.onReceive(Unknown Source:4)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler$_init_$lambda$13$$inlined$addGeneralCommandsListener$default$1.onReceive(ListenerRegistrationExtensions.kt:68)
	at org.jellyfin.sdk.api.sockets.helper.ListenerHelper.forwardMessage(ListenerHelper.kt:37)
	at org.jellyfin.sdk.api.sockets.SocketInstance.forwardMessage(SocketInstance.kt:283)
	at org.jellyfin.sdk.api.sockets.SocketInstance.access$forwardMessage(SocketInstance.kt:35)
	at org.jellyfin.sdk.api.sockets.SocketInstance$connectAndBind$2.invokeSuspend(SocketInstance.kt:220)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
2024-01-05 19:39:25 +01:00
Vasyl Belynets
fe168d46af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-05 13:30:26 -05:00
Vasyl Belynets
a2956773c9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2024-01-05 05:52:13 -05:00
MadaxDeLuXe
dc48347a99 Translated using Weblate (German)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2024-01-05 03:30:26 -05:00
Zoe
0bf24c25f9 Translated using Weblate (Norwegian Bokmål)
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/nb_NO/
2024-01-04 20:30:26 -05:00
Zoe
8406bad80c Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2024-01-04 20:30:26 -05:00
Новоженин Александр
27a5f3923d Translated using Weblate (Russian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2024-01-04 16:30:25 -05:00
Yofi-FR
10ac85c08e Translated using Weblate (French)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-01-03 12:30:25 -05:00
tan
da99fbd1d8 Translated using Weblate (Thai)
Currently translated at 39.4% (198 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/th/
2024-01-02 13:30:27 -05:00
Sebastiano Picchi
7cf5d8d5ae Translated using Weblate (Italian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2024-01-02 13:30:25 -05:00
Thomas Schwery
b8b7b2a9fb Translated using Weblate (French)
Currently translated at 99.8% (501 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2024-01-02 08:30:25 -05:00
Andi Chandler
0e14ce9013 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2024-01-01 18:30:25 -05:00
bene toffix
90799f6a6a Translated using Weblate (Catalan)
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/ca/
2024-01-01 06:30:24 -05:00
bene toffix
3e40859f6d Translated using Weblate (Catalan)
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/ca/
2023-12-31 18:30:25 -05:00
bene toffix
3ac0baf653 Translated using Weblate (Catalan)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2023-12-31 18:30:24 -05:00
Retrial
823af2c94a Translated using Weblate (Greek)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2023-12-31 14:30:24 -05:00
stanol
7198df297f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2023-12-30 09:30:24 -05:00
Csaba
fdc3586008 Translated using Weblate (Hungarian)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2023-12-30 09:30:24 -05:00
Oskari Lavinto
fb44030d83 Translated using Weblate (Finnish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2023-12-29 23:30:24 -05:00
Bas
a16959d167 Translated using Weblate (Dutch)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2023-12-29 18:30:24 -05:00
Kityn
27b1d671a3 Translated using Weblate (Polish)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2023-12-29 12:30:24 -05:00
Lukáš Kucharczyk
9c97a63a43 Translated using Weblate (Czech)
Currently translated at 100.0% (502 of 502 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2023-12-29 12:30:24 -05:00
renovate[bot]
deb29c22b4 chore(deps): update ci dependencies 2023-12-29 15:26:41 +01:00
Niels van Velzen
76acd17d81 Set TsExtractor TimestampSearchBytes parameter based on device memory capabilities 2023-12-29 15:26:19 +01:00
Niels van Velzen
e646192196 Enable tunneling in VideoManager 2023-12-29 15:26:19 +01:00
Niels van Velzen
1f0345f16f Enable tunneling in ExoPlayerBackend 2023-12-29 15:26:19 +01:00
Niels van Velzen
e354d6bee1 Add more item types to search 2023-12-29 15:26:11 +01:00
Niels van Velzen
b91fd1a0f5 Add <queries> element to Manifest for remote trailers 2023-12-29 15:26:01 +01:00
Niels van Velzen
332b6af3da Disable SocketHandler when using RewriteMediaManager 2023-12-29 15:25:53 +01:00
Konstantin Berngard
a0f44ed5a0 Translated using Weblate (Russian)
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/ru/
2023-12-27 21:30:23 -05:00
Konstantin Berngard
9b20fa189c Translated using Weblate (Russian)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2023-12-27 21:30:23 -05:00
Oskari Lavinto
8e960d955f Translated using Weblate (Finnish)
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/fi/
2023-12-27 18:30:23 -05:00
Oskari Lavinto
e72cdf3cbd Translated using Weblate (Finnish)
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/fi/
2023-12-25 12:30:23 -05:00
Niels van Velzen
bd204fdf87 Replace ExoPlayer with androidx.media3 2023-12-24 18:36:20 +01:00
Niels van Velzen
b9e19b2515 Fix crash and inconsistent ordering when using ShuffleOrderIndexProvider 2023-12-24 14:43:34 +01:00
Niels van Velzen
2bbbb93dbf Fix opening collections via search results showing empty screen 2023-12-24 11:09:15 +01:00
renovate[bot]
f248b2b254 fix(deps): update koin to v3.5.3 2023-12-23 21:28:03 +01:00
renovate[bot]
3465e24bbe fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.7 2023-12-23 21:27:48 +01:00
Niels van Velzen
8b43402192 Fix playing audio from search queues entire row 2023-12-23 21:27:19 +01:00
Niels van Velzen
c70f814064 Fix focus issue when pressing back in Fire TV soft keyboard 2023-12-23 21:27:11 +01:00
queeup
1e3abd9ae5 Translated using Weblate (Turkish)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-23 12:30:22 -05:00
BeeTwenty
fdf0fceb28 Translated using Weblate (Norwegian Bokmål)
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/nb_NO/
2023-12-21 07:30:22 -05:00
BeeTwenty
2282c26400 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.3% (496 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nb_NO/
2023-12-21 07:30:21 -05:00
Achim Walz
0637be405d Translated using Weblate (German)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2023-12-19 08:03:17 -05:00
Niels van Velzen
75b6b12147 Fix RewriteMediaManager never used in production builds 2023-12-18 22:19:09 +01:00
Niels van Velzen
aea6e98974 Remove unnecessary OptIn annotations 2023-12-18 22:15:43 +01:00
Yaron Shahrabani
450348311e Translated using Weblate (Hebrew)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2023-12-18 08:30:20 -05:00
queeup
e8af0397a7 Translated using Weblate (Turkish)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-17 19:30:20 -05:00
queeup
a97f287467 Translated using Weblate (Turkish)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-17 15:30:20 -05:00
Niels van Velzen
32849f5279 Fix crash when starting player where playback controller may be null 2023-12-17 15:56:02 +01:00
queeup
0c1aac96d7 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-17 08:30:20 -05:00
queeup
7fefb735d9 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-16 19:30:20 -05:00
queeup
00c59f8e58 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-16 15:30:20 -05:00
renovate[bot]
c87121502c chore(deps): update gradle/gradle-build-action action to v2.11.0 2023-12-15 19:04:04 +01:00
renovate[bot]
636facc8b2 fix(deps): update dependency androidx.activity:activity-ktx to v1.8.2 2023-12-15 19:03:20 +01:00
renovate[bot]
98ac4d3eda chore(deps): update github/codeql-action action to v3 2023-12-15 19:01:31 +01:00
renovate[bot]
d3cd69530a chore(deps): update actions/upload-artifact action to v4 2023-12-15 19:01:06 +01:00
queeup
9937ad3ddd Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-14 19:30:20 -05:00
queeup
9a6fc333a1 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-13 21:30:19 -05:00
xdlivecat
5811b99b35 Translated using Weblate (Croatian)
Currently translated at 77.1% (385 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hr/
2023-12-13 21:30:19 -05:00
queeup
72c97635e3 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-12 18:30:19 -05:00
Alexander Gramiak
afddc96789 util: Don't use TZ offset for media Premiere Date
TimeUtils.getDate internally calls Date.format, which translates the UTC
timestamp into local TZ timestamp. If a user is in a TZ with negative offset,
then the Premiere Date will be presented as a day earlier.

Example with TZ of UTC-6:

item.getPremiereDate()                    => 2023-12-09T00:00
TimeUtils.getDate(item.getPremiereDate()) => Sat Dec 08 18:00:00 CST 2023

Fixes #3190.
2023-12-12 15:27:24 +01:00
guiweber
a8ef52bca9 Update InfoLayoutHelper.java
Keep displaying codec for HDR content
2023-12-12 15:24:59 +01:00
Lukáš Kucharczyk
a0c81df329 Translated using Weblate (Czech)
Currently translated at 100.0% (499 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2023-12-12 08:30:19 -05:00
renovate[bot]
c4db326da6 Update dependency com.vanniktech:blurhash to v0.2.0 2023-12-12 11:53:25 +01:00
queeup
7d53fee677 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-11 17:30:18 -05:00
Niels van Velzen
fa10019f8b Fix rare case where splash screen could get stuck 2023-12-11 14:35:29 +01:00
queeup
b4fbd82c21 Translated using Weblate (Turkish)
Currently translated at 99.5% (497 of 499 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/tr/
2023-12-10 20:30:18 -05:00
renovate[bot]
db6c23877f Update dependency io.gitlab.arturbosch.detekt to v1.23.4 2023-12-10 15:10:20 +01:00
Niels van Velzen
f8fd9765ac Use androidx.work:work-runtime (non ktx version) 2023-12-10 15:10:10 +01:00
renovate[bot]
8b5ad52fb9 Update dependency androidx.work:work-runtime-ktx to v2.9.0 2023-12-10 15:10:10 +01:00
renovate[bot]
90f2623c9a Update dependency androidx.compose.compiler:compiler to v1.5.6 2023-12-10 15:03:37 +01:00
renovate[bot]
9d2dacdac3 Update Kotlin 2023-12-10 15:03:29 +01:00
renovate[bot]
174ed2e281 Update dependency androidx.media3:media3-session to v1.2.0 2023-12-10 15:00:36 +01:00
renovate[bot]
8a0b97619b Update coil to v2.5.0 2023-12-10 14:59:23 +01:00
renovate[bot]
3d2c2c7363 Update kotest to v5.8.0 2023-12-10 14:59:13 +01:00
renovate[bot]
b2fb409605 Update dependency com.android.tools.build:gradle to v8.2.0 2023-12-10 14:58:44 +01:00
renovate[bot]
f14b8ed97b Update actions/stale action to v9 2023-12-10 14:57:12 +01:00
renovate[bot]
92e91ab217 Update actions/setup-java action to v4 2023-12-10 14:57:00 +01:00
renovate[bot]
4dd363b0cf Update CI dependencies 2023-12-10 14:56:15 +01:00
renovate[bot]
a083dfcc04 Update dependency androidx.window:window to v1.2.0 2023-12-10 14:48:39 +01:00
renovate[bot]
d3dba03ee0 Update dependency com.android.tools:desugar_jdk_libs to v2.0.4 2023-12-10 14:45:11 +01:00
renovate[bot]
56d6661906 Update dependency androidx.fragment:fragment-ktx to v1.6.2 2023-12-10 14:44:44 +01:00
renovate[bot]
2fd2efa581 Update dependency androidx.activity:activity-ktx to v1.8.1 2023-12-10 14:43:51 +01:00
renovate[bot]
d866b1bce1 Update aboutlibraries to v10.9.2 2023-12-10 14:42:05 +01:00
renovate[bot]
238a9d27fd Update dependency gradle to v8.5 2023-12-10 14:36:40 +01:00
Niels van Velzen
004b39e89b Close video player on resume with invalid state
In some cases the video player may be stopped when the activity is paused. This ends up crashing the app on resume. Check for this kind of invalid state and close the video player to prevent crashing.
2023-12-10 14:35:52 +01:00
Niels van Velzen
d9eb849038 Fix system screensaver starting during video playback 2023-12-10 14:35:43 +01:00
453 changed files with 13667 additions and 12754 deletions

View File

@@ -2,16 +2,17 @@ root = true
[*]
insert_final_newline = true
max_line_length = 140
trim_trailing_whitespace = true
[{*.kts, *.kt}]
[{*.kts,*.kt}]
charset = utf-8
indent_style = tab
tab_width = 4
# Disable wildcard imports in IntelliJ/Android Studio
ij_kotlin_name_count_to_use_star_import = 1000
ij_kotlin_name_count_to_use_star_import_for_members = 1000
ij_kotlin_packages_to_use_import_on_demand =
ij_kotlin_packages_to_use_import_on_demand = unset
[*.md]
trim_trailing_whitespace = false
trim_trailing_whitespace = false

View File

@@ -15,20 +15,20 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- 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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
with:
name: build-artifacts
retention-days: 14

View File

@@ -17,18 +17,18 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Run detekt and lint tasks
run: ./gradlew detekt lint
- name: Upload SARIF files
uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
uses: github/codeql-action/upload-sarif@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Set JELLYFIN_VERSION
run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV
- name: Assemble release files
@@ -27,6 +27,8 @@ jobs:
- name: Sign APK
id: signApk
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
env:
BUILD_TOOLS_VERSION: "34.0.0"
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
@@ -36,6 +38,8 @@ jobs:
- name: Sign app bundle
id: signAab
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
env:
BUILD_TOOLS_VERSION: "34.0.0"
with:
releaseDirectory: app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
@@ -51,7 +55,7 @@ jobs:
mv ${{ steps.signAab.outputs.signedReleaseFile }} build/jellyfin-publish/jellyfin-androidtv-v${{ env.JELLYFIN_VERSION }}-release.aab
mv app/build/version.txt build/jellyfin-publish/
- name: Upload release archive to GitHub release
uses: alexellis/upload-assets@259de5111cb56966d046ced998941e93f91d2c93 # tag=0.4.0
uses: alexellis/upload-assets@13926a61cdb2cb35f5fdef1c06b8b591523236d3 # 0.4.1
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
with:
@@ -61,20 +65,24 @@ jobs:
with:
switches: -vrptz
path: build/jellyfin-publish/
remote_path: /srv/repository/releases/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
remote_path: /srv/incoming/androidtv/v${{ env.JELLYFIN_VERSION }}/
remote_host: ${{ secrets.REPO_HOST }}
remote_user: ${{ secrets.REPO_USER }}
remote_key: ${{ secrets.REPO_KEY }}
- name: Update repo.jellyfin.org symlinks
uses: appleboy/ssh-action@55dabf81b49d4120609345970c91507e2d734799 # v1.0.0
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
host: ${{ secrets.REPO_HOST }}
username: ${{ secrets.REPO_USER }}
key: ${{ secrets.REPO_KEY }}
envs: JELLYFIN_VERSION
script_stop: true
script: |
cd /srv/repository/releases/client/androidtv;
rm -rf *.apk version.txt;
ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
ln -s versions/v${JELLYFIN_VERSION}/version.txt .;
if [ -d "/srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}" ] && [ -n "${{ env.JELLYFIN_VERSION }}" ]; then
sudo rm -r /srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
fi
sudo mv /srv/incoming/androidtv/v${{ env.JELLYFIN_VERSION }} /srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
cd /srv/repository/main/client/androidtv;
sudo rm -rf *.apk version.txt;
sudo ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
sudo ln -s versions/v${JELLYFIN_VERSION}/version.txt .;

View File

@@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
- name: Run test task
run: ./gradlew test

View File

@@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # v1.1.0
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-22.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
with:
dirtyLabel: merge conflict
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120

1
.gitignore vendored
View File

@@ -4,5 +4,6 @@ local.properties
/.idea
.gradle
.kotlin/
build/
captures/

View File

@@ -12,6 +12,8 @@
- [GodTamIt](https://github.com/GodTamIt)
- [sparky3387](https://github.com/sparky3387)
- [mohd-akram](https://github.com/mohd-akram)
- [3l0w](https://github.com/3l0w)
- [MajMongoose](https://github.com/majmongoose)
# Emby Contributors

View File

@@ -1,39 +0,0 @@
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath libs.android.gradle
}
}
import com.android.tools.profgen.ArtProfileKt
import com.android.tools.profgen.ArtProfileSerializer
import com.android.tools.profgen.DexFile
/**
* This is a temporary workaround for https://issuetracker.google.com/issues/231837768
* which will be fixed in AGP 8.1.0, at which point the workaround may be deleted.
*/
project.afterEvaluate {
tasks.compileReleaseArtProfile.doLast {
outputs.files.each { file ->
if (file.toString().endsWith(".profm")) {
println("Sorting ${file}")
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
def profile = ArtProfileKt.ArtProfile(file)
def keys = new ArrayList(profile.profileData.keySet())
def sortedData = new LinkedHashMap()
Collections.sort keys, new DexFile.Companion()
keys.each { key -> sortedData[key] = profile.profileData[key] }
new FileOutputStream(file).with {
write(version.magicBytes$profgen)
write(version.versionBytes$profgen)
version.write$profgen(it, sortedData, "")
}
}
}
}
}

View File

@@ -2,19 +2,17 @@ plugins {
id("com.android.application")
kotlin("android")
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.aboutlibraries)
}
// Apply workaround
apply("baselineWorkaround.gradle")
android {
namespace = "org.jellyfin.androidtv"
compileSdk = 34
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = 21
targetSdk = 33
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
// Release version
applicationId = namespace
@@ -33,10 +31,6 @@ android {
isCoreLibraryDesugaringEnabled = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
buildTypes {
val release by getting {
isMinifyEnabled = false
@@ -86,7 +80,7 @@ aboutLibraries {
}
val versionTxt by tasks.registering {
val path = buildDir.resolve("version.txt")
val path = layout.buildDirectory.asFile.get().resolve("version.txt")
doLast {
val versionString = "v${android.defaultConfig.versionName}=${android.defaultConfig.versionCode}"
@@ -98,9 +92,9 @@ val versionTxt by tasks.registering {
dependencies {
// Jellyfin
implementation(projects.playback.core)
implementation(projects.playback.exoplayer)
implementation(projects.playback.jellyfin)
implementation(projects.playback.ui)
implementation(projects.playback.media3.exoplayer)
implementation(projects.playback.media3.session)
implementation(projects.preference)
implementation(libs.jellyfin.apiclient)
implementation(libs.jellyfin.sdk) {
@@ -121,6 +115,7 @@ dependencies {
implementation(libs.androidx.core)
implementation(libs.androidx.activity)
implementation(libs.androidx.fragment)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.leanback.core)
implementation(libs.androidx.leanback.preference)
implementation(libs.androidx.preference)
@@ -139,15 +134,15 @@ dependencies {
implementation(libs.bundles.koin)
// Media players
implementation(libs.exoplayer)
implementation(libs.jellyfin.exoplayer.ffmpegextension)
implementation(libs.libvlc)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.ui)
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
// Markdown
implementation(libs.bundles.markwon)
// Image utility
implementation(libs.blurhash)
implementation(libs.bundles.coil)
// Crash Reporting
@@ -160,10 +155,6 @@ dependencies {
implementation(libs.timber)
implementation(libs.slf4j.timber)
// Debugging
if (getProperty("leakcanary.enable")?.toBoolean() == true)
debugImplementation(libs.leakcanary)
// Compatibility (desugaring)
coreLibraryDesugaring(libs.android.desugar)

View File

@@ -28,17 +28,25 @@
android:name="android.hardware.microphone"
android:required="false" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data
android:host="youtube.com"
android:scheme="https" />
</intent>
</queries>
<application
android:name=".JellyfinApplication"
android:allowBackup="true"
android:appCategory="video"
android:banner="@drawable/app_banner"
android:banner="@mipmap/app_banner"
android:dataExtractionRules="@xml/backup_rules"
android:fullBackupContent="@xml/backup_content"
android:icon="@drawable/app_icon"
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:largeHeap="true"
android:roundIcon="@drawable/app_icon_round"
android:supportsRtl="true"
android:theme="@style/Theme.Jellyfin"
android:usesCleartextTraffic="true">
@@ -101,6 +109,7 @@
android:name=".ui.startup.StartupActivity"
android:exported="true"
android:noHistory="true"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
@@ -132,8 +141,5 @@
android:theme="@style/Theme.Jellyfin.Preferences" />
<activity android:name=".ui.playback.ExternalPlayerActivity" />
<!-- Playback Rewrite - Temporary -->
<activity android:name=".ui.playback.rewrite.PlaybackForwardingActivity" />
</application>
</manifest>

View File

@@ -2,14 +2,11 @@ package org.jellyfin.androidtv
import android.app.Application
import android.content.Context
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.work.BackoffPolicy
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.await
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -18,7 +15,6 @@ import org.jellyfin.androidtv.data.eventhandling.SocketHandler
import org.jellyfin.androidtv.data.repository.NotificationsRepository
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
import org.jellyfin.androidtv.telemetry.TelemetryService
import org.jellyfin.androidtv.util.AutoBitrate
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
@@ -39,7 +35,6 @@ class JellyfinApplication : Application() {
*/
suspend fun onSessionStart() = withContext(Dispatchers.IO) {
val workManager by inject<WorkManager>()
val autoBitrate by inject<AutoBitrate>()
val socketListener by inject<SocketHandler>()
// Update background worker
@@ -59,16 +54,6 @@ class JellyfinApplication : Application() {
// Update WebSockets
launch { socketListener.updateSession() }
// Detect auto bitrate
// running in a different scope to prevent slow startups
ProcessLifecycleOwner.get().lifecycleScope.launch { autoBitrate.detect() }
}
override fun onLowMemory() {
super.onLowMemory()
BlurHash.clearCache()
}
override fun attachBaseContext(base: Context?) {

View File

@@ -18,7 +18,7 @@ class SessionInitializer : Initializer<Unit> {
// Restore system session
ProcessLifecycleOwner.get().lifecycleScope.launch {
koin.get<SessionRepository>().restoreSession()
koin.get<SessionRepository>().restoreSession(destroyOnly = false)
}
}

View File

@@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import java.util.Date
import java.time.Instant
import java.util.UUID
/**
@@ -20,7 +20,7 @@ data class AuthenticationStoreServer(
@SerialName("login_disclaimer") val loginDisclaimer: String? = null,
@SerialName("splashscreen_enabled") val splashscreenEnabled: Boolean = false,
@SerialName("setup_completed") val setupCompleted: Boolean = true,
@SerialName("last_used") val lastUsed: Long = Date().time,
@SerialName("last_refreshed") val lastRefreshed: Long = Date().time,
@SerialName("last_used") val lastUsed: Long = Instant.now().toEpochMilli(),
@SerialName("last_refreshed") val lastRefreshed: Long = Instant.now().toEpochMilli(),
val users: Map<UUID, AuthenticationStoreUser> = emptyMap(),
)

View File

@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.auth.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Date
import java.time.Instant
/**
* Locally stored user information. New properties require default values or deserialization will fail.
@@ -10,8 +10,7 @@ import java.util.Date
@Serializable
data class AuthenticationStoreUser(
val name: String,
@SerialName("last_used") val lastUsed: Long = Date().time,
@SerialName("require_password") val requirePassword: Boolean = true,
@SerialName("last_used") val lastUsed: Long = Instant.now().toEpochMilli(),
@SerialName("image_tag") val imageTag: String? = null,
@SerialName("access_token") val accessToken: String? = null,
)

View File

@@ -3,9 +3,9 @@ package org.jellyfin.androidtv.auth.model
import org.jellyfin.sdk.api.client.exception.ApiClientException
sealed class LoginState
object AuthenticatingState : LoginState()
object RequireSignInState : LoginState()
object ServerUnavailableState : LoginState()
data object AuthenticatingState : LoginState()
data object RequireSignInState : LoginState()
data object ServerUnavailableState : LoginState()
data class ServerVersionNotSupported(val server: Server) : LoginState()
data class ApiClientErrorLoginState(val error: ApiClientException) : LoginState()
object AuthenticatedState : LoginState()
data object AuthenticatedState : LoginState()

View File

@@ -5,12 +5,12 @@ sealed class QuickConnectState
/**
* State unknown untill first poll completed.
*/
object UnknownQuickConnectState : QuickConnectState()
data object UnknownQuickConnectState : QuickConnectState()
/**
* Server does not have QuickConnect enabled.
*/
object UnavailableQuickConnectState : QuickConnectState()
data object UnavailableQuickConnectState : QuickConnectState()
/**
* Connection is pending.
@@ -20,4 +20,4 @@ data class PendingQuickConnectState(val code: String) : QuickConnectState()
/**
* User connected.
*/
object ConnectedQuickConnectState : QuickConnectState()
data object ConnectedQuickConnectState : QuickConnectState()

View File

@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.auth.model
import org.jellyfin.androidtv.auth.repository.ServerRepository
import org.jellyfin.sdk.model.ServerVersion
import java.util.Date
import java.time.Instant
import java.util.UUID
/**
@@ -16,7 +16,7 @@ data class Server(
val loginDisclaimer: String? = null,
val splashscreenEnabled: Boolean = false,
val setupCompleted: Boolean = true,
var dateLastAccessed: Date = Date(0),
var dateLastAccessed: Instant = Instant.MIN,
) {
private val serverVersion = version?.let(ServerVersion::fromString)
val versionSupported = serverVersion != null && serverVersion >= ServerRepository.minimumServerVersion

View File

@@ -10,7 +10,6 @@ sealed class User {
abstract val serverId: UUID
abstract val name: String
abstract val accessToken: String?
abstract val requirePassword: Boolean
abstract val imageTag: String?
abstract fun withToken(accessToken: String): User
@@ -34,7 +33,6 @@ data class PrivateUser(
override val serverId: UUID,
override val name: String,
override val accessToken: String?,
override val requirePassword: Boolean,
override val imageTag: String?,
val lastUsed: Long,
) : User() {
@@ -49,7 +47,6 @@ data class PublicUser(
override val serverId: UUID,
override val name: String,
override val accessToken: String?,
override val requirePassword: Boolean,
override val imageTag: String?,
) : User() {
override fun withToken(accessToken: String) = copy(accessToken = accessToken)

View File

@@ -21,7 +21,7 @@ import org.jellyfin.androidtv.auth.model.ServerVersionNotSupported
import org.jellyfin.androidtv.auth.model.User
import org.jellyfin.androidtv.auth.store.AuthenticationPreferences
import org.jellyfin.androidtv.auth.store.AuthenticationStore
import org.jellyfin.androidtv.util.ImageUtils
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.sdk.forUser
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
@@ -33,10 +33,9 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.model.DeviceInfo
import org.jellyfin.sdk.model.api.AuthenticationResult
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.UserDto
import timber.log.Timber
import java.util.Date
import java.time.Instant
/**
* Repository to manage authentication of the user in the app.
@@ -67,7 +66,7 @@ class AuthenticationRepositoryImpl(
}
private fun authenticateAutomatic(server: Server, user: User): Flow<LoginState> {
Timber.i("Authenticating user %s", user)
Timber.i("Authenticating user ${user.id}")
// Automatic logic is disabled when the always authenticate preference is enabled
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return flowOf(RequireSignInState)
@@ -75,8 +74,6 @@ class AuthenticationRepositoryImpl(
val authStoreUser = authenticationStore.getUser(server.id, user.id)
// Try login with access token
return if (authStoreUser?.accessToken != null) authenticateToken(server, user.withToken(authStoreUser.accessToken))
// Try login without password
else if (!user.requirePassword) authenticateCredential(server, user.name, "")
// Require login
else flowOf(RequireSignInState)
}
@@ -125,9 +122,8 @@ class AuthenticationRepositoryImpl(
serverId = server.id,
name = userInfo.name!!,
accessToken = result.accessToken,
requirePassword = userInfo.hasPassword,
imageTag = userInfo.primaryImageTag,
lastUsed = Date().time,
lastUsed = Instant.now().toEpochMilli(),
)
authenticateFinish(server, userInfo, accessToken)
@@ -166,13 +162,11 @@ class AuthenticationRepositoryImpl(
val updatedUser = currentUser?.copy(
name = userInfo.name!!,
lastUsed = Date().time,
requirePassword = userInfo.hasPassword,
lastUsed = Instant.now().toEpochMilli(),
imageTag = userInfo.primaryImageTag,
accessToken = accessToken,
) ?: AuthenticationStoreUser(
name = userInfo.name!!,
requirePassword = userInfo.hasPassword,
imageTag = userInfo.primaryImageTag,
accessToken = accessToken,
)
@@ -185,11 +179,11 @@ class AuthenticationRepositoryImpl(
if (authenticated) {
// Update last use in store
authenticationStore.getServer(server.id)?.let { storedServer ->
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Date().time))
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Instant.now().toEpochMilli()))
}
authenticationStore.getUser(server.id, user.id)?.let { storedUser ->
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Date().time))
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Instant.now().toEpochMilli()))
}
}
@@ -209,8 +203,7 @@ class AuthenticationRepositoryImpl(
jellyfin.createApi(server.address).imageApi.getUserImageUrl(
userId = user.id,
tag = tag,
imageType = ImageType.PRIMARY,
maxHeight = ImageUtils.MAX_PRIMARY_IMAGE_HEIGHT
maxHeight = ImageHelper.MAX_PRIMARY_IMAGE_HEIGHT
)
}
}

View File

@@ -22,11 +22,12 @@ import org.jellyfin.sdk.api.client.extensions.brandingApi
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.BrandingOptions
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber
import java.util.Date
import java.time.Instant
import java.util.UUID
/**
@@ -47,6 +48,8 @@ interface ServerRepository {
companion object {
val minimumServerVersion = Jellyfin.minimumVersion.copy(build = null)
val recommendedServerVersion = Jellyfin.apiVersion.copy(build = null)
val upcomingMinimumServerVersion = ServerVersion(10, 9, 0)
}
}
@@ -135,7 +138,7 @@ class ServerRepositoryImpl(
loginDisclaimer = branding.loginDisclaimer,
splashscreenEnabled = branding.splashscreenEnabled,
setupCompleted = systemInfo.startupWizardCompleted ?: true,
lastUsed = Date().time
lastUsed = Instant.now().toEpochMilli()
) ?: AuthenticationStoreServer(
name = systemInfo.serverName ?: "Jellyfin Server",
address = chosenRecommendation.address,
@@ -185,7 +188,7 @@ class ServerRepositoryImpl(
}
private suspend fun updateServerInternal(id: UUID, server: AuthenticationStoreServer): AuthenticationStoreServer? {
val now = Date().time
val now = Instant.now().toEpochMilli()
// Only update every 10 minutes
if (now - server.lastRefreshed < 600000 && server.version != null) return null
@@ -223,7 +226,7 @@ class ServerRepositoryImpl(
loginDisclaimer = loginDisclaimer,
splashscreenEnabled = splashscreenEnabled,
setupCompleted = setupCompleted,
dateLastAccessed = Date(lastUsed),
dateLastAccessed = Instant.ofEpochMilli(lastUsed),
)
/**

View File

@@ -35,7 +35,6 @@ class ServerUserRepositoryImpl(
serverId = server.id,
name = userInfo.name,
accessToken = authInfo?.accessToken,
requirePassword = userInfo.requirePassword,
imageTag = userInfo.imageTag,
lastUsed = userInfo.lastUsed,
)

View File

@@ -1,10 +1,12 @@
package org.jellyfin.androidtv.auth.repository
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.auth.apiclient.ApiBinder
import org.jellyfin.androidtv.auth.store.AuthenticationPreferences
import org.jellyfin.androidtv.auth.store.AuthenticationStore
@@ -39,7 +41,7 @@ interface SessionRepository {
val currentSession: StateFlow<Session?>
val state: StateFlow<SessionRepositoryState>
suspend fun restoreSession()
suspend fun restoreSession(destroyOnly: Boolean)
suspend fun switchCurrentSession(serverId: UUID, userId: UUID): Boolean
fun destroyCurrentSession()
}
@@ -61,23 +63,28 @@ class SessionRepositoryImpl(
private val _state = MutableStateFlow(SessionRepositoryState.READY)
override val state = _state.asStateFlow()
override suspend fun restoreSession(): Unit = currentSessionMutex.withLock {
Timber.i("Restoring session")
_state.value = SessionRepositoryState.RESTORING_SESSION
override suspend fun restoreSession(destroyOnly: Boolean): Unit = withContext(NonCancellable) {
currentSessionMutex.withLock {
Timber.i("Restoring session")
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return destroyCurrentSession()
_state.value = SessionRepositoryState.RESTORING_SESSION
when (authenticationPreferences[AuthenticationPreferences.autoLoginUserBehavior]) {
DISABLED -> destroyCurrentSession()
LAST_USER -> setCurrentSession(createLastUserSession())
SPECIFIC_USER -> {
val serverId = authenticationPreferences[AuthenticationPreferences.autoLoginServerId].toUUIDOrNull()
val userId = authenticationPreferences[AuthenticationPreferences.autoLoginUserId].toUUIDOrNull()
if (serverId != null && userId != null) setCurrentSession(createUserSession(serverId, userId))
val alwaysAuthenticate = authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]
val autoLoginBehavior = authenticationPreferences[AuthenticationPreferences.autoLoginUserBehavior]
when {
alwaysAuthenticate -> destroyCurrentSession()
autoLoginBehavior == DISABLED -> destroyCurrentSession()
autoLoginBehavior == LAST_USER && !destroyOnly -> setCurrentSession(createLastUserSession())
autoLoginBehavior == SPECIFIC_USER && !destroyOnly -> {
val serverId = authenticationPreferences[AuthenticationPreferences.autoLoginServerId].toUUIDOrNull()
val userId = authenticationPreferences[AuthenticationPreferences.autoLoginUserId].toUUIDOrNull()
if (serverId != null && userId != null) setCurrentSession(createUserSession(serverId, userId))
}
}
}
_state.value = SessionRepositoryState.READY
_state.value = SessionRepositoryState.READY
}
}
override suspend fun switchCurrentSession(serverId: UUID, userId: UUID): Boolean {
@@ -93,6 +100,7 @@ class SessionRepositoryImpl(
val session = createUserSession(serverId, userId)
if (session == null) {
Timber.w("Could not switch to non-existing session for user $userId")
_state.value = SessionRepositoryState.READY
return false
}
@@ -133,11 +141,6 @@ class SessionRepositoryImpl(
val applied = userApiClient.applySession(session, deviceInfo)
if (applied && session != null) {
// Update crash reporting URL
val crashReportUrl = userApiClient.clientLogApi.logFileUrl(includeCredentials = false)
telemetryPreferences[TelemetryPreferences.crashReportUrl] = crashReportUrl
telemetryPreferences[TelemetryPreferences.crashReportToken] = session.accessToken
try {
val user by userApiClient.userApi.getCurrentUser()
userRepository.updateCurrentUser(user)
@@ -146,6 +149,11 @@ class SessionRepositoryImpl(
destroyCurrentSession()
return false
}
// Update crash reporting URL
val crashReportUrl = userApiClient.clientLogApi.logFileUrl()
telemetryPreferences[TelemetryPreferences.crashReportUrl] = crashReportUrl
telemetryPreferences[TelemetryPreferences.crashReportToken] = session.accessToken
} else {
userRepository.updateCurrentUser(null)
}
@@ -176,18 +184,21 @@ class SessionRepositoryImpl(
}
private fun ApiClient.applySession(session: Session?, newDeviceInfo: DeviceInfo = defaultDeviceInfo): Boolean {
deviceInfo = newDeviceInfo
if (session == null) {
baseUrl = null
accessToken = null
userId = null
update(
baseUrl = null,
accessToken = null,
deviceInfo = newDeviceInfo,
)
} else {
val server = authenticationStore.getServer(session.serverId)
?: return false
baseUrl = server.address
accessToken = session.accessToken
userId = session.userId
update(
baseUrl = server.address,
accessToken = session.accessToken,
deviceInfo = newDeviceInfo,
)
}
return true

View File

@@ -17,6 +17,6 @@ class UserRepositoryImpl : UserRepository {
override val currentUser = MutableStateFlow<UserDto?>(null)
override fun updateCurrentUser(user: UserDto?) {
if (currentUser.value !== user) currentUser.value = user
currentUser.value = user
}
}

View File

@@ -4,9 +4,6 @@ enum class ChangeTriggerType {
LibraryUpdated,
MoviePlayback,
TvPlayback,
GuideNeedsLoad,
MusicPlayback,
Always,
VideoQueueChange,
FavoriteUpdate,
}

View File

@@ -7,6 +7,7 @@ object Codec {
const val ASF = "asf"
const val AVI = "avi"
const val DVR_MS = "dvr-ms"
const val HLS = "hls"
const val M2V = "m2v"
const val M4V = "m4v"
const val MKV = "mkv"
@@ -47,8 +48,10 @@ object Codec {
const val PCM_ALAW = "pcm_alaw"
const val PCM_MULAW = "pcm_mulaw"
const val PCM_S16LE = "pcm_s16le"
const val PCM_S20LE = "pcm_s20le"
const val PCM_S24LE = "pcm_s24le"
const val TRUEHD = "truehd"
const val VORBIS = "vorbis"
const val WAV = "wav"
const val WEBMA = "webma"
const val WMA = "wma"

View File

@@ -2,5 +2,6 @@ package org.jellyfin.androidtv.constant
object Extras {
const val Folder = "folder"
const val IsLiveTvSeriesRecordings = "is_livetv_series_recordings"
const val IncludeType = "type_include"
}

View File

@@ -5,8 +5,7 @@ import org.jellyfin.androidtv.R
@Suppress("MagicNumber")
private val qualityOptions = setOf(
0.0, // auto
120.0, 110.0, 100.0, // 100 >=
200.0, 180.0, 140.0, 120.0, 110.0, 100.0, // 100 >=
90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 15.0, 10.0, // 10 >=
5.0, 3.0, 2.0, 1.0, // 1 >=
0.72, 0.42 // 0 >=
@@ -17,7 +16,6 @@ fun getQualityProfiles(
context: Context
): Map<String, String> = qualityOptions.associate {
val value = when {
it == 0.0 -> context.getString(R.string.bitrate_auto)
it >= 1.0 -> context.getString(R.string.bitrate_mbit, it)
else -> context.getString(R.string.bitrate_kbit, it * 1000.0)
}

View File

@@ -18,7 +18,6 @@ enum class QueryType {
LiveTvProgram,
LiveTvRecording,
StaticItems,
Persons,
StaticAudioQueueItems,
Artists,
AlbumArtists,

View File

@@ -3,11 +3,12 @@ package org.jellyfin.androidtv.data.compat
import org.jellyfin.apiclient.model.dlna.DeviceProfile
import org.jellyfin.apiclient.model.dlna.EncodingContext
import org.jellyfin.sdk.model.api.MediaSourceInfo
import java.util.UUID
open class AudioOptions {
var enableDirectPlay = true
var enableDirectStream = true
var itemId: String? = null
var itemId: UUID? = null
var mediaSources: List<MediaSourceInfo>? = null
var profile: DeviceProfile? = null

View File

@@ -2,12 +2,10 @@ package org.jellyfin.androidtv.data.compat;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dlna.DlnaProfileType;
import org.jellyfin.apiclient.model.dlna.EncodingContext;
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
import org.jellyfin.apiclient.model.dlna.SubtitleProfile;
import org.jellyfin.apiclient.model.dlna.TranscodeSeekInfo;
import org.jellyfin.apiclient.model.dto.NameValuePair;
import org.jellyfin.apiclient.model.session.PlayMethod;
import org.jellyfin.sdk.model.api.MediaProtocol;
import org.jellyfin.sdk.model.api.MediaSourceInfo;
@@ -15,23 +13,16 @@ import org.jellyfin.sdk.model.api.MediaStream;
import org.jellyfin.sdk.model.api.MediaStreamType;
import java.util.ArrayList;
import java.util.UUID;
public class StreamInfo {
private static final String START_TIME_TICKS = "StartTimeTicks";
private static final String SUBTITLE_STREAM_INDEX = "SubtitleStreamIndex";
private static final String STATIC = "Static";
private UUID ItemId;
public StreamInfo() {
setAudioCodecs(new String[]{});
}
private String ItemId;
public final String getItemId() {
public final UUID getItemId() {
return ItemId;
}
public final void setItemId(String value) {
public final void setItemId(UUID value) {
ItemId = value;
}
@@ -65,16 +56,6 @@ public class StreamInfo {
Context = value;
}
private DlnaProfileType MediaType = DlnaProfileType.values()[0];
public final DlnaProfileType getMediaType() {
return MediaType;
}
public final void setMediaType(DlnaProfileType value) {
MediaType = value;
}
private String Container;
public final String getContainer() {
@@ -95,16 +76,6 @@ public class StreamInfo {
StartPositionTicks = value;
}
private String[] AudioCodecs;
public final String[] getAudioCodecs() {
return AudioCodecs;
}
public final void setAudioCodecs(String[] value) {
AudioCodecs = value;
}
private DeviceProfile DeviceProfile;
public final DeviceProfile getDeviceProfile() {
@@ -115,16 +86,6 @@ public class StreamInfo {
DeviceProfile = value;
}
private String DeviceId;
public final String getDeviceId() {
return DeviceId;
}
public final void setDeviceId(String value) {
DeviceId = value;
}
private Long RunTimeTicks = null;
public final Long getRunTimeTicks() {
@@ -171,122 +132,6 @@ public class StreamInfo {
return getMediaSource() == null ? null : getMediaSource().getId();
}
public final boolean getIsDirectStream() {
return getPlayMethod() == PlayMethod.DirectStream || getPlayMethod() == PlayMethod.DirectPlay;
}
public final String toUrl(String baseUrl, String accessToken) {
if (!Utils.isEmpty(getMediaUrl())) {
return getMediaUrl();
}
if (getPlayMethod() == PlayMethod.DirectPlay) {
return getMediaSource().getPath();
}
if (Utils.isEmpty(baseUrl)) {
throw new IllegalArgumentException(baseUrl);
}
ArrayList<String> list = new ArrayList<String>();
for (NameValuePair pair : buildParams(this, accessToken, false)) {
if (Utils.isEmpty(pair.getValue())) {
continue;
}
// Try to keep the url clean by omitting defaults
if (START_TIME_TICKS.equalsIgnoreCase(pair.getName()) && "0".equalsIgnoreCase(pair.getValue())) {
continue;
}
if (SUBTITLE_STREAM_INDEX.equalsIgnoreCase(pair.getName()) && "-1".equalsIgnoreCase(pair.getValue())) {
continue;
}
if (STATIC.equalsIgnoreCase(pair.getName()) && "false".equalsIgnoreCase(pair.getValue())) {
continue;
}
list.add(String.format("%1$s=%2$s", pair.getName(), pair.getValue()));
}
String queryString = Utils.join("&", list);
return getUrl(baseUrl, queryString);
}
private String getUrl(String baseUrl, String queryString) {
if (Utils.isEmpty(baseUrl)) {
throw new IllegalArgumentException(baseUrl);
}
String extension = Utils.isEmpty(getContainer()) ? "" : "." + getContainer();
// remove trailing slashes
baseUrl = baseUrl.replaceAll("[/]+$", "");
if (getMediaType() == DlnaProfileType.Audio) {
return String.format("%1$s/audio/%2$s/stream%3$s?%4$s", baseUrl, getItemId(), extension, queryString);
}
return String.format("%1$s/videos/%2$s/stream%3$s?%4$s", baseUrl, getItemId(), extension, queryString);
}
private static ArrayList<NameValuePair> buildParams(StreamInfo item, String accessToken, boolean isDlna) {
ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
String audioCodecs = item.getAudioCodecs().length == 0 ? "" : Utils.join(",", item.getAudioCodecs());
list.add(new NameValuePair("DeviceProfileId", ""));
String tempVar2 = item.getDeviceId();
list.add(new NameValuePair("DeviceId", (tempVar2 != null) ? tempVar2 : ""));
String tempVar3 = item.getMediaSourceId();
list.add(new NameValuePair("MediaSourceId", (tempVar3 != null) ? tempVar3 : ""));
list.add(new NameValuePair(STATIC, Boolean.valueOf(item.getIsDirectStream()).toString().toLowerCase()));
list.add(new NameValuePair("VideoCodec", ""));
list.add(new NameValuePair("AudioCodec", audioCodecs));
list.add(new NameValuePair("AudioStreamIndex", ""));
list.add(new NameValuePair("SubtitleStreamIndex", ""));
list.add(new NameValuePair("VideoBitrate", ""));
list.add(new NameValuePair("AudioBitrate", ""));
list.add(new NameValuePair("MaxAudioChannels", ""));
list.add(new NameValuePair("MaxFramerate", ""));
list.add(new NameValuePair("MaxWidth", ""));
list.add(new NameValuePair("MaxHeight", ""));
list.add(new NameValuePair(START_TIME_TICKS, String.valueOf(item.getStartPositionTicks())));
list.add(new NameValuePair("Level", ""));
list.add(new NameValuePair("MaxRefFrames", ""));
list.add(new NameValuePair("MaxVideoBitDepth", ""));
list.add(new NameValuePair("Profile", ""));
// no longer used
list.add(new NameValuePair("Cabac", ""));
String tempVar6 = item.getPlaySessionId();
list.add(new NameValuePair("PlaySessionId", (tempVar6 != null) ? tempVar6 : ""));
list.add(new NameValuePair("api_key", (accessToken != null) ? accessToken : ""));
String liveStreamId = item.getMediaSource() == null ? null : item.getMediaSource().getLiveStreamId();
list.add(new NameValuePair("LiveStreamId", (liveStreamId != null) ? liveStreamId : ""));
if (isDlna) {
list.add(new NameValuePair("ItemId", item.getItemId()));
}
list.add(new NameValuePair("CopyTimestamps", "false"));
list.add(new NameValuePair("ForceLiveStream", "false"));
list.add(new NameValuePair("SubtitleMethod", ""));
list.add(new NameValuePair("TranscodingMaxAudioChannels", ""));
list.add(new NameValuePair("EnableSubtitlesInManifest", "false"));
String tempVar7 = item.getMediaSource().getETag();
list.add(new NameValuePair("Tag", (tempVar7 != null) ? tempVar7 : ""));
return list;
}
public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSelectedTrackOnly, String baseUrl, String accessToken) {
return getSubtitleProfiles(includeSelectedTrackOnly, false, baseUrl, accessToken);
}

View File

@@ -6,56 +6,61 @@ import android.os.Build
import android.widget.Toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper
import org.jellyfin.androidtv.util.PlaybackHelper
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.sessionApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.sockets.SocketInstance
import org.jellyfin.sdk.api.sockets.SocketInstanceState
import org.jellyfin.sdk.api.sockets.addGeneralCommandsListener
import org.jellyfin.sdk.api.sockets.addListener
import org.jellyfin.sdk.api.sockets.subscribe
import org.jellyfin.sdk.api.sockets.subscribeGeneralCommand
import org.jellyfin.sdk.api.sockets.subscribeGeneralCommands
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.GeneralCommandType
import org.jellyfin.sdk.model.api.LibraryChangedMessage
import org.jellyfin.sdk.model.api.LibraryUpdateInfo
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.PlayMessage
import org.jellyfin.sdk.model.api.PlaystateCommand
import org.jellyfin.sdk.model.constant.MediaType
import org.jellyfin.sdk.model.api.PlaystateMessage
import org.jellyfin.sdk.model.extensions.get
import org.jellyfin.sdk.model.extensions.getValue
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.jellyfin.sdk.model.socket.LibraryChangedMessage
import org.jellyfin.sdk.model.socket.PlayMessage
import org.jellyfin.sdk.model.socket.PlayStateMessage
import timber.log.Timber
import java.time.Instant
import java.util.UUID
class SocketHandler(
private val context: Context,
private val api: ApiClient,
private val socketInstance: SocketInstance,
private val dataRefreshService: DataRefreshService,
private val mediaManager: MediaManager,
private val playbackControllerContainer: PlaybackControllerContainer,
private val navigationRepository: NavigationRepository,
private val audioManager: AudioManager,
private val itemLauncher: ItemLauncher,
private val playbackHelper: PlaybackHelper,
) {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
val state = socketInstance.state
suspend fun updateSession() {
try {
api.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.Video, MediaType.Audio),
playableMediaTypes = listOf(MediaType.VIDEO, MediaType.AUDIO),
supportsMediaControl = true,
supportedCommands = buildList {
add(GeneralCommandType.DISPLAY_CONTENT)
add(GeneralCommandType.SET_SUBTITLE_STREAM_INDEX)
add(GeneralCommandType.SET_AUDIO_STREAM_INDEX)
add(GeneralCommandType.DISPLAY_MESSAGE)
add(GeneralCommandType.SEND_STRING)
@@ -76,38 +81,70 @@ class SocketHandler(
Timber.e(err, "Unable to update capabilities")
return
}
val isOffline = state.value == SocketInstanceState.DISCONNECTED || state.value == SocketInstanceState.ERROR
if (isOffline) socketInstance.reconnect()
else socketInstance.updateCredentials()
}
init {
socketInstance.apply {
api.webSocket.apply {
// Library
addListener<LibraryChangedMessage> { message -> onLibraryChanged(message.info) }
subscribe<LibraryChangedMessage>()
.onEach { message -> message.data?.let(::onLibraryChanged) }
.launchIn(coroutineScope)
// Media playback
addListener<PlayMessage> { message -> onPlayMessage(message) }
addListener<PlayStateMessage> { message -> onPlayStateMessage(message) }
subscribe<PlayMessage>()
.onEach { message -> onPlayMessage(message) }
.launchIn(coroutineScope)
subscribe<PlaystateMessage>()
.onEach { message -> onPlayStateMessage(message) }
.launchIn(coroutineScope)
subscribeGeneralCommand(GeneralCommandType.SET_SUBTITLE_STREAM_INDEX)
.onEach { message ->
val index = message["index"]?.toIntOrNull() ?: return@onEach
withContext(Dispatchers.Main) {
playbackControllerContainer.playbackController?.switchSubtitleStream(index)
}
}
.launchIn(coroutineScope)
subscribeGeneralCommand(GeneralCommandType.SET_AUDIO_STREAM_INDEX)
.onEach { message ->
val index = message["index"]?.toIntOrNull() ?: return@onEach
withContext(Dispatchers.Main) {
playbackControllerContainer.playbackController?.switchAudioStream(index)
}
}
.launchIn(coroutineScope)
// General commands
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_CONTENT)) { message ->
val itemId by message
val itemType by message
subscribeGeneralCommand(GeneralCommandType.DISPLAY_CONTENT)
.onEach { message ->
val itemId by message
val itemType by message
val itemUuid = itemId?.toUUIDOrNull()
val itemKind = itemType?.let { type -> BaseItemKind.values().find { value -> value.serialName.equals(type, true) } }
val itemUuid = itemId?.toUUIDOrNull()
val itemKind = itemType?.let { type ->
BaseItemKind.entries.find { value ->
value.serialName.equals(type, true)
}
}
if (itemUuid != null && itemKind != null) onDisplayContent(itemUuid, itemKind)
}
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING)) { message ->
val header by message
val text by message
val string by message
if (itemUuid != null && itemKind != null) onDisplayContent(itemUuid, itemKind)
}
.launchIn(coroutineScope)
onDisplayMessage(header, text ?: string)
}
subscribeGeneralCommands(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING))
.onEach { message ->
val header by message
val text by message
val string by message
onDisplayMessage(header, text ?: string)
}
.launchIn(coroutineScope)
}
}
@@ -120,62 +157,69 @@ class SocketHandler(
})
if (info.itemsAdded.any() || info.itemsRemoved.any())
dataRefreshService.lastLibraryChange = System.currentTimeMillis()
dataRefreshService.lastLibraryChange = Instant.now()
}
private fun onPlayMessage(message: PlayMessage) {
val itemId = message.request.itemIds?.firstOrNull() ?: return
val itemId = message.data?.itemIds?.firstOrNull() ?: return
PlaybackHelper.retrieveAndPlay(
itemId.toString(),
false,
message.request.startPositionTicks,
context
)
runCatching {
playbackHelper.retrieveAndPlay(
itemId,
false,
message.data?.startPositionTicks,
context
)
}.onFailure { Timber.w(it, "Failed to start playback") }
}
@Suppress("ComplexMethod")
private fun onPlayStateMessage(message: PlayStateMessage) = coroutineScope.launch(Dispatchers.Main) {
Timber.i("Received PlayStateMessage with command ${message.request.command}")
val playbackController = playbackControllerContainer.playbackController
// Audio playback uses the mediaManager, video playback and live tv use the playbackController
if (mediaManager.isAudioPlayerInitialized) when (message.request.command) {
PlaystateCommand.STOP -> mediaManager.stopAudio(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> mediaManager.playPauseAudio()
PlaystateCommand.NEXT_TRACK -> mediaManager.nextAudioItem()
PlaystateCommand.PREVIOUS_TRACK -> mediaManager.prevAudioItem()
// Not implemented
PlaystateCommand.SEEK,
PlaystateCommand.REWIND,
PlaystateCommand.FAST_FORWARD -> Unit
} else when (message.request.command) {
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
PlaystateCommand.SEEK -> playbackController?.seek(
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
)
PlaystateCommand.REWIND -> playbackController?.rewind()
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
private suspend fun onPlayStateMessage(message: PlaystateMessage) = withContext(Dispatchers.Main) {
Timber.i("Received PlayStateMessage with command ${message.data?.command}")
// Audio playback uses (Rewrite)MediaManager, (legacy) video playback uses playbackController
when {
mediaManager.hasAudioQueueItems() -> {
Timber.i("Ignoring PlayStateMessage: should be handled by PlaySessionSocketService")
return@withContext
}
// PlaybackController
else -> {
val playbackController = playbackControllerContainer.playbackController
when (message.data?.command) {
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
PlaystateCommand.SEEK -> playbackController?.seek(
(message.data?.seekPositionTicks ?: 0) / TICKS_TO_MS
)
PlaystateCommand.REWIND -> playbackController?.rewind()
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
null -> Unit
}
}
}
}
private fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) {
private suspend fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) = withContext(Dispatchers.Main) {
val playbackController = playbackControllerContainer.playbackController
if (playbackController?.isPlaying == true || playbackController?.isPaused == true) {
Timber.i("Not launching $itemId: playback in progress")
return
return@withContext
}
Timber.i("Launching $itemId")
when (itemKind) {
BaseItemKind.USER_VIEW,
BaseItemKind.COLLECTION_FOLDER -> coroutineScope.launch {
BaseItemKind.COLLECTION_FOLDER -> {
val item by api.userLibraryApi.getItem(itemId = itemId)
ItemLauncher.launchUserView(item)
itemLauncher.launchUserView(item)
}
else -> navigationRepository.navigate(Destinations.itemDetails(itemId))

View File

@@ -1,16 +1,15 @@
package org.jellyfin.androidtv.data.model
import org.jellyfin.sdk.model.api.BaseItemDto
import java.time.Instant
import java.util.UUID
class DataRefreshService {
var lastDeletedItemId: UUID? = null
var lastPlayback: Long = 0
var lastMoviePlayback: Long = 0
var lastTvPlayback: Long = 0
var lastMusicPlayback: Long = 0
var lastLibraryChange: Long = 0
var lastVideoQueueChange: Long = 0
var lastFavoriteUpdate: Long = 0
var lastPlayback: Instant? = null
var lastMoviePlayback: Instant? = null
var lastTvPlayback: Instant? = null
var lastLibraryChange: Instant? = null
var lastFavoriteUpdate: Instant? = null
var lastPlayedItem: BaseItemDto? = null
}

View File

@@ -1,18 +1,15 @@
package org.jellyfin.androidtv.data.model
import org.jellyfin.apiclient.model.querying.ItemFilter
import org.jellyfin.sdk.model.api.ItemFilter
class FilterOptions {
var isFavoriteOnly = false
var isUnwatchedOnly = false
val filters: Array<ItemFilter>?
get() {
if (!isUnwatchedOnly && !isFavoriteOnly) return null
return buildList {
if (isFavoriteOnly) add(ItemFilter.IsFavorite)
if (isUnwatchedOnly) add(ItemFilter.IsUnplayed)
}.toTypedArray()
val filters: Set<ItemFilter>
get() = buildSet {
if (isFavoriteOnly) add(ItemFilter.IS_FAVORITE)
if (isUnwatchedOnly) add(ItemFilter.IS_UNPLAYED)
}
}

View File

@@ -1,3 +0,0 @@
package org.jellyfin.androidtv.data.querying;
data class AdditionalPartsQuery(val itemId: String)

View File

@@ -1,5 +0,0 @@
package org.jellyfin.androidtv.data.querying
import org.jellyfin.apiclient.model.querying.ArtistsQuery
class AlbumArtistsQuery : ArtistsQuery()

View File

@@ -0,0 +1,7 @@
package org.jellyfin.androidtv.data.querying;
import java.util.UUID
data class GetAdditionalPartsRequest(
val itemId: UUID,
)

View File

@@ -0,0 +1,3 @@
package org.jellyfin.androidtv.data.querying
data object GetSeriesTimersRequest

View File

@@ -0,0 +1,7 @@
package org.jellyfin.androidtv.data.querying
import java.util.UUID
data class GetSpecialsRequest(
val itemId: UUID,
)

View File

@@ -0,0 +1,7 @@
package org.jellyfin.androidtv.data.querying
import java.util.UUID
data class GetTrailersRequest(
val itemId: UUID,
)

View File

@@ -1,3 +1,3 @@
package org.jellyfin.androidtv.data.querying
object ViewQuery
data object GetUserViewsRequest

View File

@@ -1,5 +0,0 @@
package org.jellyfin.androidtv.data.querying
data class SpecialsQuery(
val itemId: String,
)

View File

@@ -1,26 +0,0 @@
package org.jellyfin.androidtv.data.querying
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.apiclient.model.querying.ItemFields
import org.jellyfin.apiclient.model.querying.ItemQuery
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class StdItemQuery @JvmOverloads constructor(
fields: Array<ItemFields> = defaultFields
) : ItemQuery(), KoinComponent {
companion object {
val defaultFields = arrayOf(
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount
)
}
init {
userId = get<UserRepository>().currentUser.value!!.id.toString()
setFields(fields)
}
}

View File

@@ -1,5 +0,0 @@
package org.jellyfin.androidtv.data.querying
data class TrailersQuery(
val itemId: String,
)

View File

@@ -6,6 +6,7 @@ import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.UserItemDataDto
import java.time.Instant
interface ItemMutationRepository {
suspend fun setFavorite(item: UUID, favorite: Boolean): UserItemDataDto
@@ -22,7 +23,7 @@ class ItemMutationRepositoryImpl(
else -> api.userLibraryApi.unmarkFavoriteItem(itemId = item)
}
dataRefreshService.lastFavoriteUpdate = System.currentTimeMillis()
dataRefreshService.lastFavoriteUpdate = Instant.now()
return response
}

View File

@@ -1,25 +1,27 @@
package org.jellyfin.androidtv.data.repository
import android.app.UiModeManager
import android.content.Context
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.auth.model.Server
import org.jellyfin.androidtv.auth.repository.ServerRepository
import org.jellyfin.androidtv.data.model.AppNotification
import org.jellyfin.androidtv.preference.SystemPreferences
import org.jellyfin.androidtv.util.isTvDevice
import org.jellyfin.sdk.model.ServerVersion
interface NotificationsRepository {
val notifications: StateFlow<List<AppNotification>>
fun dismissNotification(item: AppNotification)
fun addDefaultNotifications()
fun updateServerNotifications(server: Server?)
}
class NotificationsRepositoryImpl(
private val context: Context,
private val uiModeManager: UiModeManager,
private val systemPreferences: SystemPreferences,
) : NotificationsRepository {
override val notifications = MutableStateFlow(emptyList<AppNotification>())
@@ -34,15 +36,28 @@ class NotificationsRepositoryImpl(
addBetaNotification()
}
private fun addNotification(message: String, public: Boolean = false, dismiss: () -> Unit = {}) {
notifications.value = notifications.value + AppNotification(message, dismiss, public)
private fun addNotification(
message: String,
public: Boolean = false,
dismiss: () -> Unit = {}
): AppNotification {
val notification = AppNotification(message, dismiss, public)
notifications.value += notification
return notification
}
private fun removeNotification(notification: AppNotification) {
notifications.value -= notification
}
private fun addUiModeNotification() {
val disableUiModeWarning = systemPreferences[SystemPreferences.disableUiModeWarning]
if (!context.isTvDevice() && !disableUiModeWarning) {
addNotification(context.getString(R.string.app_notification_uimode_invalid), public = true)
addNotification(
context.getString(R.string.app_notification_uimode_invalid),
public = true
)
}
}
@@ -53,8 +68,28 @@ class NotificationsRepositoryImpl(
if (isBeta && currentVersion != dismissedVersion) {
addNotification(context.getString(R.string.app_notification_beta, currentVersion)) {
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] = currentVersion
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] =
currentVersion
}
}
}
// Update server notification
private var _updateServerNotification: AppNotification? = null
override fun updateServerNotifications(server: Server?) {
// Remove current update notification
_updateServerNotification?.let(::removeNotification)
val currentServerVersion = server?.version?.let(ServerVersion::fromString) ?: return
if (currentServerVersion < ServerRepository.upcomingMinimumServerVersion) {
_updateServerNotification =
addNotification(
message = context.getString(
R.string.app_notification_update_soon,
currentServerVersion,
ServerRepository.upcomingMinimumServerVersion
),
)
}
}
}

View File

@@ -5,14 +5,14 @@ import kotlinx.coroutines.flow.flow
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.constant.CollectionType
import org.jellyfin.sdk.model.api.CollectionType
interface UserViewsRepository {
val views: Flow<Collection<BaseItemDto>>
fun isSupported(collectionType: String?): Boolean
fun allowViewSelection(collectionType: String?): Boolean
fun allowGridView(collectionType: String?): Boolean
fun isSupported(collectionType: CollectionType?): Boolean
fun allowViewSelection(collectionType: CollectionType?): Boolean
fun allowGridView(collectionType: CollectionType?): Boolean
}
class UserViewsRepositoryImpl(
@@ -26,25 +26,25 @@ class UserViewsRepositoryImpl(
emit(filteredViews)
}
override fun isSupported(collectionType: String?) = collectionType !in unsupportedCollectionTypes
override fun allowViewSelection(collectionType: String?) = collectionType !in disallowViewSelectionCollectionTypes
override fun allowGridView(collectionType: String?) = collectionType !in disallowGridViewCollectionTypes
override fun isSupported(collectionType: CollectionType?) = collectionType !in unsupportedCollectionTypes
override fun allowViewSelection(collectionType: CollectionType?) = collectionType !in disallowViewSelectionCollectionTypes
override fun allowGridView(collectionType: CollectionType?) = collectionType !in disallowGridViewCollectionTypes
private companion object {
private val unsupportedCollectionTypes = arrayOf(
CollectionType.Books,
CollectionType.Folders
CollectionType.BOOKS,
CollectionType.FOLDERS
)
private val disallowViewSelectionCollectionTypes = arrayOf(
CollectionType.LiveTv,
CollectionType.Music,
CollectionType.Photos,
CollectionType.LIVETV,
CollectionType.MUSIC,
CollectionType.PHOTOS,
)
private val disallowGridViewCollectionTypes = arrayOf(
CollectionType.LiveTv,
CollectionType.Music
CollectionType.LIVETV,
CollectionType.MUSIC
)
}
}

View File

@@ -20,6 +20,7 @@ import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ImageType
import java.time.Instant
import java.util.UUID
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@@ -47,7 +48,11 @@ class BackgroundService(
private var _backgrounds = emptyList<ImageBitmap>()
private var _currentIndex = 0
private var _currentBackground = MutableStateFlow<ImageBitmap?>(null)
private var _blurBackground = MutableStateFlow(false)
private var _enabled = MutableStateFlow(true)
val currentBackground get() = _currentBackground.asStateFlow()
val blurBackground get() = _blurBackground.asStateFlow()
val enabled get() = _enabled.asStateFlow()
// Helper function for [setBackground]
private fun List<String>?.getUrls(itemId: UUID?): List<String> {
@@ -74,6 +79,9 @@ class BackgroundService(
if (baseItem == null || !userPreferences[UserPreferences.backdropEnabled])
return clearBackgrounds()
// Enable blur for backdrops
_blurBackground.value = true
// Get all backdrop urls
val itemBackdropUrls = baseItem.backdropImageTags.getUrls(baseItem.id)
val parentBackdropUrls = baseItem.parentBackdropImageTags.getUrls(baseItem.parentBackdropItemId)
@@ -94,6 +102,9 @@ class BackgroundService(
if (!server.splashscreenEnabled)
return clearBackgrounds()
// Disable blur on splashscreen
_blurBackground.value = false
// Manually grab the backdrop URL
val api = jellyfin.createApi(baseUrl = server.address)
val splashscreenUrl = api.imageApi.getSplashscreenUrl()
@@ -104,6 +115,9 @@ class BackgroundService(
private fun loadBackgrounds(backdropUrls: Set<String>) {
if (backdropUrls.isEmpty()) return clearBackgrounds()
// Re-enable backgrounds if disabled
_enabled.value = true
// Cancel current loading job
loadBackgroundsJob?.cancel()
loadBackgroundsJob = scope.launch(Dispatchers.IO) {
@@ -122,14 +136,24 @@ class BackgroundService(
fun clearBackgrounds() {
loadBackgroundsJob?.cancel()
// Re-enable backgrounds if disabled
_enabled.value = true
if (_backgrounds.isEmpty()) return
_backgrounds = emptyList()
update()
}
/**
* Disable the showing of backgrounds until any function manipulating the backgrounds is called.
*/
fun disable() {
_enabled.value = false
}
internal fun update() {
val now = System.currentTimeMillis()
val now = Instant.now().toEpochMilli()
if (lastBackgroundTimerUpdate > now - TRANSITION_DURATION.inWholeMilliseconds)
return setTimer((lastBackgroundTimerUpdate - now).milliseconds + TRANSITION_DURATION, false)

View File

@@ -21,7 +21,9 @@ import org.jellyfin.androidtv.data.repository.NotificationsRepositoryImpl
import org.jellyfin.androidtv.data.repository.UserViewsRepository
import org.jellyfin.androidtv.data.repository.UserViewsRepositoryImpl
import org.jellyfin.androidtv.data.service.BackgroundService
import org.jellyfin.androidtv.integration.dream.DreamViewModel
import org.jellyfin.androidtv.ui.ScreensaverViewModel
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.navigation.NavigationRepositoryImpl
@@ -35,13 +37,16 @@ import org.jellyfin.androidtv.ui.search.SearchViewModel
import org.jellyfin.androidtv.ui.startup.ServerAddViewModel
import org.jellyfin.androidtv.ui.startup.StartupViewModel
import org.jellyfin.androidtv.ui.startup.UserLoginViewModel
import org.jellyfin.androidtv.util.KeyProcessor
import org.jellyfin.androidtv.util.MarkdownRenderer
import org.jellyfin.androidtv.util.PlaybackHelper
import org.jellyfin.androidtv.util.apiclient.ReportingHelper
import org.jellyfin.androidtv.util.sdk.SdkPlaybackHelper
import org.jellyfin.androidtv.util.sdk.legacy
import org.jellyfin.apiclient.AppInfo
import org.jellyfin.apiclient.android
import org.jellyfin.apiclient.logging.AndroidLogger
import org.jellyfin.sdk.android.androidDevice
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.createJellyfin
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
@@ -76,9 +81,7 @@ val appModule = module {
get<JellyfinSdk>().createApi()
}
single { get<ApiClient>().ws() }
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get()) }
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
// Old apiclient
single {
@@ -112,7 +115,7 @@ val appModule = module {
single<UserRepository> { UserRepositoryImpl() }
single<UserViewsRepository> { UserViewsRepositoryImpl(get()) }
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get(), get()) }
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get()) }
single<ItemMutationRepository> { ItemMutationRepositoryImpl(get(), get()) }
single<CustomMessageRepository> { CustomMessageRepositoryImpl() }
single<NavigationRepository> { NavigationRepositoryImpl(Destinations.home) }
@@ -125,10 +128,15 @@ val appModule = module {
viewModel { PictureViewerViewModel(get()) }
viewModel { ScreensaverViewModel(get()) }
viewModel { SearchViewModel(get()) }
viewModel { DreamViewModel(get(), get(), get(), get(), get()) }
single { BackgroundService(get(), get(), get(), get(), get()) }
single { MarkdownRenderer(get()) }
single { ItemLauncher() }
single { KeyProcessor() }
single { ReportingHelper() }
single<PlaybackHelper> { SdkPlaybackHelper(get(), get(), get(), get(), get(), get(), get()) }
factory { (context: Context) -> SearchFragmentDelegate(context, get()) }
factory { (context: Context) -> SearchFragmentDelegate(context, get(), get()) }
}

View File

@@ -12,15 +12,17 @@ import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.UserSettingPreferences
import org.jellyfin.androidtv.ui.browsing.MainActivity
import org.jellyfin.androidtv.ui.playback.GarbagePlaybackLauncher
import org.jellyfin.androidtv.ui.playback.LegacyMediaManager
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.RewritePlaybackLauncher
import org.jellyfin.androidtv.ui.playback.VideoQueueManager
import org.jellyfin.androidtv.ui.playback.rewrite.RewriteMediaManager
import org.jellyfin.playback.core.mediasession.MediaSessionOptions
import org.jellyfin.playback.core.mediasession.mediaSessionPlugin
import org.jellyfin.playback.core.playbackManager
import org.jellyfin.playback.exoplayer.exoPlayerPlugin
import org.jellyfin.playback.jellyfin.jellyfinPlugin
import org.jellyfin.playback.media3.exoplayer.ExoPlayerOptions
import org.jellyfin.playback.media3.exoplayer.exoPlayerPlugin
import org.jellyfin.playback.media3.session.MediaSessionOptions
import org.jellyfin.playback.media3.session.media3SessionPlugin
import org.jellyfin.sdk.api.client.ApiClient
import org.koin.android.ext.koin.androidContext
import org.koin.core.scope.Scope
import org.koin.dsl.module
@@ -30,16 +32,7 @@ import org.jellyfin.androidtv.ui.playback.PlaybackManager as LegacyPlaybackManag
val playbackModule = module {
single { LegacyPlaybackManager(get()) }
single { VideoQueueManager() }
single { LegacyMediaManager(get()) }
single { RewriteMediaManager(get(), get(), get(), get()) }
factory {
val preferences = get<UserPreferences>()
val useRewrite = preferences[UserPreferences.playbackRewriteAudioEnabled] && BuildConfig.DEVELOPMENT
if (useRewrite) get<RewriteMediaManager>()
else get<LegacyMediaManager>()
}
single<MediaManager> { RewriteMediaManager(get(), get(), get(), get()) }
factory {
val preferences = get<UserPreferences>()
@@ -53,13 +46,10 @@ val playbackModule = module {
}
fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
install(exoPlayerPlugin(get()))
install(jellyfinPlugin(get(), get()))
val activityIntent = Intent(get(), MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(get(), 0, activityIntent, PendingIntent.FLAG_IMMUTABLE)
val notificationChannelId = "mediasession"
val notificationChannelId = "session"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
notificationChannelId,
@@ -69,12 +59,22 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
NotificationManagerCompat.from(get()).createNotificationChannel(channel)
}
install(mediaSessionPlugin(get(), MediaSessionOptions(
val api = get<ApiClient>()
val exoPlayerOptions = ExoPlayerOptions(
httpConnectTimeout = api.httpClientOptions.connectTimeout,
httpReadTimeout = api.httpClientOptions.requestTimeout
)
install(exoPlayerPlugin(get(), exoPlayerOptions))
val mediaSessionOptions = MediaSessionOptions(
channelId = notificationChannelId,
notificationId = 1,
iconSmall = R.drawable.app_icon_foreground,
openIntent = pendingIntent,
)))
)
install(media3SessionPlugin(get(), mediaSessionOptions))
install(jellyfinPlugin(get()))
// Options
val userSettingPreferences = get<UserSettingPreferences>()

View File

@@ -1,10 +1,8 @@
package org.jellyfin.androidtv.di
import org.jellyfin.androidtv.util.AutoBitrate
import org.jellyfin.androidtv.util.ImageHelper
import org.koin.dsl.module
val utilsModule = module {
single { AutoBitrate(get()) }
single { ImageHelper(get()) }
}

View File

@@ -25,9 +25,10 @@ import org.jellyfin.androidtv.data.repository.UserViewsRepository
import org.jellyfin.androidtv.integration.provider.ImageProvider
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.ui.startup.StartupActivity
import org.jellyfin.androidtv.util.ImageUtils
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.dp
import org.jellyfin.androidtv.util.sdk.isUsable
import org.jellyfin.androidtv.util.stripHtml
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.exception.TimeoutException
@@ -41,11 +42,12 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageFormat
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.constant.MediaType
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import timber.log.Timber
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import kotlin.time.Duration
@@ -67,6 +69,7 @@ class LeanbackChannelWorker(
private val api by inject<ApiClient>()
private val userPreferences by inject<UserPreferences>()
private val userViewsRepository by inject<UserViewsRepository>()
private val imageHelper by inject<ImageHelper>()
/**
* Check if the app can use Leanback features and is API level 26 or higher.
@@ -198,7 +201,7 @@ class LeanbackChannelWorker(
}
// Update logo
ResourcesCompat.getDrawable(context.resources, R.drawable.app_icon, context.theme)?.let {
ResourcesCompat.getDrawable(context.resources, R.mipmap.app_icon, context.theme)?.let {
ChannelLogoUtils.storeChannelLogo(
context,
ContentUris.parseId(uri),
@@ -256,7 +259,7 @@ class LeanbackChannelWorker(
tag = imageTags?.get(ImageType.PRIMARY),
)
else -> ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)
else -> imageHelper.getResourceUrl(context, R.drawable.tile_land_tv)
}.let(ImageProvider::getImageUri)
/**
@@ -271,7 +274,7 @@ class LeanbackChannelWorker(
fields = listOf(ItemFields.DATE_CREATED),
imageTypeLimit = 1,
limit = 10,
mediaTypes = listOf(MediaType.Video),
mediaTypes = listOf(MediaType.VIDEO),
includeItemTypes = listOf(BaseItemKind.EPISODE, BaseItemKind.MOVIE),
excludeActiveSessions = true,
).content.items.orEmpty()
@@ -279,9 +282,9 @@ class LeanbackChannelWorker(
val nextUp = async {
api.tvShowsApi.getNextUp(
userId = api.userId,
imageTypeLimit = 1,
limit = 10,
enableResumable = false,
fields = listOf(ItemFields.DATE_CREATED),
).content.items.orEmpty()
}
@@ -361,7 +364,7 @@ class LeanbackChannelWorker(
.setEpisodeTitle(if (item.type == BaseItemKind.EPISODE) item.name else null)
.setSeasonNumber(seasonString, item.parentIndexNumber ?: 0)
.setEpisodeNumber(episodeString, item.indexNumber ?: 0)
.setDescription(item.overview)
.setDescription(item.overview?.stripHtml())
.setReleaseDate(
if (item.premiereDate != null) DateTimeFormatter.ISO_DATE.format(item.premiereDate)
else null
@@ -455,7 +458,7 @@ class LeanbackChannelWorker(
setLastEngagementTimeUtcMillis(
engagement?.toInstant(ZoneOffset.UTC)?.toEpochMilli()
?: System.currentTimeMillis()
?: Instant.now().toEpochMilli()
)
// Episode runtime has been determined

View File

@@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.integration.provider.ImageProvider
import org.jellyfin.androidtv.util.ImageUtils
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.sdk.isUsable
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
@@ -41,6 +41,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
}
private val api by inject<ApiClient>()
private val imageHelper by inject<ImageHelper>()
override fun onCreate(): Boolean = api.isUsable
@@ -70,7 +71,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
* Gets the resumable items or returns null
*/
private suspend fun searchItems(query: String, limit: Int): BaseItemDtoQueryResult? = try {
val items by api.itemsApi.getItemsByUserId(
val items by api.itemsApi.getItems(
searchTerm = query,
recursive = true,
limit = limit,
@@ -106,7 +107,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
val imageUri = if (item.imageTags?.contains(ImageType.PRIMARY) == true)
ImageProvider.getImageUri(api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY))
else
ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)
imageHelper.getResourceUrl(context!!, R.drawable.tile_land_tv)
cursor.newRow().apply {
add(BaseColumns._ID, item.id)

View File

@@ -4,15 +4,19 @@ import android.service.dreams.DreamService
import androidx.annotation.CallSuper
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner, ViewModelStoreOwner {
@Suppress("LeakingThis")
private val lifecycleRegistry = LifecycleRegistry(this)
@@ -22,6 +26,7 @@ abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
}
override val lifecycle: Lifecycle get() = lifecycleRegistry
override val viewModelStore = ViewModelStore()
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
@CallSuper
@@ -46,9 +51,12 @@ abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
fun setContent(content: @Composable () -> Unit) {
val view = ComposeView(this)
// Set composition strategy
view.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
// Inject dependencies normally added by appcompat activities
view.setViewTreeLifecycleOwner(this)
view.setViewTreeViewModelStoreOwner(this)
view.setViewTreeSavedStateRegistryOwner(this)
// Set content composable

View File

@@ -0,0 +1,168 @@
package org.jellyfin.androidtv.integration.dream
import android.annotation.SuppressLint
import android.content.Context
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.ImageLoader
import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.integration.dream.model.DreamContent
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.ui.playback.AudioEventListener
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackController
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageFormat
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@SuppressLint("StaticFieldLeak")
class DreamViewModel(
private val api: ApiClient,
private val imageLoader: ImageLoader,
private val context: Context,
private val mediaManager: MediaManager,
private val userPreferences: UserPreferences,
) : ViewModel() {
private val _mediaContent = callbackFlow {
trySend(mediaManager.currentAudioItem)
val listener = object : AudioEventListener {
override fun onPlaybackStateChange(
newState: PlaybackController.PlaybackState,
currentItem: BaseItemDto?
) {
trySend(currentItem)
}
override fun onQueueStatusChanged(hasQueue: Boolean) {
trySend(mediaManager.currentAudioItem)
}
}
mediaManager.addAudioEventListener(listener)
awaitClose { mediaManager.removeAudioEventListener(listener) }
}
.distinctUntilChanged()
.map { it?.let(DreamContent::NowPlaying) }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
DreamContent.NowPlaying(mediaManager.currentAudioItem)
)
private val _libraryContent = flow {
// Load first library item after 2 seconds
// to force the logo at the start of the screensaver
emit(null)
delay(2.seconds)
while (true) {
val next = getRandomLibraryShowcase()
if (next != null) {
emit(next)
delay(30.seconds)
} else {
delay(3.seconds)
}
}
}
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val content = combine(_mediaContent, _libraryContent) { mediaContent, libraryContent ->
mediaContent ?: libraryContent ?: DreamContent.Logo
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = _mediaContent.value ?: _libraryContent.value ?: DreamContent.Logo,
)
private suspend fun getRandomLibraryShowcase(): DreamContent.LibraryShowcase? {
val requireParentalRating = userPreferences[UserPreferences.screensaverAgeRatingRequired]
val maxParentalRating = userPreferences[UserPreferences.screensaverAgeRatingMax]
try {
val response by api.itemsApi.getItems(
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
recursive = true,
sortBy = listOf(ItemSortBy.RANDOM),
limit = 5,
imageTypes = listOf(ImageType.BACKDROP),
maxOfficialRating = if (maxParentalRating == -1) null else maxParentalRating.toString(),
hasParentalRating = if (requireParentalRating) true else null,
)
val item = response.items?.firstOrNull { item ->
!item.backdropImageTags.isNullOrEmpty()
} ?: return null
Timber.i("Loading random library showcase item ${item.id}")
val backdropTag = item.backdropImageTags!!.randomOrNull()
?: item.imageTags?.get(ImageType.BACKDROP)
val logoTag = item.imageTags?.get(ImageType.LOGO)
val backdropUrl = api.imageApi.getItemImageUrl(
itemId = item.id,
imageType = ImageType.BACKDROP,
tag = backdropTag,
format = ImageFormat.WEBP,
)
val logoUrl = api.imageApi.getItemImageUrl(
itemId = item.id,
imageType = ImageType.LOGO,
tag = logoTag,
format = ImageFormat.WEBP,
)
val (logo, backdrop) = withContext(Dispatchers.IO) {
val logoDeferred = async {
imageLoader.execute(
request = ImageRequest.Builder(context).data(logoUrl).build()
).drawable?.toBitmap()
}
val backdropDeferred = async {
imageLoader.execute(
request = ImageRequest.Builder(context).data(backdropUrl).build()
).drawable?.toBitmap()
}
awaitAll(logoDeferred, backdropDeferred)
}
if (backdrop == null) {
return null
}
return DreamContent.LibraryShowcase(item, backdrop, logo)
} catch (err: ApiClientException) {
Timber.e(err)
return null
}
}
}

View File

@@ -4,7 +4,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -12,7 +12,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Text
import org.jellyfin.androidtv.integration.dream.model.DreamContent
import org.jellyfin.androidtv.ui.composable.ZoomBox
import org.jellyfin.androidtv.ui.composable.overscan
@@ -47,12 +49,21 @@ fun DreamContentLibraryShowcase(
.align(Alignment.BottomStart)
.overscan(),
) {
Text(
text = content.item.name.orEmpty(),
style = TextStyle(
color = Color.White,
fontSize = 32.sp
),
)
if (content.logo != null) {
Image(
bitmap = content.logo.asImageBitmap(),
contentDescription = content.item.name,
modifier = Modifier
.height(75.dp)
)
} else {
Text(
text = content.item.name.orEmpty(),
style = TextStyle(
color = Color.White,
fontSize = 32.sp
),
)
}
}
}

View File

@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.integration.dream.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@@ -26,5 +27,6 @@ fun DreamContentLogo() = Box(
modifier = Modifier
.align(Alignment.Center)
.width(400.dp)
.fillMaxHeight()
)
}

View File

@@ -13,8 +13,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -24,13 +22,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Text
import org.jellyfin.androidtv.integration.dream.model.DreamContent
import org.jellyfin.androidtv.ui.composable.AsyncImage
import org.jellyfin.androidtv.ui.composable.blurHashPainter
@@ -42,7 +41,7 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.model.api.ImageFormat
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.extensions.ticks
import org.koin.compose.rememberKoinInject
import org.koin.compose.koinInject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@@ -52,8 +51,8 @@ fun DreamContentNowPlaying(
) = Box(
modifier = Modifier.fillMaxSize(),
) {
val api = rememberKoinInject<ApiClient>()
val mediaManager = rememberKoinInject<MediaManager>()
val api = koinInject<ApiClient>()
val mediaManager = koinInject<MediaManager>()
val item = content.item ?: return@Box
val primaryImageTag = item.imageTags?.get(ImageType.PRIMARY)
@@ -63,7 +62,8 @@ fun DreamContentNowPlaying(
else -> null to null
}
val imageBlurHash = imageTag?.let { tag -> item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(tag) }
val imageBlurHash =
imageTag?.let { tag -> item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(tag) }
if (imageBlurHash != null) {
Image(
painter = blurHashPainter(imageBlurHash, IntSize(32, 32)),
@@ -114,11 +114,15 @@ fun DreamContentNowPlaying(
Text(
text = item.run {
if (!artists.isNullOrEmpty()) return@run artists?.joinToString(", ")
val albumArtistNames = albumArtists?.mapNotNull { it.name }
if (!albumArtistNames.isNullOrEmpty()) return@run albumArtistNames.joinToString(", ")
return@run albumArtist
}.orEmpty(),
val artistNames = artists.orEmpty()
val albumArtistNames = albumArtists?.mapNotNull { it.name }.orEmpty()
when {
artistNames.isNotEmpty() -> artistNames
albumArtistNames.isNotEmpty() -> albumArtistNames
else -> listOfNotNull(albumArtist)
}.joinToString(", ")
},
style = TextStyle(
color = Color(0.8f, 0.8f, 0.8f),
fontSize = 18.sp,
@@ -143,12 +147,17 @@ fun DreamContentNowPlaying(
}
}
LinearProgressIndicator(
progress = progress,
color = Color.White,
backgroundColor = Color.White.copy(alpha = 0.2f),
strokeCap = StrokeCap.Round,
modifier = Modifier.fillMaxWidth()
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.drawWithContent {
// Background
drawRect(Color.White, alpha = 0.2f)
// Foreground
drawRect(Color.White, size = size.copy(width = size.width * progress))
}
)
}
}

View File

@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -20,6 +19,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Text
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.composable.overscan
import org.jellyfin.androidtv.ui.composable.rememberCurrentTime

View File

@@ -1,108 +1,25 @@
package org.jellyfin.androidtv.integration.dream.composable
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.integration.dream.model.DreamContent
import org.jellyfin.androidtv.integration.dream.DreamViewModel
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.ui.composable.rememberMediaItem
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.imageApi
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageFormat
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.constant.ItemSortBy
import org.koin.compose.rememberKoinInject
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@Composable
fun DreamHost() {
val api = rememberKoinInject<ApiClient>()
val userPreferences = rememberKoinInject<UserPreferences>()
val mediaManager = rememberKoinInject<MediaManager>()
val imageLoader = rememberKoinInject<ImageLoader>()
val context = LocalContext.current
var libraryShowcase by remember { mutableStateOf<DreamContent.LibraryShowcase?>(null) }
val mediaItem by rememberMediaItem(mediaManager)
LaunchedEffect(true) {
delay(2.seconds)
while (true) {
libraryShowcase = getRandomLibraryShowcase(api, imageLoader, context)
delay(30.seconds)
}
}
val viewModel = koinViewModel<DreamViewModel>()
val userPreferences = koinInject<UserPreferences>()
val content by viewModel.content.collectAsState()
DreamView(
content = when {
mediaItem != null -> DreamContent.NowPlaying(mediaItem)
libraryShowcase != null -> libraryShowcase!!
else -> DreamContent.Logo
},
content = content,
showClock = when (userPreferences[UserPreferences.clockBehavior]) {
ClockBehavior.ALWAYS, ClockBehavior.IN_MENUS -> true
else -> false
}
)
}
private suspend fun getRandomLibraryShowcase(
api: ApiClient,
imageLoader: ImageLoader,
context: Context
): DreamContent.LibraryShowcase? {
try {
val response by api.itemsApi.getItemsByUserId(
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
recursive = true,
sortBy = listOf(ItemSortBy.Random),
limit = 5,
imageTypes = listOf(ImageType.BACKDROP),
)
val item = response.items?.firstOrNull { item ->
!item.backdropImageTags.isNullOrEmpty()
} ?: return null
Timber.i("Loading random library showcase item ${item.id}")
val tag = item.backdropImageTags!!.randomOrNull()
?: item.imageTags?.get(ImageType.BACKDROP)
val backdropUrl = api.imageApi.getItemImageUrl(
itemId = item.id,
imageType = ImageType.BACKDROP,
tag = tag,
format = ImageFormat.WEBP,
)
val backdrop = withContext(Dispatchers.IO) {
imageLoader.execute(
request = ImageRequest.Builder(context).data(backdropUrl).build()
).drawable?.toBitmap()
} ?: return null
return DreamContent.LibraryShowcase(item, backdrop)
} catch (err: ApiClientException) {
Timber.e(err)
return null
}
}

View File

@@ -1,7 +1,6 @@
package org.jellyfin.androidtv.integration.dream.composable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
@@ -13,7 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jellyfin.androidtv.integration.dream.model.DreamContent
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun DreamView(
content: DreamContent,

View File

@@ -5,6 +5,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto
sealed interface DreamContent {
data object Logo : DreamContent
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap) : DreamContent
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap, val logo: Bitmap?) : DreamContent
data class NowPlaying(val item: BaseItemDto?) : DreamContent
}

View File

@@ -6,10 +6,9 @@ import org.jellyfin.androidtv.constant.PosterSize
import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
import org.jellyfin.preference.booleanPreference
import org.jellyfin.preference.enumPreference
import org.jellyfin.preference.stringPreference
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.constant.ItemSortBy
class LibraryPreferences(
displayPreferencesId: String,
@@ -29,7 +28,7 @@ class LibraryPreferences(
val filterUnwatchedOnly = booleanPreference("FilterUnwatchedOnly", false)
// Item sorting
val sortBy = stringPreference("SortBy", ItemSortBy.SortName)
val sortBy = enumPreference("SortBy", ItemSortBy.SORT_NAME)
val sortOrder = enumPreference("SortOrder", SortOrder.ASCENDING)
}
}

View File

@@ -4,7 +4,7 @@ import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
import org.jellyfin.preference.booleanPreference
import org.jellyfin.preference.stringPreference
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.constant.ItemSortBy
import org.jellyfin.sdk.model.api.ItemSortBy
class LiveTvPreferences(
api: ApiClient,
@@ -13,7 +13,7 @@ class LiveTvPreferences(
api = api,
) {
companion object {
val channelOrder = stringPreference("livetv-channelorder", ItemSortBy.DatePlayed)
val channelOrder = stringPreference("livetv-channelorder", ItemSortBy.DATE_PLAYED.name)
val colorCodeGuide = booleanPreference("guide-colorcodedbackgrounds", false)
val favsAtTop = booleanPreference("livetv-favoritechannelsattop", true)
val showHDIndicator = booleanPreference("guide-indicator-hd", false)

View File

@@ -1,9 +1,7 @@
package org.jellyfin.androidtv.preference
import android.content.Context
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
import org.jellyfin.preference.booleanPreference
import org.jellyfin.preference.enumPreference
import org.jellyfin.preference.store.SharedPreferenceStore
import org.jellyfin.preference.stringPreference
@@ -60,11 +58,6 @@ class SystemPreferences(context: Context) : SharedPreferenceStore(
val liveTvGuideFilterSports = booleanPreference("guide_filter_sports", false)
// Other persistent variables
/**
* Chosen player for play with button. Changes every time user chooses a player with "play with" button.
*/
var chosenPlayer = enumPreference("chosen_player", PreferredVideoPlayer.VLC)
/**
* The version name for the latest dismissed beta notification or empty if none.
*/

View File

@@ -4,14 +4,12 @@ import android.content.Context
import android.view.KeyEvent
import androidx.preference.PreferenceManager
import org.jellyfin.androidtv.preference.constant.AppTheme
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
import org.jellyfin.androidtv.preference.constant.RatingType
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
import org.jellyfin.androidtv.preference.constant.defaultAudioBehavior
import org.jellyfin.androidtv.util.DeviceUtils
import org.jellyfin.preference.booleanPreference
import org.jellyfin.preference.enumPreference
import org.jellyfin.preference.intPreference
@@ -30,11 +28,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
) {
companion object {
/**
* The value used for automatic detection in [maxBitrate].
*/
const val MAX_BITRATE_AUTO = "0"
/* Display */
/**
* Select the app theme
@@ -58,8 +51,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
/* Playback - General*/
/**
* Maximum bitrate in megabit for playback. A value of [MAX_BITRATE_AUTO] is used when
* the bitrate should be automatically detected.
* Maximum bitrate in megabit for playback.
*/
var maxBitrate = stringPreference("pref_max_bitrate", "100")
@@ -91,9 +83,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
/* Playback - Video */
/**
* Preferred video player.
* Whether to use an external playback application or not.
*/
var videoPlayer = enumPreference("video_player", PreferredVideoPlayer.EXOPLAYER)
var useExternalPlayer = booleanPreference("external_player", false)
/**
* Change refresh rate to match media when device supports it
@@ -104,32 +96,17 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
/**
* Preferred behavior for audio streaming.
*/
var audioBehaviour = enumPreference("audio_behavior", defaultAudioBehavior)
var audioBehaviour = enumPreference("audio_behavior", AudioBehavior.DIRECT_STREAM)
/**
* Preferred behavior for audio streaming.
*/
var audioNightMode = enumPreference("audio_night_mode", false)
/**
* Enable DTS
*/
var dtsEnabled = booleanPreference("pref_bitstream_dts", false)
/**
* Enable AC3
*/
var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1)
/**
* Default audio delay in milliseconds for libVLC
*/
var libVLCAudioDelay = intPreference("libvlc_audio_delay", 0)
/**
* Use playback rewrite module for audio
*/
var playbackRewriteAudioEnabled = booleanPreference("playback_new_audio", true)
var ac3Enabled = booleanPreference("pref_bitstream_ac3", true)
/* Live TV */
/**
@@ -137,11 +114,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var liveTvDirectPlayEnabled = booleanPreference("pref_live_direct", true)
/**
* Preferred video player for live TV
*/
var liveTvVideoPlayer = enumPreference("live_tv_video_player", PreferredVideoPlayer.AUTO)
/**
* Shortcut used for changing the audio track
*/
@@ -218,6 +190,16 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var screensaverInAppTimeout = longPreference("screensaver_inapp_timeout", 5.minutes.inWholeMilliseconds)
/**
* Age rating used to filter items in the screensaver. Use -1 to disable (omits parameter from requests).
*/
var screensaverAgeRatingMax = intPreference("screensaver_agerating_max", 13)
/**
* Whether items shown in the screensaver are required to have an age rating set.
*/
var screensaverAgeRatingRequired = booleanPreference("screensaver_agerating_required", true)
/**
* Enable reactive homepage
*/

View File

@@ -1,7 +1,6 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.util.DeviceUtils
import org.jellyfin.preference.PreferenceEnum
enum class AudioBehavior(
@@ -17,6 +16,3 @@ enum class AudioBehavior(
*/
DOWNMIX_TO_STEREO(R.string.pref_audio_compat),
}
val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV) AudioBehavior.DOWNMIX_TO_STEREO
else AudioBehavior.DIRECT_STREAM

View File

@@ -2,17 +2,17 @@ package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
import org.jellyfin.sdk.model.constant.ItemSortBy
import org.jellyfin.sdk.model.api.ItemSortBy
enum class LiveTvChannelOrder(
override val nameRes: Int,
val stringValue: String,
) : PreferenceEnum {
LAST_PLAYED(R.string.lbl_guide_option_played, ItemSortBy.DatePlayed),
CHANNEL_NUMBER(R.string.lbl_guide_option_number, ItemSortBy.SortName);
LAST_PLAYED(R.string.lbl_guide_option_played, ItemSortBy.DATE_PLAYED.serialName),
CHANNEL_NUMBER(R.string.lbl_guide_option_number, ItemSortBy.SORT_NAME.serialName);
companion object {
fun fromString(value: String) = values()
fun fromString(value: String) = entries
.firstOrNull { it.stringValue.equals(value, ignoreCase = true) }
?: LAST_PLAYED
}

View File

@@ -1,33 +0,0 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.preference.PreferenceEnum
enum class PreferredVideoPlayer(
override val nameRes: Int,
) : PreferenceEnum {
/**
* Automatically selects between exoplayer and vlc
*/
AUTO(R.string.pref_video_player_auto),
/**
* Force ExoPlayer
*/
EXOPLAYER(R.string.pref_video_player_exoplayer),
/**
* Force libVLC
*/
VLC(R.string.pref_video_player_vlc),
/**
* Use external player
*/
EXTERNAL(R.string.pref_video_player_external),
/**
* Choose a player - play with button
*/
CHOOSE(R.string.pref_video_player_choose),
}

View File

@@ -19,7 +19,7 @@ enum class WatchedIndicatorBehavior(
/**
* Hide unwatched count indicator, show watched check mark on individual episodes only.
*/
EPISODES_ONLY(R.string.lbl_episode_marks),
EPISODES_ONLY(R.string.lbl_hide_watched_checkmark),
/**
* Never show watched indicators.

View File

@@ -119,8 +119,8 @@ abstract class DisplayPreferencesStore(
override fun <V : Enum<V>> setEnum(preference: Preference<*>, value: Enum<V>) =
setString(
preference.key, when (value) {
is PreferenceEnum -> value.serializedName ?: value.toString()
else -> value.toString()
is PreferenceEnum -> value.serializedName ?: value.name
else -> value.name
}
)

View File

@@ -12,11 +12,11 @@ import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.util.BlurHashDecoder
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.math.round
@@ -65,7 +65,7 @@ class AsyncImageView @JvmOverloads constructor(
// Only show blurhash if an image is going to be loaded from the network
if (url != null && blurHash != null) withContext(Dispatchers.IO) {
val blurHashBitmap = BlurHash.decode(
val blurHashBitmap = BlurHashDecoder.decode(
blurHash,
if (aspectRatio > 1) round(blurHashResolution * aspectRatio).toInt() else blurHashResolution,
if (aspectRatio >= 1) blurHashResolution else round(blurHashResolution / aspectRatio).toInt(),
@@ -82,7 +82,10 @@ class AsyncImageView @JvmOverloads constructor(
}.build())
} else {
imageLoader.enqueue(ImageRequest.Builder(context).apply {
crossfade(crossFadeDuration.inWholeMilliseconds.toInt())
val crossFadeDurationMs = crossFadeDuration.inWholeMilliseconds.toInt()
if (crossFadeDurationMs > 0) crossfade(crossFadeDurationMs)
else crossfade(false)
target(this@AsyncImageView)
data(url)
placeholder(placeholderOrBlurHash)

View File

@@ -11,7 +11,9 @@ 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.util.ImageUtils
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
@@ -24,6 +26,8 @@ class ClockUserView @JvmOverloads constructor(
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) {
@@ -31,17 +35,23 @@ class ClockUserView @JvmOverloads constructor(
updateClockVisibility()
}
val homeButton get() = binding.home
init {
updateClockVisibility()
val currentUser = userRepository.currentUser.value
binding.clockUserImage.load(
url = currentUser?.let(ImageUtils::getPrimaryImageUrl),
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)
}
}
private fun updateClockVisibility() {
@@ -53,5 +63,7 @@ class ClockUserView @JvmOverloads constructor(
ClockBehavior.IN_VIDEO -> isVideoPlayer
ClockBehavior.IN_MENUS -> !isVideoPlayer
}
binding.home.isVisible = !isVideoPlayer
}
}

View File

@@ -8,14 +8,15 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
import org.jellyfin.androidtv.util.TimeUtils;
import java.util.Date;
import java.time.LocalDateTime;
public class FriendlyDateButton extends FrameLayout {
private long dateVal;
private LocalDateTime dateVal;
public FriendlyDateButton(Context context, long thisDate, OnClickListener listener) {
public FriendlyDateButton(Context context, LocalDateTime thisDate, OnClickListener listener) {
super(context);
LayoutInflater inflater = LayoutInflater.from(context);
@@ -25,14 +26,13 @@ public class FriendlyDateButton extends FrameLayout {
setOnClickListener(listener);
dateVal = thisDate;
Date date = new Date(thisDate);
((TextView)v.findViewById(R.id.friendlyName)).setText(TimeUtils.getFriendlyDate(context, date, true));
((TextView)v.findViewById(R.id.date)).setText(android.text.format.DateFormat.getDateFormat(context).format(date));
((TextView)v.findViewById(R.id.friendlyName)).setText(TimeUtils.getFriendlyDate(context, thisDate, true));
((TextView)v.findViewById(R.id.date)).setText(DateTimeExtensionsKt.getDateFormatter(context).format(thisDate));
}
public long getDate() { return dateVal; }
public LocalDateTime getDate() { return dateVal; }
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {

View File

@@ -14,25 +14,24 @@ import androidx.core.content.ContextCompat;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideFragment;
import org.jellyfin.androidtv.util.ImageUtils;
import org.jellyfin.androidtv.util.ImageHelper;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.koin.java.KoinJavaComponent;
public class GuideChannelHeader extends RelativeLayout {
private AsyncImageView mChannelImage;
private ImageView mFavImage;
private ChannelInfoDto mChannel;
private BaseItemDto mChannel;
private Context mContext;
private LiveTvGuide mTvGuide;
public GuideChannelHeader(Context context, LiveTvGuide tvGuide, ChannelInfoDto channel) {
public GuideChannelHeader(Context context, LiveTvGuide tvGuide, BaseItemDto channel) {
super(context);
initComponent(context, tvGuide, channel);
}
private void initComponent(Context context, LiveTvGuide tvGuide, ChannelInfoDto channel) {
private void initComponent(Context context, LiveTvGuide tvGuide, BaseItemDto channel) {
mContext = context;
mChannel = channel;
mTvGuide = tvGuide;
@@ -50,13 +49,14 @@ public class GuideChannelHeader extends RelativeLayout {
mChannelImage = findViewById(R.id.channelImage);
mFavImage = findViewById(R.id.favImage);
if (mChannel.getUserData() != null && mChannel.getUserData().getIsFavorite())
if (mChannel.getUserData() != null && mChannel.getUserData().isFavorite())
mFavImage.setVisibility(View.VISIBLE);
}
public void loadImage() {
ImageHelper imageHelper = KoinJavaComponent.<ImageHelper>get(ImageHelper.class);
mChannelImage.load(
ImageUtils.getPrimaryImageUrl(mChannel, KoinJavaComponent.<ApiClient>get(ApiClient.class)),
imageHelper.getPrimaryImageUrl(mChannel,null, ImageHelper.MAX_PRIMARY_IMAGE_HEIGHT),
null,
null,
0.0,
@@ -64,10 +64,11 @@ public class GuideChannelHeader extends RelativeLayout {
);
}
public ChannelInfoDto getChannel() { return mChannel; }
public BaseItemDto getChannel() { return mChannel; }
public void setChannel(BaseItemDto channel) { mChannel = channel; }
public void refreshFavorite() {
if (mChannel.getUserData() != null && mChannel.getUserData().getIsFavorite())
if (mChannel.getUserData() != null && mChannel.getUserData().isFavorite())
mFavImage.setVisibility(View.VISIBLE);
else
mFavImage.setVisibility(View.GONE);
@@ -85,5 +86,4 @@ public class GuideChannelHeader extends RelativeLayout {
setBackground(ContextCompat.getDrawable(mContext, R.drawable.light_border));
}
}
}

View File

@@ -46,19 +46,12 @@ public class ItemListView extends FrameLayout {
mItemIds.clear();
}
public void addItems(List<BaseItemDto> items) {
int i = 0;
for (BaseItemDto item : items) {
addItem(item, i++);
}
}
public void addItem(BaseItemDto item, int ndx) {
mList.addView(new ItemRowView(mContext, item, ndx, mRowSelectedListener, mRowClickedListener));
mItemIds.add(item.getId());
}
public ItemRowView updatePlaying(String id) {
public ItemRowView updatePlaying(UUID id) {
//look through our song rows and update the playing indicator
ItemRowView ret = null;
for (int i = 0; i < mList.getChildCount(); i++) {

View File

@@ -12,7 +12,7 @@ fun ItemListView.refresh() {
val api by KoinJavaComponent.inject<ApiClient>(ApiClient::class.java)
findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
val response by api.itemsApi.getItemsByUserId(
val response by api.itemsApi.getItems(
ids = mItemIds,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,

View File

@@ -16,7 +16,9 @@ import org.jellyfin.androidtv.databinding.ItemRowBinding;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.constant.MediaType;
import org.jellyfin.sdk.model.api.MediaType;
import java.util.UUID;
public class ItemRowView extends FrameLayout {
Context mContext;
@@ -118,7 +120,7 @@ public class ItemRowView extends FrameLayout {
public void updateWatched() {
if (mBaseItem == null) return;
if (MediaType.Video.equals(mBaseItem.getMediaType()) && mBaseItem.getUserData() != null && mBaseItem.getUserData().getPlayed()) {
if (MediaType.VIDEO.equals(mBaseItem.getMediaType()) && mBaseItem.getUserData() != null && mBaseItem.getUserData().getPlayed()) {
mWatchedMark.setText("");
} else {
mWatchedMark.setText("");
@@ -149,7 +151,7 @@ public class ItemRowView extends FrameLayout {
return playing;
}
public boolean setPlaying(String id) {
public boolean setPlaying(UUID id) {
return setPlaying(getItem().getId().equals(id));
}

View File

@@ -1,8 +1,5 @@
package org.jellyfin.androidtv.ui;
import static org.koin.java.KoinJavaComponent.inject;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -18,36 +15,28 @@ import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewTreeLifecycleOwner;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.auth.repository.UserRepository;
import org.jellyfin.androidtv.data.model.DataRefreshService;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
import org.jellyfin.androidtv.ui.livetv.TvManager;
import org.jellyfin.androidtv.util.InfoLayoutHelper;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.koin.java.KoinJavaComponent;
import java.util.Date;
import kotlin.Lazy;
import timber.log.Timber;
import java.time.Instant;
import java.time.LocalDateTime;
public class LiveProgramDetailPopup {
private PopupWindow mPopup;
private BaseItemDto mProgram;
private ProgramGridCell mSelectedProgramView;
private Activity mActivity;
private final Lifecycle lifecycle;
final Context mContext;
final Lifecycle lifecycle;
private LiveTvGuide mTvGuide;
private TextView mDTitle;
private TextView mDSummary;
@@ -65,17 +54,15 @@ public class LiveProgramDetailPopup {
private int mPosLeft;
private int mPosTop;
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
public LiveProgramDetailPopup(Activity activity, Lifecycle lifecycle, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
mActivity = activity;
this.lifecycle = lifecycle;
public LiveProgramDetailPopup(Context context, LifecycleOwner lifecycleOwner, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
mContext = context;
lifecycle = lifecycleOwner.getLifecycle();
mTvGuide = tvGuide;
mTuneAction = tuneAction;
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.program_detail_popup, null);
int popupHeight = Utils.convertDpToPixel(activity, 400);
View layout = LayoutInflater.from(context).inflate(R.layout.program_detail_popup, null);
int popupHeight = Utils.convertDpToPixel(context, 400);
mPopup = new PopupWindow(layout, width, popupHeight);
ViewTreeLifecycleOwner.set(mPopup.getContentView(), lifecycleOwner);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
@@ -96,216 +83,127 @@ public class LiveProgramDetailPopup {
public void setContent(final BaseItemDto program, final ProgramGridCell selectedGridView) {
mProgram = program;
mSelectedProgramView = selectedGridView;
mDTitle.setText(program.getName());
mDTitle.setText(mProgram.getName());
mDButtonRow.removeAllViews();
if (program.getId() == null) {
//empty item, just offer tune button
mFirstButton = createTuneButton();
createFavoriteButton();
mDInfoRow.removeAllViews();
mDTimeline.removeAllViews();
mDSummary.setText("");
return;
}
mDSummary.setText(program.getOverview());
mDSummary.setText(mProgram.getOverview());
if (mDSummary.getLineCount() < 2) {
mDSummary.setGravity(Gravity.CENTER);
} else {
mDSummary.setGravity(Gravity.LEFT);
mDSummary.setGravity(Gravity.START);
}
// build timeline info
TvManager.setTimelineRow(mActivity, mDTimeline, program);
TvManager.setTimelineRow(mContext, mDTimeline, mProgram);
//info row
InfoLayoutHelper.addInfoRow(mActivity, ModelCompat.asSdk(program), mDInfoRow, false, false);
// TODO: Requires LifecycleOwner for compose
// InfoLayoutHelper.addInfoRow(mContext, mProgram, mDInfoRow, false);
//buttons
mFirstButton = null;
Date now = new Date();
Date local = TimeUtils.convertToLocalDate(program.getStartDate());
if (TimeUtils.convertToLocalDate(program.getEndDate()).getTime() > now.getTime()) {
if (local.getTime() <= now.getTime()) {
if (mProgram.getEndDate().isAfter(LocalDateTime.now())) {
if (mProgram.getStartDate().isBefore(LocalDateTime.now())) {
// program in progress - tune first button
mFirstButton = createTuneButton();
}
if (Utils.canManageRecordings(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue())) {
if (program.getTimerId() != null) {
if (mProgram.getTimerId() != null) {
// cancel button
Button cancel = new Button(mActivity);
cancel.setText(mActivity.getResources().getString(R.string.lbl_cancel_recording));
Button cancel = new Button(mContext);
cancel.setText(mContext.getResources().getString(R.string.lbl_cancel_recording));
cancel.setTextColor(Color.WHITE);
cancel.setBackgroundResource(R.drawable.jellyfin_button);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
apiClient.getValue().CancelLiveTvTimerAsync(program.getTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
selectedGridView.setRecTimer(null);
program.setTimerId(null);
dismiss();
Utils.showToast(mActivity, R.string.msg_recording_cancelled);
}
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
}
LiveProgramDetailPopupHelperKt.cancelTimer(LiveProgramDetailPopup.this, mProgram.getTimerId(), () -> {
selectedGridView.setRecTimer(null);
mProgram = LiveProgramDetailPopupHelperKt.copyWithTimerId(mProgram, null);
dismiss();
Utils.showToast(mContext, R.string.msg_recording_cancelled);
return null;
});
}
});
mDButtonRow.addView(cancel);
if (mFirstButton == null) mFirstButton = cancel;
// recording info
mDRecordInfo.setText(local.getTime() <= now.getTime() ? mActivity.getResources().getString(R.string.msg_recording_now) : mActivity.getResources().getString(R.string.msg_will_record));
mDRecordInfo.setText(mProgram.getStartDate().isBefore(LocalDateTime.now()) ? mContext.getResources().getString(R.string.msg_recording_now) : mContext.getResources().getString(R.string.msg_will_record));
} else {
// record button
Button rec = new Button(mActivity);
rec.setText(mActivity.getResources().getString(R.string.lbl_record));
Button rec = new Button(mContext);
rec.setText(mContext.getResources().getString(R.string.lbl_record));
rec.setTextColor(Color.WHITE);
rec.setBackgroundResource(R.drawable.jellyfin_button);
rec.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Create one-off recording with defaults
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
@Override
public void onResponse(SeriesTimerInfoDto response) {
apiClient.getValue().CreateLiveTvTimerAsync(response, new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
// we have to re-retrieve the program to get the timer id
apiClient.getValue().GetLiveTvProgramAsync(mProgram.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
mProgram = response;
mSelectedProgramView.setRecSeriesTimer(response.getSeriesTimerId());
mSelectedProgramView.setRecTimer(response.getTimerId());
Utils.showToast(mActivity, R.string.msg_set_to_record);
dismiss();
}
});
}
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Timber.e(ex, "Error creating recording");
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
});
}
@Override
public void onError(Exception exception) {
Timber.e(exception, "Error creating recording");
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
LiveProgramDetailPopupHelperKt.recordProgram(LiveProgramDetailPopup.this, mProgram.getId(), program -> {
mProgram = program;
mSelectedProgramView.setRecSeriesTimer(program.getSeriesTimerId());
mSelectedProgramView.setRecTimer(program.getTimerId());
if (mSeriesSettingsButton != null)
mSeriesSettingsButton.setVisibility(View.VISIBLE);
Utils.showToast(mContext, R.string.msg_set_to_record);
dismiss();
return null;
});
}
});
mDButtonRow.addView(rec);
if (mFirstButton == null) mFirstButton = rec;
mDRecordInfo.setText(program.getSeriesTimerId() == null ? "" : mActivity.getString(R.string.lbl_episode_not_record));
mDRecordInfo.setText(mProgram.getSeriesTimerId() == null ? "" : mContext.getString(R.string.lbl_episode_not_record));
}
if (Utils.isTrue(program.getIsSeries())) {
if (program.getSeriesTimerId() != null) {
if (Utils.isTrue(mProgram.isSeries())) {
if (mProgram.getSeriesTimerId() != null) {
// cancel series button
Button cancel = new Button(mActivity);
cancel.setText(mActivity.getResources().getString(R.string.lbl_cancel_series));
Button cancel = new Button(mContext);
cancel.setText(mContext.getResources().getString(R.string.lbl_cancel_series));
cancel.setTextColor(Color.WHITE);
cancel.setBackgroundResource(R.drawable.jellyfin_button);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(mActivity)
.setTitle(mActivity.getResources().getString(R.string.lbl_cancel_series))
.setMessage(mActivity.getResources().getString(R.string.msg_cancel_entire_series))
new AlertDialog.Builder(mContext)
.setTitle(mContext.getResources().getString(R.string.lbl_cancel_series))
.setMessage(mContext.getResources().getString(R.string.msg_cancel_entire_series))
.setNegativeButton(R.string.lbl_no, null)
.setPositiveButton(R.string.lbl_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
apiClient.getValue().CancelLiveTvSeriesTimerAsync(program.getSeriesTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
selectedGridView.setRecSeriesTimer(null);
program.setSeriesTimerId(null);
mSeriesSettingsButton.setVisibility(View.GONE);
dismiss();
Utils.showToast(mActivity, R.string.msg_recording_cancelled);
}
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
}
LiveProgramDetailPopupHelperKt.cancelSeriesTimer(LiveProgramDetailPopup.this, mProgram.getSeriesTimerId(), () -> {
selectedGridView.setRecSeriesTimer(null);
mProgram = LiveProgramDetailPopupHelperKt.copyWithSeriesTimerId(mProgram, null);
mSeriesSettingsButton.setVisibility(View.GONE);
dismiss();
Utils.showToast(mContext, R.string.msg_recording_cancelled);
return null;
});
}
}).show();
}
});
mDButtonRow.addView(cancel);
}else {
} else {
// record series button
Button rec = new Button(mActivity);
rec.setText(mActivity.getResources().getString(R.string.lbl_record_series));
Button rec = new Button(mContext);
rec.setText(mContext.getResources().getString(R.string.lbl_record_series));
rec.setTextColor(Color.WHITE);
rec.setBackgroundResource(R.drawable.jellyfin_button);
rec.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Create series recording with defaults
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
@Override
public void onResponse(SeriesTimerInfoDto response) {
apiClient.getValue().CreateLiveTvSeriesTimerAsync(response, new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
// we have to re-retrieve the program to get the timer id
apiClient.getValue().GetLiveTvProgramAsync(mProgram.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
mProgram = response;
mSelectedProgramView.setRecSeriesTimer(response.getSeriesTimerId());
mSelectedProgramView.setRecTimer(response.getTimerId());
if (mSeriesSettingsButton != null) mSeriesSettingsButton.setVisibility(View.VISIBLE);
Utils.showToast(mActivity, R.string.msg_set_to_record);
dismiss();
}
});
}
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Timber.e(ex, "Error creating recording");
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
});
}
@Override
public void onError(Exception exception) {
Timber.e(exception, "Error creating recording");
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
LiveProgramDetailPopupHelperKt.recordSeries(LiveProgramDetailPopup.this, mProgram.getId(), program -> {
mProgram = program;
mSelectedProgramView.setRecSeriesTimer(program.getSeriesTimerId());
mSelectedProgramView.setRecTimer(program.getTimerId());
if (mSeriesSettingsButton != null)
mSeriesSettingsButton.setVisibility(View.VISIBLE);
Utils.showToast(mContext, R.string.msg_set_to_record);
dismiss();
return null;
});
}
});
@@ -313,8 +211,8 @@ public class LiveProgramDetailPopup {
}
// manage series button
mSeriesSettingsButton = new Button(mActivity);
mSeriesSettingsButton.setText(mActivity.getResources().getString(R.string.lbl_series_settings));
mSeriesSettingsButton = new Button(mContext);
mSeriesSettingsButton.setText(mContext.getResources().getString(R.string.lbl_series_settings));
mSeriesSettingsButton.setTextColor(Color.WHITE);
mSeriesSettingsButton.setBackgroundResource(R.drawable.jellyfin_button);
mSeriesSettingsButton.setVisibility(mProgram.getSeriesTimerId() != null ? View.VISIBLE : View.GONE);
@@ -330,27 +228,18 @@ public class LiveProgramDetailPopup {
}
if (local.getTime() > now.getTime()) {
if (mProgram.getStartDate().isAfter(LocalDateTime.now())) {
// add tune to button for programs that haven't started yet
createTuneButton();
}
createFavoriteButton();
} else {
// program has already ended
mDRecordInfo.setText(mActivity.getResources().getString(R.string.lbl_program_ended));
mDRecordInfo.setText(mContext.getResources().getString(R.string.lbl_program_ended));
mFirstButton = createTuneButton();
}
// if (program.getIsMovie()) {
// mDSimilarRow.setVisibility(View.VISIBLE);
// mPopup.setHeight(MOVIE_HEIGHT);
// } else {
mDSimilarRow.setVisibility(View.GONE);
// mPopup.setHeight(NORMAL_HEIGHT);
//
// }
}
public Button createTuneButton() {
@@ -367,22 +256,19 @@ public class LiveProgramDetailPopup {
}
public android.widget.ImageButton createFavoriteButton() {
ChannelInfoDto channel = TvManager.getChannel(TvManager.getAllChannelsIndex(mProgram.getChannelId()));
boolean isFav = channel.getUserData() != null && channel.getUserData().getIsFavorite();
BaseItemDto channel = TvManager.getChannel(TvManager.getAllChannelsIndex(mProgram.getChannelId()));
boolean isFav = channel.getUserData() != null && channel.getUserData().isFavorite();
android.widget.ImageButton fave = addImgButton(mDButtonRow, isFav ? R.drawable.ic_heart_red : R.drawable.ic_heart);
fave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
apiClient.getValue().UpdateFavoriteStatusAsync(channel.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), !channel.getUserData().getIsFavorite(), new Response<UserItemDataDto>() {
@Override
public void onResponse(UserItemDataDto response) {
channel.setUserData(response);
fave.setImageDrawable(ContextCompat.getDrawable(mActivity, response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart));
mTvGuide.refreshFavorite(channel.getId());
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
dataRefreshService.setLastFavoriteUpdate(System.currentTimeMillis());
}
LiveProgramDetailPopupHelperKt.toggleFavorite(LiveProgramDetailPopup.this, channel, channel -> {
fave.setImageDrawable(ContextCompat.getDrawable(mContext, channel.getUserData().isFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart));
mTvGuide.refreshFavorite(channel.getId());
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
dataRefreshService.setLastFavoriteUpdate(Instant.now());
return null;
});
}
});
@@ -391,16 +277,16 @@ public class LiveProgramDetailPopup {
}
private android.widget.ImageButton addImgButton(LinearLayout layout, int imgResource) {
android.widget.ImageButton btn = new android.widget.ImageButton(mActivity);
btn.setImageDrawable(ContextCompat.getDrawable(mActivity, imgResource));
android.widget.ImageButton btn = new android.widget.ImageButton(mContext);
btn.setImageDrawable(ContextCompat.getDrawable(mContext, imgResource));
btn.setBackgroundResource(R.drawable.jellyfin_button);
layout.addView(btn);
return btn;
}
private Button addButton(LinearLayout layout, int stringResource) {
Button btn = new Button(mActivity);
btn.setText(mActivity.getResources().getString(stringResource));
Button btn = new Button(mContext);
btn.setText(mContext.getResources().getString(stringResource));
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(R.drawable.jellyfin_button);
layout.addView(btn);
@@ -424,15 +310,13 @@ public class LiveProgramDetailPopup {
private RecordPopup mRecordPopup;
public void showRecordingOptions(final boolean recordSeries) {
if (mRecordPopup == null) mRecordPopup = new RecordPopup(mActivity, lifecycle, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
apiClient.getValue().GetLiveTvSeriesTimerAsync(mProgram.getSeriesTimerId(), new Response<SeriesTimerInfoDto>() {
@Override
public void onResponse(SeriesTimerInfoDto response) {
mRecordPopup.setContent(mActivity, ModelCompat.asSdk(mProgram), response, mSelectedProgramView, recordSeries);
mRecordPopup.show();
}
if (mRecordPopup == null)
mRecordPopup = new RecordPopup(mContext, lifecycle, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
LiveProgramDetailPopupHelperKt.getSeriesTimer(this, mProgram.getSeriesTimerId(), seriesTimer -> {
mRecordPopup.setContent(mContext, mProgram, seriesTimer, mSelectedProgramView, recordSeries);
mRecordPopup.show();
return null;
});
}
}

View File

@@ -0,0 +1,151 @@
package org.jellyfin.androidtv.ui
import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
import org.jellyfin.androidtv.util.getActivity
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
import org.jellyfin.sdk.model.api.TimerInfoDto
import org.koin.android.ext.android.inject
import java.util.UUID
fun BaseItemDto.copyWithTimerId(
timerId: String?,
) = copy(
timerId = timerId,
)
fun BaseItemDto.copyWithSeriesTimerId(
seriesTimerId: String?,
) = copy(
seriesTimerId = seriesTimerId,
)
fun LiveProgramDetailPopup.cancelTimer(
timerId: String,
callback: () -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
api.liveTvApi.cancelTimer(timerId)
}.onSuccess {
callback()
}
}
}
fun LiveProgramDetailPopup.cancelSeriesTimer(
seriesTimerId: String,
callback: () -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
api.liveTvApi.cancelSeriesTimer(seriesTimerId)
}.onSuccess {
callback()
}
}
}
fun SeriesTimerInfoDto.asTimerInfoDto() = TimerInfoDto(
id = id,
type = type,
serverId = serverId,
externalId = externalId,
channelId = channelId,
externalChannelId = externalChannelId,
channelName = channelName,
channelPrimaryImageTag = channelPrimaryImageTag,
programId = programId,
externalProgramId = externalProgramId,
name = name,
overview = overview,
startDate = startDate,
endDate = endDate,
serviceName = serviceName,
priority = priority,
prePaddingSeconds = prePaddingSeconds,
postPaddingSeconds = postPaddingSeconds,
isPrePaddingRequired = isPrePaddingRequired,
parentBackdropItemId = parentBackdropItemId,
parentBackdropImageTags = parentBackdropImageTags,
isPostPaddingRequired = isPostPaddingRequired,
keepUntil = keepUntil,
)
fun LiveProgramDetailPopup.recordProgram(
programId: UUID,
callback: (program: BaseItemDto) -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
val seriesTimer by api.liveTvApi.getDefaultTimer(programId.toString())
val timer = seriesTimer.asTimerInfoDto()
api.liveTvApi.createTimer(timer)
api.liveTvApi.getProgram(programId.toString()).content
}.onSuccess { program ->
callback(program)
}
}
}
fun LiveProgramDetailPopup.recordSeries(
programId: UUID,
callback: (program: BaseItemDto) -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
val timer by api.liveTvApi.getDefaultTimer(programId.toString())
api.liveTvApi.createSeriesTimer(timer)
api.liveTvApi.getProgram(programId.toString()).content
}.onSuccess { program ->
callback(program)
}
}
}
fun LiveProgramDetailPopup.getSeriesTimer(
seriesTimerId: String,
callback: (seriesTimer: SeriesTimerInfoDto) -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
api.liveTvApi.getSeriesTimer(seriesTimerId).content
}.onSuccess { seriesTimer ->
callback(seriesTimer)
}
}
}
fun LiveProgramDetailPopup.toggleFavorite(
item: BaseItemDto,
callback: (item: BaseItemDto) -> Unit,
) {
val itemMutationRepository by mContext.getActivity()!!.inject<ItemMutationRepository>()
lifecycle.coroutineScope.launch {
runCatching {
val userData = itemMutationRepository.setFavorite(
item = item.id,
favorite = !(item.userData?.isFavorite ?: false)
)
item.copy(userData = userData)
}.onSuccess { item ->
callback(item)
}
}
}

View File

@@ -14,7 +14,7 @@ import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.playback.AudioEventListener
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackController
import org.jellyfin.androidtv.util.ImageUtils
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.TimeUtils
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -29,6 +29,7 @@ class NowPlayingView @JvmOverloads constructor(
private val mediaManager by inject<MediaManager>()
private val navigationRepository by inject<NavigationRepository>()
private val imageHelper by inject<ImageHelper>()
private var currentDuration: String = ""
init {
@@ -63,7 +64,7 @@ class NowPlayingView @JvmOverloads constructor(
private fun setInfo(item: org.jellyfin.sdk.model.api.BaseItemDto) {
val placeholder = ContextCompat.getDrawable(context, R.drawable.ic_album)
val blurHash = item.imageBlurHashes?.get(org.jellyfin.sdk.model.api.ImageType.PRIMARY)?.get(item.imageTags?.get(org.jellyfin.sdk.model.api.ImageType.PRIMARY))
binding.npIcon.load(ImageUtils.getPrimaryImageUrl(item), blurHash, placeholder, item.primaryImageAspectRatio ?: 1.0)
binding.npIcon.load(imageHelper.getPrimaryImageUrl(item), blurHash, placeholder, item.primaryImageAspectRatio ?: 1.0)
currentDuration = TimeUtils.formatMillis(if (item.runTimeTicks != null) item.runTimeTicks!! / 10_000 else 0)
binding.npDesc.text = if (item.albumArtist != null) item.albumArtist else item.name

View File

@@ -17,14 +17,12 @@ import android.widget.TextView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.preference.LiveTvPreferences;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
import org.jellyfin.androidtv.util.InfoLayoutHelper;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemDto;
import java.util.Date;
import java.time.LocalDateTime;
public class ProgramGridCell extends RelativeLayout implements RecordingIndicatorView {
@@ -60,37 +58,37 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
setCellBackground();
if (program.getStartDate() != null && program.getEndDate() != null) {
Date localStart = TimeUtils.convertToLocalDate(program.getStartDate());
if (localStart.getTime() + 60000 < activity.getCurrentLocalStartDate()) {
LocalDateTime localStart = program.getStartDate();
if (localStart.plusMinutes(1).isBefore(activity.getCurrentLocalStartDate())) {
mProgramName.setText("<< "+mProgramName.getText());
TextView time = new TextView(context);
time.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
time.setTextSize(12);
time.setText(android.text.format.DateFormat.getTimeFormat(getContext()).format(TimeUtils.convertToLocalDate(program.getStartDate())));
time.setText(DateTimeExtensionsKt.getTimeFormatter(getContext()).format(program.getStartDate()));
mInfoRow.addView(time);
}
}
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemExtensionsKt.isNew(ModelCompat.asSdk(program)) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.getIsPremiere()))) {
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_new), 10, Color.GRAY, R.drawable.dark_green_gradient);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemExtensionsKt.isNew(program) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.isPremiere()))) {
addBlockText(context.getString(R.string.lbl_new), 10, Color.GRAY, R.drawable.dark_green_gradient);
}
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) && Utils.isTrue(program.getIsPremiere())) {
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_premiere), 10, Color.GRAY, R.drawable.dark_green_gradient);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) && Utils.isTrue(program.isPremiere())) {
addBlockText(context.getString(R.string.lbl_premiere), 10, Color.GRAY, R.drawable.dark_green_gradient);
}
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowRepeatIndicator()) && Utils.isTrue(program.getIsRepeat())) {
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_repeat), 10, Color.GRAY, androidx.leanback.R.color.lb_default_brand_color);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowRepeatIndicator()) && Utils.isTrue(program.isRepeat())) {
addBlockText(context.getString(R.string.lbl_repeat), 10, Color.GRAY, androidx.leanback.R.color.lb_default_brand_color);
}
if (program.getOfficialRating() != null && !program.getOfficialRating().equals("0")) {
InfoLayoutHelper.addBlockText(context, mInfoRow, program.getOfficialRating(), 10);
addBlockText(program.getOfficialRating(), 10, Color.BLACK, R.drawable.block_text_bg);
}
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowHDIndicator()) && Utils.isTrue(program.getIsHD())) {
InfoLayoutHelper.addBlockText(context, mInfoRow, "HD", 10);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowHDIndicator()) && Utils.isTrue(program.isHd())) {
addBlockText("HD", 10, Color.BLACK, R.drawable.block_text_bg);
}
if (program.getSeriesTimerId() != null) {
@@ -111,17 +109,29 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
}
private void addBlockText(String text, int size, int textColor, int backgroundRes) {
TextView view = new TextView(getContext());
view.setTextSize(size);
view.setTextColor(textColor);
view.setText(" " + text + " ");
view.setBackgroundResource(backgroundRes);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
params.setMargins(0, Utils.convertDpToPixel(getContext(), -2), 0, 0);
view.setLayoutParams(params);
mInfoRow.addView(view);
}
public void setCellBackground() {
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getColorCodeGuide())) {
if (Utils.isTrue(mProgram.getIsMovie())) {
if (Utils.isTrue(mProgram.isMovie())) {
mBackgroundColor = getResources().getColor(R.color.guide_movie_bg);
} else if (Utils.isTrue(mProgram.getIsNews())) {
} else if (Utils.isTrue(mProgram.isNews())) {
mBackgroundColor = getResources().getColor(R.color.guide_news_bg);
} else if (Utils.isTrue(mProgram.getIsSports())) {
} else if (Utils.isTrue(mProgram.isSports())) {
mBackgroundColor = getResources().getColor(R.color.guide_sports_bg);
} else if (Utils.isTrue(mProgram.getIsKids())) {
} else if (Utils.isTrue(mProgram.isKids())) {
mBackgroundColor = getResources().getColor(R.color.guide_kids_bg);
}
@@ -150,11 +160,13 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
public boolean isFirst() { return isFirst; }
public void setRecTimer(String id) {
mProgram.setTimerId(id);
if (mProgram == null) return;
mProgram = LiveProgramDetailPopupHelperKt.copyWithTimerId(mProgram, id);
mRecIndicator.setImageResource(id != null ? (mProgram.getSeriesTimerId() != null ? R.drawable.ic_record_series_red : R.drawable.ic_record_red) : mProgram.getSeriesTimerId() != null ? R.drawable.ic_record_series : R.drawable.blank10x10);
}
public void setRecSeriesTimer(String id) {
mProgram.setSeriesTimerId(id);
if (mProgram == null) return;
mProgram = LiveProgramDetailPopupHelperKt.copyWithSeriesTimerId(mProgram, id);
mRecIndicator.setImageResource(id != null ? R.drawable.ic_record_series_red : R.drawable.blank10x10);
}
}

View File

@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui;
import static org.koin.java.KoinJavaComponent.inject;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
@@ -23,29 +22,28 @@ import android.widget.TextView;
import androidx.lifecycle.Lifecycle;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.auth.repository.UserRepository;
import org.jellyfin.androidtv.constant.CustomMessage;
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
import org.jellyfin.androidtv.util.ContextExtensionsKt;
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.koin.java.KoinJavaComponent;
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto;
import org.jellyfin.sdk.model.api.TimerInfoDto;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;
import kotlin.Lazy;
public class RecordPopup {
PopupWindow mPopup;
String mProgramId;
UUID mProgramId;
SeriesTimerInfoDto mCurrentOptions;
RecordingIndicatorView mSelectedView;
View mAnchorView;
@@ -53,8 +51,8 @@ public class RecordPopup {
int mPosTop;
boolean mRecordSeries;
Activity mActivity;
private final Lifecycle lifecycle;
Context mContext;
final Lifecycle lifecycle;
TextView mDTitle;
LinearLayout mDTimeline;
View mSeriesOptions;
@@ -70,43 +68,41 @@ public class RecordPopup {
ArrayList<String> mPaddingDisplayOptions;
ArrayList<Integer> mPaddingValues = new ArrayList<>(Arrays.asList(0,60,300,900,1800,3600,5400,7200,10800));
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
private Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
public RecordPopup(Activity activity, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
mActivity = activity;
public RecordPopup(Context context, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
mContext = context;
this.lifecycle = lifecycle;
mAnchorView = anchorView;
mPosLeft = left;
mPosTop = top;
mPaddingDisplayOptions = new ArrayList<>(Arrays.asList(
mActivity.getString(R.string.lbl_on_schedule),
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 1),
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 5),
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 15),
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 30),
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 60),
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 90),
ContextExtensionsKt.getQuantityString(activity, R.plurals.hours, 2),
ContextExtensionsKt.getQuantityString(activity, R.plurals.hours, 3)
context.getString(R.string.lbl_on_schedule),
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 1),
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 5),
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 15),
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 30),
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 60),
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 90),
ContextExtensionsKt.getQuantityString(context, R.plurals.hours, 2),
ContextExtensionsKt.getQuantityString(context, R.plurals.hours, 3)
));
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.new_program_record_popup, null);
int popupHeight = Utils.convertDpToPixel(activity, 330);
View layout = LayoutInflater.from(context).inflate(R.layout.new_program_record_popup, null);
int popupHeight = Utils.convertDpToPixel(context, 330);
mPopup = new PopupWindow(layout, width, popupHeight);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
mDTitle = (TextView)layout.findViewById(R.id.title);
mDTitle = layout.findViewById(R.id.title);
mPrePadding = (Spinner) layout.findViewById(R.id.prePadding);
mPrePadding.setAdapter(new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
mPrePadding = layout.findViewById(R.id.prePadding);
mPrePadding.setAdapter(new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
mPrePadding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
mCurrentOptions.setPrePaddingSeconds(mPaddingValues.get(position));
mCurrentOptions = RecordPopupHelperKt.copyWithPrePaddingSeconds(mCurrentOptions, mPaddingValues.get(position));
}
@Override
@@ -115,11 +111,11 @@ public class RecordPopup {
}
});
mPostPadding = (Spinner) layout.findViewById(R.id.postPadding);
mPostPadding.setAdapter(new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
mPostPadding.setAdapter(new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
mPostPadding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
mCurrentOptions.setPostPaddingSeconds(mPaddingValues.get(position));
mCurrentOptions = RecordPopupHelperKt.copyWithPostPaddingSeconds(mCurrentOptions, mPaddingValues.get(position));
}
@Override
@@ -128,68 +124,39 @@ public class RecordPopup {
}
});
mOnlyNew = (CheckBox) layout.findViewById(R.id.onlyNew);
mAnyChannel = (CheckBox) layout.findViewById(R.id.anyChannel);
mAnyTime = (CheckBox) layout.findViewById(R.id.anyTime);
mOnlyNew = layout.findViewById(R.id.onlyNew);
mAnyChannel = layout.findViewById(R.id.anyChannel);
mAnyTime = layout.findViewById(R.id.anyTime);
mSeriesOptions = layout.findViewById(R.id.seriesOptions);
mOkButton = (Button) layout.findViewById(R.id.okButton);
mOkButton = layout.findViewById(R.id.okButton);
mOkButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mRecordSeries) {
mCurrentOptions.setRecordNewOnly(mOnlyNew.isChecked());
mCurrentOptions.setRecordAnyChannel(mAnyChannel.isChecked());
mCurrentOptions.setRecordAnyTime(mAnyTime.isChecked());
apiClient.getValue().UpdateLiveTvSeriesTimerAsync(mCurrentOptions, new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
mPopup.dismiss();
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
Utils.showToast(mActivity, R.string.msg_settings_updated);
}
@Override
public void onError(Exception ex) {
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
mCurrentOptions = RecordPopupHelperKt.copyWithFilters(mCurrentOptions, mOnlyNew.isChecked(), mAnyChannel.isChecked(), mAnyTime.isChecked());
RecordPopupHelperKt.updateSeriesTimer(RecordPopup.this, mCurrentOptions, () -> {
mPopup.dismiss();
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
Utils.showToast(mContext, R.string.msg_settings_updated);
return null;
});
} else {
TimerInfoDto updated = new TimerInfoDto();
updated.setProgramId(mProgramId);
updated.setPrePaddingSeconds(mCurrentOptions.getPrePaddingSeconds());
updated.setPostPaddingSeconds(mCurrentOptions.getPostPaddingSeconds());
updated.setIsPrePaddingRequired(mCurrentOptions.getIsPrePaddingRequired());
updated.setIsPostPaddingRequired(mCurrentOptions.getIsPostPaddingRequired());
TimerInfoDto updated = RecordPopupHelperKt.createProgramTimerInfo(mProgramId, mCurrentOptions);
apiClient.getValue().UpdateLiveTvTimerAsync(updated, new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
mPopup.dismiss();
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
// we have to re-retrieve the program to get the timer id
apiClient.getValue().GetLiveTvProgramAsync(mProgramId, KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<org.jellyfin.apiclient.model.dto.BaseItemDto>() {
@Override
public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) {
mSelectedView.setRecTimer(response.getTimerId());
mSelectedView.setRecSeriesTimer(response.getSeriesTimerId());
}
});
Utils.showToast(mActivity, R.string.msg_set_to_record);
}
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
RecordPopupHelperKt.updateTimer(RecordPopup.this, updated, () -> {
mPopup.dismiss();
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
// we have to re-retrieve the program to get the timer id
RecordPopupHelperKt.getLiveTvProgram(RecordPopup.this, mProgramId, program -> {
mSelectedView.setRecTimer(program.getTimerId());
mSelectedView.setRecSeriesTimer(program.getSeriesTimerId());
return null;
});
Utils.showToast(mContext, R.string.msg_set_to_record);
return null;
});
}
}
@@ -211,7 +178,7 @@ public class RecordPopup {
}
public void setContent(Context context, BaseItemDto program, SeriesTimerInfoDto current, RecordingIndicatorView selectedView, boolean recordSeries) {
mProgramId = program.getId().toString();
mProgramId = program.getId();
mCurrentOptions = current;
mRecordSeries = recordSeries;
mSelectedView = selectedView;
@@ -263,17 +230,24 @@ public class RecordPopup {
timelineRow.removeAllViews();
if (program.getStartDate() == null) return;
Date local = TimeUtils.getDate(program.getStartDate());
TextView on = new TextView(mActivity);
on.setText(mActivity.getString(R.string.lbl_on));
LocalDateTime local = program.getStartDate();
TextView on = new TextView(mContext);
on.setText(mContext.getString(R.string.lbl_on));
timelineRow.addView(on);
TextView channel = new TextView(mActivity);
TextView channel = new TextView(mContext);
channel.setText(program.getChannelName());
channel.setTypeface(null, Typeface.BOLD);
channel.setTextColor(mActivity.getResources().getColor(android.R.color.holo_blue_light));
channel.setTextColor(mContext.getResources().getColor(android.R.color.holo_blue_light));
timelineRow.addView(channel);
TextView datetime = new TextView(mActivity);
datetime.setText(TimeUtils.getFriendlyDate(context, local)+ " @ "+android.text.format.DateFormat.getTimeFormat(mActivity).format(local)+ " ("+ DateUtils.getRelativeTimeSpanString(local.getTime())+")");
TextView datetime = new TextView(mContext);
datetime.setText(new StringBuilder()
.append(TimeUtils.getFriendlyDate(context, local))
.append(" @ ")
.append(DateTimeExtensionsKt.getTimeFormatter(mContext).format(local))
.append(" (")
.append(DateUtils.getRelativeTimeSpanString(local.toInstant(ZoneOffset.UTC).toEpochMilli(), Instant.now().toEpochMilli(), 0))
.append(")")
);
timelineRow.addView(datetime);
}

View File

@@ -0,0 +1,94 @@
package org.jellyfin.androidtv.ui
import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.util.getActivity
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
import org.jellyfin.sdk.model.api.TimerInfoDto
import org.koin.android.ext.android.inject
import java.util.UUID
fun SeriesTimerInfoDto.copyWithPrePaddingSeconds(
prePaddingSeconds: Int,
) = copy(
prePaddingSeconds = prePaddingSeconds,
)
fun SeriesTimerInfoDto.copyWithPostPaddingSeconds(
postPaddingSeconds: Int,
) = copy(
postPaddingSeconds = postPaddingSeconds,
)
fun SeriesTimerInfoDto.copyWithFilters(
recordNewOnly: Boolean,
recordAnyChannel: Boolean,
recordAnyTime: Boolean,
) = copy(
recordNewOnly = recordNewOnly,
recordAnyChannel = recordAnyChannel,
recordAnyTime = recordAnyTime,
)
fun RecordPopup.updateSeriesTimer(
seriesTimer: SeriesTimerInfoDto,
callback: () -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
val id = seriesTimer.id
if (id == null) api.liveTvApi.createSeriesTimer(seriesTimer)
else api.liveTvApi.updateSeriesTimer(id, seriesTimer)
}.onSuccess {
callback()
}
}
}
fun RecordPopup.updateTimer(
timer: TimerInfoDto,
callback: () -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
val id = timer.id
if (id == null) api.liveTvApi.createTimer(timer)
else api.liveTvApi.updateTimer(id, timer)
}.onSuccess {
callback()
}
}
}
fun createProgramTimerInfo(
programId: UUID,
options: SeriesTimerInfoDto,
) = TimerInfoDto(
programId = programId.toString(),
prePaddingSeconds = options.prePaddingSeconds,
postPaddingSeconds = options.postPaddingSeconds,
isPrePaddingRequired = options.isPrePaddingRequired,
isPostPaddingRequired = options.isPostPaddingRequired,
)
fun RecordPopup.getLiveTvProgram(
id: UUID,
callback: (program: BaseItemDto) -> Unit,
) {
val api by mContext.getActivity()!!.inject<ApiClient>()
lifecycle.coroutineScope.launch {
runCatching {
api.liveTvApi.getProgram(id.toString()).content
}.onSuccess { program ->
callback(program)
}
}
}

View File

@@ -18,11 +18,20 @@ class ScreensaverViewModel(
) : ViewModel() {
private var timer: Job? = null
private var locks = 0
val enabled get() = userPreferences[UserPreferences.screensaverInAppEnabled]
// Preferences
val inAppEnabled get() = userPreferences[UserPreferences.screensaverInAppEnabled]
private val timeout get() = userPreferences[UserPreferences.screensaverInAppTimeout].milliseconds
// State
private val _visible = MutableStateFlow(false)
val visible get() = _visible.asStateFlow()
private val _keepScreenOn = MutableStateFlow(inAppEnabled)
val keepScreenOn get() = _keepScreenOn.asStateFlow()
var activityPaused: Boolean = false
set(value) {
field = value
@@ -38,17 +47,20 @@ class ScreensaverViewModel(
timer?.cancel()
// Hide when interacted with allowed cancelation or when disabled
if (_visible.value && (canCancel || !enabled || activityPaused)) {
if (_visible.value && (canCancel || !inAppEnabled || activityPaused)) {
_visible.value = false
}
// Create new timer to show screensaver when enabled
if (enabled && !activityPaused && locks == 0) {
if (inAppEnabled && !activityPaused && locks == 0) {
timer = viewModelScope.launch {
delay(timeout)
_visible.value = true
}
}
// Update KEEP_SCREEN_ON flag value
_keepScreenOn.value = inAppEnabled || locks > 0
}
/**

View File

@@ -1,55 +0,0 @@
package org.jellyfin.androidtv.ui
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
import androidx.core.view.isVisible
import org.jellyfin.androidtv.databinding.SettingsPopupBinding
import org.jellyfin.androidtv.util.dp
class SettingsPopup(
private val context: Context,
private val anchor: View,
audioChangedListener: ValueChangedListener<Long>?,
subtitleChangedListener: ValueChangedListener<Long>?
) {
private val binding = SettingsPopupBinding.inflate(LayoutInflater.from(context), null, false)
.apply {
subtitleDelay.increment = SUBTITLE_DELAY_INCREMENT
subtitleDelay.valueChangedListener = subtitleChangedListener
audioDelay.valueChangedListener = audioChangedListener
}
val popupWindow = PopupWindow(binding.root, WIDTH.dp(context), FULL_HEIGHT.dp(context))
.apply {
isFocusable = true
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
fun show(
hasSubtitles: Boolean,
audioDelay: Long,
subtitleDelay: Long
) {
binding.audioDelay.value = audioDelay
binding.subtitleDelayTxt.isVisible = hasSubtitles
binding.subtitleDelay.isVisible = hasSubtitles
binding.subtitleDelay.value = subtitleDelay
popupWindow.height = (if (hasSubtitles) FULL_HEIGHT else SINGLE_HEIGHT).dp(context)
popupWindow.showAsDropDown(anchor, 0, 0, Gravity.END)
}
companion object {
private const val WIDTH = 240
private const val FULL_HEIGHT = 230
private const val SINGLE_HEIGHT = 130
private const val SUBTITLE_DELAY_INCREMENT = 500L
}
}

View File

@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.background
import android.graphics.drawable.ColorDrawable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
@@ -18,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
@@ -25,16 +25,17 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.data.service.BackgroundService
import org.koin.compose.rememberKoinInject
import org.koin.compose.koinInject
@Composable
private fun AppThemeBackground() {
val context = LocalContext.current
val themeBackground = remember(context.theme) {
val attributes = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
val attributes = context.theme.obtainStyledAttributes(intArrayOf(R.attr.defaultBackground))
val drawable = attributes.getDrawable(0)
attributes.recycle()
@@ -60,30 +61,35 @@ private fun AppThemeBackground() {
}
@Composable
@OptIn(ExperimentalAnimationApi::class)
fun AppBackground() {
val backgroundService = rememberKoinInject<BackgroundService>()
val backgroundService = koinInject<BackgroundService>()
val currentBackground by backgroundService.currentBackground.collectAsState()
val blurBackground by backgroundService.blurBackground.collectAsState()
val enabled by backgroundService.enabled.collectAsState()
AnimatedContent(
targetState = currentBackground,
transitionSpec = {
val duration = (BackgroundService.TRANSITION_DURATION.inWholeMilliseconds / 2).toInt()
fadeIn(tween(durationMillis = duration)) togetherWith fadeOut(snap(delayMillis = duration))
},
label = "BackgroundTransition",
) { background ->
if (background != null) {
Image(
bitmap = background,
contentDescription = null,
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
modifier = Modifier.fillMaxSize()
)
} else {
AppThemeBackground()
if (enabled) {
AnimatedContent(
targetState = currentBackground,
transitionSpec = {
val duration = (BackgroundService.TRANSITION_DURATION.inWholeMilliseconds / 2).toInt()
fadeIn(tween(durationMillis = duration)) togetherWith fadeOut(snap(delayMillis = duration))
},
label = "BackgroundTransition",
) { background ->
if (background != null) {
Image(
bitmap = background,
contentDescription = null,
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
modifier = Modifier
.fillMaxSize()
.then(if (blurBackground) Modifier.blur(10.dp) else Modifier)
)
} else {
AppThemeBackground()
}
}
}
}

View File

@@ -1,10 +1,6 @@
package org.jellyfin.androidtv.ui.browsing
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.HeaderItem
import androidx.leanback.widget.ListRow
@@ -20,17 +16,12 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.jellyfin.androidtv.constant.Extras
import org.jellyfin.androidtv.data.service.BackgroundService
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.UserPreferences.Companion.clockBehavior
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.ui.ClockUserView
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.presentation.CardPresenter
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter
import org.jellyfin.androidtv.util.Utils.convertDpToPixel
import org.jellyfin.sdk.model.api.BaseItemDto
import org.koin.android.ext.android.inject
@@ -40,8 +31,8 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
protected var rows = mutableListOf<BrowseRowDef>()
private val userPreferences by inject<UserPreferences>()
private val backgroundService by inject<BackgroundService>()
private val itemLauncher by inject<ItemLauncher>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -57,10 +48,9 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
// Add event listeners
onItemViewClickedListener = OnItemViewClickedListener { _: Presenter.ViewHolder?, item: Any?, _: RowPresenter.ViewHolder?, row: Row ->
if (item is BaseRowItem) {
ItemLauncher.launch(
itemLauncher.launch(
item,
(row as ListRow).adapter as ItemRowAdapter,
item.index,
requireContext()
)
}
@@ -70,7 +60,7 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
backgroundService.clearBackgrounds()
} else {
val adapter = (row as? ListRow)?.adapter
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(item.index.toLong())
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(adapter.indexOf(item))
backgroundService.setBackground(item.baseItem)
}
@@ -112,31 +102,4 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
}
}
}
override fun onStart() {
super.onStart()
// Check if clock should be visible
val showClock = userPreferences[clockBehavior]
if (showClock !== ClockBehavior.ALWAYS && showClock !== ClockBehavior.IN_MENUS) return
val root = requireActivity().findViewById<ViewGroup>(androidx.leanback.R.id.browse_frame)
// Move the title to the left to make way for the clock
val titleView = root.findViewById<TextView>(androidx.leanback.R.id.title_text)
if (titleView != null) {
val layoutParams = titleView.layoutParams as FrameLayout.LayoutParams
layoutParams.rightMargin = convertDpToPixel(root.context, 120)
titleView.layoutParams = layoutParams
}
// Add the clock element
val layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val clockView = ClockUserView(root.context, null)
layoutParams.gravity = Gravity.TOP or Gravity.END
layoutParams.rightMargin = convertDpToPixel(root.context, 40)
layoutParams.topMargin = convertDpToPixel(root.context, 20)
clockView.layoutParams = layoutParams
root.addView(clockView)
}
}

View File

@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.browsing;
import static org.koin.java.KoinJavaComponent.inject;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
@@ -19,6 +18,7 @@ import android.widget.PopupWindow;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.widget.BaseGridView;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
@@ -38,9 +38,7 @@ import org.jellyfin.androidtv.constant.ImageType;
import org.jellyfin.androidtv.constant.PosterSize;
import org.jellyfin.androidtv.constant.QueryType;
import org.jellyfin.androidtv.data.model.FilterOptions;
import org.jellyfin.androidtv.data.querying.AlbumArtistsQuery;
import org.jellyfin.androidtv.data.querying.StdItemQuery;
import org.jellyfin.androidtv.data.querying.ViewQuery;
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;
@@ -52,24 +50,23 @@ import org.jellyfin.androidtv.ui.AlphaPickerView;
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.Destinations;
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;
import org.jellyfin.androidtv.util.ImageUtils;
import org.jellyfin.androidtv.util.ImageHelper;
import org.jellyfin.androidtv.util.InfoLayoutHelper;
import org.jellyfin.androidtv.util.KeyProcessor;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.sdk.api.client.ApiClient;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
import org.jellyfin.sdk.model.api.CollectionType;
import org.jellyfin.sdk.model.api.ItemSortBy;
import org.jellyfin.sdk.model.api.SortOrder;
import org.jellyfin.sdk.model.constant.CollectionType;
import org.jellyfin.sdk.model.constant.ItemSortBy;
import java.text.MessageFormat;
import java.util.HashMap;
@@ -85,7 +82,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private final static int CHUNK_SIZE_MINIMUM = 25;
private String mainTitle;
private Activity mActivity;
private FragmentActivity mActivity;
private BaseRowItem mCurrentItem;
private CompositeClickedListener mClickedListener = new CompositeClickedListener();
private CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
@@ -123,6 +120,9 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private final Lazy<UserRepository> userRepository = inject(UserRepository.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);
private int mCardsScreenEst = 0;
private int mCardsScreenStride = 0;
@@ -130,7 +130,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private final int MIN_NUM_CARDS = 5; // minimum number of visible cards we allow, this results in more empty space
private final double CARD_SPACING_PCT = 1.0; // 100% expressed as relative to the padding_left/top, which depends on the mCardFocusScale and AspectRatio
private final double CARD_SPACING_HORIZONTAL_BANNER_PCT = 0.5; // 50% allow horizontal card overlapping for banners, otherwise spacing is too large
private final int MIN_GRIDSIZE_CHANGE_DELTA = 4; // minimum pixel size changes, to trigger a recreate of the grid via onGridSizeMeasurements
private final int VIEW_SELECT_UPDATE_DELAY = 250; // delay in ms until we update the top-row info for a selected item
private boolean mDirty = true; // CardHeight, RowDef or GridSize changed
@@ -145,16 +144,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
mGridHeight = Math.round(display.heightPixels / getResources().getDisplayMetrics().density - 130.6f);
mGridWidth = Math.round(display.widthPixels / getResources().getDisplayMetrics().density);
sortOptions = new HashMap<>();
{
sortOptions.put(0, new SortOption(getString(R.string.lbl_name), ItemSortBy.SortName, SortOrder.ASCENDING));
sortOptions.put(1, new SortOption(getString(R.string.lbl_date_added), ItemSortBy.DateCreated + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
sortOptions.put(2, new SortOption(getString(R.string.lbl_premier_date), ItemSortBy.PremiereDate + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
sortOptions.put(3, new SortOption(getString(R.string.lbl_rating), ItemSortBy.OfficialRating + "," + ItemSortBy.SortName, SortOrder.ASCENDING));
sortOptions.put(4, new SortOption(getString(R.string.lbl_community_rating), ItemSortBy.CommunityRating + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
sortOptions.put(5, new SortOption(getString(R.string.lbl_critic_rating), ItemSortBy.CriticRating + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
sortOptions.put(6, new SortOption(getString(R.string.lbl_last_played), ItemSortBy.DatePlayed + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
}
mActivity = getActivity();
@@ -172,6 +161,21 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
else
setGridPresenter(new HorizontalGridPresenter());
sortOptions = new HashMap<>();
{
sortOptions.put(0, new SortOption(getString(R.string.lbl_name), ItemSortBy.SORT_NAME, SortOrder.ASCENDING));
sortOptions.put(1, new SortOption(getString(R.string.lbl_date_added), ItemSortBy.DATE_CREATED, SortOrder.DESCENDING));
sortOptions.put(2, new SortOption(getString(R.string.lbl_premier_date), ItemSortBy.PREMIERE_DATE, SortOrder.DESCENDING));
sortOptions.put(3, new SortOption(getString(R.string.lbl_rating), ItemSortBy.OFFICIAL_RATING, SortOrder.ASCENDING));
sortOptions.put(4, new SortOption(getString(R.string.lbl_community_rating), ItemSortBy.COMMUNITY_RATING, SortOrder.DESCENDING));
sortOptions.put(5, new SortOption(getString(R.string.lbl_critic_rating), ItemSortBy.CRITIC_RATING, SortOrder.DESCENDING));
sortOptions.put(6, new SortOption(getString(R.string.lbl_last_played), ItemSortBy.DATE_PLAYED, SortOrder.DESCENDING));
if (mFolder.getCollectionType() != null && mFolder.getCollectionType() == CollectionType.MOVIES) {
sortOptions.put(7, new SortOption(getString(R.string.lbl_runtime), ItemSortBy.RUNTIME, SortOrder.ASCENDING));
}
}
setDefaultGridRowCols(mPosterSizeSetting, mImageType);
setAutoCardGridValues();
setupQueries();
@@ -202,7 +206,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_UP) return false;
return KeyProcessor.HandleKey(keyCode, mCurrentItem, mActivity);
return keyProcessor.getValue().handleKey(keyCode, mCurrentItem, mActivity);
}
@Override
@@ -272,7 +276,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
public void setItem(BaseRowItem item) {
if (item != null) {
binding.title.setText(item.getFullName(requireContext()));
InfoLayoutHelper.addInfoRow(requireContext(), item, binding.infoRow, true, true);
InfoLayoutHelper.addInfoRow(requireContext(), item.getBaseItem(), binding.infoRow, true);
} else {
binding.title.setText("");
binding.infoRow.removeAllViews();
@@ -281,10 +285,10 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
public class SortOption {
public String name;
public String value;
public ItemSortBy value;
public SortOrder order;
public SortOption(String name, String value, SortOrder order) {
public SortOption(String name, ItemSortBy value, SortOrder order) {
this.name = name;
this.value = value;
this.order = order;
@@ -293,13 +297,12 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private Map<Integer, SortOption> sortOptions;
private SortOption getSortOption(String value) {
for (Integer key : sortOptions.keySet()) {
SortOption option = sortOptions.get(key);
if (Objects.requireNonNull(option).value.equals(value)) return option;
private SortOption getSortOption(ItemSortBy value) {
for (SortOption sortOption : sortOptions.values()) {
if (sortOption.value.equals(value)) return sortOption;
}
return new SortOption("Unknown", "", SortOrder.ASCENDING);
return new SortOption(getString(R.string.lbl_bracket_unknown), ItemSortBy.SORT_NAME, SortOrder.ASCENDING);
}
public void setStatusText(String folderName) {
@@ -359,15 +362,15 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
BaseItemKind fType = folder.getType();
if (fType == BaseItemKind.AUDIO || fType == BaseItemKind.GENRE || fType == BaseItemKind.MUSIC_ALBUM || fType == BaseItemKind.MUSIC_ARTIST || fType == BaseItemKind.MUSIC_GENRE) {
return cardHeight;
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.Music.equals(folder.getCollectionType())) {
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.MUSIC.equals(folder.getCollectionType())) {
return cardHeight;
} else {
return cardHeight * ImageUtils.ASPECT_RATIO_2_3;
return cardHeight * ImageHelper.ASPECT_RATIO_2_3;
}
case THUMB:
return cardHeight * ImageUtils.ASPECT_RATIO_16_9;
return cardHeight * ImageHelper.ASPECT_RATIO_16_9;
case BANNER:
return cardHeight * CardPresenter.ASPECT_RATIO_BANNER;
return cardHeight * ImageHelper.ASPECT_RATIO_BANNER;
default:
throw new IllegalStateException("Unexpected value: " + imageType);
}
@@ -380,15 +383,15 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
BaseItemKind fType = folder.getType();
if (fType == BaseItemKind.AUDIO || fType == BaseItemKind.GENRE || fType == BaseItemKind.MUSIC_ALBUM || fType == BaseItemKind.MUSIC_ARTIST || fType == BaseItemKind.MUSIC_GENRE) {
return cardWidth;
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.Music.equals(folder.getCollectionType())) {
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.MUSIC.equals(folder.getCollectionType())) {
return cardWidth;
} else {
return cardWidth / ImageUtils.ASPECT_RATIO_2_3;
return cardWidth / ImageHelper.ASPECT_RATIO_2_3;
}
case THUMB:
return cardWidth / ImageUtils.ASPECT_RATIO_16_9;
return cardWidth / ImageHelper.ASPECT_RATIO_16_9;
case BANNER:
return cardWidth / CardPresenter.ASPECT_RATIO_BANNER;
return cardWidth / ImageHelper.ASPECT_RATIO_BANNER;
default:
throw new IllegalArgumentException("Unexpected value: " + imageType);
}
@@ -542,63 +545,22 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
}
private void setupQueries() {
StdItemQuery query = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount,
ItemFields.MediaSources,
ItemFields.MediaStreams,
ItemFields.DisplayPreferencesId
});
query.setParentId(mParentId.toString());
if (mFolder.getType() == BaseItemKind.USER_VIEW || mFolder.getType() == BaseItemKind.COLLECTION_FOLDER) {
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
switch (type) {
case CollectionType.Movies:
query.setIncludeItemTypes(new String[]{"Movie"});
query.setRecursive(true);
break;
case CollectionType.TvShows:
query.setIncludeItemTypes(new String[]{"Series"});
query.setRecursive(true);
break;
case CollectionType.BoxSets:
query.setIncludeItemTypes(new String[]{"BoxSet"});
query.setParentId(null);
query.setRecursive(true);
break;
case CollectionType.Music:
//Special queries needed for album artists
String includeType = getArguments().getString(Extras.IncludeType);
if ("AlbumArtist".equals(includeType)) {
AlbumArtistsQuery albumArtists = new AlbumArtistsQuery();
albumArtists.setUserId(userRepository.getValue().getCurrentUser().getValue().getId().toString());
albumArtists.setFields(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.ItemCounts,
ItemFields.ChildCount
});
albumArtists.setParentId(mParentId.toString());
setRowDef(new BrowseRowDef("", albumArtists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
return;
} else if ("Artist".equals(includeType)) {
ArtistsQuery artists = new ArtistsQuery();
artists.setUserId(userRepository.getValue().getCurrentUser().getValue().getId().toString());
artists.setFields(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.ItemCounts,
ItemFields.ChildCount
});
artists.setParentId(mParentId.toString());
setRowDef(new BrowseRowDef("", artists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
return;
}
query.setIncludeItemTypes(new String[]{includeType != null ? includeType : "MusicAlbum"});
query.setRecursive(true);
break;
CollectionType type = mFolder.getCollectionType() != null ? mFolder.getCollectionType() : CollectionType.UNKNOWN;
if (type == CollectionType.MUSIC) {
//Special queries needed for album artists
String includeType = getArguments().getString(Extras.IncludeType, null);
if ("AlbumArtist".equals(includeType)) {
setRowDef(new BrowseRowDef("", BrowsingUtils.createAlbumArtistsRequest(mParentId), CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
return;
} else if ("Artist".equals(includeType)) {
setRowDef(new BrowseRowDef("", BrowsingUtils.createArtistsRequest(mParentId), CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
return;
}
}
}
setRowDef(new BrowseRowDef("", query, CHUNK_SIZE_MINIMUM, false, true));
setRowDef(new BrowseRowDef("", BrowsingUtils.createBrowseGridItemsRequest(mFolder), CHUNK_SIZE_MINIMUM, false, true));
}
@Override
@@ -609,7 +571,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
ImageType imageType = libraryPreferences.get(LibraryPreferences.Companion.getImageType());
GridDirection gridDirection = libraryPreferences.get(LibraryPreferences.Companion.getGridDirection());
if (mImageType != imageType || mPosterSizeSetting != posterSizeSetting || mGridDirection != gridDirection || mDirty) {
determiningPosterSize = true;
@@ -665,14 +626,8 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
case NextUp:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getNextUpQuery(), true, mCardPresenter, null);
break;
case Season:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSeasonQuery(), mCardPresenter, null);
break;
case Upcoming:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getUpcomingQuery(), mCardPresenter, null);
break;
case Views:
mAdapter = new ItemRowAdapter(requireContext(), ViewQuery.INSTANCE, mCardPresenter, null);
mAdapter = new ItemRowAdapter(requireContext(), GetUserViewsRequest.INSTANCE, mCardPresenter, null);
break;
case SimilarSeries:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, null);
@@ -680,9 +635,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
case SimilarMovies:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, null);
break;
case Persons:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getPersonsQuery(), chunkSize, mCardPresenter, null);
break;
case LiveTvChannel:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getTvChannelQuery(), 40, mCardPresenter, null);
break;
@@ -718,7 +670,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
setItem(null);
updateCounter(mAdapter.getTotalItems() > 0 ? 1 : 0);
}
mLetterButton.setVisibility(ItemSortBy.SortName.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
mLetterButton.setVisibility(ItemSortBy.SORT_NAME.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
if (mAdapter.getItemsLoaded() == 0) {
mGridView.setFocusable(false);
mHandler.postDelayed(() -> {
@@ -754,11 +706,13 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private ImageButton mLetterButton;
private void updateDisplayPrefs() {
libraryPreferences.set(LibraryPreferences.Companion.getFilterFavoritesOnly(), mAdapter.getFilters().isFavoriteOnly());
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
CoroutineUtils.runBlocking((coroutineScope, continuation) -> libraryPreferences.commit(continuation));
CoroutineUtils.runOnLifecycle(getLifecycle(), (coroutineScope, continuation) -> {
libraryPreferences.set(LibraryPreferences.Companion.getFilterFavoritesOnly(), mAdapter.getFilters().isFavoriteOnly());
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
return libraryPreferences.commit(continuation);
});
}
private void addTools() {
@@ -774,12 +728,9 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
public void onClick(View v) {
//Create sort menu
PopupMenu sortMenu = new PopupMenu(getActivity(), binding.toolBar, Gravity.END);
for (Integer key : sortOptions.keySet()) {
SortOption option = sortOptions.get(key);
if (option == null) option = sortOptions.get(0);
MenuItem item = sortMenu.getMenu().add(0, key, key, Objects.requireNonNull(option).name);
if (option.value.equals(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())))
item.setChecked(true);
for (Map.Entry<Integer, SortOption> entry : sortOptions.entrySet()) {
MenuItem item = sortMenu.getMenu().add(0, entry.getKey(), entry.getKey(), entry.getValue().name);
item.setChecked(entry.getValue().value.equals(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())));
}
sortMenu.getMenu().setGroupCheckable(0, true, true);
sortMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@@ -928,31 +879,20 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
}
private void refreshCurrentItem() {
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO && mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO_ALBUM
&& mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ARTIST && mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ALBUM) {
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
BaseRowItem item = mCurrentItem;
item.refresh(new LifecycleAwareResponse<BaseItemDto>(getLifecycle()) {
@Override
public void onResponse(BaseItemDto response) {
if (!getActive()) return;
if (response == null) mAdapter.removeAt(mAdapter.indexOf(item), 1);
else mAdapter.notifyItemRangeChanged(mAdapter.indexOf(item), 1);
//Now - if filtered make sure we still pass
if (response != null && mAdapter.getFilters() != null) {
if ((mAdapter.getFilters().isFavoriteOnly() && !item.isFavorite()) || (mAdapter.getFilters().isUnwatchedOnly() && item.isPlayed())) {
// if we are about to remove the current item, throw focus to toolbar so framework doesn't crash
binding.toolBar.requestFocus();
mAdapter.remove(item);
mAdapter.setTotalItems(mAdapter.getTotalItems() - 1);
updateCounter(item.getIndex());
}
}
}
});
}
if (mCurrentItem == null) return;
Timber.d("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;
if ((mAdapter.getFilters().isFavoriteOnly() && !mCurrentItem.isFavorite()) || (mAdapter.getFilters().isUnwatchedOnly() && mCurrentItem.isPlayed())) {
// if we are about to remove the current item, throw focus to toolbar so framework doesn't crash
binding.toolBar.requestFocus();
mAdapter.remove(mCurrentItem);
mAdapter.setTotalItems(mAdapter.getTotalItems() - 1);
updateCounter(mAdapter.indexOf(mCurrentItem));
}
return null;
});
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@@ -961,7 +901,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, mAdapter, ((BaseRowItem) item).getIndex(), requireContext());
itemLauncher.getValue().launch((BaseRowItem) item, mAdapter, requireContext());
}
}
@@ -992,7 +932,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
mHandler.postDelayed(mDelayedSetItem, VIEW_SELECT_UPDATE_DELAY);
if (!determiningPosterSize)
mAdapter.loadMoreItemsIfNeeded(mCurrentItem.getIndex());
mAdapter.loadMoreItemsIfNeeded(mAdapter.indexOf(mCurrentItem));
}
}
}

View File

@@ -13,30 +13,18 @@ import androidx.leanback.widget.Row;
import androidx.lifecycle.Lifecycle;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.auth.repository.UserRepository;
import org.jellyfin.androidtv.ui.GridButton;
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
import org.jellyfin.apiclient.model.livetv.TimerQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
import org.koin.java.KoinJavaComponent;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.TimerInfoDto;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
@Override
public void onResume() {
@@ -54,125 +42,56 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
protected void setupQueries(final RowLoader rowLoader) {
showViews = true;
//Latest Recordings
RecordingQuery recordings = new RecordingQuery();
recordings.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
recordings.setEnableImages(true);
recordings.setLimit(40);
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 40));
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), BrowsingUtils.createLiveTVRecordingsRequest(40), 40));
//Movies
RecordingQuery movies = new RecordingQuery();
movies.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
movies.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
movies.setEnableImages(true);
movies.setIsMovie(true);
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), movies, 60);
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), BrowsingUtils.createLiveTVMovieRecordingsRequest(), 60);
//Shows
RecordingQuery shows = new RecordingQuery();
shows.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
shows.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
shows.setEnableImages(true);
shows.setIsSeries(true);
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), shows, 60);
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), BrowsingUtils.createLiveTVSeriesRecordingsRequest(), 60);
mRows.add(showsDef);
mRows.add(moviesDef);
//Sports
RecordingQuery sports = new RecordingQuery();
sports.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
sports.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
sports.setEnableImages(true);
sports.setIsSports(true);
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), sports, 60));
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), BrowsingUtils.createLiveTVSportsRecordingsRequest(), 60));
//Kids
RecordingQuery kids = new RecordingQuery();
kids.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
kids.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
kids.setEnableImages(true);
kids.setIsKids(true);
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), kids, 60));
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), BrowsingUtils.createLiveTVKidsRecordingsRequest(), 60));
rowLoader.loadRows(mRows);
addNext24Timers();
}
private void addNext24Timers() {
final TimerQuery scheduled = new TimerQuery();
final long ticks24 = 1000 * 60 * 60 * 24;
KoinJavaComponent.<ApiClient>get(ApiClient.class).GetLiveTvTimersAsync(scheduled, new LifecycleAwareResponse<TimerInfoDtoResult>(getLifecycle()) {
@Override
public void onResponse(TimerInfoDtoResult response) {
if (!getActive()) return;
BrowseViewFragmentHelperKt.getLiveTvTimers(this, timers -> {
List<BaseItemDto> nearTimers = new ArrayList<>();
LocalDateTime next24 = LocalDateTime.now().plusDays(1);
//Get scheduled items for next 24 hours
for (TimerInfoDto timer : timers.getItems()) {
if (timer.getStartDate().isBefore(next24)) {
nearTimers.add(BrowseViewFragmentHelperKt.getTimerProgramInfo(timer));
}
}
if (!nearTimers.isEmpty()) {
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
scheduledAdapter.Retrieve();
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
mRowsAdapter.add(0, scheduleRow);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
List<BaseItemDto> nearTimers = new ArrayList<>();
long next24 = System.currentTimeMillis() + ticks24;
//Get scheduled items for next 24 hours
for (TimerInfoDto timer : response.getItems()) {
if (TimeUtils.convertToLocalDate(timer.getStartDate()).getTime() <= next24) {
BaseItemDto programInfo = timer.getProgramInfo();
if (programInfo == null) {
programInfo = new BaseItemDto();
programInfo.setId(timer.getId());
programInfo.setChannelName(timer.getChannelName());
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
programInfo.setBaseItemType(BaseItemType.Program);
programInfo.setTimerId(timer.getId());
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
programInfo.setStartDate(timer.getStartDate());
programInfo.setEndDate(timer.getEndDate());
}
programInfo.setLocationType(LocationType.Virtual);
nearTimers.add(programInfo);
mRowsFragment.setSelectedPosition(0, true);
}
}
if (nearTimers.size() > 0) {
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
scheduledAdapter.Retrieve();
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
mRowsAdapter.add(0, scheduleRow);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return;
mRowsFragment.setSelectedPosition(0, true);
}
}, 500);
}
}, 500);
}
@Override
public void onError(Exception exception) {
if (!getActive()) return;
Utils.showToast(getContext(), exception.getLocalizedMessage());
}
return null;
}, exception -> {
Utils.showToast(getContext(), exception.getLocalizedMessage());
return null;
});
}

View File

@@ -2,41 +2,35 @@ package org.jellyfin.androidtv.ui.browsing;
import org.jellyfin.androidtv.constant.ChangeTriggerType;
import org.jellyfin.androidtv.constant.QueryType;
import org.jellyfin.androidtv.data.querying.AlbumArtistsQuery;
import org.jellyfin.androidtv.data.querying.ViewQuery;
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
import org.jellyfin.apiclient.model.querying.ItemQuery;
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
import org.jellyfin.apiclient.model.querying.PersonsQuery;
import org.jellyfin.apiclient.model.querying.SeasonQuery;
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
import org.jellyfin.apiclient.model.querying.UpcomingEpisodesQuery;
import org.jellyfin.androidtv.data.querying.GetSeriesTimersRequest;
import org.jellyfin.androidtv.data.querying.GetSpecialsRequest;
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest;
import org.jellyfin.sdk.model.api.request.GetArtistsRequest;
import org.jellyfin.sdk.model.api.request.GetItemsRequest;
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest;
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest;
import org.jellyfin.sdk.model.api.request.GetNextUpRequest;
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest;
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest;
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest;
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest;
public class BrowseRowDef {
private String headerText;
private ItemQuery query;
private NextUpQuery nextUpQuery;
private UpcomingEpisodesQuery upcomingQuery;
private SimilarItemsQuery similarQuery;
private LatestItemsQuery latestItemsQuery;
private GetItemsRequest query;
private GetNextUpRequest nextUpQuery;
private GetSimilarItemsRequest similarQuery;
private GetLatestMediaRequest latestItemsQuery;
private GetLiveTvChannelsRequest tvChannelQuery;
private GetRecommendedProgramsRequest programQuery;
private GetRecordingsRequest recordingQuery;
private GetSeriesTimersRequest seriesTimerQuery;
private PersonsQuery personsQuery;
private LiveTvChannelQuery tvChannelQuery;
private RecommendedProgramQuery programQuery;
private RecordingQuery recordingQuery;
private SeriesTimerQuery seriesTimerQuery;
private ArtistsQuery artistsQuery;
private AlbumArtistsQuery albumArtistsQuery;
private SeasonQuery seasonQuery;
private GetArtistsRequest artistsQuery;
private GetAlbumArtistsRequest albumArtistsQuery;
private GetResumeItemsRequest resumeQuery;
private GetSpecialsRequest specialsQuery;
private QueryType queryType;
private int chunkSize = 0;
@@ -45,14 +39,11 @@ public class BrowseRowDef {
private ChangeTriggerType[] changeTriggers;
public BrowseRowDef(String header, ItemQuery query, int chunkSize) {
this(header, query, chunkSize, false);
}
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb) {
this(header, query, chunkSize, preferParentThumb, false);
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize) {
this(header, query, chunkSize, false, false);
}
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight) {
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight) {
headerText = header;
this.query = query;
this.chunkSize = chunkSize;
@@ -61,15 +52,15 @@ public class BrowseRowDef {
this.queryType = QueryType.Items;
}
public BrowseRowDef(String header, ItemQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
this(header, query, chunkSize, false, false, changeTriggers);
}
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
this(header,query,chunkSize,preferParentThumb,staticHeight,changeTriggers,QueryType.Items);
}
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers, QueryType queryType) {
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers, QueryType queryType) {
headerText = header;
this.query = query;
this.chunkSize = chunkSize;
@@ -79,7 +70,7 @@ public class BrowseRowDef {
this.changeTriggers = changeTriggers;
}
public BrowseRowDef(String header, ArtistsQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
public BrowseRowDef(String header, GetArtistsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
headerText = header;
this.artistsQuery = query;
this.chunkSize = chunkSize;
@@ -87,7 +78,7 @@ public class BrowseRowDef {
this.changeTriggers = changeTriggers;
}
public BrowseRowDef(String header, AlbumArtistsQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
public BrowseRowDef(String header, GetAlbumArtistsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
headerText = header;
this.albumArtistsQuery = query;
this.chunkSize = chunkSize;
@@ -95,20 +86,14 @@ public class BrowseRowDef {
this.changeTriggers = changeTriggers;
}
public BrowseRowDef(String header, NextUpQuery query) {
headerText = header;
this.nextUpQuery = query;
this.queryType = QueryType.NextUp;
}
public BrowseRowDef(String header, SeriesTimerQuery query) {
public BrowseRowDef(String header, GetSeriesTimersRequest query) {
headerText = header;
this.seriesTimerQuery = query;
this.staticHeight = true;
this.queryType = QueryType.SeriesTimer;
}
public BrowseRowDef(String header, NextUpQuery query, ChangeTriggerType[] changeTriggers) {
public BrowseRowDef(String header, GetNextUpRequest query, ChangeTriggerType[] changeTriggers) {
headerText = header;
this.nextUpQuery = query;
this.queryType = QueryType.NextUp;
@@ -116,7 +101,7 @@ public class BrowseRowDef {
this.changeTriggers = changeTriggers;
}
public BrowseRowDef(String header, LatestItemsQuery query, ChangeTriggerType[] changeTriggers) {
public BrowseRowDef(String header, GetLatestMediaRequest query, ChangeTriggerType[] changeTriggers) {
headerText = header;
this.latestItemsQuery = query;
this.queryType = QueryType.LatestItems;
@@ -124,62 +109,36 @@ public class BrowseRowDef {
this.changeTriggers = changeTriggers;
}
public BrowseRowDef(String header, SimilarItemsQuery query) {
headerText = header;
this.similarQuery = query;
this.queryType = QueryType.SimilarSeries;
}
public BrowseRowDef(String header, LiveTvChannelQuery query) {
public BrowseRowDef(String header, GetLiveTvChannelsRequest query) {
headerText = header;
this.tvChannelQuery = query;
this.queryType = QueryType.LiveTvChannel;
}
public BrowseRowDef(String header, RecommendedProgramQuery query) {
public BrowseRowDef(String header, GetRecommendedProgramsRequest query) {
headerText = header;
this.programQuery = query;
this.queryType = QueryType.LiveTvProgram;
this.changeTriggers = new ChangeTriggerType[] {ChangeTriggerType.GuideNeedsLoad};
}
public BrowseRowDef(String header, RecordingQuery query) {
public BrowseRowDef(String header, GetRecordingsRequest query) {
this(header, query, 0);
}
public BrowseRowDef(String header, RecordingQuery query, int chunkSize) {
public BrowseRowDef(String header, GetRecordingsRequest query, int chunkSize) {
headerText = header;
this.recordingQuery = query;
this.chunkSize = chunkSize;
this.queryType = QueryType.LiveTvRecording;
}
public BrowseRowDef(String header, PersonsQuery query, int chunkSize) {
headerText = header;
this.personsQuery = query;
this.queryType = QueryType.Persons;
this.chunkSize = chunkSize;
}
public BrowseRowDef(String header, SimilarItemsQuery query, QueryType type) {
public BrowseRowDef(String header, GetSimilarItemsRequest query, QueryType type) {
headerText = header;
this.similarQuery = query;
this.queryType = type;
}
public BrowseRowDef(String header, SeasonQuery query) {
headerText = header;
this.seasonQuery = query;
this.queryType = QueryType.Season;
}
public BrowseRowDef(String header, UpcomingEpisodesQuery query) {
headerText = header;
this.upcomingQuery = query;
this.queryType = QueryType.Upcoming;
}
public BrowseRowDef(String header, ViewQuery query) {
public BrowseRowDef(String header, GetUserViewsRequest query) {
headerText = header;
this.staticHeight = true;
this.queryType = QueryType.Views;
@@ -195,6 +154,12 @@ public class BrowseRowDef {
this.changeTriggers = changeTriggers;
}
public BrowseRowDef(String header, GetSpecialsRequest query) {
headerText = header;
this.specialsQuery = query;
this.queryType = QueryType.Specials;
}
public int getChunkSize() {
return chunkSize;
}
@@ -205,55 +170,43 @@ public class BrowseRowDef {
return headerText;
}
public void setHeaderText(String headerText) {
this.headerText = headerText;
}
public ItemQuery getQuery() {
public GetItemsRequest getQuery() {
return query;
}
public NextUpQuery getNextUpQuery() {
public GetNextUpRequest getNextUpQuery() {
return nextUpQuery;
}
public LatestItemsQuery getLatestItemsQuery() { return latestItemsQuery; }
public GetLatestMediaRequest getLatestItemsQuery() { return latestItemsQuery; }
public SimilarItemsQuery getSimilarQuery() { return similarQuery; }
public GetSimilarItemsRequest getSimilarQuery() { return similarQuery; }
public QueryType getQueryType() {
return queryType;
}
public SeasonQuery getSeasonQuery() { return seasonQuery; }
public UpcomingEpisodesQuery getUpcomingQuery() {
return upcomingQuery;
}
public LiveTvChannelQuery getTvChannelQuery() {
public GetLiveTvChannelsRequest getTvChannelQuery() {
return tvChannelQuery;
}
public RecommendedProgramQuery getProgramQuery() {
public GetRecommendedProgramsRequest getProgramQuery() {
return programQuery;
}
public RecordingQuery getRecordingQuery() { return recordingQuery; }
public GetRecordingsRequest getRecordingQuery() { return recordingQuery; }
public boolean getPreferParentThumb() { return preferParentThumb; }
public PersonsQuery getPersonsQuery() {
return personsQuery;
}
public GetArtistsRequest getArtistsQuery() { return artistsQuery; }
public GetAlbumArtistsRequest getAlbumArtistsQuery() { return albumArtistsQuery; }
public ArtistsQuery getArtistsQuery() { return artistsQuery; }
public AlbumArtistsQuery getAlbumArtistsQuery() { return albumArtistsQuery; }
public SeriesTimerQuery getSeriesTimerQuery() { return seriesTimerQuery; }
public GetSeriesTimersRequest getSeriesTimerQuery() { return seriesTimerQuery; }
public GetResumeItemsRequest getResumeQuery() { return resumeQuery; }
public GetSpecialsRequest getSpecialsQuery() { return specialsQuery; }
public ChangeTriggerType[] getChangeTriggers() {
return changeTriggers;
}

View File

@@ -2,8 +2,6 @@ package org.jellyfin.androidtv.ui.browsing;
import org.jellyfin.androidtv.ui.livetv.TvManager;
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
import org.jellyfin.apiclient.model.livetv.TimerQuery;
public class BrowseScheduleFragment extends EnhancedBrowseFragment {
@@ -14,7 +12,6 @@ public class BrowseScheduleFragment extends EnhancedBrowseFragment {
@Override
protected void setupQueries(final RowLoader rowLoader) {
TvManager.getScheduleRowsAsync(requireContext(), new TimerQuery(), new CardPresenter(true), mRowsAdapter, new LifecycleAwareResponse<Integer>(getLifecycle()) {
});
TvManager.getScheduleRowsAsync(this, null, new CardPresenter(true), mRowsAdapter);
}
}

View File

@@ -1,7 +1,5 @@
package org.jellyfin.androidtv.ui.browsing;
import static org.koin.java.KoinJavaComponent.inject;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
@@ -10,478 +8,185 @@ import androidx.leanback.widget.Row;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.auth.repository.UserRepository;
import org.jellyfin.androidtv.constant.ChangeTriggerType;
import org.jellyfin.androidtv.constant.Extras;
import org.jellyfin.androidtv.constant.LiveTvOption;
import org.jellyfin.androidtv.constant.QueryType;
import org.jellyfin.androidtv.data.querying.StdItemQuery;
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;
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.BaseItemType;
import org.jellyfin.apiclient.model.entities.LocationType;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
import org.jellyfin.apiclient.model.livetv.TimerQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.apiclient.model.querying.ItemFilter;
import org.jellyfin.apiclient.model.querying.ItemQuery;
import org.jellyfin.apiclient.model.querying.ItemsResult;
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
import org.jellyfin.apiclient.model.querying.NextUpQuery;
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
import org.jellyfin.sdk.model.constant.CollectionType;
import org.jellyfin.sdk.model.constant.ItemSortBy;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
import org.jellyfin.sdk.model.api.CollectionType;
import org.jellyfin.sdk.model.api.TimerInfoDto;
import org.jellyfin.sdk.model.api.request.GetNextUpRequest;
import org.koin.java.KoinJavaComponent;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import kotlin.Lazy;
import timber.log.Timber;
public class BrowseViewFragment extends EnhancedBrowseFragment {
private boolean isLiveTvLibrary;
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
@Override
protected void setupQueries(final RowLoader rowLoader) {
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
CollectionType type = mFolder != null && mFolder.getCollectionType() != null ? mFolder.getCollectionType() : CollectionType.UNKNOWN;
switch (type) {
case CollectionType.Movies:
itemTypeString = "Movie";
case MOVIES:
itemType = BaseItemKind.MOVIE;
//Resume
StdItemQuery resumeMovies = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
resumeMovies.setIncludeItemTypes(new String[]{"Movie"});
resumeMovies.setRecursive(true);
resumeMovies.setParentId(mFolder.getId().toString());
resumeMovies.setImageTypeLimit(1);
resumeMovies.setLimit(50);
resumeMovies.setCollapseBoxSetItems(false);
resumeMovies.setEnableTotalRecordCount(false);
resumeMovies.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
resumeMovies.setSortBy(new String[]{ItemSortBy.DatePlayed});
resumeMovies.setSortOrder(SortOrder.Descending);
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resumeMovies, 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), BrowsingUtils.createResumeItemsRequest(mFolder.getId(), BaseItemKind.MOVIE), 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
//Latest
LatestItemsQuery latestMovies = new LatestItemsQuery();
latestMovies.setFields(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ChildCount,
ItemFields.MediaSources,
ItemFields.MediaStreams
});
latestMovies.setParentId(mFolder.getId().toString());
latestMovies.setLimit(50);
latestMovies.setImageTypeLimit(1);
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestMovies, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId()), new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
//Favorites
StdItemQuery favorites = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
favorites.setIncludeItemTypes(new String[]{"Movie"});
favorites.setRecursive(true);
favorites.setParentId(mFolder.getId().toString());
favorites.setImageTypeLimit(1);
favorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
favorites.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), favorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.MOVIE), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
//Collections
StdItemQuery collections = new StdItemQuery();
collections.setFields(new ItemFields[]{
ItemFields.ChildCount
});
collections.setIncludeItemTypes(new String[]{"BoxSet"});
collections.setRecursive(true);
collections.setImageTypeLimit(1);
//collections.setParentId(mFolder.getId());
collections.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(getString(R.string.lbl_collections), collections, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_collections), BrowsingUtils.createCollectionsRequest(mFolder.getId()), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
rowLoader.loadRows(mRows);
break;
case CollectionType.TvShows:
itemTypeString = "Series";
case TVSHOWS:
itemType = BaseItemKind.SERIES;
//Resume
StdItemQuery resumeEpisodes = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ChildCount,
ItemFields.MediaSources,
ItemFields.MediaStreams
});
resumeEpisodes.setIncludeItemTypes(new String[]{"Episode"});
resumeEpisodes.setLimit(50);
resumeEpisodes.setParentId(mFolder.getId().toString());
resumeEpisodes.setRecursive(true);
resumeEpisodes.setImageTypeLimit(1);
resumeEpisodes.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
resumeEpisodes.setSortBy(new String[]{ItemSortBy.DatePlayed});
resumeEpisodes.setSortOrder(SortOrder.Descending);
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resumeEpisodes, 0, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), BrowsingUtils.createResumeItemsRequest(mFolder.getId(), BaseItemKind.EPISODE), 0, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
//Next up
NextUpQuery nextUpQuery = new NextUpQuery();
nextUpQuery.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
nextUpQuery.setLimit(50);
nextUpQuery.setParentId(mFolder.getId().toString());
nextUpQuery.setImageTypeLimit(1);
nextUpQuery.setFields(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ChildCount,
ItemFields.MediaSources,
ItemFields.MediaStreams
});
mRows.add(new BrowseRowDef(getString(R.string.lbl_next_up), nextUpQuery, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
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())) {
StdItemQuery newQuery = new StdItemQuery(new ItemFields[]{
ItemFields.DateCreated,
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ChildCount
});
newQuery.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
newQuery.setIncludeItemTypes(new String[]{"Episode"});
newQuery.setParentId(mFolder.getId().toString());
newQuery.setRecursive(true);
newQuery.setIsVirtualUnaired(false);
newQuery.setIsMissing(false);
newQuery.setImageTypeLimit(1);
newQuery.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
newQuery.setSortBy(new String[]{ItemSortBy.DateCreated});
newQuery.setSortOrder(SortOrder.Descending);
newQuery.setEnableTotalRecordCount(false);
newQuery.setLimit(300);
mRows.add(new BrowseRowDef(getString(R.string.lbl_new_premieres), newQuery, 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
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
LatestItemsQuery latestSeries = new LatestItemsQuery();
latestSeries.setFields(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ChildCount,
ItemFields.MediaSources,
ItemFields.MediaStreams
});
latestSeries.setIncludeItemTypes(new String[]{"Episode"});
latestSeries.setGroupItems(true);
latestSeries.setParentId(mFolder.getId().toString());
latestSeries.setLimit(50);
latestSeries.setImageTypeLimit(1);
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestSeries, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.EPISODE, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
//Favorites
StdItemQuery tvFavorites = new StdItemQuery();
tvFavorites.setIncludeItemTypes(new String[]{"Series"});
tvFavorites.setRecursive(true);
tvFavorites.setParentId(mFolder.getId().toString());
tvFavorites.setImageTypeLimit(1);
tvFavorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
tvFavorites.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), tvFavorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.SERIES), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
rowLoader.loadRows(mRows);
break;
case CollectionType.Music:
case MUSIC:
//Latest
LatestItemsQuery latestAlbums = new LatestItemsQuery();
latestAlbums.setFields(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ChildCount
});
latestAlbums.setIncludeItemTypes(new String[]{"Audio"});
latestAlbums.setGroupItems(true);
latestAlbums.setImageTypeLimit(1);
latestAlbums.setParentId(mFolder.getId().toString());
latestAlbums.setLimit(50);
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestAlbums, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.AUDIO, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
//Last Played
StdItemQuery lastPlayed = new StdItemQuery();
lastPlayed.setIncludeItemTypes(new String[]{"Audio"});
lastPlayed.setRecursive(true);
lastPlayed.setParentId(mFolder.getId().toString());
lastPlayed.setImageTypeLimit(1);
lastPlayed.setFilters(new ItemFilter[]{ItemFilter.IsPlayed});
lastPlayed.setSortBy(new String[]{ItemSortBy.DatePlayed});
lastPlayed.setSortOrder(SortOrder.Descending);
lastPlayed.setEnableTotalRecordCount(false);
lastPlayed.setLimit(50);
mRows.add(new BrowseRowDef(getString(R.string.lbl_last_played), lastPlayed, 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_last_played), BrowsingUtils.createLastPlayedRequest(mFolder.getId()), 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
//Favorites
StdItemQuery favAlbums = new StdItemQuery();
favAlbums.setIncludeItemTypes(new String[]{"MusicAlbum", "MusicArtist"});
favAlbums.setRecursive(true);
favAlbums.setParentId(mFolder.getId().toString());
favAlbums.setImageTypeLimit(1);
favAlbums.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
favAlbums.setSortBy(new String[]{ItemSortBy.SortName});
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), favAlbums, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.MUSIC_ALBUM), 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
//AudioPlaylists
StdItemQuery playlists = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.CumulativeRunTimeTicks,
ItemFields.ChildCount
});
playlists.setIncludeItemTypes(new String[]{"Playlist"});
playlists.setImageTypeLimit(1);
playlists.setRecursive(true);
playlists.setSortBy(new String[]{ItemSortBy.DateCreated});
playlists.setSortOrder(SortOrder.Descending);
mRows.add(new BrowseRowDef(getString(R.string.lbl_playlists), playlists, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
mRows.add(new BrowseRowDef(getString(R.string.lbl_playlists), BrowsingUtils.createPlaylistsRequest(), 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
rowLoader.loadRows(mRows);
break;
case CollectionType.LiveTv:
case LIVETV:
isLiveTvLibrary = true;
showViews = true;
//On now
RecommendedProgramQuery onNow = new RecommendedProgramQuery();
onNow.setIsAiring(true);
onNow.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChannelInfo,
ItemFields.ChildCount
});
onNow.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
onNow.setImageTypeLimit(1);
onNow.setEnableTotalRecordCount(false);
onNow.setLimit(150);
mRows.add(new BrowseRowDef(getString(R.string.lbl_on_now), onNow));
mRows.add(new BrowseRowDef(getString(R.string.lbl_on_now), BrowsingUtils.createLiveTVOnNowRequest()));
//Upcoming
RecommendedProgramQuery upcomingTv = new RecommendedProgramQuery();
upcomingTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
upcomingTv.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChannelInfo,
ItemFields.ChildCount
});
upcomingTv.setIsAiring(false);
upcomingTv.setHasAired(false);
upcomingTv.setImageTypeLimit(1);
upcomingTv.setEnableTotalRecordCount(false);
upcomingTv.setLimit(150);
mRows.add(new BrowseRowDef(getString(R.string.lbl_coming_up), upcomingTv));
mRows.add(new BrowseRowDef(getString(R.string.lbl_coming_up), BrowsingUtils.createLiveTVUpcomingRequest()));
//Fav Channels
LiveTvChannelQuery favTv = new LiveTvChannelQuery();
favTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
favTv.setEnableFavoriteSorting(true);
favTv.setIsFavorite(true);
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorite_channels), favTv));
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorite_channels), BrowsingUtils.createLiveTVChannelsRequest(true)));
//Other Channels
LiveTvChannelQuery otherTv = new LiveTvChannelQuery();
otherTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
otherTv.setIsFavorite(false);
mRows.add(new BrowseRowDef(getString(R.string.lbl_other_channels), otherTv));
mRows.add(new BrowseRowDef(getString(R.string.lbl_other_channels), BrowsingUtils.createLiveTVChannelsRequest(false)));
//Latest Recordings
RecordingQuery recordings = new RecordingQuery();
recordings.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
recordings.setEnableImages(true);
recordings.setLimit(40);
//Do a straight query and then split the returned items into logical groups
apiClient.getValue().GetLiveTvRecordingsAsync(recordings, new LifecycleAwareResponse<ItemsResult>(getLifecycle()) {
@Override
public void onResponse(ItemsResult response) {
if (!getActive()) return;
final ItemsResult recordingsResponse = response;
final long ticks24 = 1000 * 60 * 60 * 24;
// Also get scheduled recordings for next 24 hours
final TimerQuery scheduled = new TimerQuery();
apiClient.getValue().GetLiveTvTimersAsync(scheduled, new LifecycleAwareResponse<TimerInfoDtoResult>(getLifecycle()) {
@Override
public void onResponse(TimerInfoDtoResult response) {
if (!getActive()) return;
List<BaseItemDto> nearTimers = new ArrayList<>();
long next24 = System.currentTimeMillis() + ticks24;
//Get scheduled items for next 24 hours
for (TimerInfoDto timer : response.getItems()) {
if (TimeUtils.convertToLocalDate(timer.getStartDate()).getTime() <= next24) {
BaseItemDto programInfo = timer.getProgramInfo();
if (programInfo == null) {
programInfo = new BaseItemDto();
programInfo.setId(timer.getId());
programInfo.setChannelName(timer.getChannelName());
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
programInfo.setBaseItemType(BaseItemType.Program);
programInfo.setTimerId(timer.getId());
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
programInfo.setStartDate(timer.getStartDate());
programInfo.setEndDate(timer.getEndDate());
}
programInfo.setLocationType(LocationType.Virtual);
nearTimers.add(programInfo);
}
}
if (recordingsResponse.getTotalRecordCount() > 0) {
List<BaseItemDto> dayItems = new ArrayList<>();
List<BaseItemDto> weekItems = new ArrayList<>();
long past24 = System.currentTimeMillis() - ticks24;
long pastWeek = System.currentTimeMillis() - (ticks24 * 7);
for (BaseItemDto item : recordingsResponse.getItems()) {
if (item.getDateCreated() != null) {
if (TimeUtils.convertToLocalDate(item.getDateCreated()).getTime() >= past24) {
dayItems.add(item);
} else if (TimeUtils.convertToLocalDate(item.getDateCreated()).getTime() >= pastWeek) {
weekItems.add(item);
}
}
}
//First put all recordings in and retrieve
//All Recordings
RecordingQuery recordings = new RecordingQuery();
recordings.setFields(new ItemFields[]{
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount
});
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
recordings.setEnableImages(true);
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 50));
rowLoader.loadRows(mRows);
//Now insert our smart rows
if (weekItems.size() > 0) {
ItemRowAdapter weekAdapter = new ItemRowAdapter(requireContext(), weekItems, mCardPresenter, mRowsAdapter, true);
weekAdapter.Retrieve();
ListRow weekRow = new ListRow(new HeaderItem("Past Week"), weekAdapter);
mRowsAdapter.add(0, weekRow);
}
if (nearTimers.size() > 0) {
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
scheduledAdapter.Retrieve();
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
mRowsAdapter.add(0, scheduleRow);
}
if (dayItems.size() > 0) {
ItemRowAdapter dayAdapter = new ItemRowAdapter(requireContext(), dayItems, mCardPresenter, mRowsAdapter, true);
dayAdapter.Retrieve();
ListRow dayRow = new ListRow(new HeaderItem("Past 24 Hours"), dayAdapter);
mRowsAdapter.add(0, dayRow);
}
} else {
// no recordings
rowLoader.loadRows(mRows);
if (nearTimers.size() > 0) {
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
scheduledAdapter.Retrieve();
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
mRowsAdapter.add(0, scheduleRow);
} else {
mTitle.setText(R.string.lbl_no_recordings);
}
}
}
});
BrowseViewFragmentHelperKt.getLiveTvRecordingsAndTimers(this, (recordings, timers) -> {
List<BaseItemDto> nearTimers = new ArrayList<>();
LocalDateTime next24 = LocalDateTime.now().plusDays(1);
//Get scheduled items for next 24 hours
for (TimerInfoDto timer : timers.getItems()) {
if (timer.getStartDate().isBefore(next24)) {
nearTimers.add(BrowseViewFragmentHelperKt.getTimerProgramInfo(timer));
}
}
@Override
public void onError(Exception exception) {
if (!getActive()) return;
if (recordings.getTotalRecordCount() > 0) {
List<BaseItemDto> dayItems = new ArrayList<>();
List<BaseItemDto> weekItems = new ArrayList<>();
Utils.showToast(getContext(), exception.getLocalizedMessage());
}
});
break;
case "seriestimers":
mRows.add(new BrowseRowDef(getString(R.string.lbl_series_recordings), new SeriesTimerQuery()));
rowLoader.loadRows(mRows);
break;
default:
// Fall back to rows defined by the view children
final List<BrowseRowDef> rows = new ArrayList<>();
final UUID userId = KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId();
ItemQuery query = new ItemQuery();
query.setParentId(mFolder.getId().toString());
query.setUserId(userId.toString());
query.setImageTypeLimit(1);
query.setSortBy(new String[]{ItemSortBy.SortName});
apiClient.getValue().GetItemsAsync(query, new LifecycleAwareResponse<ItemsResult>(getLifecycle()) {
@Override
public void onResponse(ItemsResult response) {
if (!getActive()) return;
if (response.getTotalRecordCount() > 0) {
for (BaseItemDto item : response.getItems()) {
ItemQuery rowQuery = new StdItemQuery();
rowQuery.setParentId(item.getId());
rowQuery.setUserId(userId.toString());
rows.add(new BrowseRowDef(item.getName(), rowQuery, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
LocalDateTime past24 = LocalDateTime.now().minusDays(1);
LocalDateTime pastWeek = LocalDateTime.now().minusWeeks(1);
for (BaseItemDto item : recordings.getItems()) {
if (item.getDateCreated() != null) {
if (item.getDateCreated().isAfter(past24)) {
dayItems.add(item);
} else if (item.getDateCreated().isAfter(pastWeek)) {
weekItems.add(item);
}
}
}
rowLoader.loadRows(rows);
//First put all recordings in and retrieve
//All Recordings
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), BrowsingUtils.createLiveTVRecordingsRequest(), 50));
rowLoader.loadRows(mRows);
//Now insert our smart rows
if (!weekItems.isEmpty()) {
ItemRowAdapter weekAdapter = new ItemRowAdapter(requireContext(), weekItems, mCardPresenter, mRowsAdapter, true);
weekAdapter.Retrieve();
ListRow weekRow = new ListRow(new HeaderItem(getString(R.string.past_week)), weekAdapter);
mRowsAdapter.add(0, weekRow);
}
if (!nearTimers.isEmpty()) {
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
scheduledAdapter.Retrieve();
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
mRowsAdapter.add(0, scheduleRow);
}
if (!dayItems.isEmpty()) {
ItemRowAdapter dayAdapter = new ItemRowAdapter(requireContext(), dayItems, mCardPresenter, mRowsAdapter, true);
dayAdapter.Retrieve();
ListRow dayRow = new ListRow(new HeaderItem(getString(R.string.past_24_hours)), dayAdapter);
mRowsAdapter.add(0, dayRow);
}
} else {
// no recordings
rowLoader.loadRows(mRows);
if (!nearTimers.isEmpty()) {
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
scheduledAdapter.Retrieve();
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
mRowsAdapter.add(0, scheduleRow);
} else {
mTitle.setText(R.string.lbl_no_recordings);
}
}
@Override
public void onError(Exception exception) {
exception.printStackTrace();
}
return null;
}, exception -> {
Utils.showToast(getContext(), exception.getLocalizedMessage());
return null;
});
break;
default:
boolean isRecordingsView = getArguments().getBoolean(Extras.IsLiveTvSeriesRecordings, false);
if (isRecordingsView) {
mRows.add(new BrowseRowDef(getString(R.string.lbl_series_recordings), GetSeriesTimersRequest.INSTANCE));
rowLoader.loadRows(mRows);
}
}
}

View File

@@ -0,0 +1,77 @@
package org.jellyfin.androidtv.ui.browsing
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.LocationType
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.TimerInfoDto
import org.jellyfin.sdk.model.api.TimerInfoDtoQueryResult
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.koin.android.ext.android.inject
fun EnhancedBrowseFragment.getLiveTvRecordingsAndTimers(
callback: (recordings: BaseItemDtoQueryResult, timers: TimerInfoDtoQueryResult) -> Unit,
errorCallback: (exception: Throwable) -> Unit,
) {
val api by inject<ApiClient>()
lifecycleScope.launch {
runCatching {
val recordings by api.liveTvApi.getRecordings(
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
enableImages = true,
limit = 40,
)
val timers by api.liveTvApi.getTimers()
recordings to timers
}.fold(
onSuccess = { (recordings, timers) -> callback(recordings, timers) },
onFailure = { exception -> errorCallback(exception) }
)
}
}
fun EnhancedBrowseFragment.getLiveTvTimers(
callback: (timers: TimerInfoDtoQueryResult) -> Unit,
errorCallback: (exception: Throwable) -> Unit,
) {
val api by inject<ApiClient>()
lifecycleScope.launch {
runCatching {
api.liveTvApi.getTimers().content
}.fold(
onSuccess = { timers -> callback(timers) },
onFailure = { exception -> errorCallback(exception) }
)
}
}
fun getTimerProgramInfo(timer: TimerInfoDto): BaseItemDto {
val programInfo = timer.programInfo ?: BaseItemDto(
id = requireNotNull(timer.id?.toUUIDOrNull()),
channelName = timer.channelName,
name = timer.name.orEmpty(),
type = BaseItemKind.PROGRAM,
timerId = timer.id,
seriesTimerId = timer.seriesTimerId,
startDate = timer.startDate,
endDate = timer.endDate,
mediaType = MediaType.UNKNOWN,
)
return programInfo.copy(
locationType = LocationType.VIRTUAL,
)
}

View File

@@ -0,0 +1,434 @@
package org.jellyfin.androidtv.ui.browsing
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemFilter
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest
import org.jellyfin.sdk.model.api.request.GetArtistsRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest
import org.jellyfin.sdk.model.api.request.GetSeasonsRequest
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
import org.jellyfin.sdk.model.api.request.GetUpcomingEpisodesRequest
import timber.log.Timber
import java.util.UUID
object BrowsingUtils {
@JvmStatic
fun getRandomItem(
api: ApiClient,
lifecycle: LifecycleOwner,
library: BaseItemDto,
type: BaseItemKind,
callback: (item: BaseItemDto?) -> Unit
) {
lifecycle.lifecycleScope.launch {
try {
val result by api.itemsApi.getItems(
parentId = library.id,
includeItemTypes = setOf(type),
recursive = true,
sortBy = setOf(ItemSortBy.RANDOM),
limit = 1,
)
callback(result.items?.firstOrNull())
} catch (error: ApiClientException) {
Timber.w(error, "Failed to retrieve random item")
callback(null)
}
}
}
@JvmStatic
fun createGetNextUpRequest(parentId: UUID) = GetNextUpRequest(
limit = 50,
parentId = parentId,
imageTypeLimit = 1,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.CHILD_COUNT,
ItemFields.MEDIA_SOURCES,
ItemFields.MEDIA_STREAMS,
)
)
@JvmStatic
fun createSeriesGetNextUpRequest(parentId: UUID) = GetNextUpRequest(
seriesId = parentId,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
)
)
@JvmStatic
@JvmOverloads
fun createLatestMediaRequest(
parentId: UUID,
itemType: BaseItemKind? = null,
groupItems: Boolean? = null
) = GetLatestMediaRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.CHILD_COUNT,
ItemFields.MEDIA_SOURCES,
ItemFields.MEDIA_STREAMS,
),
parentId = parentId,
limit = 50,
imageTypeLimit = 1,
includeItemTypes = itemType?.let(::setOf),
groupItems = groupItems,
)
@JvmStatic
fun createSeasonsRequest(seriesId: UUID) = GetSeasonsRequest(
seriesId = seriesId,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
)
@JvmStatic
fun createUpcomingEpisodesRequest(parentId: UUID) = GetUpcomingEpisodesRequest(
parentId = parentId,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
)
@JvmStatic
fun createSimilarItemsRequest(itemId: UUID) = GetSimilarItemsRequest(
itemId = itemId,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
limit = 20,
)
@JvmStatic
fun createLiveTVOnNowRequest() = GetRecommendedProgramsRequest(
isAiring = true,
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHANNEL_INFO,
ItemFields.CHILD_COUNT,
),
imageTypeLimit = 1,
enableTotalRecordCount = false,
limit = 150,
)
@JvmStatic
fun createLiveTVUpcomingRequest() = GetRecommendedProgramsRequest(
isAiring = false,
hasAired = false,
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHANNEL_INFO,
ItemFields.CHILD_COUNT,
),
imageTypeLimit = 1,
enableTotalRecordCount = false,
limit = 150,
)
@JvmStatic
@JvmOverloads
fun createLiveTVRecordingsRequest(limit: Int? = null) = GetRecordingsRequest(
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
enableImages = true,
limit = limit,
)
@JvmStatic
fun createLiveTVMovieRecordingsRequest() = GetRecordingsRequest(
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
enableImages = true,
limit = 60,
isMovie = true,
)
@JvmStatic
fun createLiveTVSeriesRecordingsRequest() = GetRecordingsRequest(
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
enableImages = true,
limit = 60,
isSeries = true,
)
@JvmStatic
fun createLiveTVSportsRecordingsRequest() = GetRecordingsRequest(
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
enableImages = true,
limit = 60,
isSports = true,
)
@JvmStatic
fun createLiveTVKidsRecordingsRequest() = GetRecordingsRequest(
fields = setOf(
ItemFields.OVERVIEW,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
),
enableImages = true,
limit = 60,
isKids = true,
)
@JvmStatic
fun createLiveTVChannelsRequest(isFavorite: Boolean) = GetLiveTvChannelsRequest(
isFavorite = isFavorite,
)
@JvmStatic
fun createAlbumArtistsRequest(parentId: UUID) = GetAlbumArtistsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.ITEM_COUNTS,
ItemFields.CHILD_COUNT,
),
parentId = parentId,
)
@JvmStatic
fun createArtistsRequest(parentId: UUID) = GetArtistsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.ITEM_COUNTS,
ItemFields.CHILD_COUNT,
),
parentId = parentId,
)
@JvmStatic
fun createPersonItemsRequest(personId: UUID, itemType: BaseItemKind) = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
personIds = setOf(personId),
recursive = true,
includeItemTypes = setOf(itemType),
sortBy = setOf(ItemSortBy.SORT_NAME),
)
@JvmStatic
fun createArtistItemsRequest(artistId: UUID, itemType: BaseItemKind) = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
artistIds = setOf(artistId),
recursive = true,
includeItemTypes = setOf(itemType),
sortBy = setOf(ItemSortBy.SORT_NAME),
)
@JvmStatic
fun createNextEpisodesRequest(seasonId: UUID, indexNumber: Int) = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
parentId = seasonId,
includeItemTypes = setOf(BaseItemKind.EPISODE),
startIndex = indexNumber,
limit = 20,
)
@JvmStatic
fun createResumeItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
ItemFields.MEDIA_STREAMS,
ItemFields.MEDIA_SOURCES,
),
includeItemTypes = setOf(itemType),
recursive = true,
parentId = parentId,
imageTypeLimit = 1,
limit = 50,
collapseBoxSetItems = false,
enableTotalRecordCount = false,
filters = setOf(ItemFilter.IS_RESUMABLE),
sortBy = setOf(ItemSortBy.DATE_PLAYED),
sortOrder = setOf(SortOrder.DESCENDING),
)
@JvmStatic
fun createFavoriteItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
ItemFields.MEDIA_STREAMS,
ItemFields.MEDIA_SOURCES,
),
includeItemTypes = setOf(itemType),
recursive = true,
parentId = parentId,
imageTypeLimit = 1,
filters = setOf(ItemFilter.IS_FAVORITE),
sortBy = setOf(ItemSortBy.SORT_NAME),
)
@JvmStatic
fun createCollectionsRequest(parentId: UUID) = GetItemsRequest(
fields = setOf(ItemFields.CHILD_COUNT),
includeItemTypes = setOf(BaseItemKind.BOX_SET),
recursive = true,
imageTypeLimit = 1,
parentId = parentId,
sortBy = setOf(ItemSortBy.SORT_NAME),
)
@JvmStatic
fun createPremieresRequest(parentId: UUID) = GetItemsRequest(
fields = setOf(
ItemFields.DATE_CREATED,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.CHILD_COUNT,
),
includeItemTypes = setOf(BaseItemKind.EPISODE),
parentId = parentId,
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 = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
includeItemTypes = setOf(BaseItemKind.AUDIO),
recursive = true,
parentId = parentId,
imageTypeLimit = 1,
filters = setOf(ItemFilter.IS_PLAYED),
sortBy = setOf(ItemSortBy.DATE_PLAYED),
sortOrder = setOf(SortOrder.DESCENDING),
enableTotalRecordCount = false,
limit = 50,
)
@JvmStatic
fun createPlaylistsRequest() = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CUMULATIVE_RUN_TIME_TICKS,
ItemFields.CHILD_COUNT,
),
includeItemTypes = setOf(BaseItemKind.PLAYLIST),
imageTypeLimit = 1,
recursive = true,
sortBy = setOf(ItemSortBy.DATE_CREATED),
sortOrder = setOf(SortOrder.DESCENDING),
)
@JvmStatic
fun createBrowseGridItemsRequest(parent: BaseItemDto): GetItemsRequest {
val baseRequest = GetItemsRequest(
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CHILD_COUNT,
ItemFields.MEDIA_SOURCES,
ItemFields.MEDIA_STREAMS,
ItemFields.DISPLAY_PREFERENCES_ID,
),
parentId = parent.id,
)
if (parent.type == BaseItemKind.USER_VIEW || parent.type == BaseItemKind.COLLECTION_FOLDER) {
return when (parent.collectionType) {
CollectionType.MOVIES -> baseRequest.copy(
includeItemTypes = setOf(BaseItemKind.MOVIE),
recursive = true,
)
CollectionType.TVSHOWS -> baseRequest.copy(
includeItemTypes = setOf(BaseItemKind.SERIES),
recursive = true,
)
CollectionType.BOXSETS -> baseRequest.copy(
includeItemTypes = setOf(BaseItemKind.BOX_SET),
parentId = null,
recursive = true,
)
CollectionType.MUSIC -> baseRequest.copy(
includeItemTypes = setOf(BaseItemKind.MUSIC_ALBUM),
recursive = true,
)
else -> baseRequest
}
}
return baseRequest
}
}

View File

@@ -1,9 +1,11 @@
package org.jellyfin.androidtv.ui.browsing
import org.jellyfin.androidtv.data.querying.StdItemQuery
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.genresApi
import org.jellyfin.sdk.model.constant.ItemSortBy
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.koin.android.ext.android.inject
class ByGenreFragment : BrowseFolderFragment() {
@@ -16,19 +18,25 @@ class ByGenreFragment : BrowseFolderFragment() {
// Get all genres for this folder
val genresResponse by apiClient.genresApi.getGenres(
parentId = folder?.id,
sortBy = setOf(ItemSortBy.SortName),
userId = apiClient.userId,
sortBy = setOf(ItemSortBy.SORT_NAME),
)
for (genre in genresResponse.items.orEmpty()) {
val itemsQuery = StdItemQuery().apply {
parentId = folder?.id.toString()
sortBy = arrayOf(ItemSortBy.SortName)
includeType?.let { includeItemTypes = arrayOf(it) }
genres = arrayOf(genre.name)
recursive = true
}
rows.add(BrowseRowDef(genre.name, itemsQuery, 40))
val itemsRequest = GetItemsRequest(
parentId = folder?.id,
sortBy = setOf(ItemSortBy.SORT_NAME),
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
genres = setOf(genre.name.orEmpty()),
recursive = true,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
)
rows.add(BrowseRowDef(genre.name, itemsRequest, 40))
}
rowLoader.loadRows(rows)

View File

@@ -1,8 +1,10 @@
package org.jellyfin.androidtv.ui.browsing
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.data.querying.StdItemQuery
import org.jellyfin.sdk.model.constant.ItemSortBy
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.request.GetItemsRequest
class ByLetterFragment : BrowseFolderFragment() {
override suspend fun setupQueries(rowLoader: RowLoader) {
@@ -12,27 +14,41 @@ class ByLetterFragment : BrowseFolderFragment() {
val letters = getString(R.string.byletter_letters)
// Add a '#' item
val numbersQuery = StdItemQuery().apply {
parentId = folder?.id?.toString()
sortBy = arrayOf(ItemSortBy.SortName)
includeType?.let { includeItemTypes = arrayOf(it) }
nameLessThan = letters.substring(0, 1)
recursive = true
}
val numbersItemsRequest = GetItemsRequest(
parentId = folder?.id,
sortBy = setOf(ItemSortBy.SORT_NAME),
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
nameLessThan = letters.substring(0, 1),
recursive = true,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
)
rows.add(BrowseRowDef("#", numbersQuery, 40))
rows.add(BrowseRowDef("#", numbersItemsRequest, 40))
// Add all the defined letters
for (letter in letters.toCharArray()) {
val letterQuery = StdItemQuery().apply {
parentId = folder?.id?.toString()
sortBy = arrayOf(ItemSortBy.SortName)
includeType?.let { includeItemTypes = arrayOf(it) }
nameStartsWith = letter.toString()
recursive = true
}
val letterItemsRequest = GetItemsRequest(
parentId = folder?.id,
sortBy = setOf(ItemSortBy.SORT_NAME),
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
nameStartsWith = letter.toString(),
recursive = true,
fields = setOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.OVERVIEW,
ItemFields.ITEM_COUNTS,
ItemFields.DISPLAY_PREFERENCES_ID,
ItemFields.CHILD_COUNT,
),
)
rows.add(BrowseRowDef(letter.toString(), letterQuery, 40))
rows.add(BrowseRowDef(letter.toString(), letterItemsRequest, 40))
}
rowLoader.loadRows(rows)

View File

@@ -1,52 +0,0 @@
package org.jellyfin.androidtv.ui.browsing;
import android.os.Bundle;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.data.querying.StdItemQuery;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.querying.ItemFields;
public class CollectionFragment extends EnhancedBrowseFragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override
protected void setupQueries(RowLoader rowLoader) {
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0) {
StdItemQuery movies = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
movies.setParentId(mFolder.getId().toString());
movies.setIncludeItemTypes(new String[]{"Movie"});
mRows.add(new BrowseRowDef(getString(R.string.lbl_movies), movies, 100));
StdItemQuery series = new StdItemQuery();
series.setParentId(mFolder.getId().toString());
series.setIncludeItemTypes(new String[]{"Series"});
mRows.add(new BrowseRowDef(getString(R.string.lbl_tv_series), series, 100));
StdItemQuery others = new StdItemQuery();
others.setParentId(mFolder.getId().toString());
others.setExcludeItemTypes(new String[]{"Movie", "Series"});
mRows.add(new BrowseRowDef(getString(R.string.lbl_other), others, 100));
rowLoader.loadRows(mRows);
}
}
}

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