mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
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:
parent
c4df751918
commit
f1f6b729aa
8 changed files with 241 additions and 153 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue