Various UI/UX tweaks

This commit is contained in:
Damontecres 2025-10-07 21:14:33 -04:00
parent 005d6b11ce
commit 2212e32c53
No known key found for this signature in database
22 changed files with 237 additions and 149 deletions

View file

@ -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 {

View file

@ -65,7 +65,6 @@ fun SortByButton(
)
}
// TODO switch to dialog?
DropdownMenu(
expanded = sortByDropDown,
onDismissRequest = { sortByDropDown = false },

View file

@ -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),

View file

@ -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,
)

View file

@ -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)
},
)
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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),

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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<BaseItem?>(
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),

View file

@ -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)

View file

@ -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()
}
}

View file

@ -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()
}

View file

@ -121,7 +121,7 @@ fun PlaybackContent(
AmbientPlayerListener(player)
if (stream == null) {
// TODO loading
LoadingPage()
} else {
stream?.let {
var contentScale by remember { mutableStateOf(ContentScale.Fit) }

View file

@ -288,7 +288,6 @@ fun LeftPlaybackButtons(
)
}
if (showMoreOptions) {
// TODO options need context about what to display
val options =
buildList {
addAll(moreButtonOptions.options.keys)

View file

@ -73,7 +73,7 @@ fun ServerList(
},
onClick = { onSwitchServer.invoke(server) },
onLongClick = {
// TODO dialog to remove server
showDeleteDialog = server
},
modifier = Modifier,
)

View file

@ -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
}

View file

@ -60,5 +60,6 @@
<string name="pref_key_update_last_check">preference.update.lastTimestamp</string>
<string name="confirm">Confirm</string>
<string name="remember_selected_tab">Remember selected tabs</string>
<string name="director">Director</string>
</resources>