diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/Person.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/Person.kt new file mode 100644 index 00000000..a9660376 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/Person.kt @@ -0,0 +1,30 @@ +package com.github.damontecres.dolphin.data.model + +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.imageApi +import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.api.BaseItemPerson +import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.api.PersonKind + +data class Person( + val id: UUID, + val name: String?, + val role: String?, + val type: PersonKind, + val imageUrl: String?, +) { + companion object { + fun fromDto( + dto: BaseItemPerson, + api: ApiClient, + ): Person = + Person( + id = dto.id, + name = dto.name, + role = dto.role, + type = dto.type, + imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt index f9a0cbfa..ffe126ec 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt @@ -32,11 +32,12 @@ fun ItemRow( onLongClickItem: (BaseItem) -> Unit, modifier: Modifier = Modifier, cardContent: @Composable ( + index: Int, item: BaseItem?, modifier: Modifier, onClick: () -> Unit, onLongClick: () -> Unit, - ) -> Unit = { item, modifier, onClick, onLongClick -> + ) -> Unit = { index, item, modifier, onClick, onLongClick -> ItemCard( item = item, onClick = onClick, @@ -86,6 +87,7 @@ fun ItemRow( cardOnFocus?.invoke(it.isFocused, index) } cardContent.invoke( + index, item, cardModifier, { if (item != null) onClickItem.invoke(item) }, @@ -112,7 +114,7 @@ fun BannerItemRow( onClickItem = onClickItem, onLongClickItem = onLongClickItem, modifier = modifier, - cardContent = { item, modifier, onClick, onLongClick -> + cardContent = { index, item, modifier, onClick, onLongClick -> BannerCard( imageUrl = item?.imageUrl, aspectRatio = diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/PersonCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/PersonCard.kt new file mode 100644 index 00000000..0ed2131e --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/PersonCard.kt @@ -0,0 +1,94 @@ +package com.github.damontecres.dolphin.ui.cards + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.Text +import com.github.damontecres.dolphin.data.model.Person +import com.github.damontecres.dolphin.ui.enableMarquee +import kotlinx.coroutines.delay + +@Composable +fun PersonCard( + item: Person, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + cardWidth: Dp = 150.dp, + cardHeight: Dp = 200.dp, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val hideOverlayDelay = 1_000L + + val focused = interactionSource.collectIsFocusedAsState().value + var focusedAfterDelay by remember { mutableStateOf(false) } + + if (focused) { + LaunchedEffect(Unit) { + delay(hideOverlayDelay) + if (focused) { + focusedAfterDelay = true + } else { + focusedAfterDelay = false + } + } + } else { + focusedAfterDelay = false + } + + Card( + modifier = modifier, + onClick = onClick, + onLongClick = onLongClick, + interactionSource = interactionSource, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.width(cardWidth), + ) { + ItemCardImage( + imageUrl = item.imageUrl, + name = item.name, + showOverlay = true, + favorite = false, + watched = false, + unwatchedCount = -1, + watchedPercent = null, + modifier = + Modifier + .fillMaxWidth() + .height(cardHeight), + ) + Text( + text = item.name ?: "", + maxLines = 1, + modifier = + Modifier + .enableMarquee(focusedAfterDelay), + ) + item.role?.let { + Text( + text = item.role, + maxLines = 1, + modifier = + Modifier + .enableMarquee(focusedAfterDelay), + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt index 3c62f6df..d479578f 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/PlayButtons.kt @@ -137,7 +137,6 @@ fun ExpandablePlayButtons( ) { if (resumePosition > Duration.ZERO) { item { -// LaunchedEffect(Unit) { firstFocus.tryRequestFocus() } ExpandablePlayButton( R.string.resume, resumePosition, @@ -170,6 +169,7 @@ fun ExpandablePlayButtons( ) } } + // Watched button item { ExpandableFaButton( diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/data/RowColumn.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/data/RowColumn.kt new file mode 100644 index 00000000..300f3ef1 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/data/RowColumn.kt @@ -0,0 +1,16 @@ +package com.github.damontecres.dolphin.ui.data + +import androidx.compose.runtime.saveable.Saver +import kotlinx.serialization.Serializable + +@Serializable +data class RowColumn( + val row: Int, + val column: Int, +) + +val RowColumnSaver = + Saver>( + save = { listOf(it.row, it.column) }, + restore = { RowColumn(it[0], it[1]) }, + ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt index 5df1f00f..c1dcdb6d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt @@ -2,11 +2,17 @@ package com.github.damontecres.dolphin.ui.detail.movie import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -19,30 +25,39 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.dolphin.data.model.BaseItem +import com.github.damontecres.dolphin.data.model.Person import com.github.damontecres.dolphin.data.model.Video import com.github.damontecres.dolphin.preferences.UserPreferences +import com.github.damontecres.dolphin.ui.cards.PersonCard import com.github.damontecres.dolphin.ui.components.ErrorMessage import com.github.damontecres.dolphin.ui.components.ExpandablePlayButtons import com.github.damontecres.dolphin.ui.components.LoadingPage import com.github.damontecres.dolphin.ui.detail.LoadingItemViewModel import com.github.damontecres.dolphin.ui.isNotNullOrBlank +import com.github.damontecres.dolphin.ui.letNotEmpty import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.extensions.ticks +import java.util.UUID import javax.inject.Inject import kotlin.time.Duration @@ -51,7 +66,23 @@ class MovieViewModel @Inject constructor( api: ApiClient, - ) : LoadingItemViewModel