Fix several UI focus issues (#81)

Remembers the page position & focused item on both Movie & Series detail
pages

Only loads similar movies & series once so that if you click on one and
go back, it doesn't refresh the entire list

Remember the tab on the Favorite page (if remember tabs is enabled in
settings)
This commit is contained in:
damontecres 2025-10-27 20:44:04 -04:00 committed by GitHub
parent c4df751918
commit f1f6b729aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 241 additions and 153 deletions

View file

@ -27,7 +27,6 @@ import org.jellyfin.sdk.api.okhttp.OkHttpFactory
import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.createJellyfin
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.DeviceInfo
import java.util.UUID
import javax.inject.Qualifier import javax.inject.Qualifier
import javax.inject.Singleton import javax.inject.Singleton
@ -144,11 +143,11 @@ object AppModule {
appPreference: DataStore<AppPreferences>, appPreference: DataStore<AppPreferences>,
@IoCoroutineScope scope: CoroutineScope, @IoCoroutineScope scope: CoroutineScope,
) = object : RememberTabManager { ) = object : RememberTabManager {
fun key(itemId: UUID) = "${serverRepository.currentServer?.id}_${serverRepository.currentUser?.id}_$itemId" fun key(itemId: String) = "${serverRepository.currentServer?.id}_${serverRepository.currentUser?.id}_$itemId"
override fun getRememberedTab( override fun getRememberedTab(
preferences: UserPreferences, preferences: UserPreferences,
itemId: UUID, itemId: String,
defaultTab: Int, defaultTab: Int,
): Int { ): Int {
if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) {
@ -161,7 +160,7 @@ object AppModule {
override fun saveRememberedTab( override fun saveRememberedTab(
preferences: UserPreferences, preferences: UserPreferences,
itemId: UUID, itemId: String,
tabIndex: Int, tabIndex: Int,
) { ) {
if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) {

View file

@ -321,6 +321,15 @@ fun rememberPosition(initialPosition: RowColumn = RowColumn(-1, -1)) =
) )
} }
/**
* Convenient way to [rememberSaveable] a [RowColumn]
*/
@Composable
fun rememberPosition(
row: Int,
column: Int,
) = rememberPosition(RowColumn(row, column))
/** /**
* Convenient way to [rememberSaveable] a Int * Convenient way to [rememberSaveable] a Int
*/ */

View file

@ -34,7 +34,7 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@ -46,29 +46,31 @@ fun FavoritesPage(
preferencesViewModel: PreferencesViewModel = hiltViewModel(), preferencesViewModel: PreferencesViewModel = hiltViewModel(),
) { ) {
val uiPrefs = preferences.appPreferences.interfacePreferences val uiPrefs = preferences.appPreferences.interfacePreferences
val rememberedTabIndex = 0 val rememberedTabIndex =
// TODO remember tab remember {
// if (uiPrefs.rememberSelectedTab) { preferencesViewModel.getRememberedTab(
// uiPrefs.getRememberedTabsOrDefault(destination.itemId.toString(), 0) preferences,
// } else { NavDrawerItem.Favorites.id,
// 0 0,
// } )
}
val tabs = listOf("Movies", "TV Shows", "Episodes") val tabs = listOf("Movies", "TV Shows", "Episodes")
val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } }
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val firstTabFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) {
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } tabFocusRequesters.getOrNull(selectedTabIndex)?.tryRequestFocus()
}
// if (uiPrefs.rememberSelectedTab) { LaunchedEffect(selectedTabIndex) {
// LaunchedEffect(selectedTabIndex) { preferencesViewModel.saveRememberedTab(
// preferencesViewModel.preferenceDataStore.updateData { preferences,
// preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex) NavDrawerItem.Favorites.id,
// } selectedTabIndex,
// } )
// } }
var showHeader by rememberSaveable { mutableStateOf(true) } var showHeader by rememberSaveable { mutableStateOf(true) }
val onClickItem = { item: BaseItem -> val onClickItem = { item: BaseItem ->
@ -89,7 +91,7 @@ fun FavoritesPage(
modifier = modifier =
Modifier Modifier
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp) .padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
.focusRestorer(firstTabFocusRequester) .focusRestorer(tabFocusRequesters[0])
.onFocusChanged { .onFocusChanged {
if (!it.isFocused) { if (!it.isFocused) {
focusTabIndex = selectedTabIndex focusTabIndex = selectedTabIndex
@ -126,10 +128,7 @@ fun FavoritesPage(
modifier = modifier =
Modifier Modifier
.padding(8.dp) .padding(8.dp)
.ifElse( .focusRequester(tabFocusRequesters[index]),
index == selectedTabIndex,
Modifier.focusRequester(firstTabFocusRequester),
),
) )
} }
} }

View file

@ -69,6 +69,7 @@ import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
@ -243,6 +244,12 @@ fun MovieDetails(
} }
} }
private const val HEADER_ROW = 0
private const val PEOPLE_ROW = HEADER_ROW + 1
private const val TRAILER_ROW = PEOPLE_ROW + 1
private const val CHAPTER_ROW = TRAILER_ROW + 1
private const val SIMILAR_ROW = CHAPTER_ROW + 1
@Composable @Composable
fun MovieDetailsContent( fun MovieDetailsContent(
preferences: UserPreferences, preferences: UserPreferences,
@ -262,15 +269,15 @@ fun MovieDetailsContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() } var position by rememberInt(0)
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
val dto = movie.data val dto = movie.data
val backdropImageUrl = movie.backdropImageUrl val backdropImageUrl = movie.backdropImageUrl
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// bringIntoViewRequester.bringIntoView() focusRequesters.getOrNull(position)?.tryRequestFocus()
focusRequester.tryRequestFocus()
} }
Box(modifier = modifier) { Box(modifier = modifier) {
if (backdropImageUrl.isNotNullOrBlank()) { if (backdropImageUrl.isNotNullOrBlank()) {
@ -329,18 +336,22 @@ fun MovieDetailsContent(
resumePosition = resumePosition, resumePosition = resumePosition,
watched = dto.userData?.played ?: false, watched = dto.userData?.played ?: false,
favorite = dto.userData?.isFavorite ?: false, favorite = dto.userData?.isFavorite ?: false,
playOnClick = playOnClick, playOnClick = {
position = HEADER_ROW
playOnClick.invoke(it)
},
moreOnClick = moreOnClick, moreOnClick = moreOnClick,
watchOnClick = watchOnClick, watchOnClick = watchOnClick,
favoriteOnClick = favoriteOnClick, favoriteOnClick = favoriteOnClick,
buttonOnFocusChanged = { buttonOnFocusChanged = {
if (it.isFocused) { if (it.isFocused) {
position = HEADER_ROW
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView() bringIntoViewRequester.bringIntoView()
} }
} }
}, },
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequesters[HEADER_ROW]),
) )
} }
} }
@ -348,9 +359,14 @@ fun MovieDetailsContent(
item { item {
PersonRow( PersonRow(
people = people, people = people,
onClick = {}, onClick = {
position = PEOPLE_ROW
},
onLongClick = {}, onLongClick = {},
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[PEOPLE_ROW]),
) )
} }
} }
@ -358,8 +374,14 @@ fun MovieDetailsContent(
item { item {
TrailerRow( TrailerRow(
trailers = trailers, trailers = trailers,
onClickTrailer = trailerOnClick, onClickTrailer = {
modifier = Modifier.fillMaxWidth(), position = TRAILER_ROW
trailerOnClick.invoke(it)
},
modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[TRAILER_ROW]),
) )
} }
} }
@ -367,9 +389,15 @@ fun MovieDetailsContent(
item { item {
ChapterRow( ChapterRow(
chapters = chapters, chapters = chapters,
onClick = { playOnClick.invoke(it.position) }, onClick = {
position = CHAPTER_ROW
playOnClick.invoke(it.position)
},
onLongClick = {}, onLongClick = {},
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[CHAPTER_ROW]),
) )
} }
} }
@ -378,7 +406,10 @@ fun MovieDetailsContent(
ItemRow( ItemRow(
title = stringResource(R.string.more_like_this), title = stringResource(R.string.more_like_this),
items = similar, items = similar,
onClickItem = onClickItem, onClickItem = {
position = SIMILAR_ROW
onClickItem.invoke(it)
},
onLongClickItem = {}, onLongClickItem = {},
cardContent = { index, item, mod, onClick, onLongClick -> cardContent = { index, item, mod, onClick, onLongClick ->
SeasonCard( SeasonCard(
@ -391,7 +422,10 @@ fun MovieDetailsContent(
imageWidth = Dp.Unspecified, imageWidth = Dp.Unspecified,
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[SIMILAR_ROW]),
) )
} }
} }

View file

@ -19,6 +19,8 @@ import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -47,7 +49,7 @@ class MovieViewModel
val trailers = MutableLiveData<List<Trailer>>(listOf()) val trailers = MutableLiveData<List<Trailer>>(listOf())
val people = MutableLiveData<List<Person>>(listOf()) val people = MutableLiveData<List<Person>>(listOf())
val chapters = MutableLiveData<List<Chapter>>(listOf()) val chapters = MutableLiveData<List<Chapter>>(listOf())
val similar = MutableLiveData<List<BaseItem>>(listOf()) val similar = MutableLiveData<List<BaseItem>>()
val chosenStreams = MutableLiveData<ChosenStreams?>(null) val chosenStreams = MutableLiveData<ChosenStreams?>(null)
override fun init( override fun init(
@ -55,14 +57,21 @@ class MovieViewModel
potential: BaseItem?, potential: BaseItem?,
): Job? { ): Job? {
this.itemId = itemId this.itemId = itemId
return viewModelScope.launch(ExceptionHandler()) { return viewModelScope.launch(
super.init(itemId, potential)?.join() Dispatchers.IO +
LoadingExceptionHandler(
loading,
"Error fetching movie",
),
) {
fetchAndSetItem(itemId)
item.value?.let { item -> item.value?.let { item ->
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
withContext(Dispatchers.Main) {
chosenStreams.value = result
loading.value = LoadingState.Success
}
viewModelScope.launchIO { viewModelScope.launchIO {
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
withContext(Dispatchers.Main) {
chosenStreams.value = result
}
val remoteTrailers = val remoteTrailers =
item.data.remoteTrailers item.data.remoteTrailers
?.mapNotNull { t -> ?.mapNotNull { t ->
@ -98,19 +107,20 @@ class MovieViewModel
}.orEmpty() }.orEmpty()
chapters.value = Chapter.Companion.fromDto(item.data, api) chapters.value = Chapter.Companion.fromDto(item.data, api)
} }
if (!similar.isInitialized) {
val similar = val similar =
api.libraryApi api.libraryApi
.getSimilarItems( .getSimilarItems(
GetSimilarItemsRequest( GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id, userId = serverRepository.currentUser?.id,
itemId = itemId, itemId = itemId,
fields = SlimItemFields, fields = SlimItemFields,
limit = 25, limit = 25,
), ),
).content.items ).content.items
.map { BaseItem.Companion.from(it, api) } .map { BaseItem.Companion.from(it, api) }
this@MovieViewModel.similar.setValueOnMain(similar) this@MovieViewModel.similar.setValueOnMain(similar)
}
} }
} }
} }

View file

@ -63,12 +63,10 @@ import com.github.damontecres.wholphin.ui.components.StarRating
import com.github.damontecres.wholphin.ui.components.StarRatingPrecision import com.github.damontecres.wholphin.ui.components.StarRatingPrecision
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.ifElse
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.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -183,6 +181,11 @@ fun SeriesDetails(
} }
} }
private const val HEADER_ROW = 0
private const val SEASONS_ROW = HEADER_ROW + 1
private const val PEOPLE_ROW = SEASONS_ROW + 1
private const val SIMILAR_ROW = PEOPLE_ROW + 1
@Composable @Composable
fun SeriesDetailsContent( fun SeriesDetailsContent(
preferences: UserPreferences, preferences: UserPreferences,
@ -200,11 +203,14 @@ fun SeriesDetailsContent(
favoriteOnClick: () -> Unit, favoriteOnClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scope = rememberCoroutineScope()
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
var position by rememberPosition() var position by rememberInt()
val focusRequester = remember { FocusRequester() } val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) {
focusRequesters.getOrNull(position)?.tryRequestFocus()
}
Box( Box(
modifier = modifier, modifier = modifier,
@ -256,51 +262,95 @@ fun SeriesDetailsContent(
played = played, played = played,
favorite = favorite, favorite = favorite,
overviewOnClick = overviewOnClick, overviewOnClick = overviewOnClick,
playOnClick = playOnClick, playOnClick = {
position = HEADER_ROW
playOnClick.invoke()
},
watchOnClick = watchOnClick, watchOnClick = watchOnClick,
favoriteOnClick = favoriteOnClick, favoriteOnClick = favoriteOnClick,
bringIntoViewRequester = bringIntoViewRequester, bringIntoViewRequester = bringIntoViewRequester,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(.6f) .fillMaxWidth(.7f)
.bringIntoViewRequester(bringIntoViewRequester) .bringIntoViewRequester(bringIntoViewRequester)
.padding(bottom = 80.dp) .padding(bottom = 8.dp),
.ifElse(position.row < 0, Modifier.focusRequester(focusRequester)),
) )
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier =
Modifier
.padding(start = 16.dp)
.focusRequester(focusRequesters[HEADER_ROW])
.padding(bottom = 80.dp),
) {
ExpandablePlayButton(
title = R.string.play,
resume = Duration.ZERO,
icon = Icons.Default.PlayArrow,
onClick = {
position = HEADER_ROW
playOnClick.invoke()
},
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
ExpandableFaButton(
title = if (played) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (played) R.string.fa_eye else R.string.fa_eye_slash,
onClick = watchOnClick,
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
ExpandableFaButton(
title = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
onClick = favoriteOnClick,
iconColor = if (favorite) Color.Red else Color.Unspecified,
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
}
} }
item { item {
ItemRow( ItemRow(
title = "Seasons", title = "Seasons",
items = seasons.items, items = seasons.items,
onClickItem = onClickItem, onClickItem = {
onLongClickItem = onLongClickItem, position = SEASONS_ROW
cardOnFocus = { isFocused, index -> onClickItem.invoke(it)
// if (isFocused) {
// scope.launch(ExceptionHandler()) {
// bringIntoViewRequester.bringIntoView()
// }
// }
}, },
onLongClickItem = onLongClickItem,
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.focusRequester(focusRequesters[SEASONS_ROW]),
cardContent = @Composable { index, item, mod, onClick, onLongClick -> cardContent = @Composable { index, item, mod, onClick, onLongClick ->
SeasonCard( SeasonCard(
item = item, item = item,
onClick = { onClick = onClick,
position = RowColumn(0, index)
onClick.invoke()
},
onLongClick = onLongClick, onLongClick = onLongClick,
imageHeight = Cards.height2x3, imageHeight = Cards.height2x3,
imageWidth = Dp.Unspecified, imageWidth = Dp.Unspecified,
showImageOverlay = true, showImageOverlay = true,
modifier = modifier = mod,
mod
.ifElse(
position.row == 0 && position.column == index,
Modifier.focusRequester(focusRequester),
),
) )
}, },
) )
@ -309,9 +359,14 @@ fun SeriesDetailsContent(
item { item {
PersonRow( PersonRow(
people = people, people = people,
onClick = {}, onClick = {
position = PEOPLE_ROW
},
onLongClick = {}, onLongClick = {},
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[PEOPLE_ROW]),
) )
} }
} }
@ -320,7 +375,10 @@ fun SeriesDetailsContent(
ItemRow( ItemRow(
title = stringResource(R.string.more_like_this), title = stringResource(R.string.more_like_this),
items = similar, items = similar,
onClickItem = onClickItem, onClickItem = {
position = SIMILAR_ROW
onClickItem.invoke(it)
},
onLongClickItem = {}, onLongClickItem = {},
cardContent = { index, item, mod, onClick, onLongClick -> cardContent = { index, item, mod, onClick, onLongClick ->
SeasonCard( SeasonCard(
@ -333,7 +391,10 @@ fun SeriesDetailsContent(
imageWidth = Dp.Unspecified, imageWidth = Dp.Unspecified,
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier =
Modifier
.fillMaxWidth()
.focusRequester(focusRequesters[SIMILAR_ROW]),
) )
} }
} }
@ -409,52 +470,6 @@ fun SeriesDetailsHeader(
textBoxHeight = Dp.Unspecified, textBoxHeight = Dp.Unspecified,
) )
} }
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(start = 16.dp),
) {
ExpandablePlayButton(
title = R.string.play,
resume = Duration.ZERO,
icon = Icons.Default.PlayArrow,
onClick = { playOnClick.invoke() },
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
ExpandableFaButton(
title = if (played) R.string.mark_unwatched else R.string.mark_watched,
iconStringRes = if (played) R.string.fa_eye else R.string.fa_eye_slash,
onClick = watchOnClick,
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
ExpandableFaButton(
title = if (favorite) R.string.remove_favorite else R.string.add_favorite,
iconStringRes = R.string.fa_heart,
onClick = favoriteOnClick,
iconColor = if (favorite) Color.Red else Color.Unspecified,
modifier =
Modifier.onFocusChanged {
if (it.isFocused) {
scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView()
}
}
},
)
}
} }
} }

View file

@ -71,7 +71,7 @@ class SeriesViewModel
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty()) val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading) val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
val people = MutableLiveData<List<Person>>(listOf()) val people = MutableLiveData<List<Person>>(listOf())
val similar = MutableLiveData<List<BaseItem>>(listOf()) val similar = MutableLiveData<List<BaseItem>>()
fun init( fun init(
prefs: UserPreferences, prefs: UserPreferences,
@ -107,18 +107,20 @@ class SeriesViewModel
people.map { Person.fromDto(it, api) } people.map { Person.fromDto(it, api) }
}.orEmpty() }.orEmpty()
} }
val similar = if (!similar.isInitialized) {
api.libraryApi val similar =
.getSimilarItems( api.libraryApi
GetSimilarItemsRequest( .getSimilarItems(
userId = serverRepository.currentUser?.id, GetSimilarItemsRequest(
itemId = itemId, userId = serverRepository.currentUser?.id,
fields = SlimItemFields, itemId = itemId,
limit = 25, fields = SlimItemFields,
), limit = 25,
).content.items ),
.map { BaseItem.from(it, api, true) } ).content.items
this@SeriesViewModel.similar.setValueOnMain(similar) .map { BaseItem.from(it, api, true) }
this@SeriesViewModel.similar.setValueOnMain(similar)
}
} }
} }
@ -320,7 +322,9 @@ class SeriesViewModel
).content.items ).content.items
.firstOrNull() .firstOrNull()
if (nextUp != null) { if (nextUp != null) {
navigateTo(Destination.Playback(nextUp.id, 0L)) withContext(Dispatchers.Main) {
navigateTo(Destination.Playback(nextUp.id, 0L))
}
} else { } else {
showToast( showToast(
context, context,

View file

@ -11,6 +11,15 @@ interface RememberTabManager {
preferences: UserPreferences, preferences: UserPreferences,
itemId: UUID, itemId: UUID,
defaultTab: Int, defaultTab: Int,
): Int = getRememberedTab(preferences, itemId.toString(), defaultTab)
/**
* If enabled, get the remembered tab index for the given item
*/
fun getRememberedTab(
preferences: UserPreferences,
itemId: String,
defaultTab: Int,
): Int ): Int
/** /**
@ -20,5 +29,14 @@ interface RememberTabManager {
preferences: UserPreferences, preferences: UserPreferences,
itemId: UUID, itemId: UUID,
tabIndex: Int, tabIndex: Int,
) = saveRememberedTab(preferences, itemId.toString(), tabIndex)
/**
* If enabled, save the remembered tab index for the given item
*/
fun saveRememberedTab(
preferences: UserPreferences,
itemId: String,
tabIndex: Int,
) )
} }