diff --git a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt index c6c86de6..c1f9da80 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt @@ -27,7 +27,6 @@ import org.jellyfin.sdk.api.okhttp.OkHttpFactory import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.DeviceInfo -import java.util.UUID import javax.inject.Qualifier import javax.inject.Singleton @@ -144,11 +143,11 @@ object AppModule { appPreference: DataStore, @IoCoroutineScope scope: CoroutineScope, ) = 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( preferences: UserPreferences, - itemId: UUID, + itemId: String, defaultTab: Int, ): Int { if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { @@ -161,7 +160,7 @@ object AppModule { override fun saveRememberedTab( preferences: UserPreferences, - itemId: UUID, + itemId: String, tabIndex: Int, ) { if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt index 49ab7d89..c21d9231 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt @@ -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 */ diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index c7d7b140..92fa57c9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -34,7 +34,7 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid 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.tryRequestFocus import org.jellyfin.sdk.model.api.BaseItemKind @@ -46,29 +46,31 @@ fun FavoritesPage( preferencesViewModel: PreferencesViewModel = hiltViewModel(), ) { val uiPrefs = preferences.appPreferences.interfacePreferences - val rememberedTabIndex = 0 - // TODO remember tab -// if (uiPrefs.rememberSelectedTab) { -// uiPrefs.getRememberedTabsOrDefault(destination.itemId.toString(), 0) -// } else { -// 0 -// } + val rememberedTabIndex = + remember { + preferencesViewModel.getRememberedTab( + preferences, + NavDrawerItem.Favorites.id, + 0, + ) + } val tabs = listOf("Movies", "TV Shows", "Episodes") + val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } - val firstTabFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } - -// if (uiPrefs.rememberSelectedTab) { -// LaunchedEffect(selectedTabIndex) { -// preferencesViewModel.preferenceDataStore.updateData { -// preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex) -// } -// } -// } + LaunchedEffect(Unit) { + tabFocusRequesters.getOrNull(selectedTabIndex)?.tryRequestFocus() + } + LaunchedEffect(selectedTabIndex) { + preferencesViewModel.saveRememberedTab( + preferences, + NavDrawerItem.Favorites.id, + selectedTabIndex, + ) + } var showHeader by rememberSaveable { mutableStateOf(true) } val onClickItem = { item: BaseItem -> @@ -89,7 +91,7 @@ fun FavoritesPage( modifier = Modifier .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) - .focusRestorer(firstTabFocusRequester) + .focusRestorer(tabFocusRequesters[0]) .onFocusChanged { if (!it.isFocused) { focusTabIndex = selectedTabIndex @@ -126,10 +128,7 @@ fun FavoritesPage( modifier = Modifier .padding(8.dp) - .ifElse( - index == selectedTabIndex, - Modifier.focusRequester(firstTabFocusRequester), - ), + .focusRequester(tabFocusRequesters[index]), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 3015d903..134b2b1e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -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.isNotNullOrBlank 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.util.ExceptionHandler 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 fun MovieDetailsContent( preferences: UserPreferences, @@ -262,15 +269,15 @@ fun MovieDetailsContent( modifier: Modifier = Modifier, ) { 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 backdropImageUrl = movie.backdropImageUrl val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO val bringIntoViewRequester = remember { BringIntoViewRequester() } LaunchedEffect(Unit) { -// bringIntoViewRequester.bringIntoView() - focusRequester.tryRequestFocus() + focusRequesters.getOrNull(position)?.tryRequestFocus() } Box(modifier = modifier) { if (backdropImageUrl.isNotNullOrBlank()) { @@ -329,18 +336,22 @@ fun MovieDetailsContent( resumePosition = resumePosition, watched = dto.userData?.played ?: false, favorite = dto.userData?.isFavorite ?: false, - playOnClick = playOnClick, + playOnClick = { + position = HEADER_ROW + playOnClick.invoke(it) + }, moreOnClick = moreOnClick, watchOnClick = watchOnClick, favoriteOnClick = favoriteOnClick, buttonOnFocusChanged = { if (it.isFocused) { + position = HEADER_ROW scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() } } }, - modifier = Modifier.focusRequester(focusRequester), + modifier = Modifier.focusRequester(focusRequesters[HEADER_ROW]), ) } } @@ -348,9 +359,14 @@ fun MovieDetailsContent( item { PersonRow( people = people, - onClick = {}, + onClick = { + position = PEOPLE_ROW + }, onLongClick = {}, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[PEOPLE_ROW]), ) } } @@ -358,8 +374,14 @@ fun MovieDetailsContent( item { TrailerRow( trailers = trailers, - onClickTrailer = trailerOnClick, - modifier = Modifier.fillMaxWidth(), + onClickTrailer = { + position = TRAILER_ROW + trailerOnClick.invoke(it) + }, + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[TRAILER_ROW]), ) } } @@ -367,9 +389,15 @@ fun MovieDetailsContent( item { ChapterRow( chapters = chapters, - onClick = { playOnClick.invoke(it.position) }, + onClick = { + position = CHAPTER_ROW + playOnClick.invoke(it.position) + }, onLongClick = {}, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[CHAPTER_ROW]), ) } } @@ -378,7 +406,10 @@ fun MovieDetailsContent( ItemRow( title = stringResource(R.string.more_like_this), items = similar, - onClickItem = onClickItem, + onClickItem = { + position = SIMILAR_ROW + onClickItem.invoke(it) + }, onLongClickItem = {}, cardContent = { index, item, mod, onClick, onLongClick -> SeasonCard( @@ -391,7 +422,10 @@ fun MovieDetailsContent( imageWidth = Dp.Unspecified, ) }, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[SIMILAR_ROW]), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index 2e30d5b3..bc2ce98d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -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.setValueOnMain 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -47,7 +49,7 @@ class MovieViewModel val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) val chapters = MutableLiveData>(listOf()) - val similar = MutableLiveData>(listOf()) + val similar = MutableLiveData>() val chosenStreams = MutableLiveData(null) override fun init( @@ -55,14 +57,21 @@ class MovieViewModel potential: BaseItem?, ): Job? { this.itemId = itemId - return viewModelScope.launch(ExceptionHandler()) { - super.init(itemId, potential)?.join() + return viewModelScope.launch( + Dispatchers.IO + + LoadingExceptionHandler( + loading, + "Error fetching movie", + ), + ) { + fetchAndSetItem(itemId) item.value?.let { item -> + val result = itemPlaybackRepository.getSelectedTracks(item.id, item) + withContext(Dispatchers.Main) { + chosenStreams.value = result + loading.value = LoadingState.Success + } viewModelScope.launchIO { - val result = itemPlaybackRepository.getSelectedTracks(item.id, item) - withContext(Dispatchers.Main) { - chosenStreams.value = result - } val remoteTrailers = item.data.remoteTrailers ?.mapNotNull { t -> @@ -98,19 +107,20 @@ class MovieViewModel }.orEmpty() chapters.value = Chapter.Companion.fromDto(item.data, api) } - - val similar = - api.libraryApi - .getSimilarItems( - GetSimilarItemsRequest( - userId = serverRepository.currentUser?.id, - itemId = itemId, - fields = SlimItemFields, - limit = 25, - ), - ).content.items - .map { BaseItem.Companion.from(it, api) } - this@MovieViewModel.similar.setValueOnMain(similar) + if (!similar.isInitialized) { + val similar = + api.libraryApi + .getSimilarItems( + GetSimilarItemsRequest( + userId = serverRepository.currentUser?.id, + itemId = itemId, + fields = SlimItemFields, + limit = 25, + ), + ).content.items + .map { BaseItem.Companion.from(it, api) } + this@MovieViewModel.similar.setValueOnMain(similar) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 746871e0..f14c7f53 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -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.data.ItemDetailsDialog 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.letNotEmpty 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.tryRequestFocus 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 fun SeriesDetailsContent( preferences: UserPreferences, @@ -200,11 +203,14 @@ fun SeriesDetailsContent( favoriteOnClick: () -> Unit, modifier: Modifier = Modifier, ) { + val scope = rememberCoroutineScope() val bringIntoViewRequester = remember { BringIntoViewRequester() } - var position by rememberPosition() - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + var position by rememberInt() + val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } } + LaunchedEffect(Unit) { + focusRequesters.getOrNull(position)?.tryRequestFocus() + } Box( modifier = modifier, @@ -256,51 +262,95 @@ fun SeriesDetailsContent( played = played, favorite = favorite, overviewOnClick = overviewOnClick, - playOnClick = playOnClick, + playOnClick = { + position = HEADER_ROW + playOnClick.invoke() + }, watchOnClick = watchOnClick, favoriteOnClick = favoriteOnClick, bringIntoViewRequester = bringIntoViewRequester, modifier = Modifier - .fillMaxWidth(.6f) + .fillMaxWidth(.7f) .bringIntoViewRequester(bringIntoViewRequester) - .padding(bottom = 80.dp) - .ifElse(position.row < 0, Modifier.focusRequester(focusRequester)), + .padding(bottom = 8.dp), ) + 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 { ItemRow( title = "Seasons", items = seasons.items, - onClickItem = onClickItem, - onLongClickItem = onLongClickItem, - cardOnFocus = { isFocused, index -> -// if (isFocused) { -// scope.launch(ExceptionHandler()) { -// bringIntoViewRequester.bringIntoView() -// } -// } + onClickItem = { + position = SEASONS_ROW + onClickItem.invoke(it) }, + onLongClickItem = onLongClickItem, modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .focusRequester(focusRequesters[SEASONS_ROW]), cardContent = @Composable { index, item, mod, onClick, onLongClick -> SeasonCard( item = item, - onClick = { - position = RowColumn(0, index) - onClick.invoke() - }, + onClick = onClick, onLongClick = onLongClick, imageHeight = Cards.height2x3, imageWidth = Dp.Unspecified, showImageOverlay = true, - modifier = - mod - .ifElse( - position.row == 0 && position.column == index, - Modifier.focusRequester(focusRequester), - ), + modifier = mod, ) }, ) @@ -309,9 +359,14 @@ fun SeriesDetailsContent( item { PersonRow( people = people, - onClick = {}, + onClick = { + position = PEOPLE_ROW + }, onLongClick = {}, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[PEOPLE_ROW]), ) } } @@ -320,7 +375,10 @@ fun SeriesDetailsContent( ItemRow( title = stringResource(R.string.more_like_this), items = similar, - onClickItem = onClickItem, + onClickItem = { + position = SIMILAR_ROW + onClickItem.invoke(it) + }, onLongClickItem = {}, cardContent = { index, item, mod, onClick, onLongClick -> SeasonCard( @@ -333,7 +391,10 @@ fun SeriesDetailsContent( imageWidth = Dp.Unspecified, ) }, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[SIMILAR_ROW]), ) } } @@ -409,52 +470,6 @@ fun SeriesDetailsHeader( 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() - } - } - }, - ) - } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 8ad3232d..7cfbfa32 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -71,7 +71,7 @@ class SeriesViewModel val seasons = MutableLiveData(ItemListAndMapping.empty()) val episodes = MutableLiveData(EpisodeList.Loading) val people = MutableLiveData>(listOf()) - val similar = MutableLiveData>(listOf()) + val similar = MutableLiveData>() fun init( prefs: UserPreferences, @@ -107,18 +107,20 @@ class SeriesViewModel people.map { Person.fromDto(it, api) } }.orEmpty() } - val similar = - api.libraryApi - .getSimilarItems( - GetSimilarItemsRequest( - userId = serverRepository.currentUser?.id, - itemId = itemId, - fields = SlimItemFields, - limit = 25, - ), - ).content.items - .map { BaseItem.from(it, api, true) } - this@SeriesViewModel.similar.setValueOnMain(similar) + if (!similar.isInitialized) { + val similar = + api.libraryApi + .getSimilarItems( + GetSimilarItemsRequest( + userId = serverRepository.currentUser?.id, + itemId = itemId, + fields = SlimItemFields, + limit = 25, + ), + ).content.items + .map { BaseItem.from(it, api, true) } + this@SeriesViewModel.similar.setValueOnMain(similar) + } } } @@ -320,7 +322,9 @@ class SeriesViewModel ).content.items .firstOrNull() if (nextUp != null) { - navigateTo(Destination.Playback(nextUp.id, 0L)) + withContext(Dispatchers.Main) { + navigateTo(Destination.Playback(nextUp.id, 0L)) + } } else { showToast( context, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt index ba196369..1c69d947 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt @@ -11,6 +11,15 @@ interface RememberTabManager { preferences: UserPreferences, itemId: UUID, 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 /** @@ -20,5 +29,14 @@ interface RememberTabManager { preferences: UserPreferences, itemId: UUID, 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, ) }