Compare commits

...

447 Commits

Author SHA1 Message Date
renovate[bot]
ddc193086d Update exoplayer
(cherry picked from commit b74cf235df)
2023-08-21 22:08:38 +02:00
Niels van Velzen
d9e99cbb0b Fix disabling subtitles not always working
(cherry picked from commit 3b963c9b2f)
2023-08-21 22:02:38 +02:00
Niels van Velzen
3d3e2e5333 Always use user provided fast forward and rewind values
(cherry picked from commit 1c427e82c3)
2023-08-21 22:02:18 +02:00
Niels van Velzen
c160feea59 Fix crash when location type is null in CardPresenter
(cherry picked from commit 3f896e61f1)
2023-08-21 22:00:32 +02:00
Niels van Velzen
1a8d821622 Show UI mode notification on startup screen 2023-07-01 15:34:32 +02:00
Niels van Velzen
bf4fa8ea5d Add more logging to authentication code
(cherry picked from commit f18557934c)
2023-07-01 15:24:54 +02:00
Niv Harel
df561f88f5 focus back on versions button when selecting a version
(cherry picked from commit 84e9ced5ad)
2023-07-01 15:24:02 +02:00
Niels van Velzen
20f62243aa Fix horizontal grid position resetting on resume 2023-07-01 08:59:12 +02:00
Niels van Velzen
027d1b1010 Fix ItemLauncher retrieving base item via from media manager when launching a photo 2023-06-03 14:14:37 +02:00
StubbyB
d565617e0b Fix InfoLayoutHelper not using first video stream, update details when selecting version (#2812)
(cherry picked from commit 9657743a19)
2023-06-03 14:00:33 +02:00
Niels van Velzen
208dc4feb9 Fix Live TV progress reporting
(cherry picked from commit 046917020d)
2023-06-03 14:00:33 +02:00
Niels van Velzen
3c4613c63f Fix rare race condition crash in HomeFragment
(cherry picked from commit 49b8526338)
2023-06-03 14:00:33 +02:00
Niels van Velzen
b6605958b1 Fix playback of seasons with external player
(cherry picked from commit 49a326c74c)
2023-06-03 14:00:33 +02:00
Niels van Velzen
57730b75e2 Fix race condition in HomeFragment causing a crash when profile picture loads when the view is already destroyed
(cherry picked from commit 570f9ec4ba)
2023-06-03 14:00:32 +02:00
Niels van Velzen
74fe6658cc Fix PictureViewerFragment using activityViewModel
(cherry picked from commit 61324816d7)
2023-06-03 14:00:32 +02:00
renovate[bot]
a8d45cecd7 Update dependency org.jellyfin.exoplayer:exoplayer-ffmpeg-extension to v2.18.7+1
(cherry picked from commit 79c80198c1)
2023-06-03 13:51:45 +02:00
renovate[bot]
6994234bd0 Update dependency com.google.android.exoplayer:exoplayer to v2.18.7
(cherry picked from commit fd16bf51b8)
2023-06-03 13:51:42 +02:00
Niels van Velzen
1ccfa7eae6 Hide SSA tags from SRT subtitles
(cherry picked from commit 05651d37d2)
2023-06-03 13:50:11 +02:00
Niels van Velzen
7749e401ad Fix resume row not hiding when empty
(cherry picked from commit c437842209)
2023-05-02 17:12:32 +02:00
Niels van Velzen
b2b60bf78a Add support for retrieving resume items in BaseRowAdapter using the SDK
(cherry picked from commit 1d44c83501)
2023-04-30 16:31:18 +02:00
Niels van Velzen
e2ede5b7be Allow changing the (a)ac3 bitstreaming option on all devices
(cherry picked from commit 1d91af6956)
2023-04-27 19:36:30 +02:00
renovate[bot]
985e4ef9a7 Update dependency org.jellyfin.exoplayer:exoplayer-ffmpeg-extension to v2.18.6+1
(cherry picked from commit 936356f835)
2023-04-27 17:22:46 +02:00
Niels van Velzen
7d5895134b Fix ExoPlayerProfile not always listing SubRip and SRT as external
(cherry picked from commit 991447f797)
2023-04-27 17:22:45 +02:00
Niels van Velzen
dc5bf6b47d Fix ExpandableTextView cutting content
(cherry picked from commit 3d5142c108)
2023-04-27 17:22:43 +02:00
renovate[bot]
147fc4f772 Update dependency org.jellyfin.sdk:jellyfin-core to v1.4.2
(cherry picked from commit 84efc6a693)
2023-04-27 17:22:42 +02:00
Niels van Velzen
ecead3fad8 Fix ExoPlayer extensions not used for music playback (backport) 2023-04-27 15:24:12 +02:00
Niels van Velzen
834bebb03f Add DVB subtitle support in ExoPlayer
(cherry picked from commit 93979e1d38)
2023-04-23 15:40:58 +02:00
Niels van Velzen
a001846da7 Enable PGS subtitles in ExoPlayer
(cherry picked from commit d63ac02525)
2023-04-23 10:58:46 +02:00
Niels van Velzen
5c7bf941ea Fix switching ExoPlayer subtitle track
(cherry picked from commit e903d8c347)
2023-04-23 10:58:44 +02:00
Niels van Velzen
53b92184a4 Fix offsetStreamIndex when selecting last track
(cherry picked from commit f862a17223)
2023-04-23 10:58:43 +02:00
renovate[bot]
1e369480fd Update dependency com.google.android.exoplayer:exoplayer to v2.18.6
(cherry picked from commit 4699454d5d)
2023-04-23 10:58:26 +02:00
Niels van Velzen
e00f6d3f40 Fix app crash when playback stop is requested without an active player
(cherry picked from commit f99451d22e)
2023-04-23 10:55:33 +02:00
Niels van Velzen
5e85a876bd Add F-Droid badge to README
(cherry picked from commit 1b44358f10)
2023-04-03 21:04:43 +02:00
Niels van Velzen
5249209f3a Fix "null" on Live TV guide "load channels" button 2023-04-03 19:32:56 +02:00
Niels van Velzen
276e138d7a Set screenOrientation for PreferencesActivity
(cherry picked from commit fb651b173c)
2023-04-03 19:31:41 +02:00
Niels van Velzen
00cc8f14ec Fix app closing when music queue ends
(cherry picked from commit 25e8738818)
2023-04-03 19:31:36 +02:00
Niels van Velzen
f14a8fa6b8 Store accounts in Account Manager by address
(cherry picked from commit 6806f69c62)
2023-04-03 19:31:23 +02:00
Niels van Velzen
6191044057 Always check server id when retrieving user data from AccountManagerStore
(cherry picked from commit 6712eeb781)
2023-03-12 10:34:04 +01:00
Niels van Velzen
d0365ced7e Fix SearchFragment not restoring properly
(cherry picked from commit 0a30b50904)
2023-03-12 10:34:03 +01:00
Niels van Velzen
9c8143368f Add workaround for non reproducible baseline profiles in AGP
(cherry picked from commit eada9ea21c)
2023-03-12 10:34:03 +01:00
omm
266d5738a6 Add IncludeType param when sending number query
(cherry picked from commit 68f6b3a3ef)
2023-03-12 10:34:03 +01:00
renovate[bot]
e34442a432 Update dependency com.google.android.exoplayer:exoplayer to v2.18.4
(cherry picked from commit 7a5ee0b229)
2023-03-12 10:34:02 +01:00
David Fairbrother
12e4c96304 Accept KEYCODE_ENTER as alt to KEYCODE_CENTER
Adds KEYCODE_ENTER wherever we accept KEYCODE_DPAD_CENTER. This was
reported in #2526 where third-party remotes will use the former instead
of the latter value.

I've added this to all places we accept CENTER, since our most of our
Kotlin code already accepts this as an alternative and I can't forsee a
case where we would want these to accept different things.

This was tested in the emulator to ensure we've not broken the existing
mapping, but short of using the enter key (which works) I can't verify
this fixes the original issue.

(cherry picked from commit b3bc4b0d8c)
2023-03-12 10:33:34 +01:00
Niels van Velzen
857ad84771 Add missing mediaManager injection to EnhancedBrowseFragment 2023-02-26 17:10:48 +01:00
Niels van Velzen
7dfd3b9179 Mark LibVLC as experimental
(cherry picked from commit 74a7898fcd)
2023-02-26 17:08:16 +01:00
Niels van Velzen
63de8d16d2 Fix theme not applied before creating views
(cherry picked from commit 328072a36c)
2023-02-26 16:59:46 +01:00
Niels van Velzen
b771bb4c27 Only attach background service in activities
(cherry picked from commit bbc7f3eea4)
2023-02-26 16:59:42 +01:00
Niels van Velzen
2b01ac9ffc Don't show "invalid credentials" message when authentication fails with an SDK exception
(cherry picked from commit 84dc7fc48d)
2023-02-26 16:59:34 +01:00
renovate[bot]
b857fb4f1c Update dependency com.google.android.exoplayer:exoplayer to v2.18.3
(cherry picked from commit f10cbc05d7)
2023-02-26 16:59:24 +01:00
Timmeh
19707eb9c8 Fix old items are shown when new filter is applied. (#2472)
* clear rows after applying new filter.

* Always set current item to toolbar if current item is filtered out.

(cherry picked from commit 215b0e9f2c)
2023-02-26 16:58:34 +01:00
Niels van Velzen
c60d210e87 Fix rare picture viewer crash when album is empty
(cherry picked from commit e3d5262b5b)
2023-02-26 16:58:16 +01:00
Niels van Velzen
cb046c26c5 Change opening menu to ACTION_UP instead of ACTION_DOWN
(cherry picked from commit 22b4ecf0ec)
2023-01-24 21:23:20 +01:00
Niels van Velzen
3d97b5abb0 Use HEADER_ACCEPT from SDK as Glide Accept header value
(cherry picked from commit a7b06d39e1)
2023-01-24 21:23:18 +01:00
Niels van Velzen
da8c7c2837 Set Accept header in Glide requests
(cherry picked from commit 3839d1c5d1)
2023-01-24 21:23:17 +01:00
renovate[bot]
d9c11d86ad Update dependency org.jellyfin.sdk:jellyfin-core to v1.4.1
(cherry picked from commit 80a2dd5ac1)
2023-01-24 21:23:15 +01:00
Niels van Velzen
b9172845d8 Increase amount of logs added to crash reports
Some stacktraces with Kotlin coroutines can get big. Right now we have 100 lines and this often is not enough to see what happened before the exception. Increasing the count to 250 should hopefully fix that.

(cherry picked from commit 5b83a6de9a)
2023-01-24 21:23:15 +01:00
Niels van Velzen
3d3fa76771 Remove experimental audio codecs from transcoding profile
(cherry picked from commit 3c70b8abf5)
2023-01-10 13:45:37 +01:00
Niels van Velzen
521183d427 Fix BrowseGridFragment focus not restoring when going back
(cherry picked from commit b5b87fb625)
2023-01-10 13:45:36 +01:00
Niels van Velzen
103f8babd5 Fix IndexOutOfBoundsException in setExoPlayerTrack 2023-01-04 22:06:58 +01:00
kevgrig
4ec0bcba72 Support fast forward and rewind in audio player (#2380)
* Support fast forward and rewind in audio player

Signed-off-by: Kevin G <kevin@myplaceonline.com>

* Use CDI for UserSettingPreferences

Signed-off-by: Kevin G <kevin@myplaceonline.com>

Signed-off-by: Kevin G <kevin@myplaceonline.com>
2023-01-04 08:55:00 +01:00
Niels van Velzen
096a4362c6 Allow all supported audio codecs in ExoPlayer transcoding profile 2023-01-04 08:54:06 +01:00
Martin P
3ad59c8a4d Translated using Weblate (French)
Currently translated at 100.0% (495 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2023-01-01 14:51:18 -05:00
ByKripsy
a07a2d5fbe Translated using Weblate (French)
Currently translated at 99.7% (494 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2022-12-31 11:51:17 -05:00
Niels van Velzen
ad1f12b289 Fix crash when opening menu for AlbumArtist
isFolder() returned null which caused a NPE
2022-12-30 09:30:07 +01:00
Niels van Velzen
06d98f64a8 Fix scaling of album images in audio queue on now playing screen 2022-12-29 20:52:41 +01:00
Niels van Velzen
9ad1ff0cde Update build files 2022-12-29 09:55:34 +01:00
0TTA
a3d7a69d5f Translated using Weblate (Arabic)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/ar/
2022-12-26 08:51:16 -05:00
0TTA
d9b0835687 Translated using Weblate (Arabic)
Currently translated at 98.7% (489 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ar/
2022-12-26 08:51:16 -05:00
Niels van Velzen
342b0724ed Validate current user from UserRepository in validateAuthentication() 2022-12-17 21:27:05 +01:00
Niels van Velzen
9045a65721 Fix race condition in ItemListFragment causing favorite music screen to crash 2022-12-17 16:01:08 +01:00
Weblate
d08ea45cb2 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/
2022-12-17 09:58:23 -05:00
Teo Baranga
c1c6cd1aea Remember button focus on details screen 2022-12-17 15:58:19 +01:00
Bas
d62315ffd9 Translated using Weblate (Dutch)
Currently translated at 98.3% (487 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2022-12-16 08:51:11 -05:00
Weblate
2bb5dde570 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/
2022-12-14 03:12:32 -05:00
Niels van Velzen
aca0c454cd Remove direct path option from external player (#2342)
* Remove direct path option from external player

Direct paths have been deprecated in the server a while back (and is pending removal). Now that it broke in 0.15 it's easier to remove then to fix it.

* Update app/src/main/java/org/jellyfin/androidtv/ui/playback/ExternalPlayerActivity.java

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

Co-authored-by: Cody Robibero <cody@robibe.ro>
2022-12-14 09:12:27 +01:00
DJSweder
f0b86e44d8 Translated using Weblate (Czech)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2022-12-13 06:51:11 -05:00
João Batista
747b821ec7 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2022-12-12 13:51:10 -05:00
Niels van Velzen
f0178663d5 Revert "Remember button focus on summary page"
This reverts commit 0c8fe45b06.
2022-12-10 19:20:18 +01:00
danngalann
c7b7f2f718 Translated using Weblate (Catalan)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ca/
2022-12-10 06:51:10 -05:00
renovate[bot]
965da62f28 Update dependency androidx.compose.ui:ui-tooling to v1.3.2 2022-12-10 12:29:00 +01:00
Niels van Velzen
59f16ca4cf Split compose versions in catalog 2022-12-10 12:19:47 +01:00
renovate[bot]
8bf9bd1c57 Update dependency androidx.fragment:fragment-ktx to v1.5.5 2022-12-10 11:04:03 +01:00
hoanghuy309
fb259dfe46 Translated using Weblate (Vietnamese)
Currently translated at 97.9% (488 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2022-12-07 21:51:09 -05:00
Niels van Velzen
6ab2c24e53 Fix crash when stopping playback with back button when refresh rate switching is enabled 2022-12-07 09:35:08 +01:00
Niels van Velzen
9d3c2d2db8 Fix Live TV program time in RecordPopup 2022-12-07 07:59:25 +01:00
Niels van Velzen
8cf9cc30ba Fix opening Live TV recordings in FullDetailsFragment 2022-12-07 07:59:25 +01:00
Niels van Velzen
d5b48a95f0 Fix Live TV program end time in FullDetailsFragment 2022-12-07 07:59:25 +01:00
Niels van Velzen
13a8990b81 Fix acra-core dependency using wrong name 2022-12-07 07:07:59 +01:00
renovate[bot]
fef0e76733 Update dependency org.jellyfin.sdk:jellyfin-core to v1.4.0 2022-12-06 15:11:58 +01:00
Niels van Velzen
a6729a7ec7 Rewrite session validation code to not use callbacks 2022-12-06 10:15:40 +01:00
João Batista
4777840bb0 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2022-12-05 12:51:08 -05:00
Niels van Velzen
2bcd2b9482 Fix LegacyImageCardView scaling on non-1080p devices 2022-12-04 17:42:06 +01:00
Teo Baranga
0c8fe45b06 Remember button focus on summary page 2022-12-04 17:37:32 +01:00
Bas
225b78d67a Translated using Weblate (Dutch)
Currently translated at 98.3% (490 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2022-12-03 10:51:07 -05:00
Niels van Velzen
6020ed2e74 Remove mActivity from EnhancedBrowseFragment 2022-12-03 14:16:16 +01:00
Kirill Tyurin
3439038f8c Translated using Weblate (Russian)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2022-11-29 14:51:06 -05:00
Niels van Velzen
3264d22fb2 Fix notification card not focusable on older Android versions 2022-11-29 19:32:50 +01:00
Niels van Velzen
4d4b4515bb Fix crash when opening Live TV series 2022-11-29 15:38:46 +01:00
renovate[bot]
236314050d Update exoplayer 2022-11-27 10:54:43 +01:00
wolong gl
41cd7425df Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2022-11-26 06:51:05 -05:00
fknght
b0178cca8c Translated using Weblate (Greek)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2022-11-26 06:51:04 -05:00
Niels van Velzen
675f285a72 Rewrite theming code to not use callbacks 2022-11-25 11:54:42 +01:00
Niels van Velzen
5523309223 Fix opening Live TV series timers 2022-11-25 11:54:25 +01:00
Niels van Velzen
45a29f34bc Fix opening Live TV recordings 2022-11-25 11:54:12 +01:00
Niels van Velzen
ed06b5c255 Fix TimeUtils.getDate not using UTC for offset 2022-11-22 15:23:57 +01:00
Per
6629864913 Translated using Weblate (German)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2022-11-22 05:51:03 -05:00
renovate[bot]
f7a89233bc Update dependency org.jellyfin.sdk:jellyfin-core to v1.4.0-beta.2 2022-11-20 09:36:49 +01:00
hoanghuy309
95cf3751ae Translated using Weblate (Vietnamese)
Currently translated at 96.3% (480 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2022-11-19 12:51:02 -05:00
Niels van Velzen
aeebbaf5c1 Fix NPE in ExternalPlayerActivity when getMediaSource() returns null 2022-11-17 09:52:24 +01:00
Niels van Velzen
36009c5ab4 Fix opening user view with no collection type 2022-11-17 09:52:14 +01:00
renovate[bot]
ab1d976458 Update github/codeql-action digest to 678fc3a 2022-11-16 21:27:42 +01:00
Niels van Velzen
6c91002725 Fix EnhancedBrowseFragment duplicating items on resume 2022-11-16 18:52:02 +01:00
Niels van Velzen
0c966e1936 Use ItemRowAdapterHelper.setItems for more row types 2022-11-16 18:51:53 +01:00
nextlooper42
2057ec95c3 Translated using Weblate (Slovak)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2022-11-16 06:40:45 -05:00
Niels van Velzen
5968bb0965 Delay home fragment refreshing 2022-11-14 20:38:58 +01:00
renovate[bot]
f46e5fc807 Update github/codeql-action digest to 4238421 2022-11-14 17:27:19 +01:00
Andi Chandler
a990538ffa Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2022-11-14 07:51:00 -05:00
Csaba
1276956229 Translated using Weblate (Hungarian)
Currently translated at 100.0% (498 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2022-11-14 02:51:00 -05:00
Niels van Velzen
476e73d988 Fix missing checkboxes in player audio, subtitle and zoom menu 2022-11-14 08:39:07 +01:00
Oskari Lavinto
36e1a3c2c1 Translated using Weblate (Finnish)
Currently translated at 99.1% (494 of 498 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-11-13 08:51:00 -05:00
DeathHarmony
bdf0d1ca2c Translated using Weblate (German)
Currently translated at 97.7% (486 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2022-11-12 08:28:40 -05:00
Niels van Velzen
8e9d5ef24c Add dismissible notification for beta releases 2022-11-12 14:28:35 +01:00
Bas
04acbb9e30 Translated using Weblate (Dutch)
Currently translated at 98.5% (490 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2022-11-12 05:50:59 -05:00
Larvitar
9945c870bc Translated using Weblate (Polish)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pl/
2022-11-11 19:51:00 -05:00
renovate[bot]
2a6d236b1d Update dependency com.squareup.leakcanary:leakcanary-android to v2.10 2022-11-11 15:46:18 +01:00
Tom
986ad0c17e Translated using Weblate (Lithuanian)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lt/
2022-11-11 08:51:02 -05:00
lojden
067bb13287 Translated using Weblate (Swedish)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2022-11-11 08:50:59 -05:00
renovate[bot]
fdec4d8407 Update androidx-compose to v1.3.1 2022-11-11 07:10:14 +01:00
João Batista
51df9e4c17 Translated using Weblate (Portuguese (Portugal))
Currently translated at 95.9% (477 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_PT/
2022-11-10 16:50:59 -05:00
Niels van Velzen
33df66172d Fix bad version.ref in dependency catalog 2022-11-10 22:31:30 +01:00
Tom
8f74d49e87 Translated using Weblate (Lithuanian)
Currently translated at 50.1% (249 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lt/
2022-11-10 11:51:00 -05:00
Weblate
2775f4d8de 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/
2022-11-10 08:51:00 -05:00
Tom
70f4b2c967 Translated using Weblate (Lithuanian)
Currently translated at 30.7% (153 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lt/
2022-11-10 08:50:59 -05:00
Herman Ho
1d334f94cf Translated using Weblate (Chinese (Traditional))
Currently translated at 96.7% (481 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hant/
2022-11-09 20:50:59 -05:00
Tom
2b31fd3f97 Translated using Weblate (Lithuanian)
Currently translated at 23.9% (119 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/lt/
2022-11-09 12:50:59 -05:00
Niels van Velzen
6486713146 Avoid clearing all items on ItemRowAdapter load 2022-11-09 16:03:21 +01:00
linlou
ab6ae6cd16 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.5% (485 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2022-11-09 04:50:58 -05:00
DenizenB
23250c6888 apply subtitle delay to external subs 2022-11-08 13:13:36 +01:00
The Randalorian
0619cbdbcf Add Video Codec Tag 2022-11-08 13:13:00 +01:00
Niels van Velzen
f53dba8491 Focus HomeRowsFragment by default, fix wrong focus on resume 2022-11-08 12:42:51 +01:00
Niels van Velzen
5657dfb22a Merge HomeToolbarFragment into HomeFragment 2022-11-08 12:42:51 +01:00
DJSweder
4ce0fe81b2 Translated using Weblate (Czech)
Currently translated at 97.7% (486 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2022-11-06 09:50:57 -05:00
renovate[bot]
1270123872 Update github/codeql-action digest to c3b6fce 2022-11-05 10:43:36 +01:00
Niels van Velzen
78b1de23c7 Fix app crash when playing trailers 2022-11-04 21:49:29 +01:00
Niels van Velzen
83e1fb9a59 Always reset navigation in StartupActivity 2022-11-04 21:49:18 +01:00
DenizenB
f1e8e989ed Translated using Weblate (Swedish)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2022-11-04 13:50:57 -04:00
renovate[bot]
ee9520d45b Update dependency com.google.devtools.ksp to v1.7.20-1.0.8 2022-11-04 09:17:14 +01:00
renovate[bot]
4eddfaf8f3 Update dependency io.kotest:kotest-runner-junit5 to v5.5.4 2022-11-04 09:17:05 +01:00
renovate[bot]
be51a9893b Update github/codeql-action digest to 18fe527 2022-11-03 08:50:55 +01:00
Csaba
46a7b4a345 Translated using Weblate (Hungarian)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2022-11-02 05:50:56 -04:00
Eduardo Santiago
104b707b9b Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/pt_BR/
2022-11-01 11:50:56 -04:00
lenn219
51fdd79999 Translated using Weblate (Dutch)
Currently translated at 98.5% (490 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2022-11-01 11:50:56 -04:00
Niels van Velzen
00e1eb6e08 Update Jellyfin SDK to 1.4.0-beta.1 2022-11-01 14:18:46 +01:00
Niels van Velzen
9d66eeef0d Rename xml layout starting with activity_ to fragment_ 2022-11-01 06:58:23 +01:00
Niels van Velzen
5acbae0c16 Fix StartupActivity set destination is ignored 2022-10-31 20:46:39 +01:00
Andi Chandler
9a9d513de6 Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2022-10-31 12:50:55 -04:00
renovate[bot]
4345ea301d Update eps1lon/actions-label-merge-conflict action to v2.1.0 2022-10-31 14:15:35 +01:00
Weblate
d6ca138cdf 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/
2022-10-31 05:50:56 -04:00
millallo
019c62756d Translated using Weblate (Italian)
Currently translated at 99.5% (495 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2022-10-31 05:50:55 -04:00
Niels van Velzen
52465bb3c8 Add placeholder drawable for ImageProvider 2022-10-31 07:31:43 +01:00
sensei73
c8355ae445 Translated using Weblate (French)
Currently translated at 99.3% (494 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2022-10-30 21:50:55 -04:00
Niels van Velzen
6429fb3ff5 Update Detekt to ignore FunctionNaming for @Composable 2022-10-30 12:25:25 +01:00
Niels van Velzen
099dd125bd Rewrite SplashFragment in Compose 2022-10-30 12:25:25 +01:00
Niels van Velzen
1301cb9f52 Load fresh item data in AudioTrackQueue 2022-10-29 22:16:52 +02:00
Stelios Diamantis
2b48e39195 Translated using Weblate (Greek)
Currently translated at 98.9% (492 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2022-10-29 15:50:55 -04:00
Dionysis Theofilopoulos
e336763964 Translated using Weblate (Greek)
Currently translated at 97.1% (483 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2022-10-29 06:50:54 -04:00
renovate[bot]
2d4cd0ebd8 Update alexellis/upload-assets action to v0.4.0 2022-10-29 12:17:23 +02:00
cameron
4a4efb2467 fix seekbar not updating when using ff/rw buttons 2022-10-29 11:29:49 +02:00
Peng-Hui Yang
0cf79cfd82 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.7% (486 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2022-10-29 00:50:54 -04:00
Niels van Velzen
196d3bc6bd Use fade animation when navigating 2022-10-28 21:10:56 +02:00
Niels van Velzen
b5a6e733c2 Fix EnhancedBrowseFragment registering multiple listeners 2022-10-28 21:10:48 +02:00
emidriel
d5a9744123 Translated using Weblate (Norwegian Bokmål)
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/nb_NO/
2022-10-28 12:50:54 -04:00
renovate[bot]
cb1a84862d Update kotest to v5.5.3 2022-10-28 08:48:25 +02:00
Oskari Lavinto
2fa53e7650 Translated using Weblate (Finnish)
Currently translated at 99.1% (493 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-10-27 04:50:54 -04:00
Niels van Velzen
a73e04c216 Fix setExoPlayerTrack exception when allStreams is empty 2022-10-27 07:07:40 +02:00
Niels van Velzen
0938a8a19a Rework BrandedQueueEntry and UserQueueEntry implementation 2022-10-26 23:04:28 +02:00
Niels van Velzen
539c8f5937 Add queue 2022-10-26 23:04:28 +02:00
Niels van Velzen
4db9973d65 Minor changes to subtitle coloring code 2022-10-26 22:15:25 +02:00
sparky3387
8845429e9f Initial commit
pick 13699f674 Initial commit
    squash 7e8af2dc4 Modify RichListDialog to display subtitle colors
    squash 19fcb1514 Modify RichListOption to display subtitle colors

Modify RichListDialog to display subtitle colors

Modify RichListOption to display subtitle colors

Add Subtitle options to preferences list

Added dark grey option and match master commit as much as possible

Fix line length

Fix line length

Fix API Level to Support 21 Lint Error 1

Fix API Level to Support 21 Lint Error 1

Fix API Level to Support 21 Lint Error 1

Fix API Level to Support 21 Lint Error 2

Update app/src/main/res/values/strings.xml

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

Update app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt

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

Update app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java

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

Update app/src/main/java/org/jellyfin/androidtv/ui/shared/StrokeTextView.kt

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

Requested resolutions implemented part 1

Update app/src/main/java/org/jellyfin/androidtv/ui/preference/dsl/OptionsColorList.kt

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

Update app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java

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

Update app/src/main/java/org/jellyfin/androidtv/ui/shared/StrokeTextView.kt

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

Requested resolutions implemented part 2, restore RichListDialogFragment.kt and add ColorPickerDialogFragment and ColorListPreference

Requested resolutions implemented part 3, restore distributionkey

Rebased PlaybackPreferencesScreen.kt

Update app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt

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

Update app/src/main/java/org/jellyfin/androidtv/ui/preference/dsl/OptionsColorList.kt

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

Resolve line length and magic number issues, using different background for the brighter colors

Update app/src/main/java/org/jellyfin/androidtv/ui/shared/StrokeTextView.kt

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

Resolve magic numbers attempt

Resolve line length issue

Remove unnecessary antialias

Update app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java

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

Update app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java

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

Update app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt

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

Update app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java

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

Move subtitle setting preference to where the rest of the settings are located
Resolve formatting issue in PlaybackPreferenceScreen

Update PlaybackPreferencesScreen.kt

Fixing formatting

Move subtitle setting preference to where the rest of the settings are located
Resolve formatting issue in PlaybackPreferenceScreen
Resolve import of userpreferences in StrokeTextView.kt

Removed unnecessary functions and data attributes from ColorPickerDialogFragment.k

Update app/src/main/java/org/jellyfin/androidtv/ui/playback/CustomPlaybackOverlayFragment.java

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

Update app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt

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

Resolve magic numbers on lighter colors

Resolve positioning of subtitles and importing of preferences

Correct positioning of subtitlesTextColor in UserPreferences.kt
Lower Increment

Resolve preference issue

Resolve UserPreferences import
2022-10-26 22:15:25 +02:00
Niels van Velzen
9aec957ca5 Use ImageProvider in MediaContentProvider
This fixes the issue where images would not show when the server was accessed over HTTP
2022-10-26 17:56:13 +02:00
Niels van Velzen
0408b5d842 Use ImageProvider in LeanbackChannelWorker 2022-10-26 17:56:13 +02:00
Niels van Velzen
d0c3c51ef2 Add ImageProvider for retrieval of images in platform integration 2022-10-26 17:56:13 +02:00
renovate[bot]
b6459bde73 Update github/codeql-action digest to ec3cf9c 2022-10-26 14:57:24 +02:00
KD Puvvadi
a71329a2f6 Translated using Weblate (Telugu)
Currently translated at 8.4% (41 of 485 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/te/
2022-10-26 03:50:53 -04:00
Niels van Velzen
05f8b79cd4 Don't use LibraryDreamService.TRANSITION_DURATION in FadeViewSwitcherLayout 2022-10-25 14:30:53 +02:00
Niels van Velzen
8ac5606f06 Remove direct GSON dependency
We don't use it in the app anymore, it's still added indirectly because the old apiclient uses it.
2022-10-25 14:30:42 +02:00
KD Puvvadi
12cf7d8e66 Added translation using Weblate (Telugu) 2022-10-25 07:44:53 -04:00
renovate[bot]
ecf556e416 Update koin to v3.3.0 2022-10-25 09:25:12 +02:00
renovate[bot]
bf8db9c600 Update gradle/wrapper-validation-action digest to 55e685c 2022-10-24 20:49:02 +02:00
renovate[bot]
e994238de1 Update dependency io.insert-koin:koin-androidx-compose to v3.3.0 2022-10-24 20:44:21 +02:00
renovate[bot]
47551a89e8 Update dependency androidx.activity:activity-ktx to v1.6.1 2022-10-24 16:44:16 +02:00
renovate[bot]
0ea00447b8 Update dependency androidx.fragment:fragment-ktx to v1.5.4 2022-10-24 16:44:09 +02:00
renovate[bot]
a0515002c1 Update kotest to v5.5.2 2022-10-23 22:24:35 +02:00
renovate[bot]
4738b0c0ad Update gradle/gradle-build-action digest to 3fbe033 2022-10-22 19:17:11 +02:00
renovate[bot]
d21b35830c Update dependency io.insert-koin:koin-androidx-compose to v3.2.2 2022-10-22 13:14:37 +02:00
Niels van Velzen
62c1e816b6 Add compose support 2022-10-22 10:13:45 +02:00
renovate[bot]
7c769c2f41 Update actions/upload-artifact digest to 83fd05a 2022-10-22 10:02:44 +02:00
renovate[bot]
7e386babde Update dependency org.jellyfin.sdk:jellyfin-core to v1.3.7 2022-10-19 21:46:49 +02:00
renovate[bot]
bc5441b157 Update koin to v3.2.3 2022-10-18 21:13:46 +02:00
Paul Dee
f5acc050af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (485 of 485 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2022-10-18 12:50:52 -04:00
renovate[bot]
6628b00851 Update actions/setup-java digest to de1bb2b 2022-10-18 18:50:49 +02:00
renovate[bot]
7815f80da6 Update github/codeql-action digest to cc7986c 2022-10-18 18:50:42 +02:00
newton181
de7d8e6b8b Translated using Weblate (Spanish (Mexico))
Currently translated at 100.0% (485 of 485 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_MX/
2022-10-17 21:50:50 -04:00
renovate[bot]
b4c0ca341e Update aboutlibraries to v10.5.1 2022-10-17 22:16:33 +02:00
Niels van Velzen
b12b204c80 Show keyboard on right on text search fragment 2022-10-15 14:57:31 +02:00
Weblate
eb36d61217 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/
2022-10-15 05:05:27 -04:00
Niels van Velzen
c568ec6a7a Remove PhotoPlayerActivity 2022-10-15 11:05:23 +02:00
Niels van Velzen
3048fd602f Automatically hide picture viewer actions after 4 seconds 2022-10-15 10:57:31 +02:00
renovate[bot]
37a586a91e Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.4.1 2022-10-15 10:21:58 +02:00
Segi
962d56e95e 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/
2022-10-14 01:50:48 -04:00
Segi
111d527e27 Translated using Weblate (Hebrew)
Currently translated at 100.0% (486 of 486 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2022-10-14 01:50:48 -04:00
Niels van Velzen
48a46ca146 Show picture viewer controls by default 2022-10-14 07:29:59 +02:00
renovate[bot]
c7500ec862 Update dependency com.google.devtools.ksp to v1.7.20-1.0.7 2022-10-14 07:29:48 +02:00
Weblate
e85a84f14d 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/
2022-10-13 15:16:53 -04:00
renovate[bot]
aee3e2c8ba Update dependency com.android.tools.build:gradle to v7.3.1 2022-10-13 21:16:49 +02:00
netanel
bdb4acfdc7 Translated using Weblate (Hebrew)
Currently translated at 100.0% (486 of 486 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2022-10-13 08:50:48 -04:00
Segi
f9c7cfb0e5 Translated using Weblate (Hebrew)
Currently translated at 99.7% (485 of 486 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/he/
2022-10-13 03:50:48 -04:00
Weblate
7959bd5c19 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/
2022-10-12 10:33:45 -04:00
Niels van Velzen
6960c4ec7b Rewrite smaller playback classes to Kotlin 2022-10-12 16:33:39 +02:00
Niels van Velzen
dad7d64f6c Rewrite data\compat in Kotlin 2022-10-12 16:33:39 +02:00
Weblate
43b3d121bc 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/
2022-10-11 01:30:30 -04:00
Niels van Velzen
31be62946b Remove unused strings 2022-10-11 07:30:25 +02:00
Niels van Velzen
905ca61d38 Remove unused actors and search tile drawables 2022-10-11 07:30:25 +02:00
renovate[bot]
db497c0ad9 Update kotest to v5.5.1 2022-10-09 21:54:22 +02:00
Niels van Velzen
6bdde2c2bc Don't use activity.isFinishing() 2022-10-09 19:22:54 +02:00
Niels van Velzen
e497f3998c Replace Response<> usages with LifecycleAwareResponse<> in fragments 2022-10-09 19:22:44 +02:00
millallo
409301f526 Translated using Weblate (Italian)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/it/
2022-10-09 05:54:40 -04:00
Niels van Velzen
a48c696a34 Fix PictureViewerFragment crashing when opening photo from latest row 2022-10-09 08:43:58 +02:00
wolong gl
ee2ff96327 Translated using Weblate (Chinese (Simplified))
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/zh_Hans/
2022-10-09 01:54:39 -04:00
wolong gl
52e92b995a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2022-10-09 01:54:39 -04:00
Niels van Velzen
553e218d83 Check Lifecycle.State.STARTED instead of Lifecycle.State.INITIALIZED 2022-10-08 22:59:18 +02:00
Alan Azar
964a100f0b Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/en_GB/
2022-10-08 14:54:40 -04:00
Niels van Velzen
8c3f54899e Check lifecycle state when runnable executes 2022-10-08 18:28:36 +02:00
nextlooper42
84b0291a65 Translated using Weblate (Slovak)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sk/
2022-10-08 09:54:39 -04:00
hoanghuy309
fd958874c0 Translated using Weblate (Vietnamese)
Currently translated at 98.1% (488 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/vi/
2022-10-08 06:54:39 -04:00
Niels van Velzen
4fa4cd48f5 Add lifecycle response 2022-10-08 10:12:46 +02:00
Niels van Velzen
d3c35725e8 Rewrite MyDetailsOverviewRow in Kotlin 2022-10-08 10:12:36 +02:00
Niels van Velzen
04b6be7177 Rewrite presenters in Kotlin 2022-10-08 10:12:26 +02:00
renovate[bot]
023b4a8e86 Update glide to v4.14.2 2022-10-08 10:06:14 +02:00
Niels van Velzen
b0c642a0c2 Add PictureViewerFragment as destination 2022-10-08 09:51:24 +02:00
Niels van Velzen
ee9763973f Remove GsonJsonSerializer
Also migrates a bunch of stuff to the SDK so we can use kotlinx.serialization
2022-10-07 16:19:31 +02:00
renovate[bot]
a20b893066 Update actions/stale digest to 5ebf00e 2022-10-07 16:19:22 +02:00
Niels van Velzen
0a372fe3f0 Split a long navigation line in BrowseGridFragment 2022-10-07 08:05:59 +02:00
Niels van Velzen
4317917759 Fix navigation repository history not reset when activity recreated 2022-10-07 08:05:59 +02:00
Niels van Velzen
e63314402e Fix key event not reaching nested fragments 2022-10-07 08:05:59 +02:00
Niels van Velzen
b7a8bbf0af Use NavigationRepository for PlaybackOverlayActivity 2022-10-07 08:05:59 +02:00
Niels van Velzen
b8c542ede1 Add NavigationAction to control fragment manager 2022-10-07 08:05:59 +02:00
Niels van Velzen
a80ab9c7c1 Implement display mirroring 2022-10-07 08:05:59 +02:00
Niels van Velzen
b24f135efb Rewrite navigation 2022-10-07 08:05:59 +02:00
renovate[bot]
532b6b92d2 Update github/codeql-action digest to 8075783 2022-10-07 08:05:07 +02:00
YusanTayir
c715dbd149 Added translation using Weblate (Uyghur) 2022-10-06 01:15:43 -04:00
renovate[bot]
8bb4a61d6b Update actions/checkout digest to 2541b12 2022-10-05 19:34:11 +02:00
renovate[bot]
8fc5d9c23d Update dependency org.jellyfin.exoplayer:exoplayer-ffmpeg-extension to v2.18.1+2 2022-10-05 15:43:22 +02:00
Niels van Velzen
07ea277269 Fix crash when media info contains a data stream 2022-10-04 20:28:37 +02:00
renovate[bot]
ed972667eb Update actions/checkout digest to 93ea575 2022-10-04 15:21:27 +02:00
newton181
f98be9daee Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2022-10-03 21:54:38 -04:00
florentsarat
d808943695 Translated using Weblate (French)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2022-10-03 21:54:38 -04:00
VocalFan
245e977057 Translated using Weblate (Russian)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2022-10-03 09:54:38 -04:00
VocalFan
3f4a2c5863 Translated using Weblate (Spanish)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2022-10-03 09:54:37 -04:00
renovate[bot]
fc52c6ba72 Update kotest to v5.5.0 2022-10-03 09:39:13 +02:00
Nils Fürniß
a31945b09c Translated using Weblate (German)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2022-10-02 17:54:37 -04:00
Niels van Velzen
3a9881c52e Fix audio player screensaver starting too soon 2022-10-01 19:13:14 +02:00
Niels van Velzen
5a8108a129 Add CustomMessageRepository, remove BaseActivity 2022-10-01 15:21:42 +02:00
Weevild
b2c993b058 Translated using Weblate (Swedish)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2022-09-30 12:54:36 -04:00
Niels van Velzen
26ec721c0f Use fragment arguments instead of activity intent 2022-09-30 16:27:20 +02:00
Niels van Velzen
9830c12fac Replace Kapt with KSP 2022-09-30 12:17:46 +02:00
renovate[bot]
523067bde9 Update glide to v4.14.1 2022-09-30 08:00:01 +02:00
Niels van Velzen
ab3710f0ed Remove key listener from BaseActivity 2022-09-29 19:57:41 +02:00
Niels van Velzen
fe6fcbe539 Refactor LiveTvGuideActivity to fragment 2022-09-29 19:57:34 +02:00
Niels van Velzen
4f781b9610 Refactor ItemListActivity to fragment 2022-09-29 19:57:27 +02:00
Niels van Velzen
1d411d1e3f Refactor FullDetailsActivity to fragment 2022-09-29 19:57:17 +02:00
Niels van Velzen
79bdc601c4 Refactor AudioNowPlayingActivity to fragment 2022-09-29 19:57:02 +02:00
Niels van Velzen
b3463a8975 Rename .java to .kt 2022-09-29 19:57:02 +02:00
renovate[bot]
4a0dba229c Update github/codeql-action digest to e0e5ded 2022-09-29 19:55:21 +02:00
renovate[bot]
bf61dba6ed Update Kotlin to v1.7.20 2022-09-29 19:47:17 +02:00
renovate[bot]
c06b7e7c67 Update aboutlibraries to v10.5.0 2022-09-29 19:46:23 +02:00
renovate[bot]
0c79dabecc Update gradle/gradle-build-action digest to fd32ae9 2022-09-29 19:45:08 +02:00
DW
9cd4c433ea Translated using Weblate (Dutch)
Currently translated at 96.5% (480 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/nl/
2022-09-29 09:54:36 -04:00
CoooLdk
f8d7345ff3 Translated using Weblate (Danish)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/da/
2022-09-29 09:54:36 -04:00
renovate[bot]
21e40cae24 Update dependency io.mockk:mockk to v1.13.2 2022-09-29 07:10:22 +02:00
renovate[bot]
c650bdcc20 Update glide to v4.14.0 2022-09-28 21:06:44 +02:00
Niels van Velzen
9dee1629da Rewrite search UI in Kotlin 2022-09-28 19:02:04 +02:00
Niels van Velzen
7941d72104 Rename .java to .kt 2022-09-28 19:02:04 +02:00
Niels van Velzen
d0d5d29fab Always use repeatOnLifecycle instead of launchWhenX functions 2022-09-28 19:01:55 +02:00
Niels van Velzen
4225317172 Merge StdBrowseFragment and BrowseFolderFragment and rewrite in Kotlin 2022-09-28 19:01:43 +02:00
renovate[bot]
1af9d9be9d Update actions/setup-java digest to a18c333 2022-09-27 15:10:47 +02:00
Niels van Velzen
d3b1faeb99 Remove most deprecation annotations 2022-09-27 12:34:10 +02:00
Oskari Lavinto
57e2c3acc6 Translated using Weblate (Finnish)
Currently translated at 99.1% (493 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-09-27 03:54:35 -04:00
Csaba
338be54355 Translated using Weblate (Hungarian)
Currently translated at 100.0% (497 of 497 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2022-09-27 03:54:35 -04:00
Niels van Velzen
c33a567828 Fix series queuing not working 2022-09-27 07:18:15 +02:00
Niels van Velzen
59e47b2f37 Don't use KoinJavaComponent in Kotlin files 2022-09-26 19:54:54 +02:00
Niels van Velzen
748051e261 Fix blurhash not showing in CardPresenter 2022-09-26 19:46:12 +02:00
Niels van Velzen
01dcc65b1f Add audio night mode (as preference) to level out/compress audio volume. 2022-09-26 17:37:06 +02:00
Niels van Velzen
a4fbb205fc Update GitHub actions workflows 2022-09-25 17:09:58 +02:00
Niels van Velzen
5012a1211b Use AsyncImageView for Live TV program image 2022-09-25 17:07:13 +02:00
Niels van Velzen
fc8ab38a57 Switch slf4j timber to a different library 2022-09-25 17:07:00 +02:00
renovate[bot]
7fc9819b63 Update dependency org.jellyfin.sdk:jellyfin-core to v1.3.6 2022-09-25 11:51:46 +02:00
Niels van Velzen
2277bb79a8 Fix NPE when live tv program is null 2022-09-24 18:08:44 +02:00
Niels van Velzen
03856f7e2f Move package declaration to Gradle build files 2022-09-24 18:08:10 +02:00
Niels van Velzen
2974dd3b6a Disable jetifier 2022-09-24 18:03:57 +02:00
sunrisela
b6eb83cd0e remove credit authors in-code 2022-09-24 15:42:49 +02:00
sunrisela
edd0aab718 Normalize commited code. 2022-09-24 15:42:49 +02:00
sunrisela
e6ba8a89b6 External players support high-priority external subtitles, e.g. MX Player, VLC, nPlayer. 2022-09-24 15:42:49 +02:00
renovate[bot]
fcfa242588 Update koin to v3.2.2 2022-09-24 10:00:57 +02:00
Niels van Velzen
135645ff61 Convert to Date before using DateFormat 2022-09-23 22:37:32 +02:00
Niels van Velzen
903d9fa22e Always use BaseItemDto from SDK 2022-09-23 22:37:32 +02:00
renovate[bot]
af0a5cd317 Update dependency io.mockk:mockk to v1.13.1 2022-09-23 17:06:08 +02:00
renovate[bot]
e0e5316062 Update actions/stale action to v6 2022-09-22 07:55:30 +02:00
renovate[bot]
5a5db8609d Update github/codeql-action digest to 86f3159 2022-09-22 07:54:42 +02:00
renovate[bot]
db1c7289be Update dependency androidx.activity:activity-ktx to v1.6.0 2022-09-21 20:16:20 +02:00
renovate[bot]
9bf0995599 Update dependency androidx.fragment:fragment-ktx to v1.5.3 2022-09-21 20:14:49 +02:00
Niels van Velzen
8d8cc41290 Rewrite BaseRowItem in Kotlin 2022-09-20 13:26:37 +02:00
Niels van Velzen
1cb1fc4713 Rename .java to .kt 2022-09-20 13:26:37 +02:00
Niels van Velzen
00b62e8f9d Fix crash when selecting or clicking items in grid view 2022-09-19 21:39:16 +02:00
Niels van Velzen
570c2490d5 Use MutableObjectAdapter<Object> as base for ItemRowAdapter 2022-09-19 19:20:36 +02:00
Niels van Velzen
4dd0feb417 Use MutableObjectAdapter<Row> for rows 2022-09-19 19:14:16 +02:00
Niels van Velzen
c4ea2e9b40 Merge StdRowsFragment into HomeFragment 2022-09-19 19:08:28 +02:00
Niels van Velzen
a5b4b66fe1 Rewrite CompositeListeners in Kotlin 2022-09-19 19:08:04 +02:00
Niels van Velzen
db773e0c51 Rename .java to .kt 2022-09-19 19:08:04 +02:00
Josip Volf
47c7ba369d Translated using Weblate (Croatian)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/hr/
2022-09-18 17:54:32 -04:00
Josip Volf
b1cdc91f47 Translated using Weblate (Croatian)
Currently translated at 77.1% (382 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hr/
2022-09-18 17:54:32 -04:00
Niels van Velzen
e53bd5c838 Use appcompat theme as base 2022-09-18 15:02:10 +02:00
Niels van Velzen
fcc7e5fd5f Rewrite all Java interfaces in Kotlin 2022-09-18 15:02:00 +02:00
Niels van Velzen
e61159e3f2 Rename .java to .kt 2022-09-18 15:02:00 +02:00
Niels van Velzen
3824eae7c1 Rewrite data package in Kotlin 2022-09-17 16:34:28 +02:00
Niels van Velzen
c086d023b6 Rename .java to .kt 2022-09-17 16:34:28 +02:00
Niels van Velzen
08e239a544 Clear BlurHash and Glide caches on low memory 2022-09-17 16:28:25 +02:00
Niels van Velzen
82177db96d Replace BlurHashDecoder with com.vanniktech.blurhash dependency 2022-09-17 16:28:25 +02:00
Niels van Velzen
ebe5f3fc94 Fix two crashes when trying to view a person 2022-09-17 16:24:32 +02:00
Stelios Diamantis
a45ef35972 Translated using Weblate (Greek)
Currently translated at 97.7% (484 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/el/
2022-09-16 17:54:32 -04:00
renovate[bot]
5d1bc0466d Update dependency com.android.tools.build:gradle to v7.3.0 2022-09-16 19:54:44 +02:00
renovate[bot]
9a87d5c5c3 Update github/codeql-action digest to 904260d 2022-09-16 18:07:34 +02:00
Alan Azar
ba11bb9e88 Translated using Weblate (Chinese (Simplified))
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/zh_Hans/
2022-09-16 08:54:31 -04:00
Niels van Velzen
a0da2075a8 Rewrite Utils in Kotlin 2022-09-15 21:58:22 +02:00
Niels van Velzen
1eef03706b Rewrite DeviceUtils in Kotlin 2022-09-15 21:58:22 +02:00
Niels van Velzen
dd60dbbd98 Rename .java to .kt 2022-09-15 21:58:22 +02:00
renovate[bot]
192f674b2b Update github/codeql-action digest to 6a38b7d 2022-09-15 15:25:47 +02:00
renovate[bot]
fe9ddf1816 Update dependency io.mockk:mockk to v1.12.8 2022-09-15 15:25:32 +02:00
DAVID CHEN
ea7cfa5943 Translated using Weblate (Chinese (Simplified))
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/zh_Hans/
2022-09-15 06:11:37 -04:00
Oskari Lavinto
28a88c8173 Translated using Weblate (Finnish)
Currently translated at 99.1% (491 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-09-15 06:11:37 -04:00
DAVID CHEN
551c895088 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (495 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2022-09-15 06:11:36 -04:00
renovate[bot]
26060c6779 Update actions/stale digest to 99b6c70 2022-09-14 18:31:25 +02:00
Csaba
6fc96c34b0 Translated using Weblate (Hungarian)
Currently translated at 100.0% (495 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2022-09-14 05:11:36 -04:00
Zachary Cohen
bc169d68c6 Add Quality Selection to Playback Overlay (#1924)
* Created SelectQualityAction for Playback Controller UI

* Update app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/action/SelectQualityAction.kt

Co-authored-by: Cameron <mueslimak3r@users.noreply.github.com>

* Moved QualityProfiles Enum Class to org.jellyfin.androidtv.constant. Moved Auto quality to top of list.

* Suppressing errors. Fixing line length.

* Update app/src/main/res/values/strings.xml

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

* Injected dependency via constructor.
Removed unused members.
Updated format of vector file.

* Convert to set

* Needed to target correct key/values

* Inject UserPreferences via constructor

* Removed unused import

* Moved mapping to function

* Miscellaneous formatting changes

Co-authored-by: Cameron <mueslimak3r@users.noreply.github.com>
Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
Co-authored-by: Niels van Velzen <git@ndat.nl>
2022-09-13 17:16:40 +00:00
renovate[bot]
1a13cb0f9e Update koin to v3.2.1 2022-09-12 15:36:08 +02:00
renovate[bot]
cc59a50479 Update dependency androidx.core:core-ktx to v1.9.0 2022-09-10 14:16:33 +02:00
Niels van Velzen
d984792e2d Bump Android compile & target version to 33 (Android 13) 2022-09-10 14:07:02 +02:00
renovate[bot]
ae12625366 Update actions/setup-java digest to d854b6d 2022-09-08 22:13:39 +02:00
renovate[bot]
163af9e756 Update dependency androidx.appcompat:appcompat to v1.5.1 2022-09-07 21:38:51 +02:00
Niels van Velzen
02b9fb60bb Use random device id when initializing QuickConnect 2022-09-06 18:19:28 +02:00
Niels van Velzen
b731b36656 Add SocketInstance to dependency injection
Required for playback rewrite module to share the same WebSocket connection
2022-09-06 15:01:20 +02:00
Niels van Velzen
ffe674912c Fix missing imports causing a build issue 2022-09-06 09:07:21 +02:00
Niels van Velzen
1b8bb7b327 Always set base item type on custom base item dto instances 2022-09-06 07:17:06 +02:00
Niels van Velzen
c500006b9d Replace BaseItemType usages with BaseItemKind (SDK) 2022-09-06 07:16:51 +02:00
Niels van Velzen
8713cd5317 Set defaultFocusHighlightEnabled to false in TextUnderButton 2022-09-04 19:32:19 +02:00
Niels van Velzen
e7ec5c5bd2 Fix app crashing when opening "other options" menu in FullDetailsActivity 2022-09-04 19:32:06 +02:00
Niels van Velzen
352f472769 Set defaultFocusHighlightEnabled to false in DefaultCardView
Fixes an incompatibility with Android 13
2022-09-04 17:52:11 +02:00
Niels van Velzen
46b66fa8ad Add monochrome layer to adaptive icon 2022-09-04 17:52:00 +02:00
Niels van Velzen
c8f757f467 Rewrite ChapterItemInfo to not use the apiclient 2022-09-03 22:30:24 +02:00
Niels van Velzen
0e1c79e933 Rename .java to .kt 2022-09-03 22:30:24 +02:00
renovate[bot]
fed10724e7 Update github/codeql-action digest to b398f52 2022-09-01 15:46:09 +02:00
Weblate
3c8d5c5fab 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/
2022-09-01 01:12:25 -04:00
Niels van Velzen
fdb18aca8c Add empty playback modules for rewrite 2022-09-01 07:12:21 +02:00
Weblate
98de292fa3 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/
2022-08-31 15:23:25 -04:00
Niels van Velzen
1dea42a39a Remove unused video queue from UI code 2022-08-31 21:23:21 +02:00
Niels van Velzen
3b9094fe2b Fix crash when opening favorites and video queue 2022-08-31 07:11:07 +02:00
newton181
2c395ac7c4 Translated using Weblate (Spanish (Mexico))
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_MX/
2022-08-29 19:11:31 -04:00
Niels van Velzen
6e94f99b52 Migrate favorite and played toggles in KeyProcessor to SDK 2022-08-27 22:23:00 +02:00
Niels van Velzen
c7e2789021 Rewrite MediaCodecCapabilitiesTest in Kotlin 2022-08-27 21:51:50 +02:00
Niels van Velzen
6d8b997d89 Rename .java to .kt 2022-08-27 21:51:50 +02:00
Niels van Velzen
11193c749d Replace most utils with SDK 2022-08-27 20:49:28 +02:00
Niels van Velzen
80d34e507d Use SDK in HomeFragment 2022-08-27 17:02:17 +02:00
newton181
70063e9546 Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2022-08-25 22:11:30 -04:00
newton181
0fcb678977 Translated using Weblate (Spanish (Mexico))
Currently translated at 95.9% (476 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_MX/
2022-08-25 22:11:29 -04:00
renovate[bot]
9e45e6e4e8 Update github/codeql-action digest to c7f292e 2022-08-25 19:26:53 +02:00
newton181
3ad6eddd0c Translated using Weblate (Spanish (Latin America))
Currently translated at 94.7% (470 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es_419/
2022-08-24 22:11:29 -04:00
Xavi Fortes
84fe0fd6ca Translated using Weblate (Spanish)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2022-08-24 22:11:29 -04:00
Xavi Fortes
2208e2e7a7 Translated using Weblate (Spanish)
Currently translated at 97.9% (486 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2022-08-24 18:09:06 -04:00
Xavi Fortes
593869deb3 Translated using Weblate (Spanish)
Currently translated at 97.3% (483 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/es/
2022-08-24 18:07:26 -04:00
renovate[bot]
d74581d350 Update dependency com.android.tools:desugar_jdk_libs to v1.2.2 (#2008)
* Update dependency com.android.tools:desugar_jdk_libs to v1.2.2

* Update libs.versions.toml

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
2022-08-24 19:04:43 +00:00
avionics75
947f36f28e Translated using Weblate (French)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fr/
2022-08-24 14:11:28 -04:00
Max Kremer
5b56ebf614 Translated using Weblate (German)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/de/
2022-08-23 17:11:29 -04:00
renovate[bot]
35c1da9a7e Update dependency io.mockk:mockk to v1.12.7 2022-08-23 15:39:35 +02:00
WWWesten
d0475c5eb0 Translated using Weblate (Esperanto)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/eo/
2022-08-22 17:11:28 -04:00
WWWesten
218f2e6a14 Translated using Weblate (Russian)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/ru/
2022-08-22 17:11:28 -04:00
WWWesten
ca7246afcc Translated using Weblate (Kazakh)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/kk/
2022-08-22 17:11:28 -04:00
renovate[bot]
850ec393f4 Update github/codeql-action digest to 7fee4ca 2022-08-22 14:41:13 +02:00
Cercky
0a64cfd8d2 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/zh_Hans/
2022-08-22 01:11:28 -04:00
Marian Radev
db81460102 Translated using Weblate (Bulgarian)
Currently translated at 32.0% (159 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/bg/
2022-08-21 10:11:27 -04:00
Oskari Lavinto
e805003df9 Translated using Weblate (Finnish)
Currently translated at 99.1% (492 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-08-21 06:11:27 -04:00
Niels van Velzen
38fe985bd8 Migrate most of KeyProcessor to SDK types 2022-08-21 09:16:15 +02:00
Niels van Velzen
5c3222490b Fix crash when trying to start a picture slideshow 2022-08-20 20:14:42 +02:00
Andy Walsh
7207b3d812 Keep ordering from album view in picture viewer 2022-08-20 20:14:36 +02:00
Niels van Velzen
ee3008b5cd Remove USE_DOWNLOAD_API from picture viewer 2022-08-20 20:14:29 +02:00
Niels van Velzen
acc4533868 Use MediaStream from SDK in MediaInfoCardView and InfoCardPresenter 2022-08-20 14:09:09 +02:00
Niels van Velzen
a58c6244f2 Use SDK in StartupActivity 2022-08-20 14:08:35 +02:00
Niels van Velzen
833c026bbe Use SDK SortOrder enum in LibraryPreferences 2022-08-20 08:08:28 +02:00
Niels van Velzen
86b34ac7fb Remove commented log statements 2022-08-20 08:08:16 +02:00
lyaschuchenko
d68edaca4d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/uk/
2022-08-20 00:11:27 -04:00
Niels van Velzen
e6b7ca865e Simplify reportPlaybackStopped code 2022-08-19 22:34:37 +02:00
Andy Walsh
66ea6988ff fix CommunityRating display (#1992) 2022-08-19 18:05:53 +00:00
hadicharara
7e2bbbc61a Add RTL Support (#1842)
* Initial commit

* Fixed shadow bug
2022-08-19 19:14:06 +02:00
renovate[bot]
2ae4c87595 Update github/codeql-action digest to f5d217b 2022-08-19 14:07:31 +02:00
renovate[bot]
95932bb8f5 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.4.0 2022-08-18 21:39:56 +02:00
DJSweder
186b963e3a Translated using Weblate (Czech)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/cs/
2022-08-18 04:11:26 -04:00
Kevin G
9ee8c7fb50 Support previous button in video playlist
Fixes #1940

Signed-off-by: Kevin G <kevin@myplaceonline.com>
2022-08-18 08:21:30 +02:00
Niels van Velzen
95e31975ba Always use SDK SearchHint 2022-08-18 07:43:39 +02:00
Niels van Velzen
57c3b7f2aa Use MediaType constants from SDK instead of strings 2022-08-17 18:12:04 +02:00
Niels van Velzen
91c777ea3a Use CollectionType constants from SDK instead of strings 2022-08-17 18:12:04 +02:00
Niels van Velzen
70f250e875 Use MediaType constants from SDK 2022-08-17 18:12:04 +02:00
Niels van Velzen
f37db62dbe Use ItemSortBy constants from SDK 2022-08-17 18:12:04 +02:00
Niels van Velzen
4669afb224 Remove unused ApiEventListener from legacy apiclient 2022-08-17 07:24:51 +02:00
Niels van Velzen
318c007aeb Always use SDK BaseItemPerson and PersonType 2022-08-17 07:24:38 +02:00
Csaba
a6f7364c09 Translated using Weblate (Hungarian)
Currently translated at 100.0% (496 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/hu/
2022-08-16 02:11:26 -04:00
Oskari Lavinto
b2e8517e0f Translated using Weblate (Finnish)
Currently translated at 99.1% (492 of 496 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-08-15 17:11:25 -04:00
renovate[bot]
1cafe6c539 Update dependency org.jellyfin.sdk:jellyfin-core to v1.3.5 2022-08-14 20:57:35 +02:00
Niels van Velzen
38d33536f9 Fix movie items crashing 2022-08-14 17:13:47 +02:00
Niels van Velzen
acc77901ad Fix Live TV guide crashing 2022-08-14 17:13:47 +02:00
Niels van Velzen
47844799c8 Migrate InfoLayoutHelper to use SDK models 2022-08-14 15:50:33 +02:00
Niels van Velzen
040d46e5f5 Replace GSON encoding usages in EnhancedBrowseFragment with kotlinx.serialization 2022-08-14 15:50:17 +02:00
Niels van Velzen
0a621fa9a3 Replace GSON usages in ItemLauncher with kotlinx.serialization 2022-08-14 15:50:17 +02:00
Niels van Velzen
2971e0368f Use view binding directly in BrowseGridFragment 2022-08-14 15:50:05 +02:00
Niels van Velzen
c60586c13e Use AsyncImageView in GuideChannelHeader 2022-08-14 15:49:52 +02:00
Niels van Velzen
a682757b8b Use AsyncImageView in ClockUserView 2022-08-14 15:49:40 +02:00
Niels van Velzen
454c0d6fec Add circleCrop to AsyncImageView 2022-08-14 15:49:40 +02:00
Niels van Velzen
14e7391bc2 Properly handle specials in item naming 2022-08-14 15:49:12 +02:00
Niels van Velzen
5bb1ee7819 Don't open library settings for unsupported types
Only collection types that use the grid should open the DisplayPreferencesScreen. Added a special case to open the Live TV (guide) settings as a bonus.
2022-08-14 15:49:05 +02:00
Niels van Velzen
cd33bea725 Fix ActivityLifecycleCallback calls on Android 9 and lower 2022-08-14 15:48:56 +02:00
Niels van Velzen
a404b99ab1 Migrate DefaultLifecycleObserver setup to androidx.startup 2022-08-14 15:48:48 +02:00
Niels van Velzen
6e54d2d412 Merge GridFragment into BrowseGridFragment 2022-08-14 09:30:40 +02:00
Niels van Velzen
8a3ac11643 Merge StdGridFragment into BrowseGridFragment 2022-08-14 09:30:40 +02:00
Niels van Velzen
e592017602 Allow app installation on external storage 2022-08-14 08:15:38 +02:00
Niels van Velzen
6960f808f2 Fix alignment of ClockUserView in live TV guide 2022-08-13 21:12:21 +02:00
Arceer
888eafb4b3 Translated using Weblate (Swedish)
Currently translated at 100.0% (4 of 4 strings)

Translation: Jellyfin Android/Jellyfin Android TV Fastlane
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv-fastlane/sv/
2022-08-13 07:22:31 -04:00
Arceer
7987e14a65 Translated using Weblate (Swedish)
Currently translated at 100.0% (495 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/sv/
2022-08-13 07:22:31 -04:00
renovate[bot]
8f6109e93f Update kotest to v5.4.2 2022-08-13 10:45:52 +02:00
Niels van Velzen
0074062633 Catch decoding exception in AuthenticationStore
Fixes corrupt JSON causing the app to crash
2022-08-13 09:59:17 +02:00
Oskari Lavinto
a46e88b970 Translated using Weblate (Finnish)
Currently translated at 99.1% (491 of 495 strings)

Translation: Jellyfin Android/Jellyfin Android TV
Translate-URL: https://translate.jellyfin.org/projects/jellyfin-android/jellyfin-androidtv/fi/
2022-08-12 03:22:30 -04:00
Niels van Velzen
fa94dba71a Add new experimental picture viewer (#1829)
* Add KeyHandler

* Add PictureViewerFragment to replace PhotoPlayerActivity

* Address review feedback
2022-08-11 20:56:07 +02:00
renovate[bot]
21769a926c Update dependency androidx.appcompat:appcompat to v1.5.0 2022-08-11 20:01:45 +02:00
Niels van Velzen
2b2569772c Bump Android compile and target SDK to 32 (Android 12L)
Required for the new appcompat version
2022-08-11 19:40:49 +02:00
renovate[bot]
eb8ce2d0a1 Update dependency org.videolan.android:libvlc-all to v3.5.1 2022-08-11 18:59:36 +02:00
renovate[bot]
6542595a5f Update dependency androidx.fragment:fragment-ktx to v1.5.2 2022-08-11 09:14:38 +02:00
cameron
d7db8890d7 remove removePreviousQueueItems and sync mediaManager video queue index 2022-08-10 19:17:59 +02:00
Niels van Velzen
6ac9e7d207 Remove usages of BaseItemType's that don't exist in the server anymore 2022-08-10 08:21:37 +02:00
Weblate
f4902f5a70 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/
2022-08-09 08:53:40 -04:00
Niels van Velzen
6378920ac4 Remove item delete functionality 2022-08-09 14:53:36 +02:00
437 changed files with 11132 additions and 10451 deletions

View File

@@ -1,4 +1,4 @@
name: App Build
name: App / Build
on:
push:
@@ -11,22 +11,26 @@ permissions:
jobs:
build:
runs-on: ubuntu-20.04
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup Java
uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3
uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3
with:
distribution: adopt
java-version: 11
cache: gradle
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2
- name: Assemble debug APKs
run: ./gradlew --build-cache --no-daemon --info assembleDebug
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@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with:
name: build-artifacts
retention-days: 14
if-no-files-found: error
path: app/build/outputs/apk/debug/*.apk
path: build/gh-app-publish/

View File

@@ -1,4 +1,4 @@
name: App Lint
name: App / Lint
on:
push:
@@ -9,26 +9,26 @@ on:
permissions:
contents: read
security-events: write
jobs:
lint:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
runs-on: ubuntu-20.04
name: Lint
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup Java
uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3
uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3
with:
distribution: adopt
java-version: 11
cache: gradle
- name: Run detekt and lint task
run: ./gradlew --build-cache --no-daemon --info detekt lint
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2
- name: Run detekt and lint tasks
run: ./gradlew detekt lint
- name: Upload SARIF files
uses: github/codeql-action/upload-sarif@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 # tag=v2
uses: github/codeql-action/upload-sarif@678fc3afe258fb2e0cdc165ccf77b85719de7b3c # v2
if: ${{ always() }}
with:
sarif_file: .

View File

@@ -1,4 +1,4 @@
name: App Publish
name: App / Publish
on:
push:
@@ -6,21 +6,24 @@ on:
- v*
jobs:
build:
runs-on: ubuntu-20.04
if: ${{ github.repository == 'jellyfin/jellyfin-androidtv' }}
publish:
name: Publish
runs-on: ubuntu-22.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup Java
uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3
uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3
with:
distribution: adopt
java-version: 11
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2
- name: Set JELLYFIN_VERSION
run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV
- name: Assemble release files
run: ./gradlew --no-daemon --info assemble bundleRelease versionTxt
run: ./gradlew assemble bundleRelease versionTxt
- name: Sign APK
id: signApk
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
@@ -30,7 +33,7 @@ jobs:
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
alias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Sign AAB
- name: Sign app bundle
id: signAab
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
with:
@@ -48,7 +51,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@5586bc227f8525a5e3525e6edf64da5350bfb5b1 # tag=0.3.0
uses: alexellis/upload-assets@259de5111cb56966d046ced998941e93f91d2c93 # tag=0.4.0
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
with:

View File

@@ -1,4 +1,4 @@
name: App Test
name: App / Test
on:
push:
@@ -12,15 +12,17 @@ permissions:
jobs:
test:
runs-on: ubuntu-20.04
name: Test
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Setup Java
uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3
uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3
with:
distribution: adopt
java-version: 11
cache: gradle
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2
- name: Run test task
run: ./gradlew --build-cache --no-daemon --info test
run: ./gradlew test

View File

@@ -1,4 +1,4 @@
name: Gradle update
name: Gradle / Update wrapper
on:
schedule:
@@ -6,8 +6,9 @@ on:
jobs:
update:
runs-on: ubuntu-20.04
if: github.repository == 'jellyfin/jellyfin-androidtv'
name: Update
runs-on: ubuntu-22.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3

View File

@@ -1,4 +1,4 @@
name: Gradle validate
name: Gradle / Validate wrapper
on:
push:
@@ -13,9 +13,10 @@ permissions:
jobs:
validate:
runs-on: ubuntu-20.04
name: Validate
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b # tag=v1
uses: gradle/wrapper-validation-action@55e685c48d84285a5b0418cd094606e199cca3b6 # tag=v1

View File

@@ -1,17 +0,0 @@
name: Merge conflict labeler
on:
push:
pull_request_target:
types:
- synchronize
jobs:
triage:
runs-on: ubuntu-20.04
if: github.repository == 'jellyfin/jellyfin-androidtv'
steps:
- uses: eps1lon/actions-label-merge-conflict@b8bf8341285ec9a4567d4318ba474fee998a6919 # tag=v2.0.1
with:
dirtyLabel: merge conflict
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -0,0 +1,18 @@
name: Repo / Label merge conflict
on:
push:
pull_request_target:
types:
- synchronize
jobs:
triage:
name: Triage
runs-on: ubuntu-22.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
with:
dirtyLabel: merge conflict
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -1,4 +1,4 @@
name: Issue stale check
name: Repo / Reply stale issue
on:
schedule:
@@ -10,11 +10,12 @@ permissions:
pull-requests: write
jobs:
stale:
runs-on: ubuntu-20.04
if: github.repository == 'jellyfin/jellyfin-androidtv'
triage:
name: Triage
runs-on: ubuntu-22.04
if: ${{ contains(github.repository_owner, 'jellyfin') }}
steps:
- uses: actions/stale@9c1b1c6e115ca2af09755448e0dbba24e5061cc8 # tag=v5
- uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
@@ -25,7 +26,7 @@ jobs:
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).

View File

@@ -10,6 +10,7 @@
- [joern-h](https://github.com/joern-h)
- [Niels van Velzen](https://github.com/nielsvanvelzen)
- [GodTamIt](https://github.com/GodTamIt)
- [sparky3387](https://github.com/sparky3387)
# Emby Contributors

View File

@@ -35,6 +35,9 @@
<a href="https://www.amazon.com/gp/aw/d/B07TX7Z725">
<img width="153" alt="Jellyfin on Amazon Appstore" src="https://jellyfin.org/images/store-icons/amazon.png"/>
</a>
<a href="https://f-droid.org/en/packages/org.jellyfin.androidtv/">
<img width="153" alt="Jellyfin on F-Droid" src="https://jellyfin.org/images/store-icons/fdroid.png"/>
</a>
<br/>
<a href="https://repo.jellyfin.org/releases/client/androidtv/">Download archive</a>
</p>

View File

@@ -0,0 +1,39 @@
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

@@ -1,18 +1,21 @@
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.aboutlibraries)
}
// Apply workaround
apply("baselineWorkaround.gradle")
android {
namespace = "org.jellyfin.androidtv"
compileSdk = 31
compileSdk = 33
defaultConfig {
minSdk = 21
targetSdk = 31
targetSdk = 33
// Release version
applicationId = namespace
@@ -21,17 +24,19 @@ android {
setProperty("archivesBaseName", "jellyfin-androidtv-v$versionName")
}
sourceSets["main"].java.srcDirs("src/main/kotlin")
sourceSets["test"].java.srcDirs("src/test/kotlin")
buildFeatures {
viewBinding = true
compose = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}
buildTypes {
val release by getting {
isMinifyEnabled = false
@@ -92,7 +97,10 @@ val versionTxt by tasks.registering {
dependencies {
// Jellyfin
implementation(projects.playback)
implementation(projects.playback.core)
implementation(projects.playback.exoplayer)
implementation(projects.playback.jellyfin)
implementation(projects.playback.ui)
implementation(projects.preference)
implementation(libs.jellyfin.apiclient)
implementation(libs.jellyfin.sdk) {
@@ -125,13 +133,11 @@ dependencies {
implementation(libs.androidx.window)
implementation(libs.androidx.cardview)
implementation(libs.androidx.startup)
implementation(libs.bundles.androidx.compose)
// Dependency Injection
implementation(libs.bundles.koin)
// GSON
implementation(libs.gson)
// Media players
implementation(libs.exoplayer)
implementation(libs.jellyfin.exoplayer.ffmpegextension)
@@ -141,8 +147,9 @@ dependencies {
implementation(libs.bundles.markwon)
// Image utility
implementation(libs.blurhash)
implementation(libs.glide.core)
kapt(libs.glide.compiler)
ksp(libs.glide.ksp)
implementation(libs.kenburnsview)
// Crash Reporting

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.jellyfin.androidtv">
android:installLocation="auto">
<!-- Android TV Integration -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
@@ -23,13 +23,14 @@
<uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
tools:ignore="LeanbackUsesWifi" />
<!-- Search permissions (voice to text) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Device feature requirements -->
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -48,9 +49,9 @@
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">
<service
android:name=".auth.service.AuthenticatorService"
android:exported="false">
@@ -62,6 +63,7 @@
android:resource="@xml/authenticator" />
</service>
<!-- Screensaver -->
<service
android:name=".integration.dream.LibraryDreamService"
android:exported="true"
@@ -88,6 +90,9 @@
<meta-data
android:name="org.jellyfin.androidtv.SessionInitializer"
android:value="androidx.startup" />
<meta-data
android:name="org.jellyfin.androidtv.ProcessLifecycleInitializer"
android:value="androidx.startup" />
</provider>
<provider
@@ -96,10 +101,13 @@
android:exported="true"
android:initOrder="10" />
<activity
android:name=".ui.browsing.MainActivity"
android:screenOrientation="landscape" />
<provider
android:name=".integration.provider.ImageProvider"
android:authorities="${applicationId}.integration.provider.ImageProvider"
android:exported="true"
android:initOrder="10" />
<!-- Authentication -->
<activity
android:name=".ui.startup.StartupActivity"
android:exported="true"
@@ -118,73 +126,24 @@
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity-alias
android:name=".startup.StartupActivity"
android:targetActivity=".ui.startup.StartupActivity" />
<!-- Main application -->
<activity
android:name=".ui.playback.PlaybackOverlayActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.GenericFolderActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.UserViewActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.CollectionActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.GroupedItemsActivity"
android:screenOrientation="landscape" />
android:name=".ui.browsing.MainActivity"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name=".ui.preference.PreferencesActivity"
android:screenOrientation="behind"
android:theme="@style/Theme.Jellyfin.Preferences" />
<!-- Playback related activities -->
<activity
android:name=".ui.browsing.SuggestedMoviesActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.search.SearchActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.itemdetail.FullDetailsActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.livetv.LiveTvGuideActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.GenericGridActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.BrowseRecordingsActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.browsing.BrowseScheduleActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.itemdetail.PhotoPlayerActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.itemdetail.ItemListActivity"
android:screenOrientation="landscape" />
<activity
android:name=".ui.playback.AudioNowPlayingActivity"
android:name=".ui.playback.PlaybackOverlayActivity"
android:screenOrientation="landscape" />
<activity android:name=".ui.playback.ExternalPlayerActivity" />

View File

@@ -2,8 +2,6 @@ package org.jellyfin.androidtv
import android.app.Application
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.work.BackoffPolicy
@@ -11,20 +9,18 @@ import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.await
import com.bumptech.glide.Glide
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.acra.ACRA
import org.jellyfin.androidtv.auth.repository.SessionRepository
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.get
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.concurrent.TimeUnit
@Suppress("unused")
@@ -37,28 +33,6 @@ class JellyfinApplication : Application() {
val notificationsRepository by inject<NotificationsRepository>()
notificationsRepository.addDefaultNotifications()
// Register application lifecycle events
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
/**
* Called by the Process Lifecycle when the app is created. It is called after [onCreate].
*/
override fun onCreate(owner: LifecycleOwner) {
// Register activity lifecycle callbacks
getKoin().getAll<ActivityLifecycleCallbacks>().forEach(::registerActivityLifecycleCallbacks)
}
/**
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
*/
override fun onStart(owner: LifecycleOwner) {
Timber.i("Process lifecycle started")
owner.lifecycleScope.launch {
get<SessionRepository>().restoreSession()
}
}
})
}
/**
@@ -92,6 +66,19 @@ class JellyfinApplication : Application() {
ProcessLifecycleOwner.get().lifecycleScope.launch { autoBitrate.detect() }
}
override fun onLowMemory() {
super.onLowMemory()
BlurHash.clearCache()
Glide.with(this).onLowMemory()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.with(this).onTrimMemory(level)
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)

View File

@@ -0,0 +1,50 @@
package org.jellyfin.androidtv
import android.app.Application
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.startup.AppInitializer
import androidx.startup.Initializer
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.di.KoinInitializer
import timber.log.Timber
@Suppress("unused")
class ProcessLifecycleInitializer : Initializer<Unit> {
override fun create(context: Context) {
val koin = AppInitializer.getInstance(context)
.initializeComponent(KoinInitializer::class.java)
.koin
// Register application lifecycle events
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
/**
* Called by the Process Lifecycle when the app is created. It is called after [onCreate].
*/
override fun onCreate(owner: LifecycleOwner) {
// Register activity lifecycle callbacks
val callbacks = koin.getAll<Application.ActivityLifecycleCallbacks>()
Timber.i("Registering ${callbacks.size} ActivityLifecycleCallbacks")
val app = context.applicationContext as Application
callbacks.forEach { callback -> app.registerActivityLifecycleCallbacks(callback) }
}
/**
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
*/
override fun onStart(owner: LifecycleOwner) {
Timber.i("Process lifecycle started")
owner.lifecycleScope.launch {
koin.get<SessionRepository>().restoreSession()
}
}
})
}
override fun dependencies() = listOf(KoinInitializer::class.java)
}

View File

@@ -4,6 +4,7 @@ import java.util.UUID
data class AccountManagerAccount(
val id: UUID,
val address: String,
val server: UUID,
val name: String,
val accessToken: String? = null,

View File

@@ -1,8 +1,11 @@
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 class ServerVersionNotSupported(val server: Server) : LoginState()
data class ApiClientErrorLoginState(val error: ApiClientException) : LoginState()
object AuthenticatedState : LoginState()

View File

@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.jellyfin.androidtv.auth.model.AccountManagerAccount
import org.jellyfin.androidtv.auth.model.ApiClientErrorLoginState
import org.jellyfin.androidtv.auth.model.AuthenticateMethod
import org.jellyfin.androidtv.auth.model.AuthenticatedState
import org.jellyfin.androidtv.auth.model.AuthenticatingState
@@ -69,12 +70,12 @@ class AuthenticationRepositoryImpl(
}
private fun authenticateAutomatic(server: Server, user: User): Flow<LoginState> {
Timber.d("Authenticating user %s", user)
Timber.i("Authenticating user %s", user)
// Automatic logic is disabled when the always authenticate preference is enabled
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return flowOf(RequireSignInState)
val account = accountManagerStore.getAccount(user.id)
val account = accountManagerStore.getAccount(server.id, user.id)
// Try login with access token
return if (account?.accessToken != null) authenticateToken(server, user.withToken(account.accessToken))
// Try login without password
@@ -94,7 +95,7 @@ class AuthenticationRepositoryImpl(
return@flow
} catch (err: ApiClientException) {
Timber.e(err, "Unable to sign in as $username")
emit(RequireSignInState)
emit(ApiClientErrorLoginState(err))
return@flow
}
@@ -106,9 +107,13 @@ class AuthenticationRepositoryImpl(
val result = try {
val response = api.userApi.authenticateWithQuickConnect(secret)
response.content
} catch (err: TimeoutException) {
Timber.e(err, "Failed to connect to server")
emit(ServerUnavailableState)
return@flow
} catch (err: ApiClientException) {
Timber.e(err, "Unable to sign in with Quick Connect secret")
emit(RequireSignInState)
emit(ApiClientErrorLoginState(err))
return@flow
}
@@ -116,7 +121,7 @@ class AuthenticationRepositoryImpl(
}
private fun authenticateAuthenticationResult(server: Server, result: AuthenticationResult) = flow {
val accessToken = result.accessToken ?:return@flow emit(RequireSignInState)
val accessToken = result.accessToken ?: return@flow emit(RequireSignInState)
val userInfo = result.user ?: return@flow emit(RequireSignInState)
val user = PrivateUser(
id = userInfo.id,
@@ -130,8 +135,12 @@ class AuthenticationRepositoryImpl(
authenticateFinish(server, userInfo, accessToken)
val success = setActiveSession(user, server)
if (success) emit(AuthenticatedState)
else emit(RequireSignInState)
if (success) {
emit(AuthenticatedState)
} else {
Timber.w("Failed to set active session after authenticating")
emit(RequireSignInState)
}
}
private fun authenticateToken(server: Server, user: User) = flow {
@@ -145,9 +154,13 @@ class AuthenticationRepositoryImpl(
val userInfo by userApiClient.userApi.getCurrentUser()
authenticateFinish(server, userInfo, user.accessToken.orEmpty())
emit(AuthenticatedState)
} catch (err: TimeoutException) {
Timber.e(err, "Failed to connect to server")
emit(ServerUnavailableState)
return@flow
} catch (err: ApiClientException) {
Timber.e(err, "Unable to get current user data")
emit(RequireSignInState)
emit(ApiClientErrorLoginState(err))
}
}
@@ -166,12 +179,18 @@ class AuthenticationRepositoryImpl(
)
authenticationStore.putUser(server.id, userInfo.id, updatedUser)
val accountManagerAccount = AccountManagerAccount(userInfo.id, server.id, updatedUser.name, accessToken)
val accountManagerAccount = AccountManagerAccount(
id = userInfo.id,
address = "${updatedUser.name}@${server.address}",
server = server.id,
name = updatedUser.name,
accessToken = accessToken,
)
accountManagerStore.putAccount(accountManagerAccount)
}
private suspend fun setActiveSession(user: User, server: Server): Boolean {
val authenticated = sessionRepository.switchCurrentSession(user.id)
val authenticated = sessionRepository.switchCurrentSession(server.id, user.id)
if (authenticated) {
// Update last use in store
@@ -188,7 +207,7 @@ class AuthenticationRepositoryImpl(
}
override fun logout(user: User): Boolean {
val authInfo = accountManagerStore.getAccount(user.id) ?: return false
val authInfo = accountManagerStore.getAccount(user.serverId, user.id) ?: return false
return accountManagerStore.removeAccount(authInfo)
}

View File

@@ -83,12 +83,12 @@ class ServerRepositoryImpl(
// Mutating data
override fun addServer(address: String): Flow<ServerAdditionState> = flow {
Timber.d("Adding server %s", address)
Timber.i("Adding server %s", address)
emit(ConnectingState(address))
val addressCandidates = jellyfin.discovery.getAddressCandidates(address)
Timber.d("Found ${addressCandidates.size} candidates")
Timber.i("Found ${addressCandidates.size} candidates")
val goodRecommendations = mutableListOf<RecommendedServerInfo>()
val badRecommendations = mutableListOf<RecommendedServerInfo>()

View File

@@ -31,7 +31,7 @@ class ServerUserRepositoryImpl(
) : ServerUserRepository {
override fun getStoredServerUsers(server: Server) = authenticationStore.getUsers(server.id)
?.mapNotNull { (userId, userInfo) ->
val authInfo = accountManagerStore.getAccount(userId)
val authInfo = accountManagerStore.getAccount(server.id, userId)
PrivateUser(
id = authInfo?.id ?: userId,
serverId = authInfo?.server ?: server.id,
@@ -64,7 +64,7 @@ class ServerUserRepositoryImpl(
authenticationStore.removeUser(user.serverId, user.id)
// Remove authentication info from system account manager
accountManagerStore.getAccount(user.id)?.let { accountManagerAccount ->
accountManagerStore.getAccount(user.serverId, user.id)?.let { accountManagerAccount ->
accountManagerStore.removeAccount(accountManagerAccount)
}
}

View File

@@ -41,7 +41,7 @@ interface SessionRepository {
val state: StateFlow<SessionRepositoryState>
suspend fun restoreSession()
suspend fun switchCurrentSession(userId: UUID): Boolean
suspend fun switchCurrentSession(serverId: UUID, userId: UUID): Boolean
fun destroyCurrentSession()
}
@@ -64,33 +64,37 @@ class SessionRepositoryImpl(
override val state = _state.asStateFlow()
override suspend fun restoreSession(): Unit = currentSessionMutex.withLock {
Timber.d("Restoring session")
Timber.i("Restoring session")
_state.value = SessionRepositoryState.RESTORING_SESSION
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return destroyCurrentSession()
val behavior = authenticationPreferences[AuthenticationPreferences.autoLoginUserBehavior]
val userId = authenticationPreferences[AuthenticationPreferences.autoLoginUserId].toUUIDOrNull()
when (behavior) {
when (authenticationPreferences[AuthenticationPreferences.autoLoginUserBehavior]) {
DISABLED -> destroyCurrentSession()
LAST_USER -> setCurrentSession(createLastUserSession())
SPECIFIC_USER -> setCurrentSession(createUserSession(userId))
SPECIFIC_USER -> {
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
}
override suspend fun switchCurrentSession(userId: UUID): Boolean {
override suspend fun switchCurrentSession(serverId: UUID, userId: UUID): Boolean {
// No change in user - don't switch
if (currentSession.value?.userId == userId) return false
if (currentSession.value?.userId == userId) {
Timber.d("Current session user is the same as the requested user")
return false
}
_state.value = SessionRepositoryState.SWITCHING_SESSION
Timber.d("Switching current session to user $userId")
Timber.i("Switching current session to user $userId")
val session = createUserSession(userId)
val session = createUserSession(serverId, userId)
if (session == null) {
Timber.d("Could not switch to non-existing session for user $userId")
Timber.w("Could not switch to non-existing session for user $userId")
return false
}
@@ -100,7 +104,7 @@ class SessionRepositoryImpl(
}
override fun destroyCurrentSession() {
Timber.d("Destroying current session")
Timber.i("Destroying current session")
userRepository.updateCurrentUser(null)
_currentSession.value = null
@@ -115,6 +119,7 @@ class SessionRepositoryImpl(
if (currentSession.value?.userId == session.userId) return true
// Update last active user
authenticationPreferences[AuthenticationPreferences.lastServerId] = session.serverId.toString()
authenticationPreferences[AuthenticationPreferences.lastUserId] = session.userId.toString()
// Check if server version is supported
@@ -125,7 +130,7 @@ class SessionRepositoryImpl(
// Update session after binding the apiclient settings
val deviceInfo = session?.let { defaultDeviceInfo.forUser(it.userId) } ?: defaultDeviceInfo
val success = apiBinder.updateSession(session, deviceInfo)
Timber.d("Updating current session. userId=${session?.userId} apiBindingSuccess=${success}")
Timber.i("Updating current session. userId=${session?.userId} apiBindingSuccess=${success}")
if (success) {
userApiClient.applySession(session, deviceInfo)
@@ -156,13 +161,14 @@ class SessionRepositoryImpl(
private fun createLastUserSession(): Session? {
val lastUserId = authenticationPreferences[AuthenticationPreferences.lastUserId].toUUIDOrNull()
return createUserSession(lastUserId)
val lastServerId = authenticationPreferences[AuthenticationPreferences.lastServerId].toUUIDOrNull()
return if (lastUserId != null && lastServerId != null) createUserSession(lastServerId, lastUserId)
else null
}
private fun createUserSession(userId: UUID?): Session? {
if (userId == null) return null
val account = accountManagerStore.getAccount(userId)
private fun createUserSession(serverId: UUID, userId: UUID): Session? {
val account = accountManagerStore.getAccount(serverId, userId)
if (account?.accessToken == null) return null
return Session(

View File

@@ -2,6 +2,7 @@ package org.jellyfin.androidtv.auth.store
import android.accounts.Account
import android.accounts.AccountManager
import android.os.Build
import android.os.Bundle
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.auth.model.AccountManagerAccount
@@ -22,8 +23,16 @@ class AccountManagerStore(
const val ACCOUNT_ACCESS_TOKEN_TYPE = "$ACCOUNT_TYPE.access_token"
}
private fun Array<Account>.filterServerAccount(server: UUID, account: UUID? = null) = filter {
val validServerId = accountManager.getUserData(it, ACCOUNT_DATA_SERVER)?.toUUIDOrNull() == server
val validUserId = account == null || accountManager.getUserData(it, ACCOUNT_DATA_ID)?.toUUIDOrNull() == account
validServerId && validUserId
}
private fun getAccountData(account: Account): AccountManagerAccount = AccountManagerAccount(
id = accountManager.getUserData(account, ACCOUNT_DATA_ID).toUUID(),
address = account.name,
server = accountManager.getUserData(account, ACCOUNT_DATA_SERVER).toUUID(),
name = accountManager.getUserData(account, ACCOUNT_DATA_NAME),
accessToken = accountManager.peekAuthToken(account, ACCOUNT_ACCESS_TOKEN_TYPE)
@@ -31,11 +40,12 @@ class AccountManagerStore(
suspend fun putAccount(accountManagerAccount: AccountManagerAccount) {
var androidAccount = accountManager.getAccountsByType(ACCOUNT_TYPE)
.firstOrNull { accountManager.getUserData(it, ACCOUNT_DATA_ID)?.toUUIDOrNull() == accountManagerAccount.id }
.filterServerAccount(accountManagerAccount.server, accountManagerAccount.id)
.firstOrNull()
// Update credentials
if (androidAccount == null) {
androidAccount = Account(accountManagerAccount.name, ACCOUNT_TYPE)
androidAccount = Account(accountManagerAccount.address, ACCOUNT_TYPE)
accountManager.addAccountExplicitly(
androidAccount,
@@ -46,9 +56,14 @@ class AccountManagerStore(
}
// Update name
if (androidAccount.name != accountManagerAccount.name) {
if (androidAccount.name != accountManagerAccount.address) {
androidAccount = suspendCoroutine { continuation ->
accountManager.renameAccount(androidAccount, accountManagerAccount.name, { continuation.resume(it.result) }, null)
accountManager.renameAccount(
androidAccount,
accountManagerAccount.address,
{ continuation.resume(it.result) },
null,
)
}
}
@@ -59,12 +74,13 @@ class AccountManagerStore(
fun removeAccount(accountManagerAccount: AccountManagerAccount): Boolean {
val androidAccount = accountManager.getAccountsByType(ACCOUNT_TYPE)
.firstOrNull { accountManager.getUserData(it, ACCOUNT_DATA_ID)?.toUUIDOrNull() == accountManagerAccount.id }
.filterServerAccount(accountManagerAccount.server, accountManagerAccount.id)
.firstOrNull()
?: return false
// Remove current account info
@Suppress("DEPRECATION")
return if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager.removeAccount(androidAccount, null, null)
true
} else accountManager.removeAccountExplicitly(androidAccount)
@@ -72,11 +88,12 @@ class AccountManagerStore(
fun getAccounts() = accountManager.getAccountsByType(ACCOUNT_TYPE).map(::getAccountData)
fun getAccountsByServer(server: UUID) = accountManager.getAccountsByType(ACCOUNT_TYPE).filter { account ->
accountManager.getUserData(account, ACCOUNT_DATA_SERVER)?.toUUIDOrNull() == server
}.map(::getAccountData)
fun getAccountsByServer(server: UUID) = accountManager.getAccountsByType(ACCOUNT_TYPE)
.filterServerAccount(server)
.map(::getAccountData)
fun getAccount(id: UUID) = accountManager.getAccountsByType(ACCOUNT_TYPE).firstOrNull { account ->
accountManager.getUserData(account, ACCOUNT_DATA_ID)?.toUUIDOrNull() == id
}?.let(::getAccountData)
fun getAccount(server: UUID, account: UUID) = accountManager.getAccountsByType(ACCOUNT_TYPE)
.filterServerAccount(server, account)
.firstOrNull()
?.let(::getAccountData)
}

View File

@@ -14,12 +14,30 @@ class AuthenticationPreferences(context: Context) : SharedPreferenceStore(
companion object {
// Preferences
val autoLoginUserBehavior = enumPreference("auto_login_user_behavior", UserSelectBehavior.LAST_USER)
val autoLoginServerId = stringPreference("auto_login_server_id", "")
val autoLoginUserId = stringPreference("auto_login_user_id", "")
val sortBy = enumPreference("sort_by", AuthenticationSortBy.LAST_USE)
val alwaysAuthenticate = booleanPreference("always_authenticate", false)
// Persistent state
val lastServerId = stringPreference("last_server_id", "")
val lastUserId = stringPreference("last_user_id", "")
}
init {
runMigrations {
// v0.15.4 to v0.15.5
migration(toVersion = 2) {
// Unfortunately we cannot migrate the "specific user" login option
// so we'll reset the preference to disabled if it was used
if (it.getString("auto_login_user_behavior", null) === UserSelectBehavior.SPECIFIC_USER.name) {
putString("auto_login_user_id", "")
putString("auto_login_user_behavior", UserSelectBehavior.DISABLED.name)
}
putString("last_user_id", "")
}
}
}
}

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.auth.store
import android.content.Context
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@@ -47,8 +48,15 @@ class AuthenticationStore(
// No store found
if (!storePath.exists()) return emptyMap()
// Parse JSON document
val root = try {
json.parseToJsonElement(storePath.readText()).jsonObject
} catch (e: SerializationException) {
Timber.e(e, "Unable to read JSON")
JsonObject(emptyMap())
}
// Check for version
val root = json.parseToJsonElement(storePath.readText()).jsonObject
return when (root["version"]?.jsonPrimitive?.intOrNull) {
1 -> json.decodeFromJsonElement<Map<UUID, AuthenticationStoreServer>>(root["servers"]!!)
null -> {

View File

@@ -0,0 +1,31 @@
package org.jellyfin.androidtv.auth.ui
import android.content.Intent
import androidx.fragment.app.FragmentActivity
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.ui.startup.StartupActivity
import org.koin.android.ext.android.inject
import timber.log.Timber
/**
* Extension function to check authentication. Should be called in [FragmentActivity.onCreate] and
* [FragmentActivity.onResume]. It validates the current session and opens the authentication screen
* when no session is found.
*
* @return whether to proceed creating the activity or not. When `false` is returned the
* [FragmentActivity.finish] function is automatically called.
*/
fun FragmentActivity.validateAuthentication(): Boolean {
val sessionRepository by inject<SessionRepository>()
val userRepository by inject<UserRepository>()
if (sessionRepository.currentSession.value == null || userRepository.currentUser.value == null) {
Timber.w("Activity ${this::class.qualifiedName} started without a session, bouncing to StartupActivity")
startActivity(Intent(this, StartupActivity::class.java))
finish()
return false
}
return true
}

View File

@@ -66,6 +66,7 @@ object Codec {
object Subtitle {
const val ASS = "ass"
const val DVBSUB = "dvbsub"
const val DVDSUB = "dvdsub"
const val IDX = "idx"
const val PGS = "pgs"
@@ -76,5 +77,8 @@ object Codec {
const val SUB = "sub"
const val SUBRIP = "subrip"
const val VTT = "vtt"
const val SMIL = "smil"
const val TTML = "ttml"
const val WEBVTT = "webvtt"
}
}

View File

@@ -1,6 +1,6 @@
package org.jellyfin.androidtv.constant
enum class CustomMessage {
RefreshCurrentItem,
ActionComplete,
sealed interface CustomMessage {
object RefreshCurrentItem : CustomMessage
object ActionComplete : CustomMessage
}

View File

@@ -3,7 +3,6 @@ package org.jellyfin.androidtv.constant
object LiveTvOption {
const val LIVE_TV_GUIDE_OPTION_ID = 1000
const val LIVE_TV_RECORDINGS_OPTION_ID = 2000
const val VIDEO_QUEUE_OPTION_ID = 3000
const val LIVE_TV_SCHEDULE_OPTION_ID = 4000
const val LIVE_TV_SERIES_OPTION_ID = 5000
}

View File

@@ -0,0 +1,27 @@
package org.jellyfin.androidtv.constant
import android.content.Context
import org.jellyfin.androidtv.R
@Suppress("MagicNumber")
private val qualityOptions = setOf(
0.0, // auto
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 >=
)
@Suppress("MagicNumber")
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)
}
it.toString().removeSuffix(".0") to value
}

View File

@@ -17,7 +17,6 @@ enum class QueryType {
LiveTvChannel,
LiveTvProgram,
LiveTvRecording,
LiveTvRecordingGroup,
StaticItems,
Persons,
StaticAudioQueueItems,
@@ -26,4 +25,5 @@ enum class QueryType {
LatestItems,
SeriesTimer,
Premieres,
Resume,
}

View File

@@ -1,127 +0,0 @@
package org.jellyfin.androidtv.data.compat;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
import org.jellyfin.apiclient.model.dlna.EncodingContext;
import org.jellyfin.apiclient.model.dto.MediaSourceInfo;
import java.util.ArrayList;
/**
* Class AudioOptions.
*
* @deprecated
*/
@Deprecated
public class AudioOptions {
public AudioOptions() {
setContext(EncodingContext.Streaming);
setEnableDirectPlay(true);
setEnableDirectStream(true);
}
private boolean EnableDirectPlay;
public final boolean getEnableDirectPlay() {
return EnableDirectPlay;
}
public final void setEnableDirectPlay(boolean value) {
EnableDirectPlay = value;
}
private boolean EnableDirectStream;
public final boolean getEnableDirectStream() {
return EnableDirectStream;
}
public final void setEnableDirectStream(boolean value) {
EnableDirectStream = value;
}
private String ItemId;
public final String getItemId() {
return ItemId;
}
public final void setItemId(String value) {
ItemId = value;
}
private ArrayList<MediaSourceInfo> MediaSources;
public final ArrayList<MediaSourceInfo> getMediaSources() {
return MediaSources;
}
public final void setMediaSources(ArrayList<MediaSourceInfo> value) {
MediaSources = value;
}
private DeviceProfile Profile;
public final DeviceProfile getProfile() {
return Profile;
}
public final void setProfile(DeviceProfile value) {
Profile = value;
}
/**
* Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
*/
private String MediaSourceId;
public final String getMediaSourceId() {
return MediaSourceId;
}
public final void setMediaSourceId(String value) {
MediaSourceId = value;
}
/**
* Allows an override of supported number of audio channels
* Example: DeviceProfile supports five channel, but user only has stereo speakers
*/
private Integer MaxAudioChannels = null;
public final Integer getMaxAudioChannels() {
return MaxAudioChannels;
}
public final void setMaxAudioChannels(Integer value) {
MaxAudioChannels = value;
}
/**
* The application's configured quality setting
*/
private Integer MaxBitrate = null;
public final Integer getMaxBitrate() {
return MaxBitrate;
}
public final void setMaxBitrate(Integer value) {
MaxBitrate = value;
}
/**
* Gets or sets the context.
*
* <value>The context.</value>
*/
private EncodingContext Context = EncodingContext.values()[0];
public final EncodingContext getContext() {
return Context;
}
public final void setContext(EncodingContext value) {
Context = value;
}
}

View File

@@ -0,0 +1,31 @@
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
open class AudioOptions {
var enableDirectPlay = true
var enableDirectStream = true
var itemId: String? = null
var mediaSources: List<MediaSourceInfo>? = null
var profile: DeviceProfile? = null
/**
* Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
*/
var mediaSourceId: String? = null
/**
* Allows an override of supported number of audio channels
* Example: DeviceProfile supports five channel, but user only has stereo speakers
*/
var maxAudioChannels: Int? = null
/**
* The application's configured quality setting
*/
var maxBitrate: Int? = null
var context = EncodingContext.Streaming
}

View File

@@ -1,16 +0,0 @@
package org.jellyfin.androidtv.data.compat;
import org.jellyfin.apiclient.model.dlna.PlaybackErrorCode;
@Deprecated
public class PlaybackException extends RuntimeException {
private PlaybackErrorCode ErrorCode = PlaybackErrorCode.values()[0];
public final PlaybackErrorCode getErrorCode() {
return ErrorCode;
}
public final void setErrorCode(PlaybackErrorCode value) {
ErrorCode = value;
}
}

View File

@@ -0,0 +1,7 @@
package org.jellyfin.androidtv.data.compat
import org.jellyfin.apiclient.model.dlna.PlaybackErrorCode
class PlaybackException : RuntimeException() {
var errorCode = PlaybackErrorCode.NotAllowed
}

View File

@@ -6,12 +6,11 @@ import org.jellyfin.apiclient.model.dlna.SubtitleProfile;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.session.PlayMethod;
@Deprecated
public class StreamBuilder
{
public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod)
public static SubtitleProfile getSubtitleProfile(org.jellyfin.sdk.model.api.MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod)
{
if (playMethod != PlayMethod.Transcode && !subtitleStream.getIsExternal())
if (playMethod != PlayMethod.Transcode && !subtitleStream.isExternal())
{
// Look for supported embedded subs
for (SubtitleProfile profile : subtitleProfiles)
@@ -26,7 +25,7 @@ public class StreamBuilder
continue;
}
if (subtitleStream.getIsTextSubtitleStream() == MediaStream.IsTextFormat(profile.getFormat()) && Utils.equalsIgnoreCase(profile.getFormat(), subtitleStream.getCodec()))
if (subtitleStream.isTextSubtitleStream() == MediaStream.IsTextFormat(profile.getFormat()) && Utils.equalsIgnoreCase(profile.getFormat(), subtitleStream.getCodec()))
{
return profile;
}
@@ -37,12 +36,12 @@ public class StreamBuilder
SubtitleProfile tempVar = new SubtitleProfile();
tempVar.setMethod(SubtitleDeliveryMethod.Encode);
tempVar.setFormat(subtitleStream.getCodec());
SubtitleProfile tempVar2 = GetExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, false);
SubtitleProfile tempVar3 = GetExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, true);
SubtitleProfile tempVar2 = getExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, false);
SubtitleProfile tempVar3 = getExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, true);
return (tempVar2 != null) ? tempVar2 : (tempVar3 != null) ? tempVar3 : tempVar;
}
private static SubtitleProfile GetExternalSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, boolean allowConversion)
private static SubtitleProfile getExternalSubtitleProfile(org.jellyfin.sdk.model.api.MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, boolean allowConversion)
{
for (SubtitleProfile profile : subtitleProfiles)
{
@@ -61,7 +60,7 @@ public class StreamBuilder
continue;
}
if ((profile.getMethod() == SubtitleDeliveryMethod.External && subtitleStream.getIsTextSubtitleStream() == MediaStream.IsTextFormat(profile.getFormat())) || (profile.getMethod() == SubtitleDeliveryMethod.Hls && subtitleStream.getIsTextSubtitleStream()))
if ((profile.getMethod() == SubtitleDeliveryMethod.External && subtitleStream.isTextSubtitleStream() == MediaStream.IsTextFormat(profile.getFormat())) || (profile.getMethod() == SubtitleDeliveryMethod.Hls && subtitleStream.isTextSubtitleStream()))
{
boolean requiresConversion = !Utils.equalsIgnoreCase(subtitleStream.getCodec(), profile.getFormat());
@@ -75,7 +74,7 @@ public class StreamBuilder
continue;
}
if (subtitleStream.getIsTextSubtitleStream() && subtitleStream.getSupportsExternalStream() && subtitleStream.SupportsSubtitleConversionTo(profile.getFormat()))
if (subtitleStream.isTextSubtitleStream() && subtitleStream.getSupportsExternalStream() && supportsSubtitleConversionTo(subtitleStream, profile.getFormat()))
{
return profile;
}
@@ -84,4 +83,15 @@ public class StreamBuilder
return null;
}
public static boolean supportsSubtitleConversionTo(org.jellyfin.sdk.model.api.MediaStream mediaStream, String codec)
{
if (!mediaStream.isTextSubtitleStream())
{
return false;
}
// Can't convert from this
return !("ass".equalsIgnoreCase(mediaStream.getCodec()) || "ssa".equalsIgnoreCase(mediaStream.getCodec()) || "ass".equalsIgnoreCase(codec) || "ssa".equalsIgnoreCase(codec));
}
}

View File

@@ -7,21 +7,15 @@ 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.MediaSourceInfo;
import org.jellyfin.apiclient.model.dto.NameValuePair;
import org.jellyfin.apiclient.model.entities.MediaStream;
import org.jellyfin.apiclient.model.entities.MediaStreamType;
import org.jellyfin.apiclient.model.mediainfo.MediaProtocol;
import org.jellyfin.apiclient.model.session.PlayMethod;
import org.jellyfin.sdk.model.api.MediaProtocol;
import org.jellyfin.sdk.model.api.MediaSourceInfo;
import org.jellyfin.sdk.model.api.MediaStream;
import org.jellyfin.sdk.model.api.MediaStreamType;
import java.util.ArrayList;
/**
* Class StreamInfo.
*
* @deprecated
*/
@Deprecated
public class StreamInfo {
private static final String START_TIME_TICKS = "StartTimeTicks";
private static final String SUBTITLE_STREAM_INDEX = "SubtitleStreamIndex";
@@ -181,7 +175,7 @@ public class StreamInfo {
return getPlayMethod() == PlayMethod.DirectStream || getPlayMethod() == PlayMethod.DirectPlay;
}
public final String ToUrl(String baseUrl, String accessToken) {
public final String toUrl(String baseUrl, String accessToken) {
if (!Utils.isEmpty(getMediaUrl())) {
return getMediaUrl();
}
@@ -195,7 +189,7 @@ public class StreamInfo {
}
ArrayList<String> list = new ArrayList<String>();
for (NameValuePair pair : BuildParams(this, accessToken, false)) {
for (NameValuePair pair : buildParams(this, accessToken, false)) {
if (Utils.isEmpty(pair.getValue())) {
continue;
}
@@ -216,10 +210,10 @@ public class StreamInfo {
String queryString = Utils.join("&", list);
return GetUrl(baseUrl, queryString);
return getUrl(baseUrl, queryString);
}
private String GetUrl(String baseUrl, String queryString) {
private String getUrl(String baseUrl, String queryString) {
if (Utils.isEmpty(baseUrl)) {
throw new IllegalArgumentException(baseUrl);
}
@@ -236,7 +230,7 @@ public class StreamInfo {
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) {
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());
@@ -293,20 +287,22 @@ public class StreamInfo {
return list;
}
public final ArrayList<SubtitleStreamInfo> GetSubtitleProfiles(boolean includeSelectedTrackOnly, String baseUrl, String accessToken) {
return GetSubtitleProfiles(includeSelectedTrackOnly, false, baseUrl, accessToken);
public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSelectedTrackOnly, String baseUrl, String accessToken) {
return getSubtitleProfiles(includeSelectedTrackOnly, false, baseUrl, accessToken);
}
public final ArrayList<SubtitleStreamInfo> GetSubtitleProfiles(boolean includeSelectedTrackOnly, boolean enableAllProfiles, String baseUrl, String accessToken) {
public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSelectedTrackOnly, boolean enableAllProfiles, String baseUrl, String accessToken) {
ArrayList<SubtitleStreamInfo> list = new ArrayList<SubtitleStreamInfo>();
// HLS will preserve timestamps so we can just grab the full subtitle stream
long startPositionTicks = getPlayMethod() == PlayMethod.Transcode ? getStartPositionTicks() : 0;
if (!includeSelectedTrackOnly) {
for (MediaStream stream : getMediaSource().getMediaStreams()) {
if (stream.getType() == MediaStreamType.Subtitle) {
AddSubtitleProfiles(list, stream, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
if (getMediaSource() == null) return list;
for (org.jellyfin.sdk.model.api.MediaStream stream : getMediaSource().getMediaStreams()) {
if (stream.getType() == org.jellyfin.sdk.model.api.MediaStreamType.SUBTITLE) {
addSubtitleProfiles(list, stream, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
}
}
}
@@ -314,22 +310,22 @@ public class StreamInfo {
return list;
}
private void AddSubtitleProfiles(ArrayList<SubtitleStreamInfo> list, MediaStream stream, boolean enableAllProfiles, String baseUrl, String accessToken, long startPositionTicks) {
private void addSubtitleProfiles(ArrayList<SubtitleStreamInfo> list, org.jellyfin.sdk.model.api.MediaStream stream, boolean enableAllProfiles, String baseUrl, String accessToken, long startPositionTicks) {
if (enableAllProfiles) {
for (SubtitleProfile profile : getDeviceProfile().getSubtitleProfiles()) {
SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new SubtitleProfile[]{profile});
SubtitleStreamInfo info = getSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new SubtitleProfile[]{profile});
list.add(info);
}
} else {
SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, getDeviceProfile().getSubtitleProfiles());
SubtitleStreamInfo info = getSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, getDeviceProfile().getSubtitleProfiles());
list.add(info);
}
}
private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, String baseUrl, String accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles) {
SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, subtitleProfiles, getPlayMethod());
private SubtitleStreamInfo getSubtitleStreamInfo(org.jellyfin.sdk.model.api.MediaStream stream, String baseUrl, String accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles) {
SubtitleProfile subtitleProfile = StreamBuilder.getSubtitleProfile(stream, subtitleProfiles, getPlayMethod());
SubtitleStreamInfo tempVar = new SubtitleStreamInfo();
String tempVar2 = stream.getLanguage();
tempVar.setName((tempVar2 != null) ? tempVar2 : "Unknown");
@@ -340,7 +336,7 @@ public class StreamInfo {
SubtitleStreamInfo info = tempVar;
if (info.getDeliveryMethod() == SubtitleDeliveryMethod.External) {
if (getMediaSource().getProtocol() == MediaProtocol.File || !stream.getCodec().equalsIgnoreCase(subtitleProfile.getFormat())) {
if (getMediaSource().getProtocol() == MediaProtocol.FILE || !stream.getCodec().equalsIgnoreCase(subtitleProfile.getFormat())) {
info.setUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/%5$s/Stream.%6$s", baseUrl, getItemId(), getMediaSourceId(), String.valueOf(stream.getIndex()), String.valueOf(startPositionTicks), subtitleProfile.getFormat()));
if (!Utils.isEmpty(accessToken)) {
@@ -354,14 +350,14 @@ public class StreamInfo {
return info;
}
public final ArrayList<MediaStream> GetSelectableAudioStreams() {
return GetSelectableStreams(MediaStreamType.Audio);
public final ArrayList<MediaStream> getSelectableAudioStreams() {
return getSelectableStreams(MediaStreamType.AUDIO);
}
public final ArrayList<MediaStream> GetSelectableStreams(MediaStreamType type) {
ArrayList<MediaStream> list = new ArrayList<MediaStream>();
public final ArrayList<org.jellyfin.sdk.model.api.MediaStream> getSelectableStreams(MediaStreamType type) {
ArrayList<org.jellyfin.sdk.model.api.MediaStream> list = new ArrayList<org.jellyfin.sdk.model.api.MediaStream>();
for (MediaStream stream : getMediaSource().getMediaStreams()) {
for (org.jellyfin.sdk.model.api.MediaStream stream : getMediaSource().getMediaStreams()) {
if (type == stream.getType()) {
list.add(stream);
}

View File

@@ -1,66 +0,0 @@
package org.jellyfin.androidtv.data.compat;
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
@Deprecated
public class SubtitleStreamInfo {
private String Url;
public final String getUrl() {
return Url;
}
public final void setUrl(String value) {
Url = value;
}
private String Name;
public final String getName() {
return Name;
}
public final void setName(String value) {
Name = value;
}
private String Format;
public final String getFormat() {
return Format;
}
public final void setFormat(String value) {
Format = value;
}
private String DisplayTitle;
public final String getDisplayTitle() {
return DisplayTitle;
}
public final void setDisplayTitle(String value) {
DisplayTitle = value;
}
private int Index;
public final int getIndex() {
return Index;
}
public final void setIndex(int value) {
Index = value;
}
private SubtitleDeliveryMethod DeliveryMethod = SubtitleDeliveryMethod.values()[0];
public final SubtitleDeliveryMethod getDeliveryMethod() {
return DeliveryMethod;
}
public final void setDeliveryMethod(SubtitleDeliveryMethod value) {
DeliveryMethod = value;
}
}

View File

@@ -0,0 +1,12 @@
package org.jellyfin.androidtv.data.compat
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod
class SubtitleStreamInfo {
var url: String? = null
var name: String? = null
var format: String? = null
var displayTitle: String? = null
var index = 0
var deliveryMethod = SubtitleDeliveryMethod.Encode
}

View File

@@ -1,28 +0,0 @@
package org.jellyfin.androidtv.data.compat;
/**
* Class VideoOptions.
*
* @deprecated
*/
@Deprecated
public class VideoOptions extends AudioOptions {
private Integer AudioStreamIndex;
private Integer SubtitleStreamIndex;
public final Integer getAudioStreamIndex() {
return AudioStreamIndex;
}
public final void setAudioStreamIndex(Integer value) {
AudioStreamIndex = value;
}
public final Integer getSubtitleStreamIndex() {
return SubtitleStreamIndex;
}
public final void setSubtitleStreamIndex(Integer value) {
SubtitleStreamIndex = value;
}
}

View File

@@ -0,0 +1,6 @@
package org.jellyfin.androidtv.data.compat
class VideoOptions : AudioOptions() {
var audioStreamIndex: Int? = null
var subtitleStreamIndex: Int? = null
}

View File

@@ -7,10 +7,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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.apiclient.model.entities.MediaType
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.sessionApi
@@ -19,9 +21,11 @@ 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.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.GeneralCommandType
import org.jellyfin.sdk.model.api.LibraryUpdateInfo
import org.jellyfin.sdk.model.api.PlaystateCommand
import org.jellyfin.sdk.model.constant.MediaType
import org.jellyfin.sdk.model.extensions.getValue
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.jellyfin.sdk.model.socket.LibraryChangedMessage
@@ -33,12 +37,13 @@ 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 coroutineScope = CoroutineScope(Dispatchers.IO)
private var socketInstance: SocketInstance = createInstance()
val state = socketInstance.state
suspend fun updateSession() {
@@ -49,6 +54,7 @@ class SocketHandler(
supportedCommands = listOf(
GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.SEND_STRING,
GeneralCommandType.DISPLAY_CONTENT,
),
)
} catch (err: ApiClientException) {
@@ -61,27 +67,32 @@ class SocketHandler(
else socketInstance.updateCredentials()
}
private fun createInstance() = api.ws().apply {
// Library
addListener<LibraryChangedMessage> { message -> onLibraryChanged(message.info) }
init {
socketInstance.apply {
// Library
addListener<LibraryChangedMessage> { message -> onLibraryChanged(message.info) }
// Media playback
addListener<PlayMessage> { message -> onPlayMessage(message) }
addListener<PlayStateMessage> { message -> onPlayStateMessage(message) }
// Media playback
addListener<PlayMessage> { message -> onPlayMessage(message) }
addListener<PlayStateMessage> { message -> onPlayStateMessage(message) }
// General commands
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_CONTENT)) { message ->
val itemId by message
val itemUuid = itemId?.toUUIDOrNull()
// General commands
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_CONTENT)) { message ->
val itemId by message
val itemType by message
if (itemUuid != null) onDisplayContent(itemUuid)
}
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING)) { message ->
val header by message
val text by message
val string by message
val itemUuid = itemId?.toUUIDOrNull()
val itemKind = itemType?.let { type -> BaseItemKind.values().find { value -> value.serialName.equals(type, true) } }
onDisplayMessage(header, text ?: string)
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
onDisplayMessage(header, text ?: string)
}
}
}
@@ -130,16 +141,12 @@ class SocketHandler(
PlaystateCommand.SEEK -> playbackController?.seek(
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
)
// FIXME get rewind/forward amount from displayprefs
PlaystateCommand.REWIND -> playbackController?.skip(REWIND_MS)
PlaystateCommand.FAST_FORWARD -> playbackController?.skip(FORWARD_MS)
PlaystateCommand.REWIND -> playbackController?.rewind()
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
}
}
// FIXME: Add an ItemLauncher function that accepts SDK BaseItemDto
// Add "GeneralCommandType.DISPLAY_CONTENT" to supportedCommands in capabilities to receive
// these messages. Otherwise this function is never called.
private fun onDisplayContent(itemId: UUID) {
private fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) {
val playbackController = playbackControllerContainer.playbackController
if (playbackController?.isPlaying == true || playbackController?.isPaused == true) {
@@ -149,9 +156,13 @@ class SocketHandler(
Timber.i("Launching $itemId")
coroutineScope.launch {
val item by api.userLibraryApi.getItem(itemId = itemId)
// ItemLauncher.launch(item)
when (itemKind) {
BaseItemKind.USER_VIEW,
BaseItemKind.COLLECTION_FOLDER -> coroutineScope.launch {
val item by api.userLibraryApi.getItem(itemId = itemId)
ItemLauncher.launchUserView(item)
}
else -> navigationRepository.navigate(Destinations.itemDetails(itemId))
}
}
@@ -168,7 +179,5 @@ class SocketHandler(
companion object {
const val TICKS_TO_MS = 10000L
const val REWIND_MS = -11000
const val FORWARD_MS = 30000
}
}

View File

@@ -2,4 +2,6 @@ package org.jellyfin.androidtv.data.model
data class AppNotification(
val message: String,
val dismiss: () -> Unit,
val public: Boolean,
)

View File

@@ -1,15 +0,0 @@
package org.jellyfin.androidtv.data.model;
import org.jellyfin.apiclient.model.entities.ChapterInfo;
public class ChapterItemInfo extends ChapterInfo {
private String itemId;
public String getItemId() {
return itemId;
}
public void setItemId(String itemId) {
this.itemId = itemId;
}
}

View File

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

View File

@@ -1,37 +0,0 @@
package org.jellyfin.androidtv.data.model;
import org.jellyfin.apiclient.model.querying.ItemFilter;
import java.util.ArrayList;
import java.util.List;
public class FilterOptions {
private boolean favoriteOnly;
private boolean unwatchedOnly;
public void setFavoriteOnly(boolean value) {
favoriteOnly = value;
}
public void setUnwatchedOnly(boolean value) {
unwatchedOnly = value;
}
public boolean isUnwatchedOnly() {
return unwatchedOnly;
}
public boolean isFavoriteOnly() {
return favoriteOnly;
}
public ItemFilter[] getFilters() {
if (!unwatchedOnly && !favoriteOnly) return null;
List<ItemFilter> filters = new ArrayList<>();
if (favoriteOnly) filters.add(ItemFilter.IsFavorite);
if (unwatchedOnly) filters.add(ItemFilter.IsUnplayed);
return filters.toArray(new ItemFilter[0]);
}
}

View File

@@ -0,0 +1,18 @@
package org.jellyfin.androidtv.data.model
import org.jellyfin.apiclient.model.querying.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()
}
}

View File

@@ -1,23 +0,0 @@
package org.jellyfin.androidtv.data.model;
public class InfoItem {
private String label;
private String value;
public InfoItem() {
this("", "");
}
public InfoItem(String label, String value) {
this.label = label;
this.value = value;
}
public String getLabel() {
return label;
}
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,6 @@
package org.jellyfin.androidtv.data.model
data class InfoItem @JvmOverloads constructor(
val label: String = "",
val value: String = "",
)

View File

@@ -1,13 +0,0 @@
package org.jellyfin.androidtv.data.querying;
public class SpecialsQuery {
private String ItemId;
public SpecialsQuery(String itemId) {
ItemId = itemId;
}
public String getItemId() {
return ItemId;
}
}

View File

@@ -0,0 +1,5 @@
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.java.KoinJavaComponent;
public class StdItemQuery extends ItemQuery {
public StdItemQuery(ItemFields[] fields) {
if (fields == null) {
fields = new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount
};
}
setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
setFields(fields);
}
public StdItemQuery() {
this(null);
}
}

View File

@@ -0,0 +1,26 @@
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,13 +0,0 @@
package org.jellyfin.androidtv.data.querying;
public class TrailersQuery {
private String ItemId;
public TrailersQuery(String itemId) {
ItemId = itemId;
}
public String getItemId() {
return ItemId;
}
}

View File

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

View File

@@ -1,5 +0,0 @@
package org.jellyfin.androidtv.data.querying;
public class ViewQuery {
//Dummy indicator class
}

View File

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

View File

@@ -0,0 +1,23 @@
package org.jellyfin.androidtv.data.repository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.jellyfin.androidtv.constant.CustomMessage
interface CustomMessageRepository {
val message: StateFlow<CustomMessage?>
fun pushMessage(message: CustomMessage)
}
class CustomMessageRepositoryImpl : CustomMessageRepository {
private val _message = MutableStateFlow<CustomMessage?>(null)
override val message get() = _message.asStateFlow()
override fun pushMessage(message: CustomMessage) {
// Make sure to re-emit the same message if requested
if (_message.value == message) _message.value = null
_message.value = message
}
}

View File

@@ -0,0 +1,37 @@
package org.jellyfin.androidtv.data.repository
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.sdk.api.client.ApiClient
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
interface ItemMutationRepository {
suspend fun setFavorite(item: UUID, favorite: Boolean): UserItemDataDto
suspend fun setPlayed(item: UUID, played: Boolean): UserItemDataDto
}
class ItemMutationRepositoryImpl(
private val api: ApiClient,
private val dataRefreshService: DataRefreshService,
) : ItemMutationRepository {
override suspend fun setFavorite(item: UUID, favorite: Boolean): UserItemDataDto {
val response by when {
favorite -> api.userLibraryApi.markFavoriteItem(itemId = item)
else -> api.userLibraryApi.unmarkFavoriteItem(itemId = item)
}
dataRefreshService.lastFavoriteUpdate = System.currentTimeMillis()
return response
}
override suspend fun setPlayed(item: UUID, played: Boolean): UserItemDataDto {
val response by when {
played -> api.playStateApi.markPlayedItem(itemId = item)
else -> api.playStateApi.markUnplayedItem(itemId = item)
}
return response
}
}

View File

@@ -5,8 +5,10 @@ import android.content.Context
import android.content.res.Configuration
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.data.model.AppNotification
import org.jellyfin.androidtv.preference.SystemPreferences
interface NotificationsRepository {
val notifications: StateFlow<List<AppNotification>>
@@ -18,19 +20,22 @@ interface NotificationsRepository {
class NotificationsRepositoryImpl(
private val context: Context,
private val uiModeManager: UiModeManager,
private val systemPreferences: SystemPreferences,
) : NotificationsRepository {
override val notifications = MutableStateFlow(emptyList<AppNotification>())
override fun dismissNotification(item: AppNotification) {
notifications.value = notifications.value.filter { it != item }
item.dismiss()
}
override fun addDefaultNotifications() {
addUiModeNotification()
addBetaNotification()
}
private fun addNotification(message: String) {
notifications.value = notifications.value + AppNotification(message)
private fun addNotification(message: String, public: Boolean = false, dismiss: () -> Unit = {}) {
notifications.value = notifications.value + AppNotification(message, dismiss, public)
}
private fun addUiModeNotification() {
@@ -40,7 +45,19 @@ class NotificationsRepositoryImpl(
val hasHdmiCec = context.packageManager.hasSystemFeature("android.hardware.hdmi.cec")
if (invalidUiMode && isTouch && !hasHdmiCec) {
addNotification(context.getString(R.string.app_notification_uimode_invalid))
addNotification(context.getString(R.string.app_notification_uimode_invalid), public = true)
}
}
private fun addBetaNotification() {
val dismissedVersion = systemPreferences[SystemPreferences.dismissedBetaNotificationVersion]
val currentVersion = BuildConfig.VERSION_NAME
val isBeta = currentVersion.lowercase().contains("beta")
if (isBeta && currentVersion != dismissedVersion) {
addNotification(context.getString(R.string.app_notification_beta, currentVersion)) {
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] = currentVersion
}
}
}
}

View File

@@ -2,16 +2,17 @@ package org.jellyfin.androidtv.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.jellyfin.apiclient.model.entities.CollectionType
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
interface UserViewsRepository {
val views: Flow<Collection<BaseItemDto>>
fun isSupported(collectionType: String?): Boolean
fun allowViewSelection(collectionType: String?): Boolean
fun allowGridView(collectionType: String?): Boolean
}
class UserViewsRepositoryImpl(
@@ -26,13 +27,24 @@ class UserViewsRepositoryImpl(
}
override fun isSupported(collectionType: String?) = collectionType !in unsupportedCollectionTypes
override fun allowViewSelection(collectionType: String?) = collectionType != CollectionType.Music
override fun allowViewSelection(collectionType: String?) = collectionType !in disallowViewSelectionCollectionTypes
override fun allowGridView(collectionType: String?) = collectionType !in disallowGridViewCollectionTypes
private companion object {
private val unsupportedCollectionTypes = arrayOf(
CollectionType.Books,
CollectionType.Games,
CollectionType.Folders
)
private val disallowViewSelectionCollectionTypes = arrayOf(
CollectionType.LiveTv,
CollectionType.Music,
CollectionType.Photos,
)
private val disallowGridViewCollectionTypes = arrayOf(
CollectionType.LiveTv,
CollectionType.Music
)
}
}

View File

@@ -30,18 +30,16 @@ import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.auth.model.Server
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.util.sdk.compat.asSdk
import org.jellyfin.apiclient.model.search.SearchHint
import org.jellyfin.sdk.Jellyfin
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 org.jellyfin.sdk.model.api.SearchHint
import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.ExecutionException
import org.jellyfin.apiclient.model.dto.BaseItemDto as LegacyBaseItemDto
class BackgroundService(
private val context: Context,
@@ -145,15 +143,6 @@ class BackgroundService(
}
}
/**
* Use all available backdrops from [baseItem] as background.
*/
@Deprecated(
"Use the SDK instead of the legacy apiclient",
ReplaceWith("setBackground(baseItem?.asSdk())", "org.jellyfin.androidtv.util.sdk.compat.asSdk")
)
fun setBackground(baseItem: LegacyBaseItemDto?) = setBackground(baseItem?.asSdk())
/**
* Use all available backdrops from [baseItem] as background.
*/

View File

@@ -1,12 +0,0 @@
package org.jellyfin.androidtv.di
import android.app.Application
import org.jellyfin.androidtv.ui.shared.AppThemeCallbacks
import org.jellyfin.androidtv.ui.shared.AuthenticatedUserCallbacks
import org.koin.dsl.bind
import org.koin.dsl.module
val activityLifecycleCallbacksModule = module {
single { AuthenticatedUserCallbacks(get()) } bind Application.ActivityLifecycleCallbacks::class
single { AppThemeCallbacks(get()) } bind Application.ActivityLifecycleCallbacks::class
}

View File

@@ -6,11 +6,19 @@ import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.auth.repository.UserRepositoryImpl
import org.jellyfin.androidtv.data.eventhandling.SocketHandler
import org.jellyfin.androidtv.data.model.DataRefreshService
import org.jellyfin.androidtv.data.repository.CustomMessageRepository
import org.jellyfin.androidtv.data.repository.CustomMessageRepositoryImpl
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
import org.jellyfin.androidtv.data.repository.ItemMutationRepositoryImpl
import org.jellyfin.androidtv.data.repository.NotificationsRepository
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.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.navigation.NavigationRepositoryImpl
import org.jellyfin.androidtv.ui.picture.PictureViewerViewModel
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer
import org.jellyfin.androidtv.ui.playback.nextup.NextUpViewModel
@@ -21,10 +29,9 @@ import org.jellyfin.androidtv.util.MarkdownRenderer
import org.jellyfin.androidtv.util.sdk.legacy
import org.jellyfin.apiclient.AppInfo
import org.jellyfin.apiclient.android
import org.jellyfin.apiclient.interaction.ApiEventListener
import org.jellyfin.apiclient.logging.AndroidLogger
import org.jellyfin.apiclient.serialization.GsonJsonSerializer
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
@@ -59,11 +66,11 @@ val appModule = module {
get<JellyfinSdk>().createApi()
}
single { SocketHandler(get(), get(), get(), get(), get()) }
single { get<ApiClient>().ws() }
single { SocketHandler(get(), get(), get(), get(), get(), get(), get()) }
// Old apiclient
single { GsonJsonSerializer() }
single {
JellyfinApiClient {
appInfo = AppInfo("Android TV", BuildConfig.VERSION_NAME)
@@ -74,8 +81,7 @@ val appModule = module {
single {
get<JellyfinApiClient>().createApi(
device = get<DeviceInfo>(defaultDeviceInfo).legacy(),
eventListener = ApiEventListener()
device = get<DeviceInfo>(defaultDeviceInfo).legacy()
)
}
@@ -87,12 +93,16 @@ val appModule = module {
single<UserRepository> { UserRepositoryImpl() }
single<UserViewsRepository> { UserViewsRepositoryImpl(get()) }
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get()) }
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get(), get()) }
single<ItemMutationRepository> { ItemMutationRepositoryImpl(get(), get()) }
single<CustomMessageRepository> { CustomMessageRepositoryImpl() }
single<NavigationRepository> { NavigationRepositoryImpl(Destinations.home) }
viewModel { StartupViewModel(get(), get(), get(), get()) }
viewModel { UserLoginViewModel(get(), get(), get()) }
viewModel { UserLoginViewModel(get(), get(), get(), get(defaultDeviceInfo)) }
viewModel { ServerAddViewModel(get()) }
viewModel { NextUpViewModel(get(), get(), get(), get()) }
viewModel { PictureViewerViewModel(get()) }
single { BackgroundService(get(), get(), get(), get()) }

View File

@@ -12,7 +12,6 @@ class KoinInitializer : Initializer<KoinApplication> {
androidContext(context)
modules(
activityLifecycleCallbacksModule,
androidModule,
appModule,
authModule,

View File

@@ -21,12 +21,12 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.R
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.dp
import org.jellyfin.androidtv.util.sdk.isUsable
import org.jellyfin.apiclient.model.entities.MediaType
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.exception.TimeoutException
@@ -39,6 +39,7 @@ 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.koin.core.component.KoinComponent
import org.koin.core.component.inject
import timber.log.Timber
@@ -160,7 +161,7 @@ class LeanbackChannelWorker(
.filter { userViewsRepository.isSupported(it.collectionType) }
.map { item ->
val imageUri = if (item.imageTags?.contains(ImageType.PRIMARY) == true)
api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY).toUri()
ImageProvider.getImageUri(api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY))
else
ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv).toUri()
@@ -184,35 +185,31 @@ class LeanbackChannelWorker(
* Gets the poster art for an item. Uses the [preferParentThumb] parameter to fetch the series
* image when preferred.
*/
private fun BaseItemDto.getPosterArtImageUrl(preferParentThumb: Boolean): Uri {
if (type == BaseItemKind.MOVIE) {
return api.imageApi.getItemImageUrl(
itemId = id,
imageType = ImageType.PRIMARY,
format = ImageFormat.WEBP,
width = 106.dp(context),
height = 153.dp(context),
).let(Uri::parse)
}
private fun BaseItemDto.getPosterArtImageUrl(preferParentThumb: Boolean): Uri = when {
type == BaseItemKind.MOVIE -> api.imageApi.getItemImageUrl(
itemId = id,
imageType = ImageType.PRIMARY,
format = ImageFormat.WEBP,
width = 106.dp(context),
height = 153.dp(context),
)
return when {
(preferParentThumb || imageTags?.contains(ImageType.PRIMARY) != true)
&& parentThumbItemId != null -> api.imageApi.getItemImageUrl(
itemId = parentThumbItemId!!,
imageType = ImageType.THUMB,
format = ImageFormat.WEBP,
width = 272.dp(context),
height = 153.dp(context),
)
else -> api.imageApi.getItemImageUrl(
itemId = id,
imageType = ImageType.PRIMARY,
format = ImageFormat.WEBP,
width = 272.dp(context),
height = 153.dp(context),
)
}.let(Uri::parse)
}
(preferParentThumb || imageTags?.contains(ImageType.PRIMARY) != true) && parentThumbItemId != null -> api.imageApi.getItemImageUrl(
itemId = parentThumbItemId!!,
imageType = ImageType.THUMB,
format = ImageFormat.WEBP,
width = 272.dp(context),
height = 153.dp(context),
)
else -> api.imageApi.getItemImageUrl(
itemId = id,
imageType = ImageType.PRIMARY,
format = ImageFormat.WEBP,
width = 272.dp(context),
height = 153.dp(context),
)
}.let(ImageProvider::getImageUri)
/**
* Gets the resume and next up episodes. The returned pair contains two lists:

View File

@@ -12,6 +12,7 @@ import android.provider.BaseColumns
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.sdk.isUsable
import org.jellyfin.sdk.api.client.ApiClient
@@ -102,9 +103,8 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
MatrixCursor(columns).also { cursor ->
searchResult?.items?.forEach { item ->
// Note: Image loading might fail if the image is served over HTTP
val imageUri = if (item.imageTags?.contains(ImageType.PRIMARY) == true)
api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY)
ImageProvider.getImageUri(api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY))
else
ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)

View File

@@ -16,7 +16,6 @@ import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.databinding.DreamLibraryBinding
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.apiclient.model.querying.ItemSortBy
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.extensions.imageApi
@@ -25,6 +24,7 @@ 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.constant.ItemSortBy
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.concurrent.ExecutionException

View File

@@ -0,0 +1,71 @@
package org.jellyfin.androidtv.integration.provider
import android.content.ContentProvider
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.core.net.toUri
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
class ImageProvider : ContentProvider() {
override fun onCreate(): Boolean = true
override fun getType(uri: Uri) = null
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?) = null
override fun insert(uri: Uri, values: ContentValues?) = null
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
val src = requireNotNull(uri.getQueryParameter("src")).toUri()
val (read, write) = ParcelFileDescriptor.createPipe()
val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(write)
ProcessLifecycleOwner.get().lifecycleScope.launch(Dispatchers.IO) {
Glide.with(context!!)
.asBitmap()
.error(R.drawable.placeholder_icon)
.load(src)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
@Suppress("DEPRECATION")
val format = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Bitmap.CompressFormat.WEBP_LOSSY
else -> Bitmap.CompressFormat.WEBP
}
resource.compress(format, 95, outputStream)
outputStream.close()
}
override fun onLoadCleared(placeholder: Drawable?) = outputStream.close()
})
}
return read
}
companion object {
/**
* Get a [Uri] that uses the [ImageProvider] to load an image. The input should be a valid
* Jellyfin image URL created using the SDK.
*/
fun getImageUri(src: String): Uri = Uri.Builder()
.scheme("content")
.authority("${BuildConfig.APPLICATION_ID}.integration.provider.ImageProvider")
.appendQueryParameter("src", src)
.appendQueryParameter("v", BuildConfig.VERSION_NAME)
.build()
}
}

View File

@@ -4,12 +4,12 @@ import org.jellyfin.androidtv.constant.GridDirection
import org.jellyfin.androidtv.constant.ImageType
import org.jellyfin.androidtv.constant.PosterSize
import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
import org.jellyfin.apiclient.model.entities.SortOrder
import org.jellyfin.apiclient.model.querying.ItemSortBy
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.SortOrder
import org.jellyfin.sdk.model.constant.ItemSortBy
class LibraryPreferences(
displayPreferencesId: String,
@@ -30,6 +30,6 @@ class LibraryPreferences(
// Item sorting
val sortBy = stringPreference("SortBy", ItemSortBy.SortName)
val sortOrder = enumPreference("SortOrder", SortOrder.Ascending)
val sortOrder = enumPreference("SortOrder", SortOrder.ASCENDING)
}
}

View File

@@ -1,10 +1,10 @@
package org.jellyfin.androidtv.preference
import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
import org.jellyfin.apiclient.model.querying.ItemSortBy
import org.jellyfin.preference.booleanPreference
import org.jellyfin.preference.stringPreference
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.constant.ItemSortBy
class LiveTvPreferences(
api: ApiClient,

View File

@@ -64,5 +64,10 @@ class SystemPreferences(context: Context) : SharedPreferenceStore(
* 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.
*/
val dismissedBetaNotificationVersion = stringPreference("dismissed_beta_notification_version", "")
}
}

View File

@@ -16,6 +16,7 @@ import org.jellyfin.androidtv.util.DeviceUtils
import org.jellyfin.preference.booleanPreference
import org.jellyfin.preference.enumPreference
import org.jellyfin.preference.intPreference
import org.jellyfin.preference.longPreference
import org.jellyfin.preference.migration.putEnum
import org.jellyfin.preference.store.SharedPreferenceStore
import org.jellyfin.preference.stringPreference
@@ -95,17 +96,17 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
*/
var refreshRateSwitchingBehavior = enumPreference("refresh_rate_switching_behavior", RefreshRateSwitchingBehavior.DISABLED)
/**
* Send a path instead to the external player
*/
var externalVideoPlayerSendPath = booleanPreference("pref_send_path_external", false)
/* Playback - Audio related */
/**
* Preferred behavior for audio streaming.
*/
var audioBehaviour = enumPreference("audio_behavior", defaultAudioBehavior)
/**
* Preferred behavior for audio streaming.
*/
var audioNightMode = enumPreference("audio_night_mode", false)
/**
* Enable DTS
*/
@@ -114,7 +115,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
/**
* Enable AC3
*/
var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1())
var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1)
/**
* Default audio delay in milliseconds for libVLC
@@ -179,9 +180,24 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
var subtitlesBackgroundEnabled = booleanPreference("subtitles_background_enabled", true)
/**
* Set default subtitles font size
* Subtitles font size
*/
var defaultSubtitlesSize = intPreference("subtitles_size", 28)
var subtitlesSize = intPreference("subtitles_size", 28)
/**
* Subtitles stroke size
*/
var subtitleStrokeSize = intPreference("subtitles_stroke_size", 0)
/**
* Subtitles position
*/
var subtitlePosition = intPreference("subtitles_position", 40)
/**
* Subtitles foreground color
*/
var subtitlesTextColor = longPreference("subtitles_text_color", 0xFFFFFFFF)
}
init {
@@ -213,7 +229,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
putInt("libvlc_audio_delay", it.getLong("libvlc_audio_delay", 0).toInt())
// Disable AC3 (Dolby Digital) on Fire Stick Gen 1 devices
if (DeviceUtils.isFireTvStickGen1()) putBoolean("pref_bitstream_ac3", false)
if (DeviceUtils.isFireTvStickGen1) putBoolean("pref_bitstream_ac3", false)
}
// v0.13.3 to v0.13.4

View File

@@ -18,5 +18,5 @@ enum class AudioBehavior {
DOWNMIX_TO_STEREO
}
val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV()) AudioBehavior.DOWNMIX_TO_STEREO
val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV) AudioBehavior.DOWNMIX_TO_STEREO
else AudioBehavior.DIRECT_STREAM

View File

@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.preference.dsl.EnumDisplayOptions
import org.jellyfin.apiclient.model.querying.ItemSortBy
import org.jellyfin.sdk.model.constant.ItemSortBy
enum class LiveTvChannelOrder(
val stringValue: String

View File

@@ -32,6 +32,7 @@ object TelemetryService {
buildConfigClass = BuildConfig::class.java
sharedPreferencesName = TelemetryPreferences.SHARED_PREFERENCES_NAME
pluginLoader = AcraPluginLoader(AcraReportSenderFactory::class.java)
applicationLogFileLines = 250
toast {
text = context.getString(R.string.crash_report_toast)

View File

@@ -10,10 +10,14 @@ import androidx.core.view.doOnAttach
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.vanniktech.blurhash.BlurHash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.androidtv.util.BlurHashDecoder
import org.jellyfin.androidtv.R
import org.jellyfin.sdk.api.client.ApiClient
import kotlin.math.round
import kotlin.time.Duration.Companion.milliseconds
@@ -28,12 +32,19 @@ class AsyncImageView @JvmOverloads constructor(
defStyleAttr: Int = 0,
) : AppCompatImageView(context, attrs, defStyleAttr) {
private val lifeCycleOwner get() = findViewTreeLifecycleOwner()
private val styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.AsyncImageView, defStyleAttr, 0)
/**
* The duration of the crossfade when changing switching the images of the url, blurhash and
* placeholder.
*/
var crossFadeDuration = 100.milliseconds
@Suppress("MagicNumber")
var crossFadeDuration = styledAttributes.getInt(R.styleable.AsyncImageView_crossfadeDuration, 100).milliseconds
/**
* Shape the image to a circle and remove all corners.
*/
var circleCrop = styledAttributes.getBoolean(R.styleable.AsyncImageView_circleCrop, false)
/**
* Load an image from the network using [url]. When the [url] is null or returns a bad response
@@ -52,7 +63,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 = BlurHashDecoder.decode(
val blurHashBitmap = BlurHash.decode(
blurHash,
if (aspectRatio > 1) round(blurHashResolution * aspectRatio).toInt() else blurHashResolution,
if (aspectRatio >= 1) blurHashResolution else round(blurHashResolution / aspectRatio).toInt(),
@@ -61,13 +72,23 @@ class AsyncImageView @JvmOverloads constructor(
}
// Start loading image or placeholder
Glide.with(this@AsyncImageView)
.load(url ?: placeholder)
.placeholder(placeholderOrBlurHash)
.error(placeholder)
// FIXME: Glide is unable to scale the image when transitions are enabled
//.transition(DrawableTransitionOptions.withCrossFade(crossFadeDuration.inWholeMilliseconds.toInt()))
.into(this@AsyncImageView)
if (url == null) {
Glide.with(this@AsyncImageView).load(placeholder).apply {
if (circleCrop) circleCrop()
}.into(this@AsyncImageView)
} else {
val glideUrl = GlideUrl(url, LazyHeaders.Builder().apply {
setHeader("Accept", ApiClient.HEADER_ACCEPT)
}.build())
Glide.with(this@AsyncImageView).load(glideUrl).apply {
placeholder(placeholderOrBlurHash)
error(placeholder)
if (circleCrop) circleCrop()
// FIXME: Glide is unable to scale the image when transitions are enabled
//transition(DrawableTransitionOptions.withCrossFade(crossFadeDuration.inWholeMilliseconds.toInt()))
}.into(this@AsyncImageView)
}
}
}
}

View File

@@ -4,8 +4,8 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.databinding.ClockUserBugBinding
@@ -39,20 +39,11 @@ class ClockUserView @JvmOverloads constructor(
val currentUser = userRepository.currentUser.value
if (currentUser != null) {
if (currentUser.primaryImageTag != null) {
Glide.with(context)
.load(ImageUtils.getPrimaryImageUrl(currentUser))
.placeholder(R.drawable.ic_user)
.centerInside()
.circleCrop()
.into(binding.clockUserImage)
} else {
binding.clockUserImage.setImageResource(R.drawable.ic_user)
}
binding.clockUserImage.isVisible = true
} else {
binding.clockUserImage.isVisible = false
}
binding.clockUserImage.load(
url = currentUser?.let(ImageUtils::getPrimaryImageUrl),
placeholder = ContextCompat.getDrawable(context, R.drawable.ic_user)
)
binding.clockUserImage.isVisible = currentUser != null
}
}

View File

@@ -2,8 +2,10 @@ package org.jellyfin.androidtv.ui
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnFocusChangeListener
import android.widget.FrameLayout
import org.jellyfin.androidtv.databinding.ViewRowDetailsBinding
class DetailRowView @JvmOverloads constructor(
@@ -13,4 +15,38 @@ class DetailRowView @JvmOverloads constructor(
defStyleRes: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
val binding = ViewRowDetailsBinding.inflate(LayoutInflater.from(context), this, true)
/**
* Keeps track of the last selected button and reselect it when navigating back to the buttons row.
*/
private val buttonsHierarchyChangeListener = object : OnHierarchyChangeListener {
private var lastFocusedButton: View? = null
private val focusChangeListener = OnFocusChangeListener { view, hasFocus ->
// Restore last focused button when navigating back to button row
if (hasFocus && lastFocusedButton != null) {
lastFocusedButton?.requestFocus()
lastFocusedButton = null
}
view.post {
// Store last focused button when navigating away from button row
if (binding.fdButtonRow.focusedChild == null) {
lastFocusedButton = view
}
}
}
override fun onChildViewAdded(parent: View?, child: View?) {
child?.onFocusChangeListener = focusChangeListener
}
override fun onChildViewRemoved(parent: View?, child: View?) {
child?.onFocusChangeListener = null
}
}
init {
binding.fdButtonRow.setOnHierarchyChangeListener(buttonsHierarchyChangeListener)
}
}

View File

@@ -6,7 +6,6 @@ import android.view.View
import android.view.animation.AlphaAnimation
import android.widget.FrameLayout
import androidx.annotation.IntRange
import org.jellyfin.androidtv.integration.dream.LibraryDreamService
class FadeViewSwitcherLayout @JvmOverloads constructor(
context: Context,
@@ -50,13 +49,13 @@ class FadeViewSwitcherLayout @JvmOverloads constructor(
private fun View.fadeIn() {
alpha = 1f
startAnimation(AlphaAnimation(0f, 1f).apply {
duration = LibraryDreamService.TRANSITION_DURATION
duration = 1_000
fillAfter = true
})
}
private fun View.fadeOut() = startAnimation(AlphaAnimation(1f, 0f).apply {
duration = LibraryDreamService.TRANSITION_DURATION
duration = 1_000
fillAfter = true
})
}

View File

@@ -43,7 +43,5 @@ public class FriendlyDateButton extends FrameLayout {
} else {
setBackgroundColor(0);
}
//TvApp.getApplication().getLogger().Debug("Focus on "+mProgram.getName()+ " was " +(gainFocus ? "gained" : "lost"));
}
}

View File

@@ -1,359 +0,0 @@
package org.jellyfin.androidtv.ui;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.widget.BaseGridView;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.VerticalGridPresenter;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.data.model.FilterOptions;
import org.jellyfin.androidtv.databinding.HorizontalGridBrowseBinding;
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
import org.jellyfin.androidtv.ui.presentation.HorizontalGridPresenter;
import org.jellyfin.androidtv.util.InfoLayoutHelper;
import org.jellyfin.apiclient.model.entities.SortOrder;
import org.jellyfin.apiclient.model.querying.ItemSortBy;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import timber.log.Timber;
public class GridFragment extends Fragment {
protected TextView mTitleView;
private TextView mStatusText;
private TextView mCounter;
protected ViewGroup mGridDock;
protected LinearLayout mInfoRow;
protected LinearLayout mToolBar;
private ItemRowAdapter mAdapter;
private Presenter mGridPresenter;
private Presenter.ViewHolder mGridViewHolder;
private BaseGridView mGridView;
private OnItemViewSelectedListener mOnItemViewSelectedListener;
private OnItemViewClickedListener mOnItemViewClickedListener;
private int mSelectedPosition = -1;
private int mGridHeight = -1;
private int mGridWidth = -1;
private int mGridItemSpacingHorizontal = 0;
private int mGridItemSpacingVertical = 0;
private int mGridPaddingLeft = 0;
private int mGridPaddingTop = 0;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// init with some working defaults
DisplayMetrics display = requireContext().getResources().getDisplayMetrics();
mGridHeight = display.heightPixels - (int) Math.round(display.density * 130.6); // top + bottom in dp, elements scale with density so adjust accordingly
mGridWidth = display.widthPixels;
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));
}
}
/**
* Sets the grid presenter.
*/
public void setGridPresenter(HorizontalGridPresenter gridPresenter) {
if (gridPresenter == null) {
throw new IllegalArgumentException("Grid presenter may not be null");
}
gridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
if (mOnItemViewClickedListener != null) {
gridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
}
mGridPresenter = gridPresenter;
}
/**
* Sets the grid presenter.
*/
public void setGridPresenter(VerticalGridPresenter gridPresenter) {
if (gridPresenter == null) {
throw new IllegalArgumentException("Grid presenter may not be null");
}
gridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
if (mOnItemViewClickedListener != null) {
gridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
}
mGridPresenter = gridPresenter;
}
/**
* Returns the grid presenter.
*/
public Presenter getGridPresenter() {
return mGridPresenter;
}
/**
* Sets the object adapter for the fragment.
*/
public void setAdapter(ItemRowAdapter adapter) {
mAdapter = adapter;
updateAdapter();
}
/**
* Returns the object adapter.
*/
public ObjectAdapter getAdapter() {
return mAdapter;
}
/**
* @return the GridView for the fragment.
*/
public BaseGridView getGridView() {
return mGridView;
}
public int getGridHeight() {
return mGridHeight;
}
public int getGridWidth() {
return mGridWidth;
}
protected void setGridSize(int height, int width) {
mGridHeight = height;
mGridWidth = width;
}
public void setItem(BaseRowItem item) {
if (item != null) {
mTitleView.setText(item.getFullName(requireContext()));
InfoLayoutHelper.addInfoRow(requireContext(), item, mInfoRow, true, true);
} else {
mTitleView.setText("");
mInfoRow.removeAllViews();
}
}
protected void setGridItemSpacing(int spacingHorizontal, int spacingVertical) {
mGridItemSpacingHorizontal = spacingHorizontal;
mGridItemSpacingVertical = spacingVertical;
}
protected void setGridPadding(int gridPaddingLeft, int gridPaddingTop) {
mGridPaddingLeft = gridPaddingLeft;
mGridPaddingTop = gridPaddingTop;
}
public class SortOption {
public String name;
public String value;
public SortOrder order;
public SortOption(String name, String value, SortOrder order) {
this.name = name;
this.value = value;
this.order = order;
}
}
protected Map<Integer, SortOption> sortOptions;
protected String getSortFriendlyName(String value) {
return getSortOption(value).name;
}
protected SortOption getSortOption(String value) {
for (Integer key : sortOptions.keySet()) {
SortOption option = sortOptions.get(key);
if (Objects.requireNonNull(option).value.equals(value)) return option;
}
return new SortOption("Unknown", "", SortOrder.Ascending);
}
public void setTitle(String text) {
mTitleView.setText(text);
}
public void setStatusText(String folderName) {
String text = getString(R.string.lbl_showing) + " ";
FilterOptions filters = mAdapter.getFilters();
if (filters == null || (!filters.isFavoriteOnly() && !filters.isUnwatchedOnly())) {
text += getString(R.string.lbl_all_items);
} else {
text += (filters.isUnwatchedOnly() ? getString(R.string.lbl_unwatched) : "") + " " +
(filters.isFavoriteOnly() ? getString(R.string.lbl_favorites) : "");
}
if (mAdapter.getStartLetter() != null) {
text += " " + getString(R.string.lbl_starting_with) + " " + mAdapter.getStartLetter();
}
text += " " + getString(R.string.lbl_from) + " '" + folderName + "' " + getString(R.string.lbl_sorted_by) + " " + getSortFriendlyName(mAdapter.getSortBy());
mStatusText.setText(text);
}
public LinearLayout getToolBar() {
return mToolBar;
}
final private OnItemViewSelectedListener mRowSelectedListener =
new OnItemViewSelectedListener() {
@Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
int position = mGridView.getSelectedPosition();
Timber.d("row selected position %s", position);
onRowSelected(position);
if (mOnItemViewSelectedListener != null && position >= 0) {
mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, rowViewHolder, row);
}
}
};
/**
* Sets an item selection listener.
*/
public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
mOnItemViewSelectedListener = listener;
}
private void onRowSelected(int position) {
if (position != mSelectedPosition) {
mSelectedPosition = position;
}
// Update the counter
updateCounter(position + 1);
}
public void updateCounter(int position) {
if (mAdapter != null) {
mCounter.setText(MessageFormat.format("{0} | {1}", position, mAdapter.getTotalItems()));
}
}
/**
* Sets an item clicked listener.
*/
public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
mOnItemViewClickedListener = listener;
if (mGridPresenter != null) {
if (mGridPresenter instanceof HorizontalGridPresenter)
((HorizontalGridPresenter) mGridPresenter).setOnItemViewClickedListener(mOnItemViewClickedListener);
else if (mGridPresenter instanceof VerticalGridPresenter)
((VerticalGridPresenter) mGridPresenter).setOnItemViewClickedListener(mOnItemViewClickedListener);
}
}
/**
* Returns the item clicked listener.
*/
public OnItemViewClickedListener getOnItemViewClickedListener() {
return mOnItemViewClickedListener;
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
HorizontalGridBrowseBinding binding = HorizontalGridBrowseBinding.inflate(inflater, container, false);
mTitleView = binding.title;
mStatusText = binding.statusText;
mInfoRow = binding.infoRow;
mToolBar = binding.toolBar;
mCounter = binding.counter;
mGridDock = binding.rowsFragment;
// Hide the description because we don't have room for it
binding.npBug.showDescription(false);
// NOTE: we only get the 100% correct grid size if we render it once, so hook into it here
mGridDock.post(() -> {
if (mGridDock.getHeight() > 0 && mGridDock.getWidth() > 0) {
onGridSizeMeasurements(mGridDock.getHeight(), mGridDock.getWidth());
}
});
return binding.getRoot();
}
/**
* Callback for measured GridSize.
*/
protected void onGridSizeMeasurements(int gridHeight, int gridWidth) {
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
createGrid();
}
protected void createGrid() {
mGridViewHolder = mGridPresenter.onCreateViewHolder(mGridDock);
if (mGridViewHolder instanceof HorizontalGridPresenter.ViewHolder) {
mGridView = ((HorizontalGridPresenter.ViewHolder) mGridViewHolder).getGridView();
mGridView.setGravity(Gravity.CENTER_VERTICAL);
mGridView.setPadding(mGridPaddingLeft, mGridPaddingTop, mGridPaddingLeft, mGridPaddingTop); // prevent initial card cutoffs
// Don't use fading, breaks initial view and needs special handling, while not providing much of a visual difference!
// ((HorizontalGridView)mGridView).setFadingRightEdge(true);
// ((HorizontalGridView)mGridView).setFadingRightEdgeLength(100);
} else if (mGridViewHolder instanceof VerticalGridPresenter.ViewHolder) {
mGridView = ((VerticalGridPresenter.ViewHolder) mGridViewHolder).getGridView();
mGridView.setGravity(Gravity.CENTER_HORIZONTAL);
mGridView.setPadding(mGridPaddingLeft, mGridPaddingTop, mGridPaddingLeft, mGridPaddingTop); // prevent initial card cutoffs
}
mGridView.setHorizontalSpacing(mGridItemSpacingHorizontal);
mGridView.setVerticalSpacing(mGridItemSpacingVertical);
mGridView.setFocusable(true);
mGridDock.removeAllViews();
mGridDock.addView(mGridViewHolder.view);
updateAdapter();
}
public void focusGrid() {
if (mGridView != null) mGridView.requestFocus();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mGridView = null;
}
private void updateAdapter() {
if (mGridView != null) {
mGridPresenter.onBindViewHolder(mGridViewHolder, mAdapter);
if (mSelectedPosition != -1) {
mGridView.setSelectedPosition(mSelectedPosition);
}
}
}
}

View File

@@ -11,11 +11,9 @@ import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideActivity;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideFragment;
import org.jellyfin.androidtv.util.ImageUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.interaction.ApiClient;
@@ -23,7 +21,7 @@ import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
import org.koin.java.KoinJavaComponent;
public class GuideChannelHeader extends RelativeLayout {
private ImageView mChannelImage;
private AsyncImageView mChannelImage;
private ImageView mFavImage;
private ChannelInfoDto mChannel;
private Context mContext;
@@ -43,7 +41,7 @@ public class GuideChannelHeader extends RelativeLayout {
int headerWidth = Utils.convertDpToPixel(context, 160);
v.setLayoutParams(new AbsListView.LayoutParams(
headerWidth,
Utils.convertDpToPixel(context, LiveTvGuideActivity.GUIDE_ROW_HEIGHT_DP)
Utils.convertDpToPixel(context, LiveTvGuideFragment.GUIDE_ROW_HEIGHT_DP)
));
this.addView(v);
this.setFocusable(true);
@@ -57,14 +55,13 @@ public class GuideChannelHeader extends RelativeLayout {
}
public void loadImage() {
int imageWidth = Utils.convertDpToPixel(mContext, 50);
int imageHeight = Utils.convertDpToPixel(mContext, 30);
Glide.with(mContext)
.load(ImageUtils.getPrimaryImageUrl(mChannel, KoinJavaComponent.<ApiClient>get(ApiClient.class)))
.override(imageWidth, imageHeight)
.centerInside()
.into(mChannelImage);
mChannelImage.load(
ImageUtils.getPrimaryImageUrl(mChannel, KoinJavaComponent.<ApiClient>get(ApiClient.class)),
null,
null,
0.0,
0
);
}
public ChannelInfoDto getChannel() { return mChannel; }

View File

@@ -1,6 +1,5 @@
package org.jellyfin.androidtv.ui;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -10,7 +9,7 @@ import android.widget.RelativeLayout;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.databinding.ProgramGridCellBinding;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideActivity;
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideFragment;
import org.jellyfin.androidtv.util.Utils;
public class GuidePagingButton extends RelativeLayout {
@@ -23,16 +22,16 @@ public class GuidePagingButton extends RelativeLayout {
super(context, attrs);
}
public GuidePagingButton(final Activity activity, final LiveTvGuide guide, int start, String label) {
super(activity);
public GuidePagingButton(Context context, final LiveTvGuide guide, int start, String label) {
super(context);
LayoutInflater inflater = LayoutInflater.from(activity);
LayoutInflater inflater = LayoutInflater.from(context);
ProgramGridCellBinding binding = ProgramGridCellBinding.inflate(inflater, this, true);
binding.programName.setText(label);
setBackgroundColor(Utils.getThemeColor(activity, R.attr.buttonDefaultNormalBackground));
setBackgroundColor(Utils.getThemeColor(context, R.attr.buttonDefaultNormalBackground));
setFocusable(true);
setOnClickListener(v -> guide.displayChannels(start, LiveTvGuideActivity.PAGE_SIZE));
setOnClickListener(v -> guide.displayChannels(start, LiveTvGuideFragment.PAGE_SIZE));
}
@Override

View File

@@ -14,8 +14,10 @@ import android.widget.TextView;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.databinding.ItemRowBinding;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
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.constant.MediaType;
public class ItemRowView extends FrameLayout {
Context mContext;
@@ -100,7 +102,7 @@ public class ItemRowView extends FrameLayout {
}
break;
default:
String series = item.getSeriesName() != null ? BaseItemUtils.getFullName(item, mContext) : null;
String series = item.getSeriesName() != null ? BaseItemExtensionsKt.getFullName(ModelCompat.asSdk(item), mContext) : null;
if (!TextUtils.isEmpty(series)) {
mItemName.setText(series);
mExtraName.setText(item.getName());
@@ -117,7 +119,7 @@ public class ItemRowView extends FrameLayout {
public void updateWatched() {
if (mBaseItem == null) return;
if ("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("");

View File

@@ -2,6 +2,7 @@ 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;
@@ -16,18 +17,19 @@ import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
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.ui.shared.BaseActivity;
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.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
@@ -44,7 +46,8 @@ public class LiveProgramDetailPopup {
private PopupWindow mPopup;
private BaseItemDto mProgram;
private ProgramGridCell mSelectedProgramView;
private BaseActivity mActivity;
private Activity mActivity;
private final Lifecycle lifecycle;
private LiveTvGuide mTvGuide;
private TextView mDTitle;
private TextView mDSummary;
@@ -56,7 +59,7 @@ public class LiveProgramDetailPopup {
private Button mFirstButton;
private Button mSeriesSettingsButton;
private EmptyResponse mTuneAction;
private EmptyLifecycleAwareResponse mTuneAction;
private View mAnchor;
private int mPosLeft;
@@ -64,8 +67,9 @@ public class LiveProgramDetailPopup {
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
public LiveProgramDetailPopup(BaseActivity activity, LiveTvGuide tvGuide, int width, EmptyResponse tuneAction) {
public LiveProgramDetailPopup(Activity activity, Lifecycle lifecycle, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
mActivity = activity;
this.lifecycle = lifecycle;
mTvGuide = tvGuide;
mTuneAction = tuneAction;
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -110,13 +114,12 @@ public class LiveProgramDetailPopup {
} else {
mDSummary.setGravity(Gravity.LEFT);
}
//TvApp.getApplication().getLogger().Debug("Text height: "+mDSummary.getHeight() + " (120 = "+Utils.convertDpToPixel(mActivity, 120)+")");
// build timeline info
TvManager.setTimelineRow(mActivity, mDTimeline, program);
//info row
InfoLayoutHelper.addInfoRow(mActivity, program, mDInfoRow, false, false);
InfoLayoutHelper.addInfoRow(mActivity, ModelCompat.asSdk(program), mDInfoRow, false, false);
//buttons
mFirstButton = null;
@@ -138,9 +141,11 @@ public class LiveProgramDetailPopup {
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
apiClient.getValue().CancelLiveTvTimerAsync(program.getTimerId(), new EmptyResponse() {
apiClient.getValue().CancelLiveTvTimerAsync(program.getTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
selectedGridView.setRecTimer(null);
program.setTimerId(null);
dismiss();
@@ -149,6 +154,8 @@ public class LiveProgramDetailPopup {
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
}
});
@@ -171,9 +178,11 @@ public class LiveProgramDetailPopup {
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
@Override
public void onResponse(SeriesTimerInfoDto response) {
apiClient.getValue().CreateLiveTvTimerAsync(response, new EmptyResponse() {
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
@@ -189,6 +198,8 @@ public class LiveProgramDetailPopup {
@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);
}
@@ -224,9 +235,11 @@ public class LiveProgramDetailPopup {
.setPositiveButton(R.string.lbl_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
apiClient.getValue().CancelLiveTvSeriesTimerAsync(program.getSeriesTimerId(), new EmptyResponse() {
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);
@@ -236,6 +249,8 @@ public class LiveProgramDetailPopup {
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
}
});
@@ -257,9 +272,11 @@ public class LiveProgramDetailPopup {
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
@Override
public void onResponse(SeriesTimerInfoDto response) {
apiClient.getValue().CreateLiveTvSeriesTimerAsync(response, new EmptyResponse() {
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
@@ -276,6 +293,8 @@ public class LiveProgramDetailPopup {
@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);
}
@@ -339,7 +358,7 @@ public class LiveProgramDetailPopup {
tune.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mTuneAction != null) mTuneAction.onResponse();
if (mTuneAction != null && mTuneAction.getActive()) mTuneAction.onResponse();
mPopup.dismiss();
}
});
@@ -405,11 +424,11 @@ public class LiveProgramDetailPopup {
private RecordPopup mRecordPopup;
public void showRecordingOptions(final boolean recordSeries) {
if (mRecordPopup == null) mRecordPopup = new RecordPopup(mActivity, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
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, mProgram, response, mSelectedProgramView, recordSeries);
mRecordPopup.setContent(mActivity, ModelCompat.asSdk(mProgram), response, mSelectedProgramView, recordSeries);
mRecordPopup.show();
}
});

View File

@@ -1,7 +1,6 @@
package org.jellyfin.androidtv.ui
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
@@ -10,14 +9,13 @@ import androidx.core.view.isVisible
import androidx.core.view.setPadding
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.databinding.ViewNowPlayingBinding
import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.playback.AudioEventListener
import org.jellyfin.androidtv.ui.playback.AudioNowPlayingActivity
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.TimeUtils
import org.jellyfin.apiclient.model.dto.BaseItemDto
import org.jellyfin.apiclient.model.entities.ImageType
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -30,14 +28,14 @@ class NowPlayingView @JvmOverloads constructor(
val binding = ViewNowPlayingBinding.inflate(LayoutInflater.from(context), this, true)
private val mediaManager by inject<MediaManager>()
private val navigationRepository by inject<NavigationRepository>()
private var currentDuration: String = ""
init {
setPadding(0)
if (!isInEditMode) setOnClickListener {
val intent = Intent(context, AudioNowPlayingActivity::class.java)
context.startActivity(intent)
navigationRepository.navigate(Destinations.nowPlaying)
}
}
@@ -62,12 +60,12 @@ class NowPlayingView @JvmOverloads constructor(
if (!isInEditMode) mediaManager.removeAudioEventListener(audioEventListener)
}
private fun setInfo(item: BaseItemDto) {
private fun setInfo(item: org.jellyfin.sdk.model.api.BaseItemDto) {
val placeholder = ContextCompat.getDrawable(context, R.drawable.ic_album)
val blurHash = item.imageBlurHashes?.get(ImageType.Primary)?.get(item.imageTags?.get(ImageType.Primary))
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)
currentDuration = TimeUtils.formatMillis(if (item.runTimeTicks != null) item.runTimeTicks / 10_000 else 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
}
@@ -80,7 +78,7 @@ class NowPlayingView @JvmOverloads constructor(
}
private var audioEventListener: AudioEventListener = object : AudioEventListener {
override fun onPlaybackStateChange(newState: PlaybackController.PlaybackState?, currentItem: BaseItemDto?) {
override fun onPlaybackStateChange(newState: PlaybackController.PlaybackState, currentItem: org.jellyfin.sdk.model.api.BaseItemDto?) {
when {
currentItem == null -> Unit
newState == PlaybackController.PlaybackState.PLAYING -> setInfo(currentItem)

View File

@@ -20,7 +20,8 @@ import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
import org.jellyfin.androidtv.util.InfoLayoutHelper;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.BaseItemUtils;
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
import java.util.Date;
@@ -72,7 +73,7 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemUtils.isNew(program) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.getIsPremiere()))) {
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);
}
@@ -139,8 +140,6 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
} else {
setBackgroundColor(mBackgroundColor);
}
// TvApp.getApplication().getLogger().Debug("Focus on " + mProgram.getName() + " was " + (gainFocus ? "gained" : "lost"));
}
public BaseItemDto getProgram() { return mProgram; }

View File

@@ -2,6 +2,7 @@ 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;
@@ -19,18 +20,20 @@ import android.widget.PopupWindow;
import android.widget.Spinner;
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.ui.shared.BaseActivity;
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
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.EmptyResponse;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.BaseItemDto;
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 java.util.ArrayList;
@@ -49,7 +52,8 @@ public class RecordPopup {
int mPosTop;
boolean mRecordSeries;
BaseActivity mActivity;
Activity mActivity;
private final Lifecycle lifecycle;
TextView mDTitle;
LinearLayout mDTimeline;
View mSeriesOptions;
@@ -66,9 +70,11 @@ public class RecordPopup {
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(BaseActivity activity, View anchorView, int left, int top, int width) {
public RecordPopup(Activity activity, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
mActivity = activity;
this.lifecycle = lifecycle;
mAnchorView = anchorView;
mPosLeft = left;
mPosTop = top;
@@ -136,11 +142,11 @@ public class RecordPopup {
mCurrentOptions.setRecordAnyChannel(mAnyChannel.isChecked());
mCurrentOptions.setRecordAnyTime(mAnyTime.isChecked());
apiClient.getValue().UpdateLiveTvSeriesTimerAsync(mCurrentOptions, new EmptyResponse() {
apiClient.getValue().UpdateLiveTvSeriesTimerAsync(mCurrentOptions, new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
mPopup.dismiss();
mActivity.sendMessage(CustomMessage.ActionComplete);
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
Utils.showToast(mActivity, R.string.msg_settings_updated);
}
@@ -159,15 +165,17 @@ public class RecordPopup {
updated.setIsPrePaddingRequired(mCurrentOptions.getIsPrePaddingRequired());
updated.setIsPostPaddingRequired(mCurrentOptions.getIsPostPaddingRequired());
apiClient.getValue().UpdateLiveTvTimerAsync(updated, new EmptyResponse() {
apiClient.getValue().UpdateLiveTvTimerAsync(updated, new EmptyLifecycleAwareResponse(lifecycle) {
@Override
public void onResponse() {
if (!getActive()) return;
mPopup.dismiss();
mActivity.sendMessage(CustomMessage.ActionComplete);
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<BaseItemDto>() {
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(BaseItemDto response) {
public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) {
mSelectedView.setRecTimer(response.getTimerId());
mSelectedView.setRecSeriesTimer(response.getSeriesTimerId());
}
@@ -177,6 +185,8 @@ public class RecordPopup {
@Override
public void onError(Exception ex) {
if (!getActive()) return;
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
}
});
@@ -200,7 +210,7 @@ public class RecordPopup {
}
public void setContent(Context context, BaseItemDto program, SeriesTimerInfoDto current, RecordingIndicatorView selectedView, boolean recordSeries) {
mProgramId = program.getId();
mProgramId = program.getId().toString();
mCurrentOptions = current;
mRecordSeries = recordSeries;
mSelectedView = selectedView;
@@ -252,7 +262,7 @@ public class RecordPopup {
timelineRow.removeAllViews();
if (program.getStartDate() == null) return;
Date local = TimeUtils.convertToLocalDate(program.getStartDate());
Date local = TimeUtils.getDate(program.getStartDate());
TextView on = new TextView(mActivity);
on.setText(mActivity.getString(R.string.lbl_on));
timelineRow.addView(on);

View File

@@ -1,6 +0,0 @@
package org.jellyfin.androidtv.ui;
public interface RecordingIndicatorView {
public void setRecTimer(String id);
public void setRecSeriesTimer(String id);
}

View File

@@ -0,0 +1,6 @@
package org.jellyfin.androidtv.ui
interface RecordingIndicatorView {
fun setRecTimer(id: String?)
fun setRecSeriesTimer(id: String?)
}

View File

@@ -1,5 +0,0 @@
package org.jellyfin.androidtv.ui;
public abstract class ScrollViewListener {
public abstract void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy);
}

View File

@@ -0,0 +1,5 @@
package org.jellyfin.androidtv.ui
fun interface ScrollViewListener {
fun onScrollChanged(scrollView: ObservableScrollView?, x: Int, y: Int, oldx: Int, oldy: Int)
}

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.ui
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
@@ -22,6 +23,7 @@ class TextUnderButton @JvmOverloads constructor(
isFocusable = true
isFocusableInTouchMode = true
descendantFocusability = FOCUS_BLOCK_DESCENDANTS
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) defaultFocusHighlightEnabled = false
}
fun setLabel(text: String?) {

View File

@@ -1,28 +0,0 @@
package org.jellyfin.androidtv.ui.browsing;
import android.content.Intent;
import android.os.Bundle;
import org.jellyfin.sdk.model.api.BaseItemDto;
import kotlinx.serialization.json.Json;
import timber.log.Timber;
public class BrowseFolderFragment extends StdBrowseFragment {
protected String includeType;
protected BaseItemDto mFolder;
@Override
public void onCreate(Bundle savedInstanceState) {
Intent intent = requireActivity().getIntent();
mFolder = Json.Default.decodeFromString(BaseItemDto.Companion.serializer(), intent.getStringExtra(GroupedItemsActivity.EXTRA_FOLDER));
includeType = intent.getStringExtra(GroupedItemsActivity.EXTRA_INCLUDE_TYPE);
MainTitle = mFolder.getName();
ShowBadge = false;
Timber.d("Item type: %s", includeType);
super.onCreate(savedInstanceState);
}
}

View File

@@ -0,0 +1,144 @@
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
import androidx.leanback.widget.OnItemViewClickedListener
import androidx.leanback.widget.OnItemViewSelectedListener
import androidx.leanback.widget.Presenter
import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.jellyfin.androidtv.R
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
abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
protected var folder: BaseItemDto? = null
protected var includeType: String? = null
protected var rows = mutableListOf<BrowseRowDef>()
private val userPreferences by inject<UserPreferences>()
private val backgroundService by inject<BackgroundService>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Parse intent
folder = Json.decodeFromString<BaseItemDto>(arguments?.getString(Extras.Folder)!!)
includeType = arguments?.getString(Extras.IncludeType)
// Set BrowseSupportFragment properties
title = folder?.name
headersState = HEADERS_DISABLED
// Add event listeners
onItemViewClickedListener = OnItemViewClickedListener { _: Presenter.ViewHolder?, item: Any?, _: RowPresenter.ViewHolder?, row: Row ->
if (item is BaseRowItem) {
ItemLauncher.launch(
item,
(row as ListRow).adapter as ItemRowAdapter,
item.index,
activity
)
}
}
onItemViewSelectedListener = OnItemViewSelectedListener { _: Presenter.ViewHolder?, item: Any?, _: RowPresenter.ViewHolder?, row: Row ->
if (item !is BaseRowItem) {
backgroundService.clearBackgrounds()
} else {
val adapter = (row as? ListRow)?.adapter
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(item.index.toLong())
backgroundService.setBackground(item.baseItem)
}
}
// Initialize
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
setupQueries(this@BrowseFolderFragment)
}
}
}
protected abstract suspend fun setupQueries(rowLoader: RowLoader)
override fun loadRows(rows: MutableList<BrowseRowDef>) {
val mutableAdapter = MutableObjectAdapter<Row>(PositionableListRowPresenter()).also {
adapter = it
}
val cardPresenter = CardPresenter()
for (def in rows) {
ItemRowAdapter(
requireContext(),
def.query,
def.chunkSize,
def.preferParentThumb,
def.isStaticHeight,
cardPresenter,
mutableAdapter,
def.queryType
).apply {
val row = ListRow(HeaderItem(def.headerText), this)
setReRetrieveTriggers(def.changeTriggers)
setRow(row)
Retrieve()
mutableAdapter.add(row)
}
}
}
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>(R.id.browse_frame)
// Move the title to the left to make way for the clock
val titleView = root.findViewById<TextView>(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

@@ -1,20 +1,550 @@
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;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.PopupWindow;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.widget.BaseGridView;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.VerticalGridPresenter;
import androidx.lifecycle.Lifecycle;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.auth.repository.UserRepository;
import org.jellyfin.androidtv.constant.ChangeTriggerType;
import org.jellyfin.androidtv.constant.CustomMessage;
import org.jellyfin.androidtv.constant.Extras;
import org.jellyfin.androidtv.constant.GridDirection;
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.StdItemQuery;
import org.jellyfin.androidtv.data.querying.ViewQuery;
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
import org.jellyfin.androidtv.data.repository.UserViewsRepository;
import org.jellyfin.androidtv.data.service.BackgroundService;
import org.jellyfin.androidtv.databinding.HorizontalGridBrowseBinding;
import org.jellyfin.androidtv.databinding.PopupEmptyBinding;
import org.jellyfin.androidtv.preference.LibraryPreferences;
import org.jellyfin.androidtv.preference.PreferencesRepository;
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.navigation.Destinations;
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
import org.jellyfin.androidtv.ui.playback.MediaManager;
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.InfoLayoutHelper;
import org.jellyfin.androidtv.util.KeyProcessor;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemKind;
import org.koin.java.KoinJavaComponent;
import org.jellyfin.sdk.model.api.SortOrder;
import org.jellyfin.sdk.model.constant.CollectionType;
import org.jellyfin.sdk.model.constant.ItemSortBy;
public class BrowseGridFragment extends StdGridFragment {
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import kotlin.Lazy;
import kotlinx.serialization.json.Json;
import timber.log.Timber;
public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private final static int CHUNK_SIZE_MINIMUM = 25;
private String mainTitle;
private Activity mActivity;
private BaseRowItem mCurrentItem;
private CompositeClickedListener mClickedListener = new CompositeClickedListener();
private CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
private final Handler mHandler = new Handler();
private int mCardHeight;
private BrowseRowDef mRowDef;
private CardPresenter mCardPresenter;
private boolean justLoaded = true;
private PosterSize mPosterSizeSetting = PosterSize.MED;
private ImageType mImageType = ImageType.POSTER;
private GridDirection mGridDirection = GridDirection.HORIZONTAL;
private boolean determiningPosterSize = false;
private UUID mParentId;
private BaseItemDto mFolder;
private LibraryPreferences libraryPreferences;
private HorizontalGridBrowseBinding binding;
private ItemRowAdapter mAdapter;
private Presenter mGridPresenter;
private Presenter.ViewHolder mGridViewHolder;
private BaseGridView mGridView;
private int mSelectedPosition = -1;
private int mGridHeight = -1;
private int mGridWidth = -1;
private int mGridItemSpacingHorizontal = 0;
private int mGridItemSpacingVertical = 0;
private int mGridPaddingLeft = 0;
private int mGridPaddingTop = 0;
private final Lazy<BackgroundService> backgroundService = inject(BackgroundService.class);
private final Lazy<MediaManager> mediaManager = inject(MediaManager.class);
private final Lazy<PreferencesRepository> preferencesRepository = inject(PreferencesRepository.class);
private final Lazy<UserViewsRepository> userViewsRepository = inject(UserViewsRepository.class);
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 int mCardsScreenEst = 0;
private int mCardsScreenStride = 0;
private double mCardFocusScale = 1.15; // 115%, just a default we use the resource card_scale_focus otherwise
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
@Override
protected void setupQueries() {
StdItemQuery query = new StdItemQuery(new ItemFields[] {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// init with some working defaults
DisplayMetrics display = requireContext().getResources().getDisplayMetrics();
// top + bottom in dp, elements scale with density so adjust accordingly
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();
mFolder = Json.Default.decodeFromString(BaseItemDto.Companion.serializer(), getArguments().getString(Extras.Folder));
mParentId = mFolder.getId();
mainTitle = mFolder.getName();
libraryPreferences = preferencesRepository.getValue().getLibraryPreferences(Objects.requireNonNull(mFolder.getDisplayPreferencesId()));
mPosterSizeSetting = libraryPreferences.get(LibraryPreferences.Companion.getPosterSize());
mImageType = libraryPreferences.get(LibraryPreferences.Companion.getImageType());
mGridDirection = libraryPreferences.get(LibraryPreferences.Companion.getGridDirection());
mCardFocusScale = getResources().getFraction(R.fraction.card_scale_focus, 1, 1);
if (mGridDirection.equals(GridDirection.VERTICAL))
setGridPresenter(new VerticalGridPresenter());
else
setGridPresenter(new HorizontalGridPresenter());
setDefaultGridRowCols(mPosterSizeSetting, mImageType);
setAutoCardGridValues();
setupQueries();
setupEventListeners();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = HorizontalGridBrowseBinding.inflate(inflater, container, false);
// Hide the description because we don't have room for it
binding.npBug.showDescription(false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
createGrid();
loadGrid();
addTools();
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_UP) return false;
if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
mediaManager.getValue().setCurrentMediaAdapter(mAdapter);
mediaManager.getValue().setCurrentMediaPosition(mCurrentItem.getIndex());
mediaManager.getValue().setCurrentMediaTitle(mFolder.getName());
}
return KeyProcessor.HandleKey(keyCode, mCurrentItem, mActivity);
}
@Override
public void onDestroyView() {
super.onDestroyView();
mGridView = null;
}
private void createGrid() {
mGridViewHolder = mGridPresenter.onCreateViewHolder(binding.rowsFragment);
if (mGridViewHolder instanceof HorizontalGridPresenter.ViewHolder) {
mGridView = ((HorizontalGridPresenter.ViewHolder) mGridViewHolder).getGridView();
mGridView.setGravity(Gravity.CENTER_VERTICAL);
mGridView.setPadding(mGridPaddingLeft, mGridPaddingTop, mGridPaddingLeft, mGridPaddingTop); // prevent initial card cutoffs
} else if (mGridViewHolder instanceof VerticalGridPresenter.ViewHolder) {
mGridView = ((VerticalGridPresenter.ViewHolder) mGridViewHolder).getGridView();
mGridView.setGravity(Gravity.CENTER_HORIZONTAL);
mGridView.setPadding(mGridPaddingLeft, mGridPaddingTop, mGridPaddingLeft, mGridPaddingTop); // prevent initial card cutoffs
}
mGridView.setHorizontalSpacing(mGridItemSpacingHorizontal);
mGridView.setVerticalSpacing(mGridItemSpacingVertical);
mGridView.setFocusable(true);
binding.rowsFragment.removeAllViews();
binding.rowsFragment.addView(mGridViewHolder.view);
updateAdapter();
}
private void updateAdapter() {
if (mGridView != null) {
mGridPresenter.onBindViewHolder(mGridViewHolder, mAdapter);
if (mSelectedPosition != -1) {
mGridView.setSelectedPosition(mSelectedPosition);
}
}
}
/**
* Sets the grid presenter.
*/
public void setGridPresenter(HorizontalGridPresenter gridPresenter) {
if (gridPresenter == null) {
throw new IllegalArgumentException("Grid presenter may not be null");
}
gridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
gridPresenter.setOnItemViewClickedListener(mClickedListener);
mGridPresenter = gridPresenter;
}
/**
* Sets the grid presenter.
*/
public void setGridPresenter(VerticalGridPresenter gridPresenter) {
if (gridPresenter == null) {
throw new IllegalArgumentException("Grid presenter may not be null");
}
gridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
gridPresenter.setOnItemViewClickedListener(mClickedListener);
mGridPresenter = gridPresenter;
}
public void setItem(BaseRowItem item) {
if (item != null) {
binding.title.setText(item.getFullName(requireContext()));
InfoLayoutHelper.addInfoRow(requireContext(), item, binding.infoRow, true, true);
} else {
binding.title.setText("");
binding.infoRow.removeAllViews();
}
}
public class SortOption {
public String name;
public String value;
public SortOrder order;
public SortOption(String name, String value, SortOrder order) {
this.name = name;
this.value = value;
this.order = order;
}
}
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;
}
return new SortOption("Unknown", "", SortOrder.ASCENDING);
}
public void setStatusText(String folderName) {
String text = getString(R.string.lbl_showing) + " ";
FilterOptions filters = mAdapter.getFilters();
if (filters == null || (!filters.isFavoriteOnly() && !filters.isUnwatchedOnly())) {
text += getString(R.string.lbl_all_items);
} else {
text += (filters.isUnwatchedOnly() ? getString(R.string.lbl_unwatched) : "") + " " +
(filters.isFavoriteOnly() ? getString(R.string.lbl_favorites) : "");
}
if (mAdapter.getStartLetter() != null) {
text += " " + getString(R.string.lbl_starting_with) + " " + mAdapter.getStartLetter();
}
text += " " + getString(R.string.lbl_from) + " '" + folderName + "' " + getString(R.string.lbl_sorted_by) + " " + getSortOption(mAdapter.getSortBy()).name;
binding.statusText.setText(text);
}
final private OnItemViewSelectedListener mRowSelectedListener =
new OnItemViewSelectedListener() {
@Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
int position = mGridView.getSelectedPosition();
Timber.d("row selected position %s", position);
if (position != mSelectedPosition) {
mSelectedPosition = position;
}
// Update the counter
updateCounter(position + 1);
if (position >= 0) {
mSelectedListener.onItemSelected(itemViewHolder, item, rowViewHolder, row);
}
}
};
public void updateCounter(int position) {
if (mAdapter != null) {
binding.counter.setText(MessageFormat.format("{0} | {1}", position, mAdapter.getTotalItems()));
}
}
private void setRowDef(final BrowseRowDef rowDef) {
if (mRowDef == null || mRowDef.hashCode() != rowDef.hashCode()) {
mDirty = true;
}
mRowDef = rowDef;
}
private double getCardWidthBy(final double cardHeight, ImageType imageType, BaseItemDto folder) {
switch (imageType) {
case POSTER:
// special handling for square posters
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())) {
return cardHeight;
} else {
return cardHeight * ImageUtils.ASPECT_RATIO_2_3;
}
case THUMB:
return cardHeight * ImageUtils.ASPECT_RATIO_16_9;
case BANNER:
return cardHeight * CardPresenter.ASPECT_RATIO_BANNER;
default:
throw new IllegalStateException("Unexpected value: " + imageType);
}
}
private double getCardHeightBy(final double cardWidth, ImageType imageType, BaseItemDto folder) {
switch (imageType) {
case POSTER:
// special handling for square posters
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())) {
return cardWidth;
} else {
return cardWidth / ImageUtils.ASPECT_RATIO_2_3;
}
case THUMB:
return cardWidth / ImageUtils.ASPECT_RATIO_16_9;
case BANNER:
return cardWidth / CardPresenter.ASPECT_RATIO_BANNER;
default:
throw new IllegalArgumentException("Unexpected value: " + imageType);
}
}
private void setDefaultGridRowCols(PosterSize posterSize, ImageType imageType) {
// HINT: use uneven Rows/Cols if possible, so selected middle lines up with TV middle!
if (mGridPresenter instanceof VerticalGridPresenter) {
int numCols;
switch (posterSize) {
case SMALLEST:
numCols = imageType.equals(ImageType.BANNER) ? 6 : imageType.equals(ImageType.THUMB) ? 11 : 15;
break;
case SMALL:
numCols = imageType.equals(ImageType.BANNER) ? 5 : imageType.equals(ImageType.THUMB) ? 9 : 13;
break;
case MED:
numCols = imageType.equals(ImageType.BANNER) ? 4 : imageType.equals(ImageType.THUMB) ? 7 : 11;
break;
case LARGE:
numCols = imageType.equals(ImageType.BANNER) ? 3 : imageType.equals(ImageType.THUMB) ? 5 : 7;
break;
case X_LARGE:
numCols = imageType.equals(ImageType.BANNER) ? 2 : imageType.equals(ImageType.THUMB) ? 3 : 5;
break;
default:
throw new IllegalStateException("Unexpected value: " + mPosterSizeSetting);
}
((VerticalGridPresenter) mGridPresenter).setNumberOfColumns(numCols);
} else if (mGridPresenter instanceof HorizontalGridPresenter) {
int numRows;
switch (posterSize) {
case SMALLEST:
numRows = imageType.equals(ImageType.BANNER) ? 13 : imageType.equals(ImageType.THUMB) ? 7 : 5;
break;
case SMALL:
numRows = imageType.equals(ImageType.BANNER) ? 11 : imageType.equals(ImageType.THUMB) ? 6 : 4;
break;
case MED:
numRows = imageType.equals(ImageType.BANNER) ? 9 : imageType.equals(ImageType.THUMB) ? 5 : 3;
break;
case LARGE:
numRows = imageType.equals(ImageType.BANNER) ? 7 : imageType.equals(ImageType.THUMB) ? 4 : 2;
break;
case X_LARGE:
numRows = imageType.equals(ImageType.BANNER) ? 5 : imageType.equals(ImageType.THUMB) ? 2 : 1;
break;
default:
throw new IllegalStateException("Unexpected value: " + mPosterSizeSetting);
}
((HorizontalGridPresenter) mGridPresenter).setNumberOfRows(numRows);
}
}
private void setAutoCardGridValues() {
if (mGridPresenter == null) {
Timber.e("Invalid presenter, cant calculate CardGridValues!");
return;
}
double cardScaling = Math.max(mCardFocusScale - 1.0, 0.0);
int cardHeightInt = 100;
int spacingHorizontalInt = 0;
int spacingVerticalInt = 0;
int paddingLeftInt = 0;
int paddingTopInt = 0;
int numRows = 0;
int numCols = 0;
int numCardsScreen = 0; // number of cards visible, including cutoff's
if (mGridPresenter instanceof HorizontalGridPresenter) {
numRows = ((HorizontalGridPresenter) mGridPresenter).getNumberOfRows();
if (numRows == 1) { // reduce size so minimal cards are shown
numRows = 0;
numCols = MIN_NUM_CARDS;
}
} else if (mGridPresenter instanceof VerticalGridPresenter) {
numCols = ((VerticalGridPresenter) mGridPresenter).getNumberOfColumns();
}
if (numRows > 0) {
double paddingPct = cardScaling / numRows;
double spaceingPct = ((paddingPct / 2.0) * CARD_SPACING_PCT) * (numRows - 1);
double wastedSpacePct = paddingPct + spaceingPct;
double useableCardSpace = mGridHeight / (1.0 + wastedSpacePct); // decrease size
double cardHeight = useableCardSpace / numRows;
// fix any rounding errors and make pixel perfect
cardHeightInt = (int) Math.round(cardHeight);
double cardPaddingTopBottomAdj = cardHeightInt * cardScaling;
spacingVerticalInt = Math.max((int) (Math.round((cardPaddingTopBottomAdj / 2.0) * CARD_SPACING_PCT)), 0); // round spacing
int paddingTopBottomInt = mGridHeight - ((cardHeightInt * numRows) + (spacingVerticalInt * (numRows - 1)));
paddingTopInt = Math.max(paddingTopBottomInt / 2, 0);
int sumSize = (cardHeightInt * numRows) + (spacingVerticalInt * (numRows - 1)) + (paddingTopInt * 2);
if (Math.abs(sumSize - mGridHeight) > 2) {
Timber.w("setAutoCardGridValues calculation delta > 2, something is off GridHeight <%s> sumSize <%s>!", mGridHeight, sumSize);
}
int cardWidthInt = (int) getCardWidthBy(cardHeightInt, mImageType, mFolder);
paddingLeftInt = (int) Math.round((cardWidthInt * cardScaling) / 2.0);
spacingHorizontalInt = Math.max((int) (Math.round(paddingLeftInt * CARD_SPACING_PCT)), 0); // round spacing
if (mImageType == ImageType.BANNER) {
spacingHorizontalInt = Math.max((int) (Math.round(paddingLeftInt * CARD_SPACING_HORIZONTAL_BANNER_PCT)), 0); // round spacing
}
int cardsCol = (int) Math.round(((double) mGridWidth / (cardWidthInt + spacingHorizontalInt)) + 0.5);
mCardsScreenEst = numRows * cardsCol;
mCardsScreenStride = numRows;
} else if (numCols > 0) {
double paddingPct = cardScaling / numCols;
double spaceingPct = ((paddingPct / 2.0) * CARD_SPACING_PCT) * (numCols - 1);
if (mImageType == ImageType.BANNER) {
spaceingPct = ((paddingPct / 2.0) * CARD_SPACING_HORIZONTAL_BANNER_PCT) * (numCols - 1);
}
double wastedSpacePct = paddingPct + spaceingPct;
double useableCardSpace = mGridWidth / (1.0 + wastedSpacePct); // decrease size
double cardWidth = useableCardSpace / numCols;
// fix any rounding errors and make pixel perfect
cardHeightInt = (int) Math.round(getCardHeightBy(cardWidth, mImageType, mFolder));
int cardWidthInt = (int) getCardWidthBy(cardHeightInt, mImageType, mFolder);
double cardPaddingLeftRightAdj = cardWidthInt * cardScaling;
spacingHorizontalInt = Math.max((int) (Math.round((cardPaddingLeftRightAdj / 2.0) * CARD_SPACING_PCT)), 0); // round spacing
if (mImageType == ImageType.BANNER) {
spacingHorizontalInt = Math.max((int) (Math.round((cardPaddingLeftRightAdj / 2.0) * CARD_SPACING_HORIZONTAL_BANNER_PCT)), 0); // round spacing
}
int paddingLeftRightInt = mGridWidth - ((cardWidthInt * numCols) + (spacingHorizontalInt * (numCols - 1)));
paddingLeftInt = Math.max(paddingLeftRightInt / 2, 0);
int sumSize = (cardWidthInt * numCols) + (spacingHorizontalInt * (numCols - 1)) + (paddingLeftInt * 2);
if (Math.abs(sumSize - mGridWidth) > 2) {
Timber.w("setAutoCardGridValues calculation delta > 2, something is off GridWidth <%s> sumSize <%s>!", mGridWidth, sumSize);
}
paddingTopInt = (int) Math.round((cardHeightInt * cardScaling) / 2.0);
spacingVerticalInt = Math.max((int) (Math.round(paddingTopInt * CARD_SPACING_PCT)), 0); // round spacing
int cardsRow = (int) Math.round(((double) mGridHeight / (cardHeightInt + spacingVerticalInt)) + 0.5);
mCardsScreenEst = numCols * cardsRow;
mCardsScreenStride = numCols;
}
Timber.d("numCardsScreen <%s>", numCardsScreen);
if (mCardHeight != cardHeightInt) {
mDirty = true;
}
mCardHeight = cardHeightInt;
mGridItemSpacingHorizontal = spacingHorizontalInt;
mGridItemSpacingVertical = spacingVerticalInt;
mGridPaddingLeft = paddingLeftInt;
mGridPaddingTop = paddingTopInt;
}
private void setupQueries() {
StdItemQuery query = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.ChildCount,
ItemFields.MediaSources,
@@ -25,32 +555,32 @@ public class BrowseGridFragment extends StdGridFragment {
if (mFolder.getType() == BaseItemKind.USER_VIEW || mFolder.getType() == BaseItemKind.COLLECTION_FOLDER) {
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
switch (type) {
case "movies":
case CollectionType.Movies:
query.setIncludeItemTypes(new String[]{"Movie"});
query.setRecursive(true);
break;
case "tvshows":
case CollectionType.TvShows:
query.setIncludeItemTypes(new String[]{"Series"});
query.setRecursive(true);
break;
case "boxsets":
case CollectionType.BoxSets:
query.setIncludeItemTypes(new String[]{"BoxSet"});
query.setParentId(null);
query.setRecursive(true);
break;
case "music":
case CollectionType.Music:
//Special queries needed for album artists
String includeType = requireActivity().getIntent().getStringExtra(Extras.IncludeType);
String includeType = getArguments().getString(Extras.IncludeType);
if ("AlbumArtist".equals(includeType)) {
ArtistsQuery albumArtists = new ArtistsQuery();
albumArtists.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
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[] {}));
setRowDef(new BrowseRowDef("", albumArtists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
return;
}
query.setIncludeItemTypes(new String[]{includeType != null ? includeType : "MusicAlbum"});
@@ -61,4 +591,393 @@ public class BrowseGridFragment extends StdGridFragment {
setRowDef(new BrowseRowDef("", query, CHUNK_SIZE_MINIMUM, false, true));
}
@Override
public void onResume() {
super.onResume();
PosterSize posterSizeSetting = libraryPreferences.get(LibraryPreferences.Companion.getPosterSize());
ImageType imageType = libraryPreferences.get(LibraryPreferences.Companion.getImageType());
GridDirection gridDirection = libraryPreferences.get(LibraryPreferences.Companion.getGridDirection());
if (mImageType != imageType || mPosterSizeSetting != posterSizeSetting || mGridDirection != gridDirection || mDirty) {
determiningPosterSize = true;
mImageType = imageType;
mPosterSizeSetting = posterSizeSetting;
mGridDirection = gridDirection;
if (mGridDirection.equals(GridDirection.VERTICAL) && (mGridPresenter == null || !(mGridPresenter instanceof VerticalGridPresenter))) {
setGridPresenter(new VerticalGridPresenter());
} else if (mGridDirection.equals(GridDirection.HORIZONTAL) && (mGridPresenter == null || !(mGridPresenter instanceof HorizontalGridPresenter))) {
setGridPresenter(new HorizontalGridPresenter());
}
setDefaultGridRowCols(mPosterSizeSetting, mImageType);
setAutoCardGridValues();
createGrid();
loadGrid();
determiningPosterSize = false;
}
if (!justLoaded) {
//Re-retrieve anything that needs it but delay slightly so we don't take away gui landing
if (mAdapter != null) {
mHandler.postDelayed(() -> {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
if (mAdapter != null && mAdapter.size() > 0) {
if (!mAdapter.ReRetrieveIfNeeded()) {
refreshCurrentItem();
}
}
}, 500);
}
} else {
justLoaded = false;
}
}
private void buildAdapter() {
mCardPresenter = new CardPresenter(false, mImageType, mCardHeight);
mCardPresenter.setUniformAspect(true); // make grid layouts always uniform
Timber.d("buildAdapter cardHeight <%s> getCardWidthBy <%s> chunks <%s> type <%s>", mCardHeight, (int) getCardWidthBy(mCardHeight, mImageType, mFolder), mRowDef.getChunkSize(), mRowDef.getQueryType().toString());
// adapt chunk size if needed
int chunkSize = mRowDef.getChunkSize();
if (mCardsScreenEst > 0 && mCardsScreenEst >= chunkSize) {
chunkSize = Math.min(mCardsScreenEst + mCardsScreenStride, 150); // cap at 150
Timber.d("buildAdapter adjusting chunkSize to <%s> screenEst <%s>", chunkSize, mCardsScreenEst);
}
switch (mRowDef.getQueryType()) {
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);
break;
case SimilarSeries:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, null);
break;
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;
case LiveTvProgram:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getProgramQuery(), mCardPresenter, null);
break;
case LiveTvRecording:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getRecordingQuery(), chunkSize, mCardPresenter, null);
break;
case AlbumArtists:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getArtistsQuery(), chunkSize, mCardPresenter, null);
break;
default:
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getQuery(), chunkSize, mRowDef.getPreferParentThumb(), mRowDef.isStaticHeight(), mCardPresenter, null);
break;
}
mDirty = false;
FilterOptions filters = new FilterOptions();
filters.setFavoriteOnly(libraryPreferences.get(LibraryPreferences.Companion.getFilterFavoritesOnly()));
filters.setUnwatchedOnly(libraryPreferences.get(LibraryPreferences.Companion.getFilterUnwatchedOnly()));
mAdapter.setRetrieveFinishedListener(new EmptyLifecycleAwareResponse(getLifecycle()) {
@Override
public void onResponse() {
if (!getActive()) return;
setStatusText(mFolder.getName());
if (mCurrentItem == null) { // don't mess-up pos via loadMoreItemsIfNeeded
setItem(null);
updateCounter(mAdapter.getTotalItems() > 0 ? 1 : 0);
}
mLetterButton.setVisibility(ItemSortBy.SortName.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
if (mAdapter.getTotalItems() == 0) {
binding.toolBar.requestFocus();
mHandler.postDelayed(() -> {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
binding.title.setText(mFolder.getName());
}, 500);
} else {
if (mGridView != null) mGridView.requestFocus();
}
}
});
mAdapter.setFilters(filters);
updateAdapter();
}
public void loadGrid() {
if (mCardPresenter == null || mAdapter == null || mDirty) {
buildAdapter();
}
mAdapter.setSortBy(getSortOption(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())));
mAdapter.Retrieve();
}
private ImageButton mSortButton;
private ImageButton mSettingsButton;
private ImageButton mUnwatchedButton;
private ImageButton mFavoriteButton;
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));
}
private void addTools() {
//Add tools
int size = Utils.convertDpToPixel(requireContext(), 26);
mSortButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mSortButton.setImageResource(R.drawable.ic_sort);
mSortButton.setMaxHeight(size);
mSortButton.setAdjustViewBounds(true);
mSortButton.setOnClickListener(new View.OnClickListener() {
@Override
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);
}
sortMenu.getMenu().setGroupCheckable(0, true, true);
sortMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
mAdapter.setSortBy(Objects.requireNonNull(sortOptions.get(item.getItemId())));
mAdapter.Retrieve();
item.setChecked(true);
updateDisplayPrefs();
return true;
}
});
sortMenu.show();
}
});
mSortButton.setContentDescription(getString(R.string.lbl_sort_by));
binding.toolBar.addView(mSortButton);
if (mRowDef.getQueryType() == QueryType.Items) {
mUnwatchedButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mUnwatchedButton.setImageResource(R.drawable.ic_unwatch);
mUnwatchedButton.setActivated(mAdapter.getFilters().isUnwatchedOnly());
mUnwatchedButton.setMaxHeight(size);
mUnwatchedButton.setAdjustViewBounds(true);
mUnwatchedButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FilterOptions filters = mAdapter.getFilters();
if (filters == null) filters = new FilterOptions();
filters.setUnwatchedOnly(!filters.isUnwatchedOnly());
mUnwatchedButton.setActivated(filters.isUnwatchedOnly());
mAdapter.setFilters(filters);
mAdapter.Retrieve();
updateDisplayPrefs();
}
});
mUnwatchedButton.setContentDescription(getString(R.string.lbl_unwatched));
binding.toolBar.addView(mUnwatchedButton);
}
mFavoriteButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mFavoriteButton.setImageResource(R.drawable.ic_heart);
mFavoriteButton.setActivated(mAdapter.getFilters().isFavoriteOnly());
mFavoriteButton.setMaxHeight(size);
mFavoriteButton.setAdjustViewBounds(true);
mFavoriteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FilterOptions filters = mAdapter.getFilters();
if (filters == null) filters = new FilterOptions();
filters.setFavoriteOnly(!filters.isFavoriteOnly());
mFavoriteButton.setActivated(filters.isFavoriteOnly());
mAdapter.setFilters(filters);
mAdapter.Retrieve();
updateDisplayPrefs();
}
});
mFavoriteButton.setContentDescription(getString(R.string.lbl_favorite));
binding.toolBar.addView(mFavoriteButton);
JumplistPopup jumplistPopup = new JumplistPopup();
mLetterButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mLetterButton.setImageResource(R.drawable.ic_jump_letter);
mLetterButton.setMaxHeight(size);
mLetterButton.setAdjustViewBounds(true);
mLetterButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Open letter jump popup
jumplistPopup.show();
}
});
mLetterButton.setContentDescription(getString(R.string.lbl_by_letter));
binding.toolBar.addView(mLetterButton);
mSettingsButton = new ImageButton(requireContext(), null, 0, R.style.Button_Icon);
mSettingsButton.setImageResource(R.drawable.ic_settings);
mSettingsButton.setMaxHeight(size);
mSettingsButton.setAdjustViewBounds(true);
mSettingsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean allowViewSelection = userViewsRepository.getValue().allowViewSelection(mFolder.getCollectionType());
navigationRepository.getValue().navigate(Destinations.INSTANCE.displayPreferences(mFolder.getDisplayPreferencesId(), allowViewSelection));
}
});
mSettingsButton.setContentDescription(getString(R.string.lbl_settings));
binding.toolBar.addView(mSettingsButton);
}
class JumplistPopup {
private final int WIDTH = Utils.convertDpToPixel(requireContext(), 900);
private final int HEIGHT = Utils.convertDpToPixel(requireContext(), 55);
private final PopupWindow popupWindow;
private final AlphaPickerView alphaPicker;
JumplistPopup() {
PopupEmptyBinding layout = PopupEmptyBinding.inflate(getLayoutInflater(), binding.rowsFragment, false);
popupWindow = new PopupWindow(layout.emptyPopup, WIDTH, HEIGHT, true);
popupWindow.setOutsideTouchable(true);
popupWindow.setAnimationStyle(R.style.WindowAnimation_SlideTop);
alphaPicker = new AlphaPickerView(requireContext(), null);
alphaPicker.setOnAlphaSelected(letter -> {
mAdapter.setStartLetter(letter.toString());
loadGrid();
dismiss();
return null;
});
layout.emptyPopup.addView(alphaPicker);
}
public void show() {
popupWindow.showAtLocation(binding.rowsFragment, Gravity.TOP, binding.rowsFragment.getLeft(), binding.rowsFragment.getTop());
if (mAdapter.getStartLetter() != null && !mAdapter.getStartLetter().isEmpty()) {
alphaPicker.focus(mAdapter.getStartLetter().charAt(0));
}
}
public void dismiss() {
if (popupWindow != null && popupWindow.isShowing()) {
popupWindow.dismiss();
}
}
}
private void setupEventListeners() {
if (mGridPresenter != null) {
if (mGridPresenter instanceof HorizontalGridPresenter)
((HorizontalGridPresenter) mGridPresenter).setOnItemViewClickedListener(mClickedListener);
else if (mGridPresenter instanceof VerticalGridPresenter)
((VerticalGridPresenter) mGridPresenter).setOnItemViewClickedListener(mClickedListener);
}
mClickedListener.registerListener(new ItemViewClickedListener());
mSelectedListener.registerListener(new ItemViewSelectedListener());
CoroutineUtils.readCustomMessagesOnLifecycle(getLifecycle(), customMessageRepository.getValue(), message -> {
if (message.equals(CustomMessage.RefreshCurrentItem.INSTANCE)) refreshCurrentItem();
return null;
});
}
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()));
mCurrentItem.refresh(new EmptyLifecycleAwareResponse(getLifecycle()) {
@Override
public void onResponse() {
if (!getActive()) return;
mAdapter.notifyItemRangeChanged(mAdapter.indexOf(mCurrentItem), 1);
//Now - if filtered make sure we still pass
if (mAdapter.getFilters() != 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(mCurrentItem.getIndex());
}
}
}
});
}
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (!(item instanceof BaseRowItem)) return;
ItemLauncher.launch((BaseRowItem) item, mAdapter, ((BaseRowItem) item).getIndex(), getActivity());
}
}
private final Runnable mDelayedSetItem = new Runnable() {
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return;
backgroundService.getValue().setBackground(mCurrentItem.getBaseItem());
setItem(mCurrentItem);
}
};
private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
@Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
mHandler.removeCallbacks(mDelayedSetItem);
if (!(item instanceof BaseRowItem)) {
mCurrentItem = null;
binding.title.setText(mainTitle);
//fill in default background
backgroundService.getValue().clearBackgrounds();
} else {
mCurrentItem = (BaseRowItem) item;
binding.title.setText(mCurrentItem.getName(requireContext()));
binding.infoRow.removeAllViews();
mHandler.postDelayed(mDelayedSetItem, VIEW_SELECT_UPDATE_DELAY);
if (!determiningPosterSize)
mAdapter.loadMoreItemsIfNeeded(mCurrentItem.getIndex());
}
}
}
}

View File

@@ -1,15 +0,0 @@
package org.jellyfin.androidtv.ui.browsing
import android.os.Bundle
import org.jellyfin.androidtv.ui.shared.BaseActivity
class BrowseRecordingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, BrowseRecordingsFragment())
.commit()
}
}

View File

@@ -1,24 +1,30 @@
package org.jellyfin.androidtv.ui.browsing;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ListRow;
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.interaction.Response;
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.RecordingGroupQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
import org.jellyfin.apiclient.model.livetv.TimerQuery;
@@ -37,10 +43,16 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
super.onResume();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mTitle.setText(getString(R.string.lbl_loading_elipses));
}
@Override
protected void setupQueries(final RowLoader rowLoader) {
showViews = true;
mTitle.setText(getString(R.string.lbl_loading_elipses));
//Latest Recordings
RecordingQuery recordings = new RecordingQuery();
recordings.setFields(new ItemFields[]{
@@ -51,7 +63,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
recordings.setEnableImages(true);
recordings.setLimit(40);
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_recent_recordings), recordings, 40));
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 40));
//Movies
RecordingQuery movies = new RecordingQuery();
@@ -63,7 +75,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
movies.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
movies.setEnableImages(true);
movies.setIsMovie(true);
BrowseRowDef moviesDef = new BrowseRowDef(mActivity.getString(R.string.lbl_movies), movies, 60);
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), movies, 60);
//Shows
RecordingQuery shows = new RecordingQuery();
@@ -75,7 +87,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
shows.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
shows.setEnableImages(true);
shows.setIsSeries(true);
BrowseRowDef showsDef = new BrowseRowDef(mActivity.getString(R.string.lbl_tv_series), shows, 60);
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), shows, 60);
mRows.add(showsDef);
mRows.add(moviesDef);
@@ -90,7 +102,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
sports.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
sports.setEnableImages(true);
sports.setIsSports(true);
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_sports), sports, 60));
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), sports, 60));
//Kids
RecordingQuery kids = new RecordingQuery();
@@ -102,12 +114,7 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
kids.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
kids.setEnableImages(true);
kids.setIsKids(true);
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_kids), kids, 60));
//All Recordings by group - will only be there for non-internal TV
RecordingGroupQuery recordingGroups = new RecordingGroupQuery();
recordingGroups.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
mRows.add(new BrowseRowDef(mActivity.getString(R.string.lbl_all_recordings), recordingGroups));
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), kids, 60));
rowLoader.loadRows(mRows);
addNext24Timers();
@@ -116,9 +123,11 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
private void addNext24Timers() {
final TimerQuery scheduled = new TimerQuery();
final long ticks24 = 1000 * 60 * 60 * 24;
KoinJavaComponent.<ApiClient>get(ApiClient.class).GetLiveTvTimersAsync(scheduled, new Response<TimerInfoDtoResult>() {
KoinJavaComponent.<ApiClient>get(ApiClient.class).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
@@ -149,6 +158,8 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return;
mRowsFragment.setSelectedPosition(0, true);
}
}, 500);
@@ -157,20 +168,22 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
@Override
public void onError(Exception exception) {
Utils.showToast(getContext(), exception.getLocalizedMessage());
}
if (!getActive()) return;
Utils.showToast(getContext(), exception.getLocalizedMessage());
}
});
}
@Override
protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) {
protected void addAdditionalRows(MutableObjectAdapter<Row> rowAdapter) {
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), getString(R.string.lbl_views));
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
gridRowAdapter.add(new GridButton(SCHEDULE, getString(R.string.lbl_schedule), R.drawable.tile_port_time));
gridRowAdapter.add(new GridButton(SERIES, mActivity.getString(R.string.lbl_series_recordings), R.drawable.tile_port_series_timer));
gridRowAdapter.add(new GridButton(SERIES, getString(R.string.lbl_series_recordings), R.drawable.tile_port_series_timer));
rowAdapter.add(new ListRow(gridHeader, gridRowAdapter));
}
}

View File

@@ -5,7 +5,6 @@ import org.jellyfin.androidtv.constant.QueryType;
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.RecordingGroupQuery;
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
@@ -16,6 +15,7 @@ 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.sdk.model.api.request.GetResumeItemsRequest;
public class BrowseRowDef {
private String headerText;
@@ -30,11 +30,11 @@ public class BrowseRowDef {
private LiveTvChannelQuery tvChannelQuery;
private RecommendedProgramQuery programQuery;
private RecordingQuery recordingQuery;
private RecordingGroupQuery recordingGroupQuery;
private SeriesTimerQuery seriesTimerQuery;
private ArtistsQuery artistsQuery;
private SeasonQuery seasonQuery;
private GetResumeItemsRequest resumeQuery;
private QueryType queryType;
private int chunkSize = 0;
@@ -145,12 +145,6 @@ public class BrowseRowDef {
this.queryType = QueryType.LiveTvRecording;
}
public BrowseRowDef(String header, RecordingGroupQuery query) {
headerText = header;
this.recordingGroupQuery = query;
this.queryType = QueryType.LiveTvRecordingGroup;
}
public BrowseRowDef(String header, PersonsQuery query, int chunkSize) {
headerText = header;
this.personsQuery = query;
@@ -182,6 +176,16 @@ public class BrowseRowDef {
this.queryType = QueryType.Views;
}
public BrowseRowDef(String header, GetResumeItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
headerText = header;
this.resumeQuery = query;
this.chunkSize = chunkSize;
this.queryType = QueryType.Resume;
this.staticHeight = staticHeight;
this.preferParentThumb = preferParentThumb;
this.changeTriggers = changeTriggers;
}
public int getChunkSize() {
return chunkSize;
}
@@ -238,11 +242,9 @@ public class BrowseRowDef {
public SeriesTimerQuery getSeriesTimerQuery() { return seriesTimerQuery; }
public GetResumeItemsRequest getResumeQuery() { return resumeQuery; }
public ChangeTriggerType[] getChangeTriggers() {
return changeTriggers;
}
public RecordingGroupQuery getRecordingGroupQuery() {
return recordingGroupQuery;
}
}

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