Skip to content

Commit 2ac5c58

Browse files
author
John Corser
committed
Respect server-side user preference for live TV default view
1 parent 2960b96 commit 2ac5c58

File tree

7 files changed

+267
-3
lines changed

7 files changed

+267
-3
lines changed

Diff for: app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt

+3
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,7 @@ val appModule = module {
132132
single<PlaybackHelper> { SdkPlaybackHelper(get(), get(), get(), get(), get(), get(), get()) }
133133

134134
factory { (context: Context) -> SearchFragmentDelegate(context, get(), get()) }
135+
136+
// Add coroutine scope for async operations
137+
single { kotlinx.coroutines.MainScope() }
135138
}

Diff for: app/src/main/java/org/jellyfin/androidtv/di/KoinInitializer.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ class KoinInitializer : Initializer<KoinApplication> {
1818
playbackModule,
1919
preferenceModule,
2020
utilsModule,
21+
liveTvModule,
2122
)
2223
}
2324

2425
override fun dependencies() = listOf(LogInitializer::class.java)
2526
}
26-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.jellyfin.androidtv.di
2+
3+
import org.jellyfin.androidtv.ui.home.HomeFragmentLiveTVRowWithUserPreferenceCheck
4+
import org.koin.dsl.module
5+
6+
val liveTvModule = module {
7+
factory { (activity: android.app.Activity) ->
8+
HomeFragmentLiveTVRowWithUserPreferenceCheck(
9+
activity = activity,
10+
userRepository = get(),
11+
navigationRepository = get(),
12+
api = get(),
13+
coroutineScope = get()
14+
)
15+
}
16+
}

Diff for: app/src/main/java/org/jellyfin/androidtv/ui/home/HomeFragmentHelper.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import org.jellyfin.androidtv.auth.repository.UserRepository
66
import org.jellyfin.androidtv.constant.ChangeTriggerType
77
import org.jellyfin.androidtv.data.repository.ItemRepository
88
import org.jellyfin.androidtv.ui.browsing.BrowseRowDef
9+
import org.koin.core.component.KoinComponent
10+
import org.koin.core.component.get
11+
import org.koin.core.parameter.parametersOf
912
import org.jellyfin.sdk.model.api.BaseItemDto
1013
import org.jellyfin.sdk.model.api.BaseItemKind
1114
import org.jellyfin.sdk.model.api.MediaType
@@ -17,7 +20,7 @@ import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
1720
class HomeFragmentHelper(
1821
private val context: Context,
1922
private val userRepository: UserRepository,
20-
) {
23+
) : KoinComponent {
2124
fun loadRecentlyAdded(userViews: Collection<BaseItemDto>): HomeFragmentRow {
2225
return HomeFragmentLatestRow(userRepository, userViews)
2326
}
@@ -76,6 +79,10 @@ class HomeFragmentHelper(
7679
return HomeFragmentBrowseRowDefRow(BrowseRowDef(context.getString(R.string.lbl_on_now), query))
7780
}
7881

82+
fun loadLiveTv(): HomeFragmentRow {
83+
return get<HomeFragmentLiveTVRowWithUserPreferenceCheck> { parametersOf(context as android.app.Activity) }
84+
}
85+
7986
companion object {
8087
// Maximum amount of items loaded for a row
8188
private const val ITEM_LIMIT_RESUME = 50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.jellyfin.androidtv.ui.home
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
import androidx.leanback.widget.ArrayObjectAdapter
6+
import androidx.leanback.widget.HeaderItem
7+
import androidx.leanback.widget.ListRow
8+
import androidx.leanback.widget.OnItemViewClickedListener
9+
import androidx.leanback.widget.Presenter
10+
import androidx.leanback.widget.Row
11+
import androidx.leanback.widget.RowPresenter
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.launch
15+
import kotlinx.coroutines.withContext
16+
import org.jellyfin.androidtv.R
17+
import org.jellyfin.androidtv.auth.repository.UserRepository
18+
import org.jellyfin.androidtv.constant.LiveTvOption
19+
import org.jellyfin.androidtv.ui.GridButton
20+
import org.jellyfin.androidtv.ui.navigation.Destinations
21+
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
22+
import org.jellyfin.androidtv.ui.presentation.CardPresenter
23+
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter
24+
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter
25+
import org.jellyfin.androidtv.util.LiveTvDefaultViewHelper
26+
import org.jellyfin.androidtv.util.Utils
27+
import org.jellyfin.sdk.api.client.ApiClient
28+
import org.jellyfin.sdk.api.client.extensions.userViewsApi
29+
import org.jellyfin.sdk.model.api.BaseItemDto
30+
import org.jellyfin.sdk.model.api.CollectionType
31+
import timber.log.Timber
32+
33+
/**
34+
* Enhanced version of HomeFragmentLiveTVRow that respects the user's default Live TV view preference
35+
*/
36+
class HomeFragmentLiveTVRowWithUserPreferenceCheck(
37+
private val activity: Activity,
38+
private val userRepository: UserRepository,
39+
private val navigationRepository: NavigationRepository,
40+
private val api: ApiClient,
41+
private val coroutineScope: CoroutineScope
42+
) : HomeFragmentRow, OnItemViewClickedListener {
43+
override fun addToRowsAdapter(context: Context, cardPresenter: CardPresenter, rowsAdapter: MutableObjectAdapter<Row>) {
44+
val header = HeaderItem(rowsAdapter.size().toLong(), activity.getString(R.string.pref_live_tv_cat))
45+
val adapter = ArrayObjectAdapter(GridButtonPresenter())
46+
47+
// Live TV Guide button
48+
adapter.add(GridButton(LiveTvOption.LIVE_TV_GUIDE_OPTION_ID, activity.getString(R.string.lbl_live_tv_guide)))
49+
// Live TV Recordings button
50+
adapter.add(GridButton(LiveTvOption.LIVE_TV_RECORDINGS_OPTION_ID, activity.getString(R.string.lbl_recorded_tv)))
51+
if (Utils.canManageRecordings(userRepository.currentUser.value)) {
52+
// Recording Schedule button
53+
adapter.add(GridButton(LiveTvOption.LIVE_TV_SCHEDULE_OPTION_ID, activity.getString(R.string.lbl_schedule)))
54+
// Recording Series button
55+
adapter.add(GridButton(LiveTvOption.LIVE_TV_SERIES_OPTION_ID, activity.getString(R.string.lbl_series)))
56+
}
57+
58+
rowsAdapter.add(ListRow(header, adapter))
59+
}
60+
61+
override fun onItemClicked(itemViewHolder: Presenter.ViewHolder?, item: Any?, rowViewHolder: RowPresenter.ViewHolder?, row: Row?) {
62+
if (item !is GridButton) return
63+
64+
when (item.id) {
65+
LiveTvOption.LIVE_TV_GUIDE_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvGuide)
66+
LiveTvOption.LIVE_TV_SCHEDULE_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvSchedule)
67+
LiveTvOption.LIVE_TV_RECORDINGS_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvRecordings)
68+
LiveTvOption.LIVE_TV_SERIES_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvSeriesRecordings)
69+
}
70+
}
71+
72+
/**
73+
* Handle click on the Live TV card from the home screen
74+
* Checks user preferences and navigates directly to the preferred view if set
75+
*/
76+
fun onLiveTvCardClicked() {
77+
coroutineScope.launch {
78+
try {
79+
// First, get the Live TV library item
80+
val liveTvItem = getLiveTvLibraryItem()
81+
82+
// Then check for default view preference
83+
val defaultViewId = withContext(Dispatchers.IO) {
84+
LiveTvDefaultViewHelper.getDefaultLiveTvView(api)
85+
}
86+
87+
withContext(Dispatchers.Main) {
88+
if (defaultViewId != null) {
89+
// Navigate directly to the preferred view
90+
when (defaultViewId) {
91+
LiveTvOption.LIVE_TV_GUIDE_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvGuide)
92+
LiveTvOption.LIVE_TV_SCHEDULE_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvSchedule)
93+
LiveTvOption.LIVE_TV_RECORDINGS_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvRecordings)
94+
LiveTvOption.LIVE_TV_SERIES_OPTION_ID -> navigationRepository.navigate(Destinations.liveTvSeriesRecordings)
95+
else -> {
96+
// Fallback to the standard Live TV selection screen
97+
if (liveTvItem != null) {
98+
navigationRepository.navigate(Destinations.librarySmartScreen(liveTvItem))
99+
} else {
100+
// If we couldn't get the Live TV item, navigate to the guide as a fallback
101+
navigationRepository.navigate(Destinations.liveTvGuide)
102+
}
103+
}
104+
}
105+
} else {
106+
// No preference set, show the standard Live TV selection screen
107+
if (liveTvItem != null) {
108+
navigationRepository.navigate(Destinations.librarySmartScreen(liveTvItem))
109+
} else {
110+
// If we couldn't get the Live TV item, navigate to the guide as a fallback
111+
navigationRepository.navigate(Destinations.liveTvGuide)
112+
}
113+
}
114+
}
115+
} catch (e: Exception) {
116+
// Fallback to the Live TV guide on error
117+
navigationRepository.navigate(Destinations.liveTvGuide)
118+
}
119+
}
120+
}
121+
122+
/**
123+
* Gets the Live TV library item from the user's views
124+
*/
125+
private suspend fun getLiveTvLibraryItem(): BaseItemDto? {
126+
return try {
127+
val userViews = api.userViewsApi.getUserViews().content?.items ?: emptyList()
128+
userViews.find { it.collectionType == CollectionType.LIVETV }
129+
} catch (e: Exception) {
130+
null
131+
}
132+
}
133+
}

Diff for: app/src/main/java/org/jellyfin/androidtv/ui/itemhandling/ItemLauncher.java

+41-1
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,51 @@ public class ItemLauncher {
4242
public void launchUserView(@Nullable final BaseItemDto baseItem) {
4343
Timber.d("**** Collection type: %s", baseItem.getCollectionType());
4444

45-
Destination destination = getUserViewDestination(baseItem);
45+
// Special handling for Live TV to check for default view preference
46+
if (baseItem != null && baseItem.getCollectionType() == CollectionType.LIVETV) {
47+
launchLiveTvWithUserPreferenceCheck(baseItem);
48+
return;
49+
}
4650

51+
Destination destination = getUserViewDestination(baseItem);
4752
navigationRepository.getValue().navigate(destination);
4853
}
4954

55+
private void launchLiveTvWithUserPreferenceCheck(@Nullable final BaseItemDto baseItem) {
56+
try {
57+
org.jellyfin.sdk.api.client.ApiClient api = org.koin.java.KoinJavaComponent.get(org.jellyfin.sdk.api.client.ApiClient.class);
58+
Integer defaultViewId = org.jellyfin.androidtv.util.LiveTvDefaultViewHelper.getDefaultLiveTvViewBlocking(api);
59+
60+
if (defaultViewId != null) {
61+
// Navigate directly to the preferred view
62+
switch (defaultViewId) {
63+
case LiveTvOption.LIVE_TV_GUIDE_OPTION_ID:
64+
navigationRepository.getValue().navigate(Destinations.INSTANCE.getLiveTvGuide());
65+
break;
66+
case LiveTvOption.LIVE_TV_SCHEDULE_OPTION_ID:
67+
navigationRepository.getValue().navigate(Destinations.INSTANCE.getLiveTvSchedule());
68+
break;
69+
case LiveTvOption.LIVE_TV_RECORDINGS_OPTION_ID:
70+
navigationRepository.getValue().navigate(Destinations.INSTANCE.getLiveTvRecordings());
71+
break;
72+
case LiveTvOption.LIVE_TV_SERIES_OPTION_ID:
73+
navigationRepository.getValue().navigate(Destinations.INSTANCE.getLiveTvSeriesRecordings());
74+
break;
75+
default:
76+
// Fallback to the standard Live TV selection screen
77+
navigationRepository.getValue().navigate(Destinations.INSTANCE.librarySmartScreen(baseItem));
78+
break;
79+
}
80+
} else {
81+
// No preference set, show the standard Live TV selection screen
82+
navigationRepository.getValue().navigate(Destinations.INSTANCE.librarySmartScreen(baseItem));
83+
}
84+
} catch (Exception e) {
85+
// Fallback to the standard Live TV selection screen on error
86+
navigationRepository.getValue().navigate(Destinations.INSTANCE.librarySmartScreen(baseItem));
87+
}
88+
}
89+
5090
public Destination.Fragment getUserViewDestination(@Nullable final BaseItemDto baseItem) {
5191
CollectionType collectionType = baseItem == null ? CollectionType.UNKNOWN : baseItem.getCollectionType();
5292
if (collectionType == null) collectionType = CollectionType.UNKNOWN;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.jellyfin.androidtv.util
2+
3+
import org.jellyfin.androidtv.auth.repository.UserRepository
4+
import org.jellyfin.androidtv.constant.LiveTvOption
5+
import org.jellyfin.sdk.api.client.ApiClient
6+
import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi
7+
import org.koin.java.KoinJavaComponent
8+
import timber.log.Timber
9+
import java.util.UUID
10+
11+
/**
12+
* Helper class to handle Live TV default view preferences
13+
*/
14+
object LiveTvDefaultViewHelper {
15+
/**
16+
* Gets the user's preferred default Live TV screen from server settings
17+
*
18+
* @param api The API client to use for the request
19+
* @return The LiveTvOption ID to navigate to, or null if we should show the default selection screen
20+
*/
21+
suspend fun getDefaultLiveTvView(api: ApiClient): Int? {
22+
try {
23+
// Get the current user ID from the UserRepository
24+
val userRepository = KoinJavaComponent.get<UserRepository>(UserRepository::class.java)
25+
val userId = userRepository.currentUser.value?.id
26+
27+
if (userId == null) {
28+
return null
29+
}
30+
31+
var displayPreferences = api.displayPreferencesApi.getDisplayPreferences(
32+
displayPreferencesId = "usersettings",
33+
userId = userId,
34+
client = "emby"
35+
).content
36+
37+
// Check for the "landing-livetv" preference
38+
var defaultView = displayPreferences?.customPrefs?.get("landing-livetv")
39+
40+
// Map the server preference to our LiveTvOption constants
41+
val result = when (defaultView?.lowercase()) {
42+
"guide" -> LiveTvOption.LIVE_TV_GUIDE_OPTION_ID
43+
"recordings" -> LiveTvOption.LIVE_TV_RECORDINGS_OPTION_ID
44+
"schedule" -> LiveTvOption.LIVE_TV_SCHEDULE_OPTION_ID
45+
"seriestimers" -> LiveTvOption.LIVE_TV_SERIES_OPTION_ID
46+
else -> null
47+
}
48+
49+
return result
50+
} catch (e: Exception) {
51+
return null
52+
}
53+
}
54+
55+
/**
56+
* Java-friendly wrapper for getDefaultLiveTvView
57+
* This method blocks the current thread until the result is available
58+
*/
59+
@JvmStatic
60+
fun getDefaultLiveTvViewBlocking(api: ApiClient): Int? {
61+
return kotlinx.coroutines.runBlocking {
62+
getDefaultLiveTvView(api)
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)