Show last played date on overview dialog (#1248)

## Description
Adds a line to the overview/media info dialog with the date when the
item was last played.

### Related issues
N/A

### Testing
Emulator

## Screenshots
<img width="1280" height="720" alt="last_played Large"
src="https://github.com/user-attachments/assets/1c38141d-1000-43d0-bf28-a0845edfacc2"
/>

## AI or LLM usage
None
This commit is contained in:
Ray 2026-04-16 00:26:57 -04:00 committed by GitHub
parent b18416c954
commit b5422fe82b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 35 additions and 60 deletions

View file

@ -47,6 +47,8 @@ fun formatDateTime(dateTime: LocalDateTime): String = getDateFormatter().format(
fun formatDate(dateTime: LocalDate): String = getDateFormatter().format(dateTime) fun formatDate(dateTime: LocalDate): String = getDateFormatter().format(dateTime)
fun formatDate(dateTime: LocalDateTime): String = getDateFormatter().format(dateTime)
fun toLocalDate(date: String?): LocalDate? = fun toLocalDate(date: String?): LocalDate? =
date?.let { date?.let {
try { try {

View file

@ -19,9 +19,12 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
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.studioNames
import com.github.damontecres.wholphin.ui.components.ScrollableDialog import com.github.damontecres.wholphin.ui.components.ScrollableDialog
import com.github.damontecres.wholphin.ui.formatBitrate import com.github.damontecres.wholphin.ui.formatBitrate
import com.github.damontecres.wholphin.ui.formatBytes import com.github.damontecres.wholphin.ui.formatBytes
import com.github.damontecres.wholphin.ui.formatDate
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.util.StreamFormatting.formatAudioCodec import com.github.damontecres.wholphin.ui.util.StreamFormatting.formatAudioCodec
@ -33,6 +36,7 @@ import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.VideoRange import org.jellyfin.sdk.model.api.VideoRange
import org.jellyfin.sdk.model.api.VideoRangeType import org.jellyfin.sdk.model.api.VideoRangeType
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import java.time.LocalDateTime
import java.util.Locale import java.util.Locale
data class ItemDetailsDialogInfo( data class ItemDetailsDialogInfo(
@ -41,7 +45,17 @@ data class ItemDetailsDialogInfo(
val genres: List<String>, val genres: List<String>,
val files: List<MediaSourceInfo>, val files: List<MediaSourceInfo>,
val studios: List<String> = emptyList(), val studios: List<String> = emptyList(),
val lastPlayed: LocalDateTime? = null,
) {
constructor(item: BaseItem) : this(
title = item.name ?: "",
overview = item.data.overview,
genres = item.data.genres.orEmpty(),
files = item.data.mediaSources.orEmpty(),
studios = item.studioNames,
lastPlayed = item.data.userData?.lastPlayedDate,
) )
}
/** /**
* Dialog showing metadata about an item * Dialog showing metadata about an item
@ -62,6 +76,7 @@ fun ItemDetailsDialog(
val bitrateLabel = stringResource(R.string.bitrate) val bitrateLabel = stringResource(R.string.bitrate)
val unknown = stringResource(R.string.unknown) val unknown = stringResource(R.string.unknown)
val runtimeLabel = stringResource(R.string.runtime_sort) val runtimeLabel = stringResource(R.string.runtime_sort)
val lastPlayedLabel = stringResource(R.string.last_played)
ScrollableDialog( ScrollableDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -93,6 +108,14 @@ fun ItemDetailsDialog(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
} }
val lastPlayed =
remember(info.lastPlayed) { info.lastPlayed?.let { formatDate(it) } }
if (lastPlayed != null) {
Text(
text = "$lastPlayedLabel: $lastPlayed",
style = MaterialTheme.typography.bodyMedium,
)
}
} }
} }

View file

@ -191,13 +191,7 @@ fun CollectionDetails(
modifier = modifier, modifier = modifier,
overviewOnClick = { overviewOnClick = {
val collection = state.collection!! val collection = state.collection!!
overviewDialog = overviewDialog = ItemDetailsDialogInfo(collection)
ItemDetailsDialogInfo(
title = collection.title ?: "",
overview = collection.data.overview,
genres = collection.data.genres.orEmpty(),
files = emptyList(),
)
}, },
favoriteOnClick = favoriteOnClick =
remember { remember {

View file

@ -149,12 +149,7 @@ fun EpisodeDetails(
}, },
overviewOnClick = { overviewOnClick = {
overviewDialog = overviewDialog =
ItemDetailsDialogInfo( ItemDetailsDialogInfo(ep)
title = ep.name ?: context.getString(R.string.unknown),
overview = ep.data.overview,
genres = ep.data.genres.orEmpty(),
files = ep.data.mediaSources.orEmpty(),
)
}, },
moreOnClick = { moreOnClick = {
moreDialog = moreDialog =
@ -218,13 +213,7 @@ fun EpisodeDetails(
onShowOverview = { onShowOverview = {
val source = chosenStreams?.source ?: ep.data.mediaSources?.firstOrNull() val source = chosenStreams?.source ?: ep.data.mediaSources?.firstOrNull()
if (source != null) { if (source != null) {
overviewDialog = overviewDialog = ItemDetailsDialogInfo(ep)
ItemDetailsDialogInfo(
title = ep.name ?: context.getString(R.string.unknown),
overview = ep.data.overview,
genres = ep.data.genres.orEmpty(),
files = listOf(source),
)
} }
}, },
onClearChosenStreams = { onClearChosenStreams = {

View file

@ -35,7 +35,6 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.data.model.aspectRatioFloat
import com.github.damontecres.wholphin.data.model.studioNames
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.AspectRatios
@ -181,13 +180,7 @@ fun MovieDetails(
}, },
overviewOnClick = { overviewOnClick = {
overviewDialog = overviewDialog =
ItemDetailsDialogInfo( ItemDetailsDialogInfo(movie)
title = movie.name ?: unknownStr,
overview = movie.data.overview,
genres = movie.data.genres.orEmpty(),
files = movie.data.mediaSources.orEmpty(),
studios = movie.studioNames,
)
}, },
moreOnClick = { moreOnClick = {
moreDialog = moreDialog =
@ -250,13 +243,7 @@ fun MovieDetails(
} }
}, },
onShowOverview = { onShowOverview = {
overviewDialog = overviewDialog = ItemDetailsDialogInfo(movie)
ItemDetailsDialogInfo(
title = movie.name ?: unknownStr,
overview = movie.data.overview,
genres = movie.data.genres.orEmpty(),
files = movie.data.mediaSources.orEmpty(),
)
}, },
onClearChosenStreams = { onClearChosenStreams = {
viewModel.clearChosenStreams(chosenStreams) viewModel.clearChosenStreams(chosenStreams)

View file

@ -210,14 +210,7 @@ fun SeriesDetails(
} }
}, },
overviewOnClick = { overviewOnClick = {
overviewDialog = overviewDialog = ItemDetailsDialogInfo(item)
ItemDetailsDialogInfo(
title = item.name ?: context.getString(R.string.unknown),
overview = item.data.overview,
genres = item.data.genres.orEmpty(),
studios = item.studioNames,
files = listOf(),
)
}, },
playOnClick = { shuffle -> playOnClick = { shuffle ->
if (shuffle) { if (shuffle) {

View file

@ -265,13 +265,7 @@ fun SeriesOverview(
} }
}, },
onShowOverview = { onShowOverview = {
overviewDialog = overviewDialog = ItemDetailsDialogInfo(ep)
ItemDetailsDialogInfo(
title = ep.name ?: context.getString(R.string.unknown),
overview = ep.data.overview,
genres = ep.data.genres.orEmpty(),
files = ep.data.mediaSources.orEmpty(),
)
}, },
onClearChosenStreams = { onClearChosenStreams = {
viewModel.clearChosenStreams(ep, chosenStreams) viewModel.clearChosenStreams(ep, chosenStreams)
@ -357,15 +351,7 @@ fun SeriesOverview(
}, },
overviewOnClick = { overviewOnClick = {
episodeList?.getOrNull(position.episodeRowIndex)?.let { episodeList?.getOrNull(position.episodeRowIndex)?.let {
scope.launchDefault { overviewDialog = ItemDetailsDialogInfo(it)
overviewDialog =
ItemDetailsDialogInfo(
title = it.name ?: context.getString(R.string.unknown),
overview = it.data.overview,
genres = it.data.genres.orEmpty(),
files = it.data.mediaSources.orEmpty(),
)
}
} }
}, },
personOnClick = { personOnClick = {

View file

@ -756,6 +756,7 @@
<string name="discover_tv">Discover TV Shows</string> <string name="discover_tv">Discover TV Shows</string>
<string name="discover_movies">Discover Movies</string> <string name="discover_movies">Discover Movies</string>
<string name="studios_in">Studios in %1$s</string> <string name="studios_in">Studios in %1$s</string>
<string name="last_played">Last played</string>
<string name="ass_subtitle_mode_libass">Direct play with libass</string> <string name="ass_subtitle_mode_libass">Direct play with libass</string>
<string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string> <string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string>