mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Improved home page support for Live TV & Recordings (#890)
## Description Improves home page customization for Live TV & Recordings - Add row for live tv channels - Add row for "on now" programs - Clicking on a TV channel or TV program starts playback - Better name for "Recently recorded" instead of "Recently added in Recordings" Also fixes a back stack issue after adding rows ### Related issues Follow up from #803 ### Testing Emulator ## Screenshots <img width="663" height="190" alt="image" src="https://github.com/user-attachments/assets/49d41953-42ab-4621-92f1-b7936aab7e97" /> ## AI or LLM usage None
This commit is contained in:
parent
7fcd66f407
commit
91900af7a9
13 changed files with 229 additions and 64 deletions
|
|
@ -187,6 +187,25 @@ data class BaseItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BaseItemKind.TV_CHANNEL -> {
|
||||||
|
Destination.Playback(
|
||||||
|
itemId = id,
|
||||||
|
positionMs = 0L,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseItemKind.PROGRAM -> {
|
||||||
|
val channelId = data.channelId
|
||||||
|
if (channelId != null) {
|
||||||
|
Destination.Playback(
|
||||||
|
itemId = channelId,
|
||||||
|
positionMs = 0L,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Destination.MediaItem(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Destination.MediaItem(this)
|
Destination.MediaItem(this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ sealed interface HomeRowConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Currently recording
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("Recordings")
|
@SerialName("Recordings")
|
||||||
|
|
@ -122,7 +122,7 @@ sealed interface HomeRowConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Programs on now/recommended
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("TvPrograms")
|
@SerialName("TvPrograms")
|
||||||
|
|
@ -132,6 +132,17 @@ sealed interface HomeRowConfig {
|
||||||
override fun updateViewOptions(viewOptions: HomeRowViewOptions): TvPrograms = this.copy(viewOptions = viewOptions)
|
override fun updateViewOptions(viewOptions: HomeRowViewOptions): TvPrograms = this.copy(viewOptions = viewOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live TV channels
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("TvChannels")
|
||||||
|
data class TvChannels(
|
||||||
|
override val viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
|
||||||
|
) : HomeRowConfig {
|
||||||
|
override fun updateViewOptions(viewOptions: HomeRowViewOptions): TvChannels = this.copy(viewOptions = viewOptions)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch suggestions from [com.github.damontecres.wholphin.services.SuggestionService] for the given parent ID
|
* Fetch suggestions from [com.github.damontecres.wholphin.services.SuggestionService] for the given parent ID
|
||||||
*/
|
*/
|
||||||
|
|
@ -217,5 +228,11 @@ data class HomeRowViewOptions(
|
||||||
heightDp = Cards.HEIGHT_EPISODE,
|
heightDp = Cards.HEIGHT_EPISODE,
|
||||||
aspectRatio = AspectRatio.WIDE,
|
aspectRatio = AspectRatio.WIDE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val channelsDefault =
|
||||||
|
HomeRowViewOptions(
|
||||||
|
heightDp = 96,
|
||||||
|
aspectRatio = AspectRatio.WIDE,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -660,6 +660,7 @@ sealed interface AppPreference<Pref, T> {
|
||||||
AppDestinationPreference<AppPreferences>(
|
AppDestinationPreference<AppPreferences>(
|
||||||
title = R.string.customize_home,
|
title = R.string.customize_home,
|
||||||
destination = Destination.HomeSettings,
|
destination = Destination.HomeSettings,
|
||||||
|
summary = R.string.customize_home_summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
val SendCrashReports =
|
val SendCrashReports =
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,14 @@ import com.github.damontecres.wholphin.preferences.HomePagePreferences
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
|
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
import com.github.damontecres.wholphin.ui.main.settings.Library
|
import com.github.damontecres.wholphin.ui.main.settings.Library
|
||||||
|
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
|
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetPersonsHandler
|
import com.github.damontecres.wholphin.util.GetPersonsHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Error
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success
|
||||||
import com.github.damontecres.wholphin.util.supportedHomeCollectionTypes
|
import com.github.damontecres.wholphin.util.supportedHomeCollectionTypes
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
@ -43,6 +44,7 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
|
@ -255,11 +257,12 @@ class HomeSettingsService
|
||||||
suspend fun createDefault(userId: UUID): HomePageResolvedSettings {
|
suspend fun createDefault(userId: UUID): HomePageResolvedSettings {
|
||||||
Timber.v("Creating default settings")
|
Timber.v("Creating default settings")
|
||||||
val user = serverRepository.currentUser.value?.takeIf { it.id == userId }
|
val user = serverRepository.currentUser.value?.takeIf { it.id == userId }
|
||||||
|
val userDto = serverRepository.currentUserDto.value?.takeIf { it.id == userId }
|
||||||
val libraries =
|
val libraries =
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
navDrawerService.getFilteredUserLibraries(user)
|
navDrawerService.getFilteredUserLibraries(user, userDto?.tvAccess ?: false)
|
||||||
} else {
|
} else {
|
||||||
navDrawerService.getAllUserLibraries(userId)
|
navDrawerService.getAllUserLibraries(userId, userDto?.tvAccess ?: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val prefs =
|
val prefs =
|
||||||
|
|
@ -269,18 +272,23 @@ class HomeSettingsService
|
||||||
libraries
|
libraries
|
||||||
.mapIndexed { index, it ->
|
.mapIndexed { index, it ->
|
||||||
val parentId = it.itemId
|
val parentId = it.itemId
|
||||||
val name = it.name.takeIf { it.isNotNullOrBlank() }
|
val title = getRecentlyAddedTitle(context, it)
|
||||||
val title =
|
if (it.collectionType == CollectionType.LIVETV) {
|
||||||
name?.let { context.getString(R.string.recently_added_in, it) }
|
HomeRowConfigDisplay(
|
||||||
?: context.getString(R.string.recently_added)
|
id = index,
|
||||||
HomeRowConfigDisplay(
|
title = context.getString(R.string.live_tv),
|
||||||
id = index,
|
config = HomeRowConfig.TvPrograms(),
|
||||||
title = title,
|
)
|
||||||
config = HomeRowConfig.RecentlyAdded(parentId),
|
} else {
|
||||||
)
|
HomeRowConfigDisplay(
|
||||||
|
id = index,
|
||||||
|
title = title,
|
||||||
|
config = HomeRowConfig.RecentlyAdded(parentId),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val continueWatchingRows =
|
val continueWatchingRows =
|
||||||
if (prefs.combineContinueNext) { // TODO
|
if (prefs.combineContinueNext) {
|
||||||
listOf(
|
listOf(
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = includedIds.size + 1,
|
id = includedIds.size + 1,
|
||||||
|
|
@ -363,7 +371,7 @@ class HomeSettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeSectionType.LIVE_TV -> {
|
HomeSectionType.LIVE_TV -> {
|
||||||
if (userDto.policy?.enableLiveTvAccess == true) {
|
if (userDto.tvAccess) {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title = context.getString(R.string.live_tv),
|
title = context.getString(R.string.live_tv),
|
||||||
|
|
@ -523,6 +531,14 @@ class HomeSettingsService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is HomeRowConfig.TvChannels -> {
|
||||||
|
HomeRowConfigDisplay(
|
||||||
|
id = id,
|
||||||
|
title = context.getString(R.string.channels),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is HomeRowConfig.Suggestions -> {
|
is HomeRowConfig.Suggestions -> {
|
||||||
val name =
|
val name =
|
||||||
api.userLibraryApi
|
api.userLibraryApi
|
||||||
|
|
@ -654,13 +670,10 @@ class HomeSettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowConfig.RecentlyAdded -> {
|
is HomeRowConfig.RecentlyAdded -> {
|
||||||
val name =
|
val library =
|
||||||
libraries
|
libraries
|
||||||
.firstOrNull { it.itemId == row.parentId }
|
.firstOrNull { it.itemId == row.parentId }
|
||||||
?.name
|
val title = getRecentlyAddedTitle(context, library)
|
||||||
val title =
|
|
||||||
name?.let { context.getString(R.string.recently_added_in, it) }
|
|
||||||
?: context.getString(R.string.recently_added)
|
|
||||||
val request =
|
val request =
|
||||||
GetLatestMediaRequest(
|
GetLatestMediaRequest(
|
||||||
fields = SlimItemFields,
|
fields = SlimItemFields,
|
||||||
|
|
@ -862,6 +875,23 @@ class HomeSettingsService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is HomeRowConfig.TvChannels -> {
|
||||||
|
api.liveTvApi
|
||||||
|
.getLiveTvChannels(
|
||||||
|
userId = userDto.id,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
limit = limit,
|
||||||
|
enableImages = true,
|
||||||
|
).toBaseItems(api, row.viewOptions.useSeries)
|
||||||
|
.let {
|
||||||
|
Success(
|
||||||
|
context.getString(R.string.channels),
|
||||||
|
it,
|
||||||
|
row.viewOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is HomeRowConfig.Suggestions -> {
|
is HomeRowConfig.Suggestions -> {
|
||||||
val library =
|
val library =
|
||||||
api.userLibraryApi
|
api.userLibraryApi
|
||||||
|
|
@ -888,7 +918,7 @@ class HomeSettingsService
|
||||||
row.viewOptions,
|
row.viewOptions,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
HomeRowLoadingState.Error(
|
Error(
|
||||||
title,
|
title,
|
||||||
message = "Unsupported type ${library.collectionType}",
|
message = "Unsupported type ${library.collectionType}",
|
||||||
)
|
)
|
||||||
|
|
@ -949,3 +979,14 @@ class UnsupportedHomeSettingsVersionException(
|
||||||
val unsupportedVersion: Int?,
|
val unsupportedVersion: Int?,
|
||||||
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
|
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
|
||||||
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion")
|
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion")
|
||||||
|
|
||||||
|
fun getRecentlyAddedTitle(
|
||||||
|
context: Context,
|
||||||
|
library: Library?,
|
||||||
|
): String =
|
||||||
|
if (library?.isRecordingFolder == true) {
|
||||||
|
context.getString(R.string.recently_recorded)
|
||||||
|
} else {
|
||||||
|
library?.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||||
|
?: context.getString(R.string.recently_added)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||||
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
||||||
|
|
@ -68,25 +67,49 @@ class NavDrawerService
|
||||||
}.launchIn(coroutineScope)
|
}.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAllUserLibraries(userId: UUID): List<Library> {
|
suspend fun getAllUserLibraries(
|
||||||
|
userId: UUID,
|
||||||
|
tvAccess: Boolean,
|
||||||
|
): List<Library> {
|
||||||
val userViews =
|
val userViews =
|
||||||
api.userViewsApi
|
api.userViewsApi
|
||||||
.getUserViews(userId = userId)
|
.getUserViews(userId = userId)
|
||||||
.content.items
|
.content.items
|
||||||
|
val recordingFolders =
|
||||||
|
if (tvAccess) {
|
||||||
|
api.liveTvApi
|
||||||
|
.getRecordingFolders(userId = userId)
|
||||||
|
.content.items
|
||||||
|
.map { it.id }
|
||||||
|
.toSet()
|
||||||
|
} else {
|
||||||
|
setOf()
|
||||||
|
}
|
||||||
val libraries =
|
val libraries =
|
||||||
userViews
|
userViews
|
||||||
.filter { it.collectionType in supportedCollectionTypes }
|
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
|
||||||
.map { Library(it.id, it.name ?: "", it.collectionType ?: CollectionType.UNKNOWN) }
|
.map {
|
||||||
|
Library(
|
||||||
|
itemId = it.id,
|
||||||
|
name = it.name ?: "",
|
||||||
|
type = it.type,
|
||||||
|
collectionType = it.collectionType ?: CollectionType.UNKNOWN,
|
||||||
|
isRecordingFolder = it.id in recordingFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
return libraries
|
return libraries
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getFilteredUserLibraries(user: JellyfinUser): List<Library> {
|
suspend fun getFilteredUserLibraries(
|
||||||
|
user: JellyfinUser,
|
||||||
|
tvAccess: Boolean,
|
||||||
|
): List<Library> {
|
||||||
val pins =
|
val pins =
|
||||||
serverPreferencesDao
|
serverPreferencesDao
|
||||||
.getNavDrawerPinnedItems(user)
|
.getNavDrawerPinnedItems(user)
|
||||||
.associateBy { it.itemId }
|
.associateBy { it.itemId }
|
||||||
val libraries =
|
val libraries =
|
||||||
getAllUserLibraries(user.id)
|
getAllUserLibraries(user.id, tvAccess)
|
||||||
.filterNot { pins[ServerNavDrawerItem.getId(it.itemId)]?.type == NavPinType.UNPINNED }
|
.filterNot { pins[ServerNavDrawerItem.getId(it.itemId)]?.type == NavPinType.UNPINNED }
|
||||||
return libraries
|
return libraries
|
||||||
}
|
}
|
||||||
|
|
@ -95,39 +118,26 @@ class NavDrawerService
|
||||||
user: JellyfinUser,
|
user: JellyfinUser,
|
||||||
userDto: UserDto,
|
userDto: UserDto,
|
||||||
) {
|
) {
|
||||||
val tvAccess = userDto.policy?.enableLiveTvAccess ?: false
|
|
||||||
val userViews =
|
|
||||||
api.userViewsApi
|
|
||||||
.getUserViews(userId = user.id)
|
|
||||||
.content.items
|
|
||||||
val recordingFolders =
|
|
||||||
if (tvAccess) {
|
|
||||||
api.liveTvApi
|
|
||||||
.getRecordingFolders(userId = user.id)
|
|
||||||
.content.items
|
|
||||||
.map { it.id }
|
|
||||||
.toSet()
|
|
||||||
} else {
|
|
||||||
setOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
val builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
|
val builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
|
||||||
|
val allLibraries = getAllUserLibraries(user.id, userDto.tvAccess)
|
||||||
val libraries =
|
val libraries =
|
||||||
userViews
|
allLibraries
|
||||||
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
|
|
||||||
.map {
|
.map {
|
||||||
val destination =
|
val destination =
|
||||||
if (it.id in recordingFolders) {
|
if (it.isRecordingFolder) {
|
||||||
Destination.Recordings(it.id)
|
Destination.Recordings(it.itemId)
|
||||||
} else {
|
} else {
|
||||||
BaseItem.from(it, api).destination()
|
Destination.MediaItem(
|
||||||
|
it.itemId,
|
||||||
|
it.type,
|
||||||
|
it.collectionType,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ServerNavDrawerItem(
|
ServerNavDrawerItem(
|
||||||
itemId = it.id,
|
itemId = it.itemId,
|
||||||
name = it.name ?: it.id.toString(),
|
name = it.name,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
type = it.collectionType ?: CollectionType.UNKNOWN,
|
type = it.collectionType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val allItems = builtins + libraries
|
val allItems = builtins + libraries
|
||||||
|
|
@ -170,3 +180,5 @@ data class NavDrawerItemState(
|
||||||
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
|
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val UserDto.tvAccess: Boolean get() = policy?.enableLiveTvAccess == true
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ val PhotoItemFields =
|
||||||
object Cards {
|
object Cards {
|
||||||
const val HEIGHT_2X3_DP = 172
|
const val HEIGHT_2X3_DP = 172
|
||||||
val height2x3 = HEIGHT_2X3_DP.dp
|
val height2x3 = HEIGHT_2X3_DP.dp
|
||||||
val HEIGHT_EPISODE = (HEIGHT_2X3_DP * .75f).toInt().let { it - it % 4 }
|
const val HEIGHT_EPISODE = 128
|
||||||
val heightEpisode = HEIGHT_EPISODE.dp
|
val heightEpisode = HEIGHT_EPISODE.dp
|
||||||
val playedPercentHeight = 6.dp
|
val playedPercentHeight = 6.dp
|
||||||
val serverUserCircle = height2x3 * .75f
|
val serverUserCircle = height2x3 * .75f
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.services.MediaReportService
|
||||||
import com.github.damontecres.wholphin.services.NavDrawerService
|
import com.github.damontecres.wholphin.services.NavDrawerService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
|
import com.github.damontecres.wholphin.services.tvAccess
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -68,7 +69,8 @@ class HomeViewModel
|
||||||
val prefs = preferences.appPreferences.homePagePreferences
|
val prefs = preferences.appPreferences.homePagePreferences
|
||||||
|
|
||||||
serverRepository.currentUserDto.value?.let { userDto ->
|
serverRepository.currentUserDto.value?.let { userDto ->
|
||||||
val libraries = navDrawerService.getAllUserLibraries(userDto.id)
|
val libraries =
|
||||||
|
navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess)
|
||||||
val settings =
|
val settings =
|
||||||
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
|
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
|
||||||
val state = state.value
|
val state = state.value
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.services.SuggestionsWorker
|
import com.github.damontecres.wholphin.services.SuggestionsWorker
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeLibraryRowTypeList(
|
fun HomeLibraryRowTypeList(
|
||||||
|
|
@ -63,11 +64,38 @@ fun HomeLibraryRowTypeList(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSupportedRowTypes(library: Library): List<LibraryRowType> {
|
fun getSupportedRowTypes(library: Library): List<LibraryRowType> {
|
||||||
val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType)
|
val supportsSuggestions = SuggestionsWorker.getTypeForCollection(library.collectionType) != null
|
||||||
return if (itemKind != null) {
|
return when {
|
||||||
LibraryRowType.entries
|
library.isRecordingFolder -> {
|
||||||
} else {
|
listOf(
|
||||||
LibraryRowType.entries.toMutableList().apply { remove(LibraryRowType.SUGGESTIONS) }
|
LibraryRowType.RECENTLY_RECORDED,
|
||||||
|
LibraryRowType.GENRES,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
library.collectionType == CollectionType.LIVETV -> {
|
||||||
|
listOf(
|
||||||
|
LibraryRowType.TV_CHANNELS,
|
||||||
|
LibraryRowType.TV_PROGRAMS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsSuggestions -> {
|
||||||
|
listOf(
|
||||||
|
LibraryRowType.RECENTLY_ADDED,
|
||||||
|
LibraryRowType.RECENTLY_RELEASED,
|
||||||
|
LibraryRowType.SUGGESTIONS,
|
||||||
|
LibraryRowType.GENRES,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
listOf(
|
||||||
|
LibraryRowType.RECENTLY_ADDED,
|
||||||
|
LibraryRowType.RECENTLY_RELEASED,
|
||||||
|
LibraryRowType.GENRES,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,4 +106,7 @@ enum class LibraryRowType(
|
||||||
RECENTLY_RELEASED(R.string.recently_released),
|
RECENTLY_RELEASED(R.string.recently_released),
|
||||||
SUGGESTIONS(R.string.suggestions),
|
SUGGESTIONS(R.string.suggestions),
|
||||||
GENRES(R.string.genres),
|
GENRES(R.string.genres),
|
||||||
|
TV_CHANNELS(R.string.channels),
|
||||||
|
TV_PROGRAMS(R.string.live_tv),
|
||||||
|
RECENTLY_RECORDED(R.string.recently_recorded),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ fun HomeSettingsPage(
|
||||||
// Adds a row, waits until its done loading, then scrolls to the new row
|
// Adds a row, waits until its done loading, then scrolls to the new row
|
||||||
fun addRow(func: () -> Job) {
|
fun addRow(func: () -> Job) {
|
||||||
scope.launch(ExceptionHandler(autoToast = true)) {
|
scope.launch(ExceptionHandler(autoToast = true)) {
|
||||||
backStack.add(HomeSettingsDestination.RowList)
|
while (backStack.size > 1) {
|
||||||
|
backStack.removeAt(backStack.lastIndex)
|
||||||
|
}
|
||||||
func.invoke().join()
|
func.invoke().join()
|
||||||
listState.animateScrollToItem(state.rows.lastIndex)
|
listState.animateScrollToItem(state.rows.lastIndex)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException
|
import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
|
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
|
||||||
|
import com.github.damontecres.wholphin.services.tvAccess
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.showToast
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
|
|
@ -83,8 +84,8 @@ class HomeSettingsViewModel
|
||||||
init {
|
init {
|
||||||
addCloseable { saveToLocal() }
|
addCloseable { saveToLocal() }
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val userId = serverRepository.currentUser.value?.id ?: return@launchIO
|
val userDto = serverRepository.currentUserDto.value ?: return@launchIO
|
||||||
val libraries = navDrawerService.getAllUserLibraries(userId)
|
val libraries = navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess)
|
||||||
val currentSettings =
|
val currentSettings =
|
||||||
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
|
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
|
||||||
Timber.v("currentSettings=%s", currentSettings)
|
Timber.v("currentSettings=%s", currentSettings)
|
||||||
|
|
@ -292,6 +293,36 @@ class HomeSettingsViewModel
|
||||||
config = Suggestions(library.itemId),
|
config = Suggestions(library.itemId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LibraryRowType.TV_CHANNELS -> {
|
||||||
|
val title = context.getString(R.string.channels)
|
||||||
|
HomeRowConfigDisplay(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
config =
|
||||||
|
HomeRowConfig.TvChannels(
|
||||||
|
viewOptions = HomeRowViewOptions.channelsDefault,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryRowType.TV_PROGRAMS -> {
|
||||||
|
val title = context.getString(R.string.watch_live)
|
||||||
|
HomeRowConfigDisplay(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
config = HomeRowConfig.TvPrograms(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryRowType.RECENTLY_RECORDED -> {
|
||||||
|
val title = context.getString(R.string.recently_recorded)
|
||||||
|
HomeRowConfigDisplay(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
config = RecentlyAdded(library.itemId),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateState {
|
updateState {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
@ -614,6 +645,10 @@ class HomeSettingsViewModel
|
||||||
is HomeRowConfig.TvPrograms -> {
|
is HomeRowConfig.TvPrograms -> {
|
||||||
it.config.updateViewOptions(preset.tvLibrary)
|
it.config.updateViewOptions(preset.tvLibrary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is HomeRowConfig.TvChannels -> {
|
||||||
|
it.config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
it.copy(config = newConfig)
|
it.copy(config = newConfig)
|
||||||
}
|
}
|
||||||
|
|
@ -658,5 +693,7 @@ data class HomePageSettingsState(
|
||||||
data class Library(
|
data class Library(
|
||||||
@Serializable(UUIDSerializer::class) val itemId: UUID,
|
@Serializable(UUIDSerializer::class) val itemId: UUID,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val type: BaseItemKind,
|
||||||
val collectionType: CollectionType,
|
val collectionType: CollectionType,
|
||||||
|
val isRecordingFolder: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ fun DestinationContent(
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Timber.w("Unsupported item type: ${destination.type}")
|
Timber.w("Unsupported item type: ${destination.type}")
|
||||||
Text("Unsupported item type: ${destination.type}")
|
Text("Unsupported item type: ${destination.type}", modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,7 @@
|
||||||
<string name="settings_saved">Settings saved</string>
|
<string name="settings_saved">Settings saved</string>
|
||||||
<string name="display_presets">Display presets</string>
|
<string name="display_presets">Display presets</string>
|
||||||
<string name="display_presets_description">Built-in presets to quickly style all rows</string>
|
<string name="display_presets_description">Built-in presets to quickly style all rows</string>
|
||||||
|
<string name="customize_home_summary">Choose rows and images on the home page</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ class TestHomeRowSamples {
|
||||||
HomeRowConfig.Favorite(kind = BaseItemKind.SERIES),
|
HomeRowConfig.Favorite(kind = BaseItemKind.SERIES),
|
||||||
HomeRowConfig.Recordings(),
|
HomeRowConfig.Recordings(),
|
||||||
HomeRowConfig.TvPrograms(),
|
HomeRowConfig.TvPrograms(),
|
||||||
|
HomeRowConfig.TvChannels(),
|
||||||
HomeRowConfig.Suggestions(parentId = UUID.randomUUID()),
|
HomeRowConfig.Suggestions(parentId = UUID.randomUUID()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +97,7 @@ class TestHomeRowSamples {
|
||||||
is HomeRowConfig.Recordings -> foundTypes.add(it::class)
|
is HomeRowConfig.Recordings -> foundTypes.add(it::class)
|
||||||
is HomeRowConfig.TvPrograms -> foundTypes.add(it::class)
|
is HomeRowConfig.TvPrograms -> foundTypes.add(it::class)
|
||||||
is HomeRowConfig.Suggestions -> foundTypes.add(it::class)
|
is HomeRowConfig.Suggestions -> foundTypes.add(it::class)
|
||||||
|
is HomeRowConfig.TvChannels -> foundTypes.add(it::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.assertEquals(HomeRowConfig::class.sealedSubclasses.size, foundTypes.size)
|
Assert.assertEquals(HomeRowConfig::class.sealedSubclasses.size, foundTypes.size)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue