Add long click to home page (#66)

Add a popup when long clicking items on the home page

Also fixes a bug where the latest Recorded TV row would show non-Live TV
items if nothing has ever been recorded.
This commit is contained in:
damontecres 2025-10-26 08:50:24 -04:00 committed by GitHub
parent 2667090485
commit 143fc03d9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 212 additions and 20 deletions

View file

@ -156,6 +156,7 @@ fun RecommendedMovie(
HomePageContent( HomePageContent(
homeRows = rows, homeRows = rows,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = {},
onFocusPosition = onFocusPosition, onFocusPosition = onFocusPosition,
modifier = modifier, modifier = modifier,
) )

View file

@ -168,6 +168,7 @@ fun RecommendedTvShow(
HomePageContent( HomePageContent(
homeRows = rows, homeRows = rows,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = {},
onFocusPosition = onFocusPosition, onFocusPosition = onFocusPosition,
modifier = modifier, modifier = modifier,
) )

View file

@ -2,17 +2,22 @@ package com.github.damontecres.wholphin.ui.detail
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.supportedPlayableTypes
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.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/** /**
* Build the [DialogItem]s when clicking "More" * Build the [DialogItem]s when clicking "More"
@ -131,3 +136,104 @@ fun buildMoreDialogItems(
} }
} }
} }
fun buildMoreDialogItemsForHome(
item: BaseItem,
seriesId: UUID?,
playbackPosition: Duration,
watched: Boolean,
favorite: Boolean,
navigateTo: (Destination) -> Unit,
onClickWatch: (UUID, Boolean) -> Unit,
onClickFavorite: (UUID, Boolean) -> Unit,
): List<DialogItem> =
buildList {
val itemId = item.id
add(
DialogItem(
"Go To",
Icons.Default.ArrowForward,
) {
navigateTo(item.destination())
},
)
if (item.type in supportedPlayableTypes) {
if (playbackPosition >= 1.seconds) {
add(
DialogItem(
"Resume",
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
Destination.Playback(
itemId,
playbackPosition.inWholeMilliseconds,
),
)
},
)
add(
DialogItem(
"Restart",
Icons.Default.Refresh,
// iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
Destination.Playback(
itemId,
0L,
),
)
},
)
} else {
add(
DialogItem(
"Play",
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
Destination.Playback(
itemId,
0L,
),
)
},
)
}
}
add(
DialogItem(
text = if (watched) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
) {
onClickWatch.invoke(itemId, !watched)
},
)
add(
DialogItem(
text = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
) {
onClickFavorite.invoke(itemId, !favorite)
},
)
seriesId?.let {
add(
DialogItem(
"Go to series",
Icons.AutoMirrored.Filled.ArrowForward,
) {
navigateTo(
Destination.MediaItem(
it,
BaseItemKind.SERIES,
),
)
},
)
}
}

View file

@ -45,11 +45,14 @@ import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.data.RowColumnSaver import com.github.damontecres.wholphin.ui.data.RowColumnSaver
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundMinutes
@ -60,6 +63,7 @@ import com.github.damontecres.wholphin.util.formatDateTime
import com.github.damontecres.wholphin.util.seasonEpisode import com.github.damontecres.wholphin.util.seasonEpisode
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
data class HomeRow( data class HomeRow(
val section: HomeSection, val section: HomeSection,
@ -88,14 +92,48 @@ fun HomePage(
-> LoadingPage() -> LoadingPage()
LoadingState.Success -> { LoadingState.Success -> {
var dialog by remember { mutableStateOf<DialogParams?>(null) }
HomePageContent( HomePageContent(
homeRows, homeRows,
onClickItem = { onClickItem = {
viewModel.navigationManager.navigateTo(it.destination()) viewModel.navigationManager.navigateTo(it.destination())
}, },
onLongClickItem = {
val dialogItems =
buildMoreDialogItemsForHome(
item = it,
seriesId = it.data.seriesId,
playbackPosition =
it.data.userData
?.playbackPositionTicks
?.ticks
?: Duration.ZERO,
watched = it.data.userData?.played ?: false,
favorite = it.data.userData?.isFavorite ?: false,
navigateTo = viewModel.navigationManager::navigateTo,
onClickWatch = { itemId, played ->
viewModel.setWatched(itemId, played)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite)
},
)
dialog =
DialogParams(
title = it.title ?: "",
fromLongClick = true,
items = dialogItems,
)
},
loadingState = loading, loadingState = loading,
modifier = modifier, modifier = modifier,
) )
dialog?.let { params ->
DialogPopup(
params = params,
onDismissRequest = { dialog = null },
)
}
} }
} }
} }
@ -104,6 +142,7 @@ fun HomePage(
fun HomePageContent( fun HomePageContent(
homeRows: List<HomeRow>, homeRows: List<HomeRow>,
onClickItem: (BaseItem) -> Unit, onClickItem: (BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null, onFocusPosition: ((RowColumn) -> Unit)? = null,
loadingState: LoadingState? = null, loadingState: LoadingState? = null,
@ -183,7 +222,7 @@ fun HomePageContent(
position = RowColumn(rowIndex, index) position = RowColumn(rowIndex, index)
} }
}, },
onLongClickItem = {}, onLongClickItem = onLongClickItem,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
cardContent = { index, item, cardModifier, onClick, onLongClick -> cardContent = { index, item, cardModifier, onClick, onLongClick ->
// TODO better aspect ration handling? // TODO better aspect ration handling?

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.supportItemKinds import com.github.damontecres.wholphin.util.supportItemKinds
@ -21,6 +22,7 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.liveTvApi import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.api.client.extensions.userViewsApi
@ -46,8 +48,11 @@ class HomeViewModel
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending) val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
val homeRows = MutableLiveData<List<HomeRow>>() val homeRows = MutableLiveData<List<HomeRow>>()
private lateinit var preferences: UserPreferences
fun init(preferences: UserPreferences): Job { fun init(preferences: UserPreferences): Job {
loadingState.value = LoadingState.Loading loadingState.value = LoadingState.Loading
this.preferences = preferences
val prefs = preferences.appPreferences.homePagePreferences val prefs = preferences.appPreferences.homePagePreferences
val limit = prefs.maxItemsPerRow val limit = prefs.maxItemsPerRow
return viewModelScope.launch( return viewModelScope.launch(
@ -171,7 +176,7 @@ class HomeViewModel
latestMediaIncludes latestMediaIncludes
.mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } } .mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } }
.filter { it.collectionType in supportedLatestCollectionTypes } .filter { it.collectionType in supportedLatestCollectionTypes }
.map { view -> .mapNotNull { view ->
val title = val title =
if (view.collectionType == CollectionType.LIVETV) { if (view.collectionType == CollectionType.LIVETV) {
"Recently Recorded" "Recently Recorded"
@ -189,28 +194,58 @@ class HomeViewModel
} else { } else {
view.id view.id
} }
val request = viewId?.let {
GetLatestMediaRequest( val request =
fields = SlimItemFields, GetLatestMediaRequest(
imageTypeLimit = 1, fields = SlimItemFields,
parentId = viewId, imageTypeLimit = 1,
groupItems = true, parentId = viewId,
limit = limit, groupItems = true,
isPlayed = null, // Server will handle user's preference limit = limit,
isPlayed = null, // Server will handle user's preference
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
section = HomeSection.LATEST_MEDIA,
items = latest,
title = title,
) )
val latest = }
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
section = HomeSection.LATEST_MEDIA,
items = latest,
title = title,
)
} }
return rows return rows
} }
fun setWatched(
itemId: UUID,
played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
withContext(Dispatchers.Main) {
init(preferences)
}
}
fun setFavorite(
itemId: UUID,
favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
withContext(Dispatchers.Main) {
init(preferences)
}
}
} }
val supportedLatestCollectionTypes = val supportedLatestCollectionTypes =

View file

@ -38,3 +38,13 @@ val supportedCollectionTypes =
CollectionType.BOXSETS, CollectionType.BOXSETS,
CollectionType.LIVETV, CollectionType.LIVETV,
) )
val supportedPlayableTypes =
setOf(
BaseItemKind.MOVIE,
BaseItemKind.EPISODE,
BaseItemKind.VIDEO,
BaseItemKind.TV_CHANNEL,
BaseItemKind.TV_PROGRAM,
BaseItemKind.RECORDING,
)