diff --git a/app/src/main/java/hu/bbara/purefin/ui/common/card/MediaImageCard.kt b/app/src/main/java/hu/bbara/purefin/ui/common/card/MediaImageCard.kt index 4ef8b125..bba968bc 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/common/card/MediaImageCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/common/card/MediaImageCard.kt @@ -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 = 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,26 +84,55 @@ fun MediaImageCard( ) imageOverlay() } - Column(modifier = Modifier.padding(textPadding)) { - Text( - text = title, - style = titleStyle, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - subtitle - ?.takeIf { it.isNotBlank() } - ?.let { text -> - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = text, - style = subtitleStyle, - color = scheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.padding(textPadding)) { + Text( + text = title, + style = titleStyle, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + subtitle + ?.takeIf { it.isNotBlank() } + ?.let { text -> + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = text, + style = subtitleStyle, + color = scheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + 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 + ) + } + } } + } } } } diff --git a/app/src/main/java/hu/bbara/purefin/ui/model/MediaAction.kt b/app/src/main/java/hu/bbara/purefin/ui/model/MediaAction.kt new file mode 100644 index 00000000..f5a58418 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/ui/model/MediaAction.kt @@ -0,0 +1,6 @@ +package hu.bbara.purefin.ui.model + +data class MediaAction( + val name: String, + val onClick: () -> Unit +) \ No newline at end of file diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt index 1e1b42e1..e4c79fca 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt @@ -91,6 +91,7 @@ fun AppScreen( library.name ) }, + onMarkWatched = viewModel::markAsWatched, onProfileClick = {}, onCheckForUpdates = { updateViewModel.checkForUpdates() }, isCheckingForUpdates = isCheckingForUpdates, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt index 4177dd5d..1acd8fe1 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/HomeScreen.kt @@ -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) ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt index 1a53f11f..37e43528 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeContent.kt @@ -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 ) } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt index 8a35af7c..85b673b8 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingCard.kt @@ -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, diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt index 0798b67c..801ed5c7 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/continuewatching/ContinueWatchingSection.kt @@ -21,6 +21,7 @@ import hu.bbara.purefin.ui.model.MediaUiModel fun ContinueWatchingSection( items: List, 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 ) } } diff --git a/core/src/main/java/hu/bbara/purefin/data/MediaProgressWriter.kt b/core/src/main/java/hu/bbara/purefin/data/MediaUpdater.kt similarity index 100% rename from core/src/main/java/hu/bbara/purefin/data/MediaProgressWriter.kt rename to core/src/main/java/hu/bbara/purefin/data/MediaUpdater.kt diff --git a/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt b/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt index a16a19df..92e3ce58 100644 --- a/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/feature/browse/home/AppViewModel.kt @@ -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)))