mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat(MediaImageCard): add dropdown menu for marking media as watched/unwatched
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package hu.bbara.purefin.ui.common.card
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -12,11 +14,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -26,6 +39,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||
import hu.bbara.purefin.ui.model.MediaAction
|
||||
|
||||
@Composable
|
||||
fun MediaImageCard(
|
||||
@@ -34,6 +48,7 @@ fun MediaImageCard(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: String? = null,
|
||||
popupActions: List<MediaAction> = emptyList(),
|
||||
imageModifier: Modifier = Modifier,
|
||||
shapeSize: Dp = 12.dp,
|
||||
imageAspectRatio: Float = 16f / 10f,
|
||||
@@ -45,6 +60,8 @@ fun MediaImageCard(
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val shape = RoundedCornerShape(shapeSize)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Card(
|
||||
onClick = onClick,
|
||||
shape = shape,
|
||||
@@ -67,6 +84,11 @@ fun MediaImageCard(
|
||||
)
|
||||
imageOverlay()
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.padding(textPadding)) {
|
||||
Text(
|
||||
text = title,
|
||||
@@ -88,6 +110,30 @@ fun MediaImageCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (popupActions.isNotEmpty()) {
|
||||
Box() {
|
||||
IconButton(
|
||||
onClick = { expanded = !expanded },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = "More actions"
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
popupActions.forEach { action ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = action.name) },
|
||||
onClick = action.onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package hu.bbara.purefin.ui.model
|
||||
|
||||
data class MediaAction(
|
||||
val name: String,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
@@ -91,6 +91,7 @@ fun AppScreen(
|
||||
library.name
|
||||
)
|
||||
},
|
||||
onMarkWatched = viewModel::markAsWatched,
|
||||
onProfileClick = {},
|
||||
onCheckForUpdates = { updateViewModel.checkForUpdates() },
|
||||
isCheckingForUpdates = isCheckingForUpdates,
|
||||
|
||||
@@ -26,6 +26,7 @@ fun HomeScreen(
|
||||
onRefresh: () -> Unit,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
onMarkWatched: (MediaUiModel, Boolean) -> Unit,
|
||||
onProfileClick: () -> Unit,
|
||||
onCheckForUpdates: () -> Unit,
|
||||
isCheckingForUpdates: Boolean,
|
||||
@@ -71,6 +72,7 @@ fun HomeScreen(
|
||||
onMediaSelected = onMediaSelected,
|
||||
onLibrarySelected = onLibrarySelected,
|
||||
onBrowseLibrariesClick = { onTabSelected(1) },
|
||||
onMarkAsWatched = onMarkWatched,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ fun HomeContent(
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
onLibrarySelected: (LibraryUiModel) -> Unit,
|
||||
onBrowseLibrariesClick: () -> Unit,
|
||||
onMarkAsWatched: (MediaUiModel, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
@@ -105,6 +106,7 @@ fun HomeContent(
|
||||
item(key = "continue-watching") {
|
||||
ContinueWatchingSection(
|
||||
items = continueWatching,
|
||||
onMarkAsWatched = onMarkAsWatched,
|
||||
onMediaSelected = onMediaSelected
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import hu.bbara.purefin.ui.common.bar.MediaProgressBar
|
||||
import hu.bbara.purefin.ui.common.card.MediaImageCard
|
||||
import hu.bbara.purefin.ui.common.media.homeMediaSharedBoundsSource
|
||||
import hu.bbara.purefin.ui.common.media.rememberHomeMediaSharedBoundsClick
|
||||
import hu.bbara.purefin.ui.model.MediaAction
|
||||
import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
|
||||
@Composable
|
||||
@@ -22,6 +23,7 @@ internal fun ContinueWatchingCard(
|
||||
item: MediaUiModel,
|
||||
sharedBoundsKey: String,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
onMarkAsWatched: (MediaUiModel, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
@@ -34,6 +36,16 @@ internal fun ContinueWatchingCard(
|
||||
title = item.primaryText,
|
||||
subtitle = item.secondaryText,
|
||||
onClick = onClick,
|
||||
popupActions = listOf(
|
||||
MediaAction(
|
||||
name = "Mark as watched",
|
||||
onClick = { onMarkAsWatched(item, true) }
|
||||
),
|
||||
MediaAction(
|
||||
name = "Mark as unwatched",
|
||||
onClick = { onMarkAsWatched(item, false) }
|
||||
)
|
||||
),
|
||||
imageModifier = Modifier.homeMediaSharedBoundsSource(sharedBoundsKey),
|
||||
shapeSize = 26.dp,
|
||||
imageAspectRatio = 16f / 9f,
|
||||
|
||||
@@ -21,6 +21,7 @@ import hu.bbara.purefin.ui.model.MediaUiModel
|
||||
fun ContinueWatchingSection(
|
||||
items: List<MediaUiModel>,
|
||||
onMediaSelected: (MediaUiModel) -> Unit,
|
||||
onMarkAsWatched: (MediaUiModel, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (items.isEmpty()) return
|
||||
@@ -47,7 +48,8 @@ fun ContinueWatchingSection(
|
||||
ContinueWatchingCard(
|
||||
item = item,
|
||||
sharedBoundsKey = homeMediaSharedBoundsKey("continue-$index", item.id),
|
||||
onMediaSelected = onMediaSelected
|
||||
onMediaSelected = onMediaSelected,
|
||||
onMarkAsWatched = onMarkAsWatched
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import hu.bbara.purefin.data.HomeRepository
|
||||
import hu.bbara.purefin.data.LocalMediaRepository
|
||||
import hu.bbara.purefin.data.UserSessionRepository
|
||||
import hu.bbara.purefin.download.MediaDownloadController
|
||||
import hu.bbara.purefin.jellyfin.JellyfinMediaMetadataUpdater
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import hu.bbara.purefin.model.Media
|
||||
import hu.bbara.purefin.navigation.EpisodeDto
|
||||
@@ -34,8 +35,9 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class AppViewModel @Inject constructor(
|
||||
private val homeRepository: HomeRepository,
|
||||
private val mediaCatalogReader: LocalMediaRepository,
|
||||
private val localMediaRepository: LocalMediaRepository,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val mediaDownloadManager: MediaDownloadController,
|
||||
) : ViewModel() {
|
||||
@@ -59,8 +61,8 @@ class AppViewModel @Inject constructor(
|
||||
posterUrl = it.posterUrl,
|
||||
size = it.size,
|
||||
isEmpty = when (it.type) {
|
||||
LibraryKind.MOVIES -> mediaCatalogReader.movies.value.isEmpty()
|
||||
LibraryKind.SERIES -> mediaCatalogReader.series.value.isEmpty()
|
||||
LibraryKind.MOVIES -> localMediaRepository.movies.value.isEmpty()
|
||||
LibraryKind.SERIES -> localMediaRepository.series.value.isEmpty()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -68,9 +70,9 @@ class AppViewModel @Inject constructor(
|
||||
|
||||
val suggestions = combine(
|
||||
homeRepository.suggestions,
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.series,
|
||||
mediaCatalogReader.episodes
|
||||
localMediaRepository.movies,
|
||||
localMediaRepository.series,
|
||||
localMediaRepository.episodes
|
||||
) { list, moviesMap, seriesMap, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -94,8 +96,8 @@ class AppViewModel @Inject constructor(
|
||||
|
||||
val continueWatching = combine(
|
||||
homeRepository.continueWatching,
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.episodes
|
||||
localMediaRepository.movies,
|
||||
localMediaRepository.episodes
|
||||
) { list, moviesMap, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -116,7 +118,7 @@ class AppViewModel @Inject constructor(
|
||||
|
||||
val nextUp = combine(
|
||||
homeRepository.nextUp,
|
||||
mediaCatalogReader.episodes
|
||||
localMediaRepository.episodes
|
||||
) { list, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -134,9 +136,9 @@ class AppViewModel @Inject constructor(
|
||||
|
||||
val latestLibraryContent = combine(
|
||||
homeRepository.latestLibraryContent,
|
||||
mediaCatalogReader.movies,
|
||||
mediaCatalogReader.series,
|
||||
mediaCatalogReader.episodes
|
||||
localMediaRepository.movies,
|
||||
localMediaRepository.series,
|
||||
localMediaRepository.episodes
|
||||
) { libraryMap, moviesMap, seriesMap, episodesMap ->
|
||||
libraryMap.mapValues { (_, items) ->
|
||||
items.mapNotNull { media ->
|
||||
@@ -162,6 +164,12 @@ class AppViewModel @Inject constructor(
|
||||
initialValue = emptyMap()
|
||||
)
|
||||
|
||||
fun markAsWatched(mediaUiModel: MediaUiModel, watched: Boolean) {
|
||||
viewModelScope.launch {
|
||||
jellyfinMediaMetadataUpdater.markAsWatched(mediaUiModel.id, watched)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLibrarySelected(id: UUID, name: String) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.LibraryRoute(library = LibraryDto(id = id, name = name)))
|
||||
|
||||
Reference in New Issue
Block a user