Add long clicking context menus everywhere (#202)

Adds long click context menus to just about every card throughout the
app. Previously only a few pages had this.

Closes #196
This commit is contained in:
damontecres 2025-11-12 15:36:36 -05:00 committed by GitHub
parent 5e2c75bb70
commit 2e3e4e3f5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 766 additions and 289 deletions

View file

@ -11,6 +11,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
@Serializable
data class BaseItem(
@ -18,29 +19,29 @@ data class BaseItem(
val imageUrl: String?,
val backdropImageUrl: String? = null,
) {
@Transient val id = data.id
val id get() = data.id
@Transient val type = data.type
val type get() = data.type
@Transient val name = data.name
val name get() = data.name
@Transient
val title = if (type == BaseItemKind.EPISODE) data.seriesName else name
val title get() = if (type == BaseItemKind.EPISODE) data.seriesName else name
@Transient
val subtitle =
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
@Transient
val resumeMs =
data.userData
?.playbackPositionTicks
?.ticks
?.inWholeMilliseconds
val subtitle
get() =
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
@Transient
val indexNumber = data.indexNumber ?: dateAsIndex()
val playbackPosition get() = data.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
val resumeMs get() = playbackPosition.inWholeMilliseconds
val played get() = data.userData?.played ?: false
val favorite get() = data.userData?.isFavorite ?: false
private fun dateAsIndex(): Int? =
data.premiereDate
?.let {

View file

@ -11,6 +11,8 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.FavoriteWatchManagerImpl
import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.Module
import dagger.Provides
@ -23,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
import org.jellyfin.sdk.createJellyfin
@ -182,4 +185,8 @@ object AppModule {
@Singleton
@IoCoroutineScope
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@Provides
@Singleton
fun favoriteWatchManager(api: ApiClient): FavoriteWatchManager = FavoriteWatchManagerImpl(api)
}

View file

@ -21,16 +21,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
/**
@ -45,6 +50,7 @@ fun BannerCard(
modifier: Modifier = Modifier,
cornerText: String? = null,
played: Boolean = false,
favorite: Boolean = false,
playPercent: Double = 0.0,
cardHeight: Dp = 140.dp * .85f,
aspectRatio: Float = 16f / 9,
@ -118,6 +124,18 @@ fun BannerCard(
}
}
}
if (favorite) {
Text(
modifier =
Modifier
.align(Alignment.TopStart)
.padding(8.dp),
color = colorResource(android.R.color.holo_red_light),
text = stringResource(R.string.fa_heart),
fontSize = 16.sp,
fontFamily = FontAwesome,
)
}
if (playPercent > 0 && playPercent < 100) {
Box(
modifier =

View file

@ -28,8 +28,8 @@ import com.github.damontecres.wholphin.util.FocusPair
fun ItemRow(
title: String,
items: List<BaseItem?>,
onClickItem: (BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
cardContent: @Composable (
index: Int,
item: BaseItem?,
@ -83,8 +83,8 @@ fun ItemRow(
index,
item,
cardModifier,
{ if (item != null) onClickItem.invoke(item) },
{ if (item != null) onLongClickItem.invoke(item) },
{ if (item != null) onClickItem.invoke(index, item) },
{ if (item != null) onLongClickItem.invoke(index, item) },
)
}
}
@ -95,8 +95,8 @@ fun ItemRow(
fun BannerItemRow(
title: String,
items: List<BaseItem?>,
onClickItem: (BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
modifier: Modifier = Modifier,
focusPair: FocusPair? = null,
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,

View file

@ -28,7 +28,7 @@ fun PersonRow(
people: List<Person>,
onClick: (Person) -> Unit,
modifier: Modifier = Modifier,
onLongClick: ((Person) -> Unit)? = null,
onLongClick: ((Int, Person) -> Unit)? = null,
) {
val firstFocus = remember { FocusRequester() }
Column(
@ -54,7 +54,7 @@ fun PersonRow(
PersonCard(
item = item,
onClick = { onClick.invoke(item) },
onLongClick = { onLongClick?.invoke(item) },
onLongClick = { onLongClick?.invoke(index, item) },
modifier =
Modifier
.width(120.dp)

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@ -37,12 +38,20 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
@ -57,6 +66,7 @@ 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.ItemSortBy
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -71,6 +81,8 @@ class CollectionFolderViewModel
@param:ApplicationContext private val context: Context,
private val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
private val favoriteWatchManager: FavoriteWatchManager,
val navigationManager: NavigationManager,
) : ItemViewModel(api) {
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val pager = MutableLiveData<List<BaseItem?>>(listOf())
@ -228,6 +240,24 @@ class CollectionFolderViewModel
result.totalRecordCount
}
}
fun setWatched(
position: Int,
itemId: UUID,
played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
fun setFavorite(
position: Int,
itemId: UUID,
favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
}
}
/**
@ -241,7 +271,7 @@ fun CollectionFolderGrid(
itemId: UUID,
initialFilter: GetItemsFilter,
recursive: Boolean,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
sortOptions: List<ItemSortBy>,
modifier: Modifier = Modifier,
initialSortAndDirection: SortAndDirection? = null,
@ -266,14 +296,16 @@ fun CollectionFolderGrid(
itemId: String,
initialFilter: GetItemsFilter,
recursive: Boolean,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
sortOptions: List<ItemSortBy>,
modifier: Modifier = Modifier,
viewModel: CollectionFolderViewModel = hiltViewModel(),
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
initialSortAndDirection: SortAndDirection? = null,
showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) {
val context = LocalContext.current
OneTimeLaunchedEffect {
viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter)
}
@ -283,6 +315,10 @@ fun CollectionFolderGrid(
val item by viewModel.item.observeAsState()
val pager by viewModel.pager.observeAsState()
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading,
@ -297,6 +333,9 @@ fun CollectionFolderGrid(
sortAndDirection = sortAndDirection!!,
modifier = modifier,
onClickItem = onClickItem,
onLongClickItem = { position, item ->
moreDialog.makePresent(PositionItem(position, item))
},
onSortChange = {
viewModel.onSortChange(it, recursive, filter)
},
@ -308,6 +347,55 @@ fun CollectionFolderGrid(
}
}
}
moreDialog.compose { (position, item) ->
DialogPopup(
showDialog = true,
title = item.title ?: "",
dialogItems =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = null,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
actions =
MoreDialogActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickWatch = { itemId, watched ->
viewModel.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, watched ->
viewModel.setFavorite(position, itemId, watched)
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it)
},
),
),
onDismissRequest = { moreDialog.makeAbsent() },
dismissOnClick = true,
waitToLoad = true,
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
}
}
@Composable
@ -316,7 +404,8 @@ fun CollectionFolderGridContent(
item: BaseItem?,
pager: List<BaseItem?>,
sortAndDirection: SortAndDirection,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
onSortChange: (SortAndDirection) -> Unit,
letterPosition: suspend (Char) -> Int,
sortOptions: List<ItemSortBy>,
@ -358,7 +447,7 @@ fun CollectionFolderGridContent(
CardGrid(
pager = pager,
onClickItem = onClickItem,
onLongClickItem = {},
onLongClickItem = onLongClickItem,
letterPosition = letterPosition,
gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference
@ -372,3 +461,8 @@ fun CollectionFolderGridContent(
)
}
}
data class PositionItem(
val position: Int,
val item: BaseItem,
)

View file

@ -103,7 +103,7 @@ fun GenreCardGrid(
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
CardGrid(
pager = genres,
onClickItem = { genre ->
onClickItem = { _, genre ->
viewModel.navigationManager.navigateTo(
Destination.FilteredCollection(
itemId = itemId,
@ -112,7 +112,7 @@ fun GenreCardGrid(
),
)
},
onLongClickItem = {},
onLongClickItem = { _, _ -> },
letterPosition = { viewModel.positionOfLetter(it) },
gridFocusRequester = gridFocusRequester,
showJumpButtons = false,

View file

@ -29,7 +29,7 @@ fun LoadingRow(
rowIndex: Int,
position: RowColumn,
focusRequester: FocusRequester,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onClickPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier,
showIfEmpty: Boolean = true,
@ -82,7 +82,7 @@ fun LoadingRow(
title = title,
items = r.items,
onClickItem = onClickItem,
onLongClickItem = {},
onLongClickItem = { _, _ -> },
modifier = modifier,
cardContent = cardContent,
horizontalPadding = horizontalPadding,

View file

@ -0,0 +1,31 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class Optional<T> private constructor(
initialValue: T?,
) {
private var value by mutableStateOf(initialValue)
@Composable
fun compose(run: @Composable (T) -> Unit) {
value?.let { run.invoke(it) }
}
fun makeAbsent() {
value = null
}
fun makePresent(value: T) {
this.value = value
}
companion object {
fun <T> absent() = Optional<T>(null)
fun <T> present(value: T) = Optional(value)
}
}

View file

@ -0,0 +1,178 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.sdk.model.api.MediaType
import java.util.UUID
abstract class RecommendedViewModel(
val navigationManager: NavigationManager,
val favoriteWatchManager: FavoriteWatchManager,
) : ViewModel() {
abstract fun init()
abstract val rows: MutableStateFlow<MutableList<HomeRowLoadingState>>
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
fun refreshItem(
position: RowColumn,
itemId: UUID,
) {
viewModelScope.launchIO {
val row = rows.value.getOrNull(position.row)
if (row is HomeRowLoadingState.Success) {
(row.items as? ApiRequestPager<*>)?.refreshItem(position.column, itemId)
}
}
}
fun setWatched(
position: RowColumn,
itemId: UUID,
watched: Boolean,
) {
viewModelScope.launchIO {
favoriteWatchManager.setWatched(itemId, watched)
refreshItem(position, itemId)
}
}
fun setFavorite(
position: RowColumn,
itemId: UUID,
watched: Boolean,
) {
viewModelScope.launchIO {
favoriteWatchManager.setFavorite(itemId, watched)
refreshItem(position, itemId)
}
}
}
@Composable
fun RecommendedContent(
preferences: UserPreferences,
viewModel: RecommendedViewModel,
modifier: Modifier = Modifier,
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
onFocusPosition: ((RowColumn) -> Unit)? = null,
) {
val context = LocalContext.current
var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
OneTimeLaunchedEffect {
viewModel.init()
}
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val rows by viewModel.rows.collectAsState()
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading,
LoadingState.Pending,
-> LoadingPage()
LoadingState.Success ->
HomePageContent(
homeRows = rows,
onClickItem = { _, item ->
viewModel.navigationManager.navigateTo(item.destination())
},
onLongClickItem = { position, item ->
moreDialog.makePresent(RowColumnItem(position, item))
},
onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier,
)
}
moreDialog.compose { (position, item) ->
DialogPopup(
showDialog = true,
title = item.title ?: "",
dialogItems =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = null,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
actions =
MoreDialogActions(
navigateTo = { viewModel.navigationManager.navigateTo(it) },
onClickWatch = { itemId, watched ->
viewModel.setWatched(position, itemId, watched)
},
onClickFavorite = { itemId, watched ->
viewModel.setFavorite(position, itemId, watched)
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(it)
},
),
),
onDismissRequest = { moreDialog.makeAbsent() },
dismissOnClick = true,
waitToLoad = true,
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
}
}
private data class RowColumnItem(
val position: RowColumn,
val item: BaseItem,
)

View file

@ -2,25 +2,19 @@ package com.github.damontecres.wholphin.ui.components
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
@ -54,15 +48,15 @@ class RecommendedMovieViewModel
private val api: ApiClient,
private val serverRepository: ServerRepository,
@Assisted val parentId: UUID,
) : ViewModel() {
navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager,
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
@AssistedFactory
interface Factory {
fun create(parentId: UUID): RecommendedMovieViewModel
}
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val rows =
override val rows =
MutableStateFlow<MutableList<HomeRowLoadingState>>(
rowTitles
.map {
@ -72,7 +66,7 @@ class RecommendedMovieViewModel
}.toMutableList(),
)
fun init() {
override fun init() {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
try {
val resumeItemsRequest =
@ -211,7 +205,6 @@ class RecommendedMovieViewModel
fun RecommendedMovie(
preferences: UserPreferences,
parentId: UUID,
onClickItem: (BaseItem) -> Unit,
onFocusPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier,
viewModel: RecommendedMovieViewModel =
@ -219,27 +212,10 @@ fun RecommendedMovie(
creationCallback = { it.create(parentId) },
),
) {
OneTimeLaunchedEffect {
viewModel.init()
}
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val rows by viewModel.rows.collectAsState()
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading,
LoadingState.Pending,
-> LoadingPage()
LoadingState.Success ->
HomePageContent(
homeRows = rows,
onClickItem = onClickItem,
onLongClickItem = {},
onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier,
)
}
RecommendedContent(
preferences = preferences,
viewModel = viewModel,
onFocusPosition = onFocusPosition,
modifier = modifier,
)
}

View file

@ -2,25 +2,19 @@ package com.github.damontecres.wholphin.ui.components
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
@ -56,15 +50,15 @@ class RecommendedTvShowViewModel
private val api: ApiClient,
private val serverRepository: ServerRepository,
@Assisted val parentId: UUID,
) : ViewModel() {
navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager,
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
@AssistedFactory
interface Factory {
fun create(parentId: UUID): RecommendedTvShowViewModel
}
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val rows =
override val rows =
MutableStateFlow<MutableList<HomeRowLoadingState>>(
rowTitles
.map {
@ -74,7 +68,7 @@ class RecommendedTvShowViewModel
}.toMutableList(),
)
fun init() {
override fun init() {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
try {
val resumeItemsRequest =
@ -240,7 +234,6 @@ class RecommendedTvShowViewModel
fun RecommendedTvShow(
preferences: UserPreferences,
parentId: UUID,
onClickItem: (BaseItem) -> Unit,
onFocusPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier,
viewModel: RecommendedTvShowViewModel =
@ -248,28 +241,10 @@ fun RecommendedTvShow(
creationCallback = { it.create(parentId) },
),
) {
OneTimeLaunchedEffect {
viewModel.init()
}
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val rows by viewModel.rows.collectAsState()
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading,
LoadingState.Pending,
-> LoadingPage()
LoadingState.Success ->
HomePageContent(
homeRows = rows,
onClickItem = onClickItem,
onLongClickItem = {},
onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier,
)
}
RecommendedContent(
preferences = preferences,
viewModel = viewModel,
onFocusPosition = onFocusPosition,
modifier = modifier,
)
}

View file

@ -71,8 +71,8 @@ private const val DEBUG = false
@Composable
fun CardGrid(
pager: List<BaseItem?>,
onClickItem: (BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int,
gridFocusRequester: FocusRequester,
showJumpButtons: Boolean,
@ -270,10 +270,10 @@ fun CardGrid(
{
if (item != null) {
focusedIndex = index
onClickItem.invoke(item)
onClickItem.invoke(index, item)
}
},
{ if (item != null) onLongClickItem.invoke(item) },
{ if (item != null) onLongClickItem.invoke(index, item) },
mod
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
.onFocusChanged { focusState ->

View file

@ -30,7 +30,7 @@ fun CollectionFolderGeneric(
var showHeader by remember { mutableStateOf(true) }
CollectionFolderGrid(
preferences = preferences,
onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) },
onClickItem = { _, item -> preferencesViewModel.navigationManager.navigateTo(item.destination()) },
itemId = itemId,
initialFilter = filter,
showTitle = showHeader,

View file

@ -106,7 +106,7 @@ fun CollectionFolderLiveTv(
logTab("livetv", selectedTabIndex)
viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex)
}
val onClickItem = { item: BaseItem ->
val onClickItem = { position: Int, item: BaseItem ->
viewModel.navigationManager.navigateTo(item.destination())
}

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
@ -69,10 +68,6 @@ fun CollectionFolderMovie(
var showHeader by rememberSaveable { mutableStateOf(true) }
val onClickItem = { item: BaseItem ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
}
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column(
modifier = modifier,
@ -96,7 +91,6 @@ fun CollectionFolderMovie(
0 -> {
RecommendedMovie(
preferences = preferences,
onClickItem = onClickItem,
parentId = destination.itemId,
onFocusPosition = { pos ->
showHeader = pos.row < 1
@ -111,7 +105,9 @@ fun CollectionFolderMovie(
1 -> {
CollectionFolderGrid(
preferences = preferences,
onClickItem = onClickItem,
onClickItem = { _, item ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
},
itemId = destination.itemId,
initialFilter =
GetItemsFilter(
@ -133,7 +129,9 @@ fun CollectionFolderMovie(
2 -> {
CollectionFolderGrid(
preferences = preferences,
onClickItem = onClickItem,
onClickItem = { _, item ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
},
itemId = destination.itemId,
initialFilter =
GetItemsFilter(

View file

@ -95,7 +95,6 @@ fun CollectionFolderTv(
RecommendedTvShow(
preferences = preferences,
parentId = destination.itemId,
onClickItem = onClickItem,
onFocusPosition = { pos ->
showHeader = pos.row < 1
},
@ -125,7 +124,9 @@ fun CollectionFolderTv(
positionCallback = { columns, position ->
showHeader = position < columns
},
onClickItem = onClickItem,
onClickItem = { _, item ->
preferencesViewModel.navigationManager.navigateTo(item.destination())
},
)
}
2 -> {

View file

@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Refresh
import androidx.compose.ui.graphics.Color
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
@ -20,6 +21,13 @@ import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
data class MoreDialogActions(
val navigateTo: (Destination) -> Unit,
var onClickWatch: (UUID, Boolean) -> Unit,
var onClickFavorite: (UUID, Boolean) -> Unit,
var onClickAddPlaylist: (UUID) -> Unit,
)
/**
* Build the [DialogItem]s when clicking "More"
*
@ -43,12 +51,9 @@ fun buildMoreDialogItems(
sourceId: UUID?,
watched: Boolean,
favorite: Boolean,
navigateTo: (Destination) -> Unit,
onClickWatch: (Boolean) -> Unit,
onClickFavorite: (Boolean) -> Unit,
actions: MoreDialogActions,
onChooseVersion: () -> Unit,
onChooseTracks: (MediaStreamType) -> Unit,
onClickAddPlaylist: () -> Unit,
): List<DialogItem> =
buildList {
add(
@ -57,7 +62,7 @@ fun buildMoreDialogItems(
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
actions.navigateTo(
Destination.Playback(
item.id,
item.resumeMs ?: 0L,
@ -119,7 +124,7 @@ fun buildMoreDialogItems(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
onClickAddPlaylist.invoke()
actions.onClickAddPlaylist.invoke(item.id)
},
)
add(
@ -127,7 +132,7 @@ fun buildMoreDialogItems(
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(!watched)
actions.onClickWatch.invoke(item.id, !watched)
},
)
add(
@ -136,7 +141,7 @@ fun buildMoreDialogItems(
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
) {
onClickFavorite.invoke(!favorite)
actions.onClickFavorite.invoke(item.id, !favorite)
},
)
series?.let {
@ -145,7 +150,7 @@ fun buildMoreDialogItems(
context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward,
) {
navigateTo(
actions.navigateTo(
Destination.MediaItem(
series.id,
BaseItemKind.SERIES,
@ -160,7 +165,7 @@ fun buildMoreDialogItems(
context.getString(R.string.play_with_transcoding),
Icons.Default.PlayArrow,
) {
navigateTo(
actions.navigateTo(
Destination.Playback(
item.id,
item.resumeMs ?: 0L,
@ -179,10 +184,7 @@ fun buildMoreDialogItemsForHome(
playbackPosition: Duration,
watched: Boolean,
favorite: Boolean,
navigateTo: (Destination) -> Unit,
onClickWatch: (UUID, Boolean) -> Unit,
onClickFavorite: (UUID, Boolean) -> Unit,
onClickAddPlaylist: (UUID) -> Unit,
actions: MoreDialogActions,
): List<DialogItem> =
buildList {
val itemId = item.id
@ -191,7 +193,7 @@ fun buildMoreDialogItemsForHome(
context.getString(R.string.go_to),
Icons.Default.ArrowForward,
) {
navigateTo(item.destination())
actions.navigateTo(item.destination())
},
)
if (item.type in supportedPlayableTypes) {
@ -202,7 +204,7 @@ fun buildMoreDialogItemsForHome(
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
actions.navigateTo(
Destination.Playback(
itemId,
playbackPosition.inWholeMilliseconds,
@ -216,7 +218,7 @@ fun buildMoreDialogItemsForHome(
Icons.Default.Refresh,
// iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
actions.navigateTo(
Destination.Playback(
itemId,
0L,
@ -231,7 +233,7 @@ fun buildMoreDialogItemsForHome(
Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f),
) {
navigateTo(
actions.navigateTo(
Destination.Playback(
itemId,
0L,
@ -246,7 +248,7 @@ fun buildMoreDialogItemsForHome(
text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul,
) {
onClickAddPlaylist.invoke(itemId)
actions.onClickAddPlaylist.invoke(itemId)
},
)
add(
@ -254,7 +256,7 @@ fun buildMoreDialogItemsForHome(
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)
actions.onClickWatch.invoke(itemId, !watched)
},
)
add(
@ -263,7 +265,7 @@ fun buildMoreDialogItemsForHome(
iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified,
) {
onClickFavorite.invoke(itemId, !favorite)
actions.onClickFavorite.invoke(itemId, !favorite)
},
)
seriesId?.let {
@ -272,7 +274,7 @@ fun buildMoreDialogItemsForHome(
context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward,
) {
navigateTo(
actions.navigateTo(
Destination.MediaItem(
it,
BaseItemKind.SERIES,
@ -282,3 +284,31 @@ fun buildMoreDialogItemsForHome(
)
}
}
fun buildMoreDialogItemsForPerson(
context: Context,
person: Person,
// favorite: Boolean,
actions: MoreDialogActions,
): List<DialogItem> =
buildList {
val itemId = person.id
add(
DialogItem(
context.getString(R.string.go_to),
Icons.Default.ArrowForward,
) {
actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON))
},
)
// TODO need way to get person's favorite status
// 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,
// ) {
// actions.onClickFavorite.invoke(itemId, !favorite)
// },
// )
}

View file

@ -99,7 +99,7 @@ fun FavoritesPage(
0 -> {
CollectionFolderGrid(
preferences = preferences,
onClickItem = onClickItem,
onClickItem = { _, item -> onClickItem.invoke(item) },
itemId = "${NavDrawerItem.Favorites.id}_movies",
initialFilter =
GetItemsFilter(
@ -122,7 +122,7 @@ fun FavoritesPage(
1 -> {
CollectionFolderGrid(
preferences = preferences,
onClickItem = onClickItem,
onClickItem = { _, item -> onClickItem.invoke(item) },
itemId = "${NavDrawerItem.Favorites.id}_series",
initialFilter =
GetItemsFilter(
@ -145,7 +145,7 @@ fun FavoritesPage(
2 -> {
CollectionFolderGrid(
preferences = preferences,
onClickItem = onClickItem,
onClickItem = { _, item -> onClickItem.invoke(item) },
itemId = "${NavDrawerItem.Favorites.id}_episodes",
initialFilter =
GetItemsFilter(

View file

@ -26,9 +26,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -186,8 +183,8 @@ fun PersonPage(
movies = movies,
series = series,
episodes = episodes,
onClickItem = {
viewModel.navigationManager.navigateTo(it.destination())
onClickItem = { index, item ->
viewModel.navigationManager.navigateTo(item.destination())
},
overviewOnClick = { showOverviewDialog = true },
modifier = modifier,
@ -226,7 +223,7 @@ fun PersonPageContent(
movies: RowLoadingState,
series: RowLoadingState,
episodes: RowLoadingState,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
overviewOnClick: () -> Unit,
modifier: Modifier = Modifier,
) {

View file

@ -64,14 +64,18 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
@ -83,6 +87,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
import kotlin.time.Duration
@Composable
@ -112,9 +117,24 @@ fun MovieDetails(
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf(false) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
val moreActions =
MoreDialogActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(itemId, watched)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
)
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading,
@ -139,8 +159,8 @@ fun MovieDetails(
chapters = chapters,
trailers = trailers,
similar = similar,
onClickItem = {
viewModel.navigateTo(it.destination())
onClickItem = { index, item ->
viewModel.navigateTo(item.destination())
},
onClickPerson = {
viewModel.navigateTo(
@ -180,9 +200,7 @@ fun MovieDetails(
favorite = movie.data.userData?.isFavorite ?: false,
series = null,
sourceId = chosenStreams?.sourceId,
navigateTo = viewModel::navigateTo,
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
actions = moreActions,
onChooseVersion = {
chooseVersion =
chooseVersionParams(
@ -218,18 +236,46 @@ fun MovieDetails(
)
}
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = true
},
),
)
},
watchOnClick = {
viewModel.setWatched((movie.data.userData?.played ?: false).not())
viewModel.setWatched(movie.id, !movie.played)
},
favoriteOnClick = {
viewModel.setFavorite((movie.data.userData?.isFavorite ?: false).not())
viewModel.setFavorite(movie.id, !movie.favorite)
},
onLongClickPerson = { index, person ->
val items =
buildMoreDialogItemsForPerson(
context = context,
person = person,
actions = moreActions,
)
moreDialog =
DialogParams(
fromLongClick = true,
title = person.name ?: "",
items = items,
)
},
onLongClickSimilar = { index, similar ->
val items =
buildMoreDialogItemsForHome(
context = context,
item = similar,
seriesId = null,
playbackPosition = similar.playbackPosition,
watched = similar.played,
favorite = similar.favorite,
actions = moreActions,
)
moreDialog =
DialogParams(
fromLongClick = true,
title = similar.title ?: "",
items = items,
)
},
trailerOnClick = { trailer ->
when (trailer) {
@ -283,19 +329,19 @@ fun MovieDetails(
waitToLoad = params.fromLongClick,
)
}
if (showPlaylistDialog) {
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog = false },
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, destination.itemId)
showPlaylistDialog = false
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, destination.itemId)
showPlaylistDialog = false
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
@ -323,8 +369,10 @@ fun MovieDetailsContent(
watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onClickPerson: (Person) -> Unit,
onLongClickPerson: (Int, Person) -> Unit,
onLongClickSimilar: (Int, BaseItem) -> Unit,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
@ -422,7 +470,10 @@ fun MovieDetailsContent(
position = PEOPLE_ROW
onClickPerson.invoke(it)
},
onLongClick = {},
onLongClick = { index, person ->
position = PEOPLE_ROW
onLongClickPerson.invoke(index, person)
},
modifier =
Modifier
.fillMaxWidth()
@ -466,11 +517,14 @@ fun MovieDetailsContent(
ItemRow(
title = stringResource(R.string.more_like_this),
items = similar,
onClickItem = {
onClickItem = { index, item ->
position = SIMILAR_ROW
onClickItem.invoke(it)
onClickItem.invoke(index, item)
},
onLongClickItem = { index, similar ->
position = SIMILAR_ROW
onLongClickSimilar.invoke(index, similar)
},
onLongClickItem = {},
cardContent = { index, item, mod, onClick, onLongClick ->
SeasonCard(
item = item,

View file

@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.ThemeSongPlayer
@ -39,7 +40,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
@ -55,6 +55,7 @@ class MovieViewModel
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager,
@Assisted val itemId: UUID,
) : ViewModel() {
@AssistedFactory
@ -157,25 +158,21 @@ class MovieViewModel
}
}
fun setWatched(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
fetchAndSetItem()
}
fun setWatched(
itemId: UUID,
played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setWatched(itemId, played)
fetchAndSetItem()
}
fun setFavorite(favorite: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
fetchAndSetItem()
}
fun setFavorite(
itemId: UUID,
favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
favoriteWatchManager.setFavorite(itemId, favorite)
fetchAndSetItem()
}
fun savePlayVersion(
item: BaseItem,

View file

@ -60,10 +60,17 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.Optional
import com.github.damontecres.wholphin.ui.components.OverviewText
import com.github.damontecres.wholphin.ui.components.SimpleStarRating
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
@ -74,7 +81,9 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID
import kotlin.time.Duration
@Composable
@ -83,6 +92,7 @@ fun SeriesDetails(
destination: Destination.MediaItem,
modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(),
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
@ -98,6 +108,8 @@ fun SeriesDetails(
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showWatchConfirmation by remember { mutableStateOf(false) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state)
@ -126,7 +138,9 @@ fun SeriesDetails(
played = played,
favorite = item.data.userData?.isFavorite ?: false,
modifier = modifier,
onClickItem = { viewModel.navigateTo(it.destination()) },
onClickItem = { index, item ->
viewModel.navigateTo(item.destination())
},
onClickPerson = {
viewModel.navigateTo(
Destination.MediaItem(
@ -135,7 +149,7 @@ fun SeriesDetails(
),
)
},
onLongClickItem = { season ->
onLongClickItem = { index, season ->
seasonDialog =
buildDialogForSeason(
context = context,
@ -160,6 +174,20 @@ fun SeriesDetails(
val favorite = item.data.userData?.isFavorite ?: false
viewModel.setFavorite(item.id, !favorite, null)
},
moreActions =
MoreDialogActions(
navigateTo = { viewModel.navigateTo(it) },
onClickWatch = { itemId, played ->
viewModel.setWatched(itemId, played, null)
},
onClickFavorite = { itemId, played ->
viewModel.setFavorite(itemId, played, null)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog.makePresent(itemId)
},
),
)
if (showWatchConfirmation) {
ConfirmDialog(
@ -194,6 +222,23 @@ fun SeriesDetails(
onDismissRequest = { seasonDialog = null },
)
}
showPlaylistDialog.compose { itemId ->
PlaylistDialog(
title = stringResource(R.string.add_to_playlist),
state = playlistState,
onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = {
playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog.makeAbsent()
},
createEnabled = true,
onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog.makeAbsent()
},
elevation = 3.dp,
)
}
}
private const val HEADER_ROW = 0
@ -210,15 +255,17 @@ fun SeriesDetailsContent(
people: List<Person>,
played: Boolean,
favorite: Boolean,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onClickPerson: (Person) -> Unit,
onLongClickItem: (BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
overviewOnClick: () -> Unit,
playOnClick: () -> Unit,
watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit,
moreActions: MoreDialogActions,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val bringIntoViewRequester = remember { BringIntoViewRequester() }
@ -227,6 +274,7 @@ fun SeriesDetailsContent(
LaunchedEffect(Unit) {
focusRequesters.getOrNull(position)?.tryRequestFocus()
}
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
Box(
modifier = modifier,
@ -349,11 +397,14 @@ fun SeriesDetailsContent(
ItemRow(
title = stringResource(R.string.tv_seasons),
items = seasons,
onClickItem = {
onClickItem = { index, item ->
position = SEASONS_ROW
onClickItem.invoke(it)
onClickItem.invoke(index, item)
},
onLongClickItem = { index, item ->
position = SEASONS_ROW
onLongClickItem.invoke(index, item)
},
onLongClickItem = onLongClickItem,
modifier =
Modifier
.fillMaxWidth()
@ -379,7 +430,21 @@ fun SeriesDetailsContent(
position = PEOPLE_ROW
onClickPerson.invoke(it)
},
onLongClick = {},
onLongClick = { index, person ->
position = PEOPLE_ROW
val items =
buildMoreDialogItemsForPerson(
context = context,
person = person,
actions = moreActions,
)
moreDialog =
DialogParams(
fromLongClick = true,
title = person.name ?: "",
items = items,
)
},
modifier =
Modifier
.fillMaxWidth()
@ -392,11 +457,29 @@ fun SeriesDetailsContent(
ItemRow(
title = stringResource(R.string.more_like_this),
items = similar,
onClickItem = {
onClickItem = { index, item ->
position = SIMILAR_ROW
onClickItem.invoke(it)
onClickItem.invoke(index, item)
},
onLongClickItem = { index, item ->
position = SIMILAR_ROW
val items =
buildMoreDialogItemsForHome(
context = context,
item = item,
seriesId = null,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
actions = moreActions,
)
moreDialog =
DialogParams(
fromLongClick = true,
title = item.name ?: "",
items = items,
)
},
onLongClickItem = {},
cardContent = { index, item, mod, onClick, onLongClick ->
SeasonCard(
item = item,
@ -418,6 +501,16 @@ fun SeriesDetailsContent(
}
}
}
moreDialog?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { moreDialog = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
}
@Composable

View file

@ -32,6 +32,7 @@ import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
@ -196,29 +197,28 @@ fun SeriesOverview(
favorite = ep.data.userData?.isFavorite ?: false,
series = series,
sourceId = chosenStreams?.sourceId,
navigateTo = viewModel::navigateTo,
onClickWatch = { played ->
episodeList
?.getOrNull(position.episodeRowIndex)
?.let {
actions =
MoreDialogActions(
navigateTo = viewModel::navigateTo,
onClickWatch = { itemId, watched ->
viewModel.setWatched(
it.id,
played,
itemId,
watched,
position.episodeRowIndex,
)
}
},
onClickFavorite = { favorite ->
episodeList
?.getOrNull(position.episodeRowIndex)
?.let {
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(
it.id,
itemId,
favorite,
position.episodeRowIndex,
)
}
},
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = it
},
),
onChooseVersion = {
chooseVersion =
chooseVersionParams(
@ -254,10 +254,6 @@ fun SeriesOverview(
)
}
},
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = ep.id
},
),
)

View file

@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
@ -36,9 +37,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.libraryApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemSortBy
@ -61,6 +60,7 @@ class SeriesViewModel
private val navigationManager: NavigationManager,
private val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager,
) : ItemViewModel(api) {
private lateinit var seriesId: UUID
private lateinit var prefs: UserPreferences
@ -240,11 +240,7 @@ class SeriesViewModel
played: Boolean,
listIndex: Int?,
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
if (played) {
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
favoriteWatchManager.setWatched(itemId, played)
listIndex?.let {
refreshEpisode(itemId, listIndex)
}
@ -255,11 +251,7 @@ class SeriesViewModel
favorite: Boolean,
listIndex: Int?,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
favoriteWatchManager.setFavorite(itemId, favorite)
if (listIndex != null) {
refreshEpisode(itemId, listIndex)
} else {
@ -279,11 +271,7 @@ class SeriesViewModel
fun setWatchedSeries(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(seriesId)
} else {
api.playStateApi.markUnplayedItem(seriesId)
}
favoriteWatchManager.setWatched(seriesId, played)
val series = fetchItem(seriesId)
val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons)

View file

@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
@ -75,7 +76,6 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID
import kotlin.time.Duration
@Composable
fun HomePage(
@ -121,37 +121,36 @@ fun HomePage(
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
HomePageContent(
watchingRows + latestRows,
onClickItem = {
viewModel.navigationManager.navigateTo(it.destination())
onClickItem = { position, item ->
viewModel.navigationManager.navigateTo(item.destination())
},
onLongClickItem = {
onLongClickItem = { position, item ->
val dialogItems =
buildMoreDialogItemsForHome(
context = context,
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)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId
},
item = item,
seriesId = item.data.seriesId,
playbackPosition = item.playbackPosition,
watched = item.played,
favorite = item.favorite,
actions =
MoreDialogActions(
navigateTo = viewModel.navigationManager::navigateTo,
onClickWatch = { itemId, played ->
viewModel.setWatched(itemId, played)
},
onClickFavorite = { itemId, favorite ->
viewModel.setFavorite(itemId, favorite)
},
onClickAddPlaylist = { itemId ->
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId
},
),
)
dialog =
DialogParams(
title = it.title ?: "",
title = item.title ?: "",
fromLongClick = true,
items = dialogItems,
)
@ -190,8 +189,8 @@ fun HomePage(
@Composable
fun HomePageContent(
homeRows: List<HomeRowLoadingState>,
onClickItem: (BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit,
onClickItem: (RowColumn, BaseItem) -> Unit,
onLongClickItem: (RowColumn, BaseItem) -> Unit,
showClock: Boolean,
modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null,
@ -317,14 +316,18 @@ fun HomePageContent(
ItemRow(
title = row.title,
items = row.items,
onClickItem = onClickItem,
onClickItem = { index, item ->
onClickItem.invoke(RowColumn(rowIndex, index), item)
},
cardOnFocus = { isFocused, index ->
if (isFocused) {
focusedItem = row.items.getOrNull(index)
position = RowColumn(rowIndex, index)
}
},
onLongClickItem = onLongClickItem,
onLongClickItem = { index, item ->
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
},
modifier =
Modifier
.fillMaxWidth()
@ -339,6 +342,7 @@ fun HomePageContent(
item?.data?.indexNumber?.let { "E$it" }
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
played = item?.data?.userData?.played ?: false,
favorite = item?.favorite ?: false,
playPercent =
item?.data?.userData?.playedPercentage
?: 0.0,

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.FavoriteWatchManager
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
@ -27,7 +28,6 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.itemsApi
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.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi
@ -50,6 +50,7 @@ class HomeViewModel
val navigationManager: NavigationManager,
val serverRepository: ServerRepository,
val navDrawerItemRepository: NavDrawerItemRepository,
private val favoriteWatchManager: FavoriteWatchManager,
) : ViewModel() {
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
@ -258,11 +259,7 @@ class HomeViewModel
itemId: UUID,
played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) {
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
favoriteWatchManager.setWatched(itemId, played)
withContext(Dispatchers.Main) {
init(preferences)
}
@ -272,11 +269,7 @@ class HomeViewModel
itemId: UUID,
favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
favoriteWatchManager.setFavorite(itemId, favorite)
withContext(Dispatchers.Main) {
init(preferences)
}

View file

@ -174,6 +174,9 @@ fun SearchPage(
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
}
val onClickItem = { index: Int, item: BaseItem ->
viewModel.navigationManager.navigateTo(item.destination())
}
LazyColumn(
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 44.dp),
@ -207,7 +210,7 @@ fun SearchPage(
rowIndex = MOVIE_ROW,
position = position,
focusRequester = focusRequester,
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
onClickItem = onClickItem,
onClickPosition = { position = it },
modifier = Modifier.fillMaxWidth(),
)
@ -217,7 +220,7 @@ fun SearchPage(
rowIndex = COLLECTION_ROW,
position = position,
focusRequester = focusRequester,
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
onClickItem = onClickItem,
onClickPosition = { position = it },
modifier = Modifier.fillMaxWidth(),
)
@ -227,7 +230,7 @@ fun SearchPage(
rowIndex = SERIES_ROW,
position = position,
focusRequester = focusRequester,
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
onClickItem = onClickItem,
onClickPosition = { position = it },
modifier = Modifier.fillMaxWidth(),
)
@ -237,7 +240,7 @@ fun SearchPage(
rowIndex = EPISODE_ROW,
position = position,
focusRequester = focusRequester,
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
onClickItem = onClickItem,
onClickPosition = { position = it },
modifier = Modifier.fillMaxWidth(),
cardContent = @Composable { index, item, mod, onClick, onLongClick ->
@ -268,7 +271,7 @@ fun LazyListScope.searchResultRow(
rowIndex: Int,
position: RowColumn,
focusRequester: FocusRequester,
onClickItem: (BaseItem) -> Unit,
onClickItem: (Int, BaseItem) -> Unit,
onClickPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier,
cardContent: @Composable (
@ -328,7 +331,7 @@ fun LazyListScope.searchResultRow(
title = title,
items = r.items,
onClickItem = onClickItem,
onLongClickItem = {},
onLongClickItem = { _, _ -> },
modifier = modifier,
cardContent = cardContent,
)

View file

@ -0,0 +1,43 @@
package com.github.damontecres.wholphin.util
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.api.UserItemDataDto
import java.util.UUID
interface FavoriteWatchManager {
suspend fun setWatched(
itemId: UUID,
played: Boolean,
): UserItemDataDto
suspend fun setFavorite(
itemId: UUID,
favorite: Boolean,
): UserItemDataDto
}
class FavoriteWatchManagerImpl(
private val api: ApiClient,
) : FavoriteWatchManager {
override suspend fun setWatched(
itemId: UUID,
played: Boolean,
): UserItemDataDto =
if (played) {
api.playStateApi.markPlayedItem(itemId).content
} else {
api.playStateApi.markUnplayedItem(itemId).content
}
override suspend fun setFavorite(
itemId: UUID,
favorite: Boolean,
): UserItemDataDto =
if (favorite) {
api.userLibraryApi.markFavoriteItem(itemId).content
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId).content
}
}