From 2212e32c53fa0c4a8362444b97163dab6230d911 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 7 Oct 2025 21:14:33 -0400 Subject: [PATCH] Various UI/UX tweaks --- .../damontecres/dolphin/DolphinApplication.kt | 6 +- .../dolphin/ui/components/SortByButton.kt | 1 - .../dolphin/ui/components/SwitchWithLabel.kt | 6 - .../damontecres/dolphin/ui/detail/CardGrid.kt | 10 +- .../ui/detail/CollectionFolderDetails.kt | 50 +++++--- .../ui/detail/CollectionFolderMovie.kt | 106 +++++++++-------- .../dolphin/ui/detail/CollectionFolderTv.kt | 107 ++++++++++-------- .../dolphin/ui/detail/SeriesViewModel.kt | 2 +- .../dolphin/ui/detail/movie/MovieDetails.kt | 7 +- .../ui/detail/movie/MovieDetailsHeader.kt | 19 +++- .../ui/detail/series/FocusedEpisodeHeader.kt | 2 +- .../ui/detail/series/SeriesOverview.kt | 3 +- .../ui/detail/series/SeriesOverviewContent.kt | 33 +++++- .../damontecres/dolphin/ui/main/HomePage.kt | 6 +- .../damontecres/dolphin/ui/nav/Destination.kt | 3 - .../dolphin/ui/nav/DestinationContent.kt | 5 +- .../damontecres/dolphin/ui/nav/NavDrawer.kt | 2 +- .../dolphin/ui/playback/PlaybackContent.kt | 2 +- .../dolphin/ui/playback/PlaybackControls.kt | 1 - .../dolphin/ui/setup/ServerList.kt | 2 +- .../damontecres/dolphin/util/Formatting.kt | 12 +- app/src/main/res/values/strings.xml | 1 + 22 files changed, 237 insertions(+), 149 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt b/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt index 67977f1f..c15fcb6b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/DolphinApplication.kt @@ -9,8 +9,10 @@ class DolphinApplication : Application() { init { instance = this - // TODO only plant in debug builds - Timber.plant(Timber.DebugTree()) + if (BuildConfig.DEBUG) { + // TODO minimal logging for release builds? + Timber.plant(Timber.DebugTree()) + } } companion object { diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt index 57f17b91..74c40c27 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SortByButton.kt @@ -65,7 +65,6 @@ fun SortByButton( ) } - // TODO switch to dialog? DropdownMenu( expanded = sortByDropDown, onDismissRequest = { sortByDropDown = false }, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SwitchWithLabel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SwitchWithLabel.kt index d6dd0c74..5b3a25a3 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/SwitchWithLabel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/SwitchWithLabel.kt @@ -1,6 +1,5 @@ package com.github.damontecres.dolphin.ui.components -import android.widget.Toast import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -51,11 +50,6 @@ fun SwitchWithLabel( onClick = { if (enabled) { onStateChange(!checked) - } else { - // TODO there are other uses, so shouldn't hardcode this toast - Toast - .makeText(context, "Item has no children", Toast.LENGTH_SHORT) - .show() } }, ).padding(8.dp), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt index 3caee1a0..542c29ab 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType @@ -253,6 +254,7 @@ fun CardGrid( Modifier .fillMaxSize() .focusGroup() + .focusRestorer(firstFocus) .focusRequester(gridFocusRequester) .focusProperties { onExit = { @@ -264,6 +266,7 @@ fun CardGrid( onEnter = { focusedIndexOnExit = -1 if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { + Timber.d("onEnter: focusedIndex=$focusedIndex, savedFocusedIndex=$savedFocusedIndex") focusedIndex = startPosition firstFocus.tryRequestFocus() } @@ -317,7 +320,12 @@ fun CardGrid( } }, item = item, - onClick = { if (item != null) itemOnClick.invoke(item) }, + onClick = { + if (item != null) { + itemOnClick.invoke(item) + savedFocusedIndex = index + } + }, onLongClick = { if (item != null) longClicker.invoke(item) }, cardHeight = null, ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt index d87aba42..b6a62508 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt @@ -1,5 +1,10 @@ package com.github.damontecres.dolphin.ui.detail +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -8,7 +13,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.text.style.TextAlign @@ -136,6 +144,7 @@ fun CollectionFolderDetails( SortOrder.ASCENDING, ), showTitle: Boolean = true, + positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { OneTimeLaunchedEffect { viewModel.init(destination.itemId, destination.item, initialSortAndDirection) @@ -163,6 +172,7 @@ fun CollectionFolderDetails( viewModel.loadResults(it) }, showTitle = showTitle, + positionCallback = positionCallback, ) } } @@ -180,6 +190,7 @@ fun CollectionDetails( onSortChange: (SortAndDirection) -> Unit, modifier: Modifier = Modifier, showTitle: Boolean = true, + positionCallback: ((columns: Int, position: Int) -> Unit)? = null, ) { val title = library.name ?: item.data.name ?: item.data.collectionType?.name ?: "Collection" val sortOptions = @@ -190,27 +201,35 @@ fun CollectionDetails( else -> listOf(ItemSortBy.SORT_NAME, ItemSortBy.DATE_CREATED, ItemSortBy.RANDOM) } + var showHeader by rememberSaveable { mutableStateOf(true) } + val gridFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() } Column( verticalArrangement = Arrangement.spacedBy(0.dp), modifier = modifier, ) { - if (showTitle) { - Text( - text = title, - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), + AnimatedVisibility( + showHeader, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + if (showTitle) { + Text( + text = title, + style = MaterialTheme.typography.displayMedium, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } + SortByButton( + sortOptions = sortOptions, + current = sortAndDirection, + onSortChange = onSortChange, + modifier = Modifier, ) } - SortByButton( - sortOptions = sortOptions, - current = sortAndDirection, - onSortChange = onSortChange, - modifier = Modifier, - ) CardGrid( pager = pager, itemOnClick = { @@ -229,7 +248,10 @@ fun CollectionDetails( showJumpButtons = false, // TODO add preference modifier = Modifier.fillMaxSize(), initialPosition = 0, - positionCallback = { _, _ -> }, + positionCallback = { columns, position -> + showHeader = position < columns + positionCallback?.invoke(columns, position) + }, ) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt index b6db39f5..101d304b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderMovie.kt @@ -1,5 +1,10 @@ package com.github.damontecres.dolphin.ui.detail +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -7,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -64,62 +70,69 @@ fun CollectionFolderMovie( } } } + var showHeader by rememberSaveable { mutableStateOf(true) } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Column( modifier = modifier, ) { - TabRow( - selectedTabIndex = focusTabIndex, - modifier = - Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) - .focusRestorer(firstTabFocusRequester) - .onFocusChanged { - if (!it.isFocused) { - focusTabIndex = selectedTabIndex - } - }, - indicator = - @Composable { tabPositions, doesTabRowHaveFocus -> - tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition -> + AnimatedVisibility( + showHeader, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + TabRow( + selectedTabIndex = focusTabIndex, + modifier = + Modifier + .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .focusRestorer(firstTabFocusRequester) + .onFocusChanged { + if (!it.isFocused) { + focusTabIndex = selectedTabIndex + } + }, + indicator = + @Composable { tabPositions, doesTabRowHaveFocus -> + tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition -> // TabRowDefaults.PillIndicator( // currentTabPosition = currentTabPosition, // doesTabRowHaveFocus = doesTabRowHaveFocus, // ) - TabRowDefaults.UnderlinedIndicator( - currentTabPosition = currentTabPosition, - doesTabRowHaveFocus = doesTabRowHaveFocus, - activeColor = MaterialTheme.colorScheme.border, - ) + TabRowDefaults.UnderlinedIndicator( + currentTabPosition = currentTabPosition, + doesTabRowHaveFocus = doesTabRowHaveFocus, + activeColor = MaterialTheme.colorScheme.border, + ) + } + }, + tabs = { + tabs.forEachIndexed { index, title -> + Tab( + selected = focusTabIndex == index, + onClick = { selectedTabIndex = index }, + onFocus = { focusTabIndex = index }, + colors = + TabDefaults.pillIndicatorTabColors( + focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .padding(8.dp) + .ifElse( + index == selectedTabIndex, + Modifier.focusRequester(firstTabFocusRequester), + ), + ) + } } }, - tabs = { - tabs.forEachIndexed { index, title -> - Tab( - selected = focusTabIndex == index, - onClick = { selectedTabIndex = index }, - onFocus = { focusTabIndex = index }, - colors = - TabDefaults.pillIndicatorTabColors( - focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - modifier = - Modifier - .padding(8.dp) - .ifElse( - index == selectedTabIndex, - Modifier.focusRequester(firstTabFocusRequester), - ), - ) - } - } - }, - ) + ) + } when (selectedTabIndex) { 0 -> { RecommendedMovie( @@ -144,6 +157,9 @@ fun CollectionFolderMovie( .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, ) } else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt index 5c2e3581..be7c6631 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderTv.kt @@ -1,5 +1,10 @@ package com.github.damontecres.dolphin.ui.detail +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -7,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -65,61 +71,69 @@ fun CollectionFolderTv( } } + var showHeader by rememberSaveable { mutableStateOf(true) } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } Column( modifier = modifier, ) { - TabRow( - selectedTabIndex = focusTabIndex, - modifier = - Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) - .focusRestorer(firstTabFocusRequester) - .onFocusChanged { - if (!it.isFocused) { - focusTabIndex = selectedTabIndex - } - }, - indicator = - @Composable { tabPositions, doesTabRowHaveFocus -> - tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition -> + AnimatedVisibility( + showHeader, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + TabRow( + selectedTabIndex = focusTabIndex, + modifier = + Modifier + .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .focusRestorer(firstTabFocusRequester) + .onFocusChanged { + if (!it.isFocused) { + focusTabIndex = selectedTabIndex + } + }, + indicator = + @Composable { tabPositions, doesTabRowHaveFocus -> + tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition -> // TabRowDefaults.PillIndicator( // currentTabPosition = currentTabPosition, // doesTabRowHaveFocus = doesTabRowHaveFocus, // ) - TabRowDefaults.UnderlinedIndicator( - currentTabPosition = currentTabPosition, - doesTabRowHaveFocus = doesTabRowHaveFocus, - activeColor = MaterialTheme.colorScheme.border, - ) + TabRowDefaults.UnderlinedIndicator( + currentTabPosition = currentTabPosition, + doesTabRowHaveFocus = doesTabRowHaveFocus, + activeColor = MaterialTheme.colorScheme.border, + ) + } + }, + tabs = { + tabs.forEachIndexed { index, title -> + Tab( + selected = focusTabIndex == index, + onClick = { selectedTabIndex = index }, + onFocus = { focusTabIndex = index }, + colors = + TabDefaults.pillIndicatorTabColors( + focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .padding(8.dp) + .ifElse( + index == selectedTabIndex, + Modifier.focusRequester(firstTabFocusRequester), + ), + ) + } } }, - tabs = { - tabs.forEachIndexed { index, title -> - Tab( - selected = focusTabIndex == index, - onClick = { selectedTabIndex = index }, - onFocus = { focusTabIndex = index }, - colors = - TabDefaults.pillIndicatorTabColors( - focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - modifier = - Modifier - .padding(8.dp) - .ifElse( - index == selectedTabIndex, - Modifier.focusRequester(firstTabFocusRequester), - ), - ) - } - } - }, - ) + ) + } when (selectedTabIndex) { 0 -> { RecommendedTvShow( @@ -144,6 +158,9 @@ fun CollectionFolderTv( .padding(start = 16.dp) .fillMaxSize() .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, ) } else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt index c25c3c76..c5305509 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesViewModel.kt @@ -225,7 +225,7 @@ class SeriesViewModel try { loadEpisodesInternal(season) } catch (e: Exception) { - Timber.Forest.e(e, "Error loading episodes for $seriesId for season $season") + Timber.e(e, "Error loading episodes for $seriesId for season $season") // TODO show error in UI? ItemListAndMapping.empty() } 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 dde4faf5..793c1008 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 @@ -90,9 +90,10 @@ class MovieViewModel super.init(itemId, potential)?.join() item.value?.let { item -> people.value = - item.data.people?.letNotEmpty { people -> - people.map { Person.fromDto(it, api) } - } + item.data.people + ?.letNotEmpty { people -> + people.map { Person.fromDto(it, api) } + }.orEmpty() chapters.value = Chapter.fromDto(item.data, api) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetailsHeader.kt index 31cc4937..e4d53218 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetailsHeader.kt @@ -43,6 +43,7 @@ import com.github.damontecres.dolphin.ui.playSoundOnFocus import com.github.damontecres.dolphin.ui.roundMinutes import com.github.damontecres.dolphin.ui.timeRemaining import org.jellyfin.sdk.model.api.MediaStreamType +import org.jellyfin.sdk.model.api.PersonKind import org.jellyfin.sdk.model.extensions.ticks @Composable @@ -93,13 +94,13 @@ fun MovieDetailsHeader( add(it) } dto.timeRemaining?.roundMinutes?.let { add("$it left") } + dto.officialRating?.let(::add) } DotSeparatedRow( texts = details, textStyle = MaterialTheme.typography.titleLarge, ) } - // TODO ratings? dto.communityRating?.let { if (it > 0f) { StarRating( @@ -196,15 +197,21 @@ fun MovieDetailsHeader( ) } } - // TODO director - // TODO writers - dto.studios?.letNotEmpty { + // TODO add writers, studio, etc to overview dialog + dto.people?.firstOrNull { it.type == PersonKind.DIRECTOR }?.name?.let { TitleValueText( - stringResource(R.string.studios), - it.joinToString(", ") { s -> s.name ?: "" }, + stringResource(R.string.director), + it, modifier = Modifier.widthIn(max = 80.dp), ) } +// dto.studios?.letNotEmpty { +// TitleValueText( +// stringResource(R.string.studios), +// it.joinToString(", ") { s -> s.name ?: "" }, +// modifier = Modifier.widthIn(max = 80.dp), +// ) +// } dto.genres?.letNotEmpty { TitleValueText( stringResource(R.string.genres), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt index fa3e2433..1e6de9a1 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -67,6 +67,7 @@ fun FocusedEpisodeHeader( add(it) } dto.timeRemaining?.roundMinutes?.let { add("$it left") } + dto.officialRating?.let(::add) } DotSeparatedRow( texts = details, @@ -77,7 +78,6 @@ fun FocusedEpisodeHeader( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - // TODO ratings? dto.communityRating?.let { if (it > 0f) { StarRating( diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt index 83bfcf34..cc0e71a5 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt @@ -80,7 +80,6 @@ fun SeriesOverview( val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty()) val episodes by viewModel.episodes.observeAsState(ItemListAndMapping.empty()) - // TODO need to translate season/episode into tab/row index in case of missing seasons/episodes var position by rememberSaveable( destination, loading, @@ -103,7 +102,7 @@ fun SeriesOverview( LaunchedEffect(episodes.items) { if (episodes.items.isNotEmpty()) { - // TODO focus on first episode when changing seasons + // TODO focus on first episode when changing seasons? // firstItemFocusRequester.requestFocus() episodes.items.getOrNull(position.episodeRowIndex)?.let { viewModel.refreshEpisode(it.id, position.episodeRowIndex) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt index 768aab59..f738054f 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverviewContent.kt @@ -30,6 +30,7 @@ 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.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale @@ -38,6 +39,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Tab import androidx.tv.material3.TabDefaults import androidx.tv.material3.TabRow +import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.dolphin.data.model.BaseItem @@ -67,7 +69,7 @@ fun SeriesOverviewContent( modifier: Modifier = Modifier, ) { val bringIntoViewRequester = remember { BringIntoViewRequester() } - // TODO need to map between season index and tab index in case of missing seasons + var focusTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) } var selectedTabIndex by rememberSaveable(position) { mutableIntStateOf(position.seasonTabIndex) } val focusRequesters = remember(seasons.size) { List(seasons.size) { FocusRequester() } } var resolvedTabIndex by remember { mutableIntStateOf(selectedTabIndex) } @@ -121,7 +123,7 @@ fun SeriesOverviewContent( ) { item { TabRow( - selectedTabIndex = selectedTabIndex, + selectedTabIndex = focusTabIndex, modifier = Modifier .ifElse( @@ -129,20 +131,39 @@ fun SeriesOverviewContent( { Modifier.focusRestorer(focusRequesters[selectedTabIndex]) }, ).focusRequester(tabRowFocusRequester) .padding(horizontal = 16.dp) - .fillMaxWidth(), + .fillMaxWidth() + .onFocusChanged { + if (!it.isFocused) { + focusTabIndex = selectedTabIndex + } + }, + indicator = + @Composable { tabPositions, doesTabRowHaveFocus -> + tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition -> +// TabRowDefaults.PillIndicator( +// currentTabPosition = currentTabPosition, +// doesTabRowHaveFocus = doesTabRowHaveFocus, +// ) + TabRowDefaults.UnderlinedIndicator( + currentTabPosition = currentTabPosition, + doesTabRowHaveFocus = doesTabRowHaveFocus, + activeColor = MaterialTheme.colorScheme.border, + ) + } + }, ) { seasons.forEachIndexed { index, season -> season?.let { season -> Tab( - selected = index == selectedTabIndex, - onFocus = {}, + selected = focusTabIndex == index, + onFocus = { focusTabIndex = index }, onClick = { selectedTabIndex = index onFocus.invoke(SeriesOverviewPosition(index, 0)) }, colors = TabDefaults.pillIndicatorTabColors( - // TODO + focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, ), modifier = Modifier diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt index 431e1fea..2253d917 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt @@ -96,7 +96,7 @@ fun HomePageContent( var position by rememberSaveable(stateSaver = RowColumnSaver) { mutableStateOf(RowColumn(0, 0)) } - // TODO use position instead? + var focusedItem by remember { mutableStateOf( homeRows.getOrNull(0)?.items?.getOrNull( @@ -178,7 +178,7 @@ fun HomePageContent( }, onLongClickItem = {}, modifier = Modifier.fillMaxWidth(), - cardContent = { index, item, modifier, onClick, onLongClick -> + cardContent = { index, item, cardModifier, onClick, onLongClick -> // TODO better aspect ration handling? BannerCard( imageUrl = item?.imageUrl, @@ -189,7 +189,7 @@ fun HomePageContent( onClick = onClick, onLongClick = onLongClick, modifier = - modifier + cardModifier .ifElse( focusedItem == item, Modifier.focusRequester(focusRequester), diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt index 432ed775..169cceea 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/Destination.kt @@ -16,9 +16,6 @@ import java.util.UUID sealed class Destination( val fullScreen: Boolean = false, ) : NavKey { - @Serializable - data object Setup : Destination(true) - @Serializable data object ServerList : Destination(true) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt index 6bc6744d..dc5cdf6b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/DestinationContent.kt @@ -11,7 +11,6 @@ import com.github.damontecres.dolphin.ui.detail.CollectionFolderTv import com.github.damontecres.dolphin.ui.detail.EpisodeDetails import com.github.damontecres.dolphin.ui.detail.SeasonDetails import com.github.damontecres.dolphin.ui.detail.SeriesDetails -import com.github.damontecres.dolphin.ui.detail.VideoDetails import com.github.damontecres.dolphin.ui.detail.movie.MovieDetails import com.github.damontecres.dolphin.ui.detail.series.SeriesOverview import com.github.damontecres.dolphin.ui.main.HomePage @@ -105,7 +104,8 @@ fun DestinationContent( ) BaseItemKind.VIDEO -> - VideoDetails( + // TODO Use VideoDetails + MovieDetails( preferences, navigationManager, destination, @@ -153,6 +153,5 @@ fun DestinationContent( userPreferences = preferences, modifier = modifier, ) - Destination.Setup -> TODO() } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt index 1f7c3561..a70a4c12 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt @@ -120,7 +120,7 @@ fun NavDrawer( val scope = rememberCoroutineScope() val context = LocalContext.current val drawerFocusRequester = remember { FocusRequester() } - BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination == Destination.Main)) { + BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Main)) { drawerState.setValue(DrawerValue.Open) drawerFocusRequester.requestFocus() } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt index afe8c6cd..622b83bc 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackContent.kt @@ -121,7 +121,7 @@ fun PlaybackContent( AmbientPlayerListener(player) if (stream == null) { - // TODO loading + LoadingPage() } else { stream?.let { var contentScale by remember { mutableStateOf(ContentScale.Fit) } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt index dc4a7b72..886308ee 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt @@ -288,7 +288,6 @@ fun LeftPlaybackButtons( ) } if (showMoreOptions) { - // TODO options need context about what to display val options = buildList { addAll(moreButtonOptions.options.keys) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt index cdc39f52..bf559de3 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt @@ -73,7 +73,7 @@ fun ServerList( }, onClick = { onSwitchServer.invoke(server) }, onLongClick = { - // TODO dialog to remove server + showDeleteDialog = server }, modifier = Modifier, ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/Formatting.kt b/app/src/main/java/com/github/damontecres/dolphin/util/Formatting.kt index f6064ee9..c61d7fc1 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/Formatting.kt @@ -15,10 +15,11 @@ fun formatDateTime(dateTime: LocalDateTime): String = dateTime.toString() } -// TODO multi episode support val BaseItemDto.seasonEpisode: String? get() = - if (parentIndexNumber != null && indexNumber != null) { + if (parentIndexNumber != null && indexNumber != null && indexNumberEnd != null) { + "S$parentIndexNumber E$indexNumber-E$indexNumberEnd" + } else if (parentIndexNumber != null && indexNumber != null) { "S$parentIndexNumber E$indexNumber" } else { null @@ -29,7 +30,12 @@ val BaseItemDto.seasonEpisodePadded: String? if (parentIndexNumber != null && indexNumber != null) { val season = parentIndexNumber?.toString()?.padStart(2, '0') val episode = indexNumber?.toString()?.padStart(2, '0') - "S${season}E$episode" + val endEpisode = indexNumberEnd?.toString()?.padStart(2, '0') + if (endEpisode != null) { + "S${season}E$episode-E$endEpisode" + } else { + "S${season}E$episode" + } } else { null } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d4fe706..8b90cb7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,5 +60,6 @@ preference.update.lastTimestamp Confirm Remember selected tabs + Director