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.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
@Serializable @Serializable
data class BaseItem( data class BaseItem(
@ -18,29 +19,29 @@ data class BaseItem(
val imageUrl: String?, val imageUrl: String?,
val backdropImageUrl: String? = null, 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 get() = if (type == BaseItemKind.EPISODE) data.seriesName else name
val title = if (type == BaseItemKind.EPISODE) data.seriesName else name
@Transient val subtitle
val subtitle = get() =
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString() if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
@Transient
val resumeMs =
data.userData
?.playbackPositionTicks
?.ticks
?.inWholeMilliseconds
@Transient @Transient
val indexNumber = data.indexNumber ?: dateAsIndex() 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? = private fun dateAsIndex(): Int? =
data.premiereDate data.premiereDate
?.let { ?.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.UserPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.util.ExceptionHandler 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 com.github.damontecres.wholphin.util.RememberTabManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -23,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.Jellyfin 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.client.util.AuthorizationHeaderBuilder
import org.jellyfin.sdk.api.okhttp.OkHttpFactory import org.jellyfin.sdk.api.okhttp.OkHttpFactory
import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.createJellyfin
@ -182,4 +185,8 @@ object AppModule {
@Singleton @Singleton
@IoCoroutineScope @IoCoroutineScope
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) 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.draw.clip
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale 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.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.Card
import androidx.tv.material3.CardDefaults import androidx.tv.material3.CardDefaults
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
/** /**
@ -45,6 +50,7 @@ fun BannerCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cornerText: String? = null, cornerText: String? = null,
played: Boolean = false, played: Boolean = false,
favorite: Boolean = false,
playPercent: Double = 0.0, playPercent: Double = 0.0,
cardHeight: Dp = 140.dp * .85f, cardHeight: Dp = 140.dp * .85f,
aspectRatio: Float = 16f / 9, 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) { if (playPercent > 0 && playPercent < 100) {
Box( Box(
modifier = modifier =

View file

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

View file

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

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields 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.data.SortAndDirection
import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.detail.ItemViewModel 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.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ApiRequestPager 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.GetItemsRequestHandler
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
@ -57,6 +66,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType 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.MediaType
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -71,6 +81,8 @@ class CollectionFolderViewModel
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
private val libraryDisplayInfoDao: LibraryDisplayInfoDao, private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
private val favoriteWatchManager: FavoriteWatchManager,
val navigationManager: NavigationManager,
) : ItemViewModel(api) { ) : ItemViewModel(api) {
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val pager = MutableLiveData<List<BaseItem?>>(listOf()) val pager = MutableLiveData<List<BaseItem?>>(listOf())
@ -228,6 +240,24 @@ class CollectionFolderViewModel
result.totalRecordCount 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, itemId: UUID,
initialFilter: GetItemsFilter, initialFilter: GetItemsFilter,
recursive: Boolean, recursive: Boolean,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
sortOptions: List<ItemSortBy>, sortOptions: List<ItemSortBy>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
initialSortAndDirection: SortAndDirection? = null, initialSortAndDirection: SortAndDirection? = null,
@ -266,14 +296,16 @@ fun CollectionFolderGrid(
itemId: String, itemId: String,
initialFilter: GetItemsFilter, initialFilter: GetItemsFilter,
recursive: Boolean, recursive: Boolean,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
sortOptions: List<ItemSortBy>, sortOptions: List<ItemSortBy>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: CollectionFolderViewModel = hiltViewModel(), viewModel: CollectionFolderViewModel = hiltViewModel(),
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
initialSortAndDirection: SortAndDirection? = null, initialSortAndDirection: SortAndDirection? = null,
showTitle: Boolean = true, showTitle: Boolean = true,
positionCallback: ((columns: Int, position: Int) -> Unit)? = null, positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
) { ) {
val context = LocalContext.current
OneTimeLaunchedEffect { OneTimeLaunchedEffect {
viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter) viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter)
} }
@ -283,6 +315,10 @@ fun CollectionFolderGrid(
val item by viewModel.item.observeAsState() val item by viewModel.item.observeAsState()
val pager by viewModel.pager.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) { when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state) is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading, LoadingState.Loading,
@ -297,6 +333,9 @@ fun CollectionFolderGrid(
sortAndDirection = sortAndDirection!!, sortAndDirection = sortAndDirection!!,
modifier = modifier, modifier = modifier,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { position, item ->
moreDialog.makePresent(PositionItem(position, item))
},
onSortChange = { onSortChange = {
viewModel.onSortChange(it, recursive, filter) 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 @Composable
@ -316,7 +404,8 @@ fun CollectionFolderGridContent(
item: BaseItem?, item: BaseItem?,
pager: List<BaseItem?>, pager: List<BaseItem?>,
sortAndDirection: SortAndDirection, sortAndDirection: SortAndDirection,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
onSortChange: (SortAndDirection) -> Unit, onSortChange: (SortAndDirection) -> Unit,
letterPosition: suspend (Char) -> Int, letterPosition: suspend (Char) -> Int,
sortOptions: List<ItemSortBy>, sortOptions: List<ItemSortBy>,
@ -358,7 +447,7 @@ fun CollectionFolderGridContent(
CardGrid( CardGrid(
pager = pager, pager = pager,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = {}, onLongClickItem = onLongClickItem,
letterPosition = letterPosition, letterPosition = letterPosition,
gridFocusRequester = gridFocusRequester, gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference 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() } LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
CardGrid( CardGrid(
pager = genres, pager = genres,
onClickItem = { genre -> onClickItem = { _, genre ->
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
Destination.FilteredCollection( Destination.FilteredCollection(
itemId = itemId, itemId = itemId,
@ -112,7 +112,7 @@ fun GenreCardGrid(
), ),
) )
}, },
onLongClickItem = {}, onLongClickItem = { _, _ -> },
letterPosition = { viewModel.positionOfLetter(it) }, letterPosition = { viewModel.positionOfLetter(it) },
gridFocusRequester = gridFocusRequester, gridFocusRequester = gridFocusRequester,
showJumpButtons = false, showJumpButtons = false,

View file

@ -29,7 +29,7 @@ fun LoadingRow(
rowIndex: Int, rowIndex: Int,
position: RowColumn, position: RowColumn,
focusRequester: FocusRequester, focusRequester: FocusRequester,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onClickPosition: (RowColumn) -> Unit, onClickPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showIfEmpty: Boolean = true, showIfEmpty: Boolean = true,
@ -82,7 +82,7 @@ fun LoadingRow(
title = title, title = title,
items = r.items, items = r.items,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = {}, onLongClickItem = { _, _ -> },
modifier = modifier, modifier = modifier,
cardContent = cardContent, cardContent = cardContent,
horizontalPadding = horizontalPadding, 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 android.content.Context
import androidx.compose.runtime.Composable 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.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
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.preferences.UserPreferences 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.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO 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.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler 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.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
@ -54,15 +48,15 @@ class RecommendedMovieViewModel
private val api: ApiClient, private val api: ApiClient,
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
@Assisted val parentId: UUID, @Assisted val parentId: UUID,
) : ViewModel() { navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager,
) : RecommendedViewModel(navigationManager, favoriteWatchManager) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(parentId: UUID): RecommendedMovieViewModel fun create(parentId: UUID): RecommendedMovieViewModel
} }
val loading = MutableLiveData<LoadingState>(LoadingState.Loading) override val rows =
val rows =
MutableStateFlow<MutableList<HomeRowLoadingState>>( MutableStateFlow<MutableList<HomeRowLoadingState>>(
rowTitles rowTitles
.map { .map {
@ -72,7 +66,7 @@ class RecommendedMovieViewModel
}.toMutableList(), }.toMutableList(),
) )
fun init() { override fun init() {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
try { try {
val resumeItemsRequest = val resumeItemsRequest =
@ -211,7 +205,6 @@ class RecommendedMovieViewModel
fun RecommendedMovie( fun RecommendedMovie(
preferences: UserPreferences, preferences: UserPreferences,
parentId: UUID, parentId: UUID,
onClickItem: (BaseItem) -> Unit,
onFocusPosition: (RowColumn) -> Unit, onFocusPosition: (RowColumn) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecommendedMovieViewModel = viewModel: RecommendedMovieViewModel =
@ -219,27 +212,10 @@ fun RecommendedMovie(
creationCallback = { it.create(parentId) }, creationCallback = { it.create(parentId) },
), ),
) { ) {
OneTimeLaunchedEffect { RecommendedContent(
viewModel.init() preferences = preferences,
} viewModel = viewModel,
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, onFocusPosition = onFocusPosition,
showClock = preferences.appPreferences.interfacePreferences.showClock,
modifier = modifier, modifier = modifier,
) )
} }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ 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.data.model.Person
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
@ -20,6 +21,13 @@ import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds 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" * Build the [DialogItem]s when clicking "More"
* *
@ -43,12 +51,9 @@ fun buildMoreDialogItems(
sourceId: UUID?, sourceId: UUID?,
watched: Boolean, watched: Boolean,
favorite: Boolean, favorite: Boolean,
navigateTo: (Destination) -> Unit, actions: MoreDialogActions,
onClickWatch: (Boolean) -> Unit,
onClickFavorite: (Boolean) -> Unit,
onChooseVersion: () -> Unit, onChooseVersion: () -> Unit,
onChooseTracks: (MediaStreamType) -> Unit, onChooseTracks: (MediaStreamType) -> Unit,
onClickAddPlaylist: () -> Unit,
): List<DialogItem> = ): List<DialogItem> =
buildList { buildList {
add( add(
@ -57,7 +62,7 @@ fun buildMoreDialogItems(
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f), iconColor = Color.Green.copy(alpha = .8f),
) { ) {
navigateTo( actions.navigateTo(
Destination.Playback( Destination.Playback(
item.id, item.id,
item.resumeMs ?: 0L, item.resumeMs ?: 0L,
@ -119,7 +124,7 @@ fun buildMoreDialogItems(
text = R.string.add_to_playlist, text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul, iconStringRes = R.string.fa_list_ul,
) { ) {
onClickAddPlaylist.invoke() actions.onClickAddPlaylist.invoke(item.id)
}, },
) )
add( add(
@ -127,7 +132,7 @@ fun buildMoreDialogItems(
text = if (watched) R.string.mark_unwatched else R.string.mark_watched, 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, iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
) { ) {
onClickWatch.invoke(!watched) actions.onClickWatch.invoke(item.id, !watched)
}, },
) )
add( add(
@ -136,7 +141,7 @@ fun buildMoreDialogItems(
iconStringRes = R.string.fa_heart, iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified, iconColor = if (favorite) Color.Red else Color.Unspecified,
) { ) {
onClickFavorite.invoke(!favorite) actions.onClickFavorite.invoke(item.id, !favorite)
}, },
) )
series?.let { series?.let {
@ -145,7 +150,7 @@ fun buildMoreDialogItems(
context.getString(R.string.go_to_series), context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward, Icons.AutoMirrored.Filled.ArrowForward,
) { ) {
navigateTo( actions.navigateTo(
Destination.MediaItem( Destination.MediaItem(
series.id, series.id,
BaseItemKind.SERIES, BaseItemKind.SERIES,
@ -160,7 +165,7 @@ fun buildMoreDialogItems(
context.getString(R.string.play_with_transcoding), context.getString(R.string.play_with_transcoding),
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
) { ) {
navigateTo( actions.navigateTo(
Destination.Playback( Destination.Playback(
item.id, item.id,
item.resumeMs ?: 0L, item.resumeMs ?: 0L,
@ -179,10 +184,7 @@ fun buildMoreDialogItemsForHome(
playbackPosition: Duration, playbackPosition: Duration,
watched: Boolean, watched: Boolean,
favorite: Boolean, favorite: Boolean,
navigateTo: (Destination) -> Unit, actions: MoreDialogActions,
onClickWatch: (UUID, Boolean) -> Unit,
onClickFavorite: (UUID, Boolean) -> Unit,
onClickAddPlaylist: (UUID) -> Unit,
): List<DialogItem> = ): List<DialogItem> =
buildList { buildList {
val itemId = item.id val itemId = item.id
@ -191,7 +193,7 @@ fun buildMoreDialogItemsForHome(
context.getString(R.string.go_to), context.getString(R.string.go_to),
Icons.Default.ArrowForward, Icons.Default.ArrowForward,
) { ) {
navigateTo(item.destination()) actions.navigateTo(item.destination())
}, },
) )
if (item.type in supportedPlayableTypes) { if (item.type in supportedPlayableTypes) {
@ -202,7 +204,7 @@ fun buildMoreDialogItemsForHome(
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f), iconColor = Color.Green.copy(alpha = .8f),
) { ) {
navigateTo( actions.navigateTo(
Destination.Playback( Destination.Playback(
itemId, itemId,
playbackPosition.inWholeMilliseconds, playbackPosition.inWholeMilliseconds,
@ -216,7 +218,7 @@ fun buildMoreDialogItemsForHome(
Icons.Default.Refresh, Icons.Default.Refresh,
// iconColor = Color.Green.copy(alpha = .8f), // iconColor = Color.Green.copy(alpha = .8f),
) { ) {
navigateTo( actions.navigateTo(
Destination.Playback( Destination.Playback(
itemId, itemId,
0L, 0L,
@ -231,7 +233,7 @@ fun buildMoreDialogItemsForHome(
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
iconColor = Color.Green.copy(alpha = .8f), iconColor = Color.Green.copy(alpha = .8f),
) { ) {
navigateTo( actions.navigateTo(
Destination.Playback( Destination.Playback(
itemId, itemId,
0L, 0L,
@ -246,7 +248,7 @@ fun buildMoreDialogItemsForHome(
text = R.string.add_to_playlist, text = R.string.add_to_playlist,
iconStringRes = R.string.fa_list_ul, iconStringRes = R.string.fa_list_ul,
) { ) {
onClickAddPlaylist.invoke(itemId) actions.onClickAddPlaylist.invoke(itemId)
}, },
) )
add( add(
@ -254,7 +256,7 @@ fun buildMoreDialogItemsForHome(
text = if (watched) R.string.mark_unwatched else R.string.mark_watched, 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, iconStringRes = if (watched) R.string.fa_eye else R.string.fa_eye_slash,
) { ) {
onClickWatch.invoke(itemId, !watched) actions.onClickWatch.invoke(itemId, !watched)
}, },
) )
add( add(
@ -263,7 +265,7 @@ fun buildMoreDialogItemsForHome(
iconStringRes = R.string.fa_heart, iconStringRes = R.string.fa_heart,
iconColor = if (favorite) Color.Red else Color.Unspecified, iconColor = if (favorite) Color.Red else Color.Unspecified,
) { ) {
onClickFavorite.invoke(itemId, !favorite) actions.onClickFavorite.invoke(itemId, !favorite)
}, },
) )
seriesId?.let { seriesId?.let {
@ -272,7 +274,7 @@ fun buildMoreDialogItemsForHome(
context.getString(R.string.go_to_series), context.getString(R.string.go_to_series),
Icons.AutoMirrored.Filled.ArrowForward, Icons.AutoMirrored.Filled.ArrowForward,
) { ) {
navigateTo( actions.navigateTo(
Destination.MediaItem( Destination.MediaItem(
it, it,
BaseItemKind.SERIES, 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 -> { 0 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = onClickItem, onClickItem = { _, item -> onClickItem.invoke(item) },
itemId = "${NavDrawerItem.Favorites.id}_movies", itemId = "${NavDrawerItem.Favorites.id}_movies",
initialFilter = initialFilter =
GetItemsFilter( GetItemsFilter(
@ -122,7 +122,7 @@ fun FavoritesPage(
1 -> { 1 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = onClickItem, onClickItem = { _, item -> onClickItem.invoke(item) },
itemId = "${NavDrawerItem.Favorites.id}_series", itemId = "${NavDrawerItem.Favorites.id}_series",
initialFilter = initialFilter =
GetItemsFilter( GetItemsFilter(
@ -145,7 +145,7 @@ fun FavoritesPage(
2 -> { 2 -> {
CollectionFolderGrid( CollectionFolderGrid(
preferences = preferences, preferences = preferences,
onClickItem = onClickItem, onClickItem = { _, item -> onClickItem.invoke(item) },
itemId = "${NavDrawerItem.Favorites.id}_episodes", itemId = "${NavDrawerItem.Favorites.id}_episodes",
initialFilter = initialFilter =
GetItemsFilter( GetItemsFilter(

View file

@ -26,9 +26,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -186,8 +183,8 @@ fun PersonPage(
movies = movies, movies = movies,
series = series, series = series,
episodes = episodes, episodes = episodes,
onClickItem = { onClickItem = { index, item ->
viewModel.navigationManager.navigateTo(it.destination()) viewModel.navigationManager.navigateTo(item.destination())
}, },
overviewOnClick = { showOverviewDialog = true }, overviewOnClick = { showOverviewDialog = true },
modifier = modifier, modifier = modifier,
@ -226,7 +223,7 @@ fun PersonPageContent(
movies: RowLoadingState, movies: RowLoadingState,
series: RowLoadingState, series: RowLoadingState,
episodes: RowLoadingState, episodes: RowLoadingState,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
modifier: Modifier = Modifier, 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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
import com.github.damontecres.wholphin.ui.components.LoadingPage 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.chooseStream
import com.github.damontecres.wholphin.ui.components.chooseVersionParams import com.github.damontecres.wholphin.ui.components.chooseVersionParams
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo 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.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems 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.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt 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.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUID
import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
@Composable @Composable
@ -112,9 +117,24 @@ fun MovieDetails(
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) } var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
var chooseVersion 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 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) { when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state) is LoadingState.Error -> ErrorMessage(state)
LoadingState.Loading, LoadingState.Loading,
@ -139,8 +159,8 @@ fun MovieDetails(
chapters = chapters, chapters = chapters,
trailers = trailers, trailers = trailers,
similar = similar, similar = similar,
onClickItem = { onClickItem = { index, item ->
viewModel.navigateTo(it.destination()) viewModel.navigateTo(item.destination())
}, },
onClickPerson = { onClickPerson = {
viewModel.navigateTo( viewModel.navigateTo(
@ -180,9 +200,7 @@ fun MovieDetails(
favorite = movie.data.userData?.isFavorite ?: false, favorite = movie.data.userData?.isFavorite ?: false,
series = null, series = null,
sourceId = chosenStreams?.sourceId, sourceId = chosenStreams?.sourceId,
navigateTo = viewModel::navigateTo, actions = moreActions,
onClickWatch = viewModel::setWatched,
onClickFavorite = viewModel::setFavorite,
onChooseVersion = { onChooseVersion = {
chooseVersion = chooseVersion =
chooseVersionParams( chooseVersionParams(
@ -218,18 +236,46 @@ fun MovieDetails(
) )
} }
}, },
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = true
},
), ),
) )
}, },
watchOnClick = { watchOnClick = {
viewModel.setWatched((movie.data.userData?.played ?: false).not()) viewModel.setWatched(movie.id, !movie.played)
}, },
favoriteOnClick = { 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 -> trailerOnClick = { trailer ->
when (trailer) { when (trailer) {
@ -283,19 +329,19 @@ fun MovieDetails(
waitToLoad = params.fromLongClick, waitToLoad = params.fromLongClick,
) )
} }
if (showPlaylistDialog) { showPlaylistDialog.compose { itemId ->
PlaylistDialog( PlaylistDialog(
title = stringResource(R.string.add_to_playlist), title = stringResource(R.string.add_to_playlist),
state = playlistState, state = playlistState,
onDismissRequest = { showPlaylistDialog = false }, onDismissRequest = { showPlaylistDialog.makeAbsent() },
onClick = { onClick = {
playlistViewModel.addToPlaylist(it.id, destination.itemId) playlistViewModel.addToPlaylist(it.id, itemId)
showPlaylistDialog = false showPlaylistDialog.makeAbsent()
}, },
createEnabled = true, createEnabled = true,
onCreatePlaylist = { onCreatePlaylist = {
playlistViewModel.createPlaylistAndAddItem(it, destination.itemId) playlistViewModel.createPlaylistAndAddItem(it, itemId)
showPlaylistDialog = false showPlaylistDialog.makeAbsent()
}, },
elevation = 3.dp, elevation = 3.dp,
) )
@ -323,8 +369,10 @@ fun MovieDetailsContent(
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
moreOnClick: () -> Unit, moreOnClick: () -> Unit,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onClickPerson: (Person) -> Unit, onClickPerson: (Person) -> Unit,
onLongClickPerson: (Int, Person) -> Unit,
onLongClickSimilar: (Int, BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -422,7 +470,10 @@ fun MovieDetailsContent(
position = PEOPLE_ROW position = PEOPLE_ROW
onClickPerson.invoke(it) onClickPerson.invoke(it)
}, },
onLongClick = {}, onLongClick = { index, person ->
position = PEOPLE_ROW
onLongClickPerson.invoke(index, person)
},
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -466,11 +517,14 @@ fun MovieDetailsContent(
ItemRow( ItemRow(
title = stringResource(R.string.more_like_this), title = stringResource(R.string.more_like_this),
items = similar, items = similar,
onClickItem = { onClickItem = { index, item ->
position = SIMILAR_ROW 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 -> cardContent = { index, item, mod, onClick, onLongClick ->
SeasonCard( SeasonCard(
item = item, 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.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler 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.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.ThemeSongPlayer import com.github.damontecres.wholphin.util.ThemeSongPlayer
@ -39,7 +40,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.libraryApi 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.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
@ -55,6 +55,7 @@ class MovieViewModel
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository, val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer, private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager,
@Assisted val itemId: UUID, @Assisted val itemId: UUID,
) : ViewModel() { ) : ViewModel() {
@AssistedFactory @AssistedFactory
@ -157,23 +158,19 @@ class MovieViewModel
} }
} }
fun setWatched(played: Boolean) = fun setWatched(
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { itemId: UUID,
if (played) { played: Boolean,
api.playStateApi.markPlayedItem(itemId) ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
} else { favoriteWatchManager.setWatched(itemId, played)
api.playStateApi.markUnplayedItem(itemId)
}
fetchAndSetItem() fetchAndSetItem()
} }
fun setFavorite(favorite: Boolean) = fun setFavorite(
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { itemId: UUID,
if (favorite) { favorite: Boolean,
api.userLibraryApi.markFavoriteItem(itemId) ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
} else { favoriteWatchManager.setFavorite(itemId, favorite)
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
fetchAndSetItem() fetchAndSetItem()
} }

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.ExpandableFaButton
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton
import com.github.damontecres.wholphin.ui.components.LoadingPage 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.OverviewText
import com.github.damontecres.wholphin.ui.components.SimpleStarRating 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.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo 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.isNotNullOrBlank
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
@ -74,7 +81,9 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemKind 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.extensions.ticks
import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
@Composable @Composable
@ -83,6 +92,7 @@ fun SeriesDetails(
destination: Destination.MediaItem, destination: Destination.MediaItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel(), viewModel: SeriesViewModel = hiltViewModel(),
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -98,6 +108,8 @@ fun SeriesDetails(
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var showWatchConfirmation by remember { mutableStateOf(false) } var showWatchConfirmation by remember { mutableStateOf(false) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) } 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) { when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state) is LoadingState.Error -> ErrorMessage(state)
@ -126,7 +138,9 @@ fun SeriesDetails(
played = played, played = played,
favorite = item.data.userData?.isFavorite ?: false, favorite = item.data.userData?.isFavorite ?: false,
modifier = modifier, modifier = modifier,
onClickItem = { viewModel.navigateTo(it.destination()) }, onClickItem = { index, item ->
viewModel.navigateTo(item.destination())
},
onClickPerson = { onClickPerson = {
viewModel.navigateTo( viewModel.navigateTo(
Destination.MediaItem( Destination.MediaItem(
@ -135,7 +149,7 @@ fun SeriesDetails(
), ),
) )
}, },
onLongClickItem = { season -> onLongClickItem = { index, season ->
seasonDialog = seasonDialog =
buildDialogForSeason( buildDialogForSeason(
context = context, context = context,
@ -160,6 +174,20 @@ fun SeriesDetails(
val favorite = item.data.userData?.isFavorite ?: false val favorite = item.data.userData?.isFavorite ?: false
viewModel.setFavorite(item.id, !favorite, null) 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) { if (showWatchConfirmation) {
ConfirmDialog( ConfirmDialog(
@ -194,6 +222,23 @@ fun SeriesDetails(
onDismissRequest = { seasonDialog = null }, 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 private const val HEADER_ROW = 0
@ -210,15 +255,17 @@ fun SeriesDetailsContent(
people: List<Person>, people: List<Person>,
played: Boolean, played: Boolean,
favorite: Boolean, favorite: Boolean,
onClickItem: (BaseItem) -> Unit, onClickItem: (Int, BaseItem) -> Unit,
onClickPerson: (Person) -> Unit, onClickPerson: (Person) -> Unit,
onLongClickItem: (BaseItem) -> Unit, onLongClickItem: (Int, BaseItem) -> Unit,
overviewOnClick: () -> Unit, overviewOnClick: () -> Unit,
playOnClick: () -> Unit, playOnClick: () -> Unit,
watchOnClick: () -> Unit, watchOnClick: () -> Unit,
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
moreActions: MoreDialogActions,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
@ -227,6 +274,7 @@ fun SeriesDetailsContent(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequesters.getOrNull(position)?.tryRequestFocus() focusRequesters.getOrNull(position)?.tryRequestFocus()
} }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
Box( Box(
modifier = modifier, modifier = modifier,
@ -349,11 +397,14 @@ fun SeriesDetailsContent(
ItemRow( ItemRow(
title = stringResource(R.string.tv_seasons), title = stringResource(R.string.tv_seasons),
items = seasons, items = seasons,
onClickItem = { onClickItem = { index, item ->
position = SEASONS_ROW position = SEASONS_ROW
onClickItem.invoke(it) onClickItem.invoke(index, item)
},
onLongClickItem = { index, item ->
position = SEASONS_ROW
onLongClickItem.invoke(index, item)
}, },
onLongClickItem = onLongClickItem,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -379,7 +430,21 @@ fun SeriesDetailsContent(
position = PEOPLE_ROW position = PEOPLE_ROW
onClickPerson.invoke(it) 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 =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -392,11 +457,29 @@ fun SeriesDetailsContent(
ItemRow( ItemRow(
title = stringResource(R.string.more_like_this), title = stringResource(R.string.more_like_this),
items = similar, items = similar,
onClickItem = { onClickItem = { index, item ->
position = SIMILAR_ROW 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 -> cardContent = { index, item, mod, onClick, onLongClick ->
SeasonCard( SeasonCard(
item = item, 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 @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.AddPlaylistViewModel
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo 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.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
@ -196,29 +197,28 @@ fun SeriesOverview(
favorite = ep.data.userData?.isFavorite ?: false, favorite = ep.data.userData?.isFavorite ?: false,
series = series, series = series,
sourceId = chosenStreams?.sourceId, sourceId = chosenStreams?.sourceId,
actions =
MoreDialogActions(
navigateTo = viewModel::navigateTo, navigateTo = viewModel::navigateTo,
onClickWatch = { played -> onClickWatch = { itemId, watched ->
episodeList
?.getOrNull(position.episodeRowIndex)
?.let {
viewModel.setWatched( viewModel.setWatched(
it.id, itemId,
played, watched,
position.episodeRowIndex, position.episodeRowIndex,
) )
}
}, },
onClickFavorite = { favorite -> onClickFavorite = { itemId, favorite ->
episodeList
?.getOrNull(position.episodeRowIndex)
?.let {
viewModel.setFavorite( viewModel.setFavorite(
it.id, itemId,
favorite, favorite,
position.episodeRowIndex, position.episodeRowIndex,
) )
}
}, },
onClickAddPlaylist = {
playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = it
},
),
onChooseVersion = { onChooseVersion = {
chooseVersion = chooseVersion =
chooseVersionParams( 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.ui.showToast
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler 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.GetEpisodesRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler
@ -36,9 +37,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.libraryApi 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.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.ItemSortBy
@ -61,6 +60,7 @@ class SeriesViewModel
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
private val itemPlaybackRepository: ItemPlaybackRepository, private val itemPlaybackRepository: ItemPlaybackRepository,
private val themeSongPlayer: ThemeSongPlayer, private val themeSongPlayer: ThemeSongPlayer,
private val favoriteWatchManager: FavoriteWatchManager,
) : ItemViewModel(api) { ) : ItemViewModel(api) {
private lateinit var seriesId: UUID private lateinit var seriesId: UUID
private lateinit var prefs: UserPreferences private lateinit var prefs: UserPreferences
@ -240,11 +240,7 @@ class SeriesViewModel
played: Boolean, played: Boolean,
listIndex: Int?, listIndex: Int?,
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { ) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
if (played) { favoriteWatchManager.setWatched(itemId, played)
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
listIndex?.let { listIndex?.let {
refreshEpisode(itemId, listIndex) refreshEpisode(itemId, listIndex)
} }
@ -255,11 +251,7 @@ class SeriesViewModel
favorite: Boolean, favorite: Boolean,
listIndex: Int?, listIndex: Int?,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) { favoriteWatchManager.setFavorite(itemId, favorite)
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
if (listIndex != null) { if (listIndex != null) {
refreshEpisode(itemId, listIndex) refreshEpisode(itemId, listIndex)
} else { } else {
@ -279,11 +271,7 @@ class SeriesViewModel
fun setWatchedSeries(played: Boolean) = fun setWatchedSeries(played: Boolean) =
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) { favoriteWatchManager.setWatched(seriesId, played)
api.playStateApi.markPlayedItem(seriesId)
} else {
api.playStateApi.markUnplayedItem(seriesId)
}
val series = fetchItem(seriesId) val series = fetchItem(seriesId)
val seasons = getSeasons(series) val seasons = getSeasons(series)
this@SeriesViewModel.seasons.setValueOnMain(seasons) 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.AddPlaylistViewModel
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.MoreDialogActions
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome 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.api.MediaType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID import java.util.UUID
import kotlin.time.Duration
@Composable @Composable
fun HomePage( fun HomePage(
@ -121,22 +121,20 @@ fun HomePage(
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
HomePageContent( HomePageContent(
watchingRows + latestRows, watchingRows + latestRows,
onClickItem = { onClickItem = { position, item ->
viewModel.navigationManager.navigateTo(it.destination()) viewModel.navigationManager.navigateTo(item.destination())
}, },
onLongClickItem = { onLongClickItem = { position, item ->
val dialogItems = val dialogItems =
buildMoreDialogItemsForHome( buildMoreDialogItemsForHome(
context = context, context = context,
item = it, item = item,
seriesId = it.data.seriesId, seriesId = item.data.seriesId,
playbackPosition = playbackPosition = item.playbackPosition,
it.data.userData watched = item.played,
?.playbackPositionTicks favorite = item.favorite,
?.ticks actions =
?: Duration.ZERO, MoreDialogActions(
watched = it.data.userData?.played ?: false,
favorite = it.data.userData?.isFavorite ?: false,
navigateTo = viewModel.navigationManager::navigateTo, navigateTo = viewModel.navigationManager::navigateTo,
onClickWatch = { itemId, played -> onClickWatch = { itemId, played ->
viewModel.setWatched(itemId, played) viewModel.setWatched(itemId, played)
@ -148,10 +146,11 @@ fun HomePage(
playlistViewModel.loadPlaylists(MediaType.VIDEO) playlistViewModel.loadPlaylists(MediaType.VIDEO)
showPlaylistDialog = itemId showPlaylistDialog = itemId
}, },
),
) )
dialog = dialog =
DialogParams( DialogParams(
title = it.title ?: "", title = item.title ?: "",
fromLongClick = true, fromLongClick = true,
items = dialogItems, items = dialogItems,
) )
@ -190,8 +189,8 @@ fun HomePage(
@Composable @Composable
fun HomePageContent( fun HomePageContent(
homeRows: List<HomeRowLoadingState>, homeRows: List<HomeRowLoadingState>,
onClickItem: (BaseItem) -> Unit, onClickItem: (RowColumn, BaseItem) -> Unit,
onLongClickItem: (BaseItem) -> Unit, onLongClickItem: (RowColumn, BaseItem) -> Unit,
showClock: Boolean, showClock: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onFocusPosition: ((RowColumn) -> Unit)? = null, onFocusPosition: ((RowColumn) -> Unit)? = null,
@ -317,14 +316,18 @@ fun HomePageContent(
ItemRow( ItemRow(
title = row.title, title = row.title,
items = row.items, items = row.items,
onClickItem = onClickItem, onClickItem = { index, item ->
onClickItem.invoke(RowColumn(rowIndex, index), item)
},
cardOnFocus = { isFocused, index -> cardOnFocus = { isFocused, index ->
if (isFocused) { if (isFocused) {
focusedItem = row.items.getOrNull(index) focusedItem = row.items.getOrNull(index)
position = RowColumn(rowIndex, index) position = RowColumn(rowIndex, index)
} }
}, },
onLongClickItem = onLongClickItem, onLongClickItem = { index, item ->
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
},
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -339,6 +342,7 @@ fun HomePageContent(
item?.data?.indexNumber?.let { "E$it" } item?.data?.indexNumber?.let { "E$it" }
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null }, ?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
played = item?.data?.userData?.played ?: false, played = item?.data?.userData?.played ?: false,
favorite = item?.favorite ?: false,
playPercent = playPercent =
item?.data?.userData?.playedPercentage item?.data?.userData?.playedPercentage
?: 0.0, ?: 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.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler 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.HomeRowLoadingState
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
@ -27,7 +28,6 @@ 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
@ -50,6 +50,7 @@ class HomeViewModel
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val navDrawerItemRepository: NavDrawerItemRepository, val navDrawerItemRepository: NavDrawerItemRepository,
private val favoriteWatchManager: FavoriteWatchManager,
) : ViewModel() { ) : ViewModel() {
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending) val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending) val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
@ -258,11 +259,7 @@ class HomeViewModel
itemId: UUID, itemId: UUID,
played: Boolean, played: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (played) { favoriteWatchManager.setWatched(itemId, played)
api.playStateApi.markPlayedItem(itemId)
} else {
api.playStateApi.markUnplayedItem(itemId)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
init(preferences) init(preferences)
} }
@ -272,11 +269,7 @@ class HomeViewModel
itemId: UUID, itemId: UUID,
favorite: Boolean, favorite: Boolean,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
if (favorite) { favoriteWatchManager.setFavorite(itemId, favorite)
api.userLibraryApi.markFavoriteItem(itemId)
} else {
api.userLibraryApi.unmarkFavoriteItem(itemId)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
init(preferences) init(preferences)
} }

View file

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