mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Various UI/UX tweaks
This commit is contained in:
parent
005d6b11ce
commit
2212e32c53
22 changed files with 237 additions and 149 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ fun SortByButton(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO switch to dialog?
|
||||
DropdownMenu(
|
||||
expanded = sortByDropDown,
|
||||
onDismissRequest = { sortByDropDown = false },
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ fun PlaybackContent(
|
|||
AmbientPlayerListener(player)
|
||||
|
||||
if (stream == null) {
|
||||
// TODO loading
|
||||
LoadingPage()
|
||||
} else {
|
||||
stream?.let {
|
||||
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
|
||||
|
|
|
|||
|
|
@ -288,7 +288,6 @@ fun LeftPlaybackButtons(
|
|||
)
|
||||
}
|
||||
if (showMoreOptions) {
|
||||
// TODO options need context about what to display
|
||||
val options =
|
||||
buildList {
|
||||
addAll(moreButtonOptions.options.keys)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ fun ServerList(
|
|||
},
|
||||
onClick = { onSwitchServer.invoke(server) },
|
||||
onLongClick = {
|
||||
// TODO dialog to remove server
|
||||
showDeleteDialog = server
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue