mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
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:
parent
5e2c75bb70
commit
2e3e4e3f5f
29 changed files with 766 additions and 289 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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)
|
onFocusPosition = onFocusPosition,
|
||||||
val rows by viewModel.rows.collectAsState()
|
modifier = modifier,
|
||||||
|
)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
onFocusPosition = onFocusPosition,
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
modifier = modifier,
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 -> {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,25 +158,21 @@ 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun savePlayVersion(
|
fun savePlayVersion(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
navigateTo = viewModel::navigateTo,
|
actions =
|
||||||
onClickWatch = { played ->
|
MoreDialogActions(
|
||||||
episodeList
|
navigateTo = viewModel::navigateTo,
|
||||||
?.getOrNull(position.episodeRowIndex)
|
onClickWatch = { itemId, watched ->
|
||||||
?.let {
|
|
||||||
viewModel.setWatched(
|
viewModel.setWatched(
|
||||||
it.id,
|
itemId,
|
||||||
played,
|
watched,
|
||||||
position.episodeRowIndex,
|
position.episodeRowIndex,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
},
|
onClickFavorite = { itemId, favorite ->
|
||||||
onClickFavorite = { 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
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,37 +121,36 @@ 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,
|
navigateTo = viewModel.navigationManager::navigateTo,
|
||||||
favorite = it.data.userData?.isFavorite ?: false,
|
onClickWatch = { itemId, played ->
|
||||||
navigateTo = viewModel.navigationManager::navigateTo,
|
viewModel.setWatched(itemId, played)
|
||||||
onClickWatch = { itemId, played ->
|
},
|
||||||
viewModel.setWatched(itemId, played)
|
onClickFavorite = { itemId, favorite ->
|
||||||
},
|
viewModel.setFavorite(itemId, favorite)
|
||||||
onClickFavorite = { itemId, favorite ->
|
},
|
||||||
viewModel.setFavorite(itemId, favorite)
|
onClickAddPlaylist = { itemId ->
|
||||||
},
|
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
||||||
onClickAddPlaylist = { itemId ->
|
showPlaylistDialog = itemId
|
||||||
playlistViewModel.loadPlaylists(MediaType.VIDEO)
|
},
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue