diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 1df5ee90..fc6d132a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -22,7 +23,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign @@ -42,6 +42,7 @@ import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.detail.GridItemDetails +import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.main.HomePageHeader import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec @@ -288,6 +289,7 @@ data class GridClickActions( @Composable fun CollectionFolderHeader( + listIsNotEmpty: Boolean, showHeader: Boolean, showTitle: Boolean, playEnabled: Boolean, @@ -298,12 +300,14 @@ fun CollectionFolderHeader( getPossibleFilterValues: suspend (ItemFilterBy<*>) -> List, onClickShowViewOptions: () -> Unit, onClickPlayAll: (Boolean) -> Unit, + onClickRandom: () -> Unit, modifier: Modifier = Modifier, currentFilter: GetItemsFilter = GetItemsFilter(), filterOptions: List> = listOf(), onFilterChange: (GetItemsFilter) -> Unit = {}, onShowFilterDropdown: ((Boolean) -> Unit)? = null, filterButtonFocusRequester: FocusRequester = remember { FocusRequester() }, + randomButtonFocusRequester: FocusRequester = remember { FocusRequester() }, ) { AnimatedVisibility( showHeader, @@ -311,6 +315,7 @@ fun CollectionFolderHeader( exit = shrinkVertically(), modifier = modifier, ) { + val focusRequester = remember { FocusRequester() } Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth(), @@ -320,63 +325,84 @@ fun CollectionFolderHeader( } val endPadding = 16.dp + if (sortAndDirection.sort == ItemSortBy.SORT_NAME) 24.dp else 0.dp + Row( - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(start = 16.dp, end = endPadding) - .focusRestorer() .fillMaxWidth(), ) { - if (sortOptions.isNotEmpty() || filterOptions.isNotEmpty()) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.focusRestorer(), - ) { - if (sortOptions.isNotEmpty()) { - SortByButton( - sortOptions = sortOptions, - current = sortAndDirection, - onSortChange = onSortChange, - modifier = Modifier, - ) - } - if (filterOptions.isNotEmpty()) { - FilterByButton( - filterOptions = filterOptions, - current = currentFilter, - onFilterChange = onFilterChange, - getPossibleValues = getPossibleFilterValues, - modifier = Modifier.focusRequester(filterButtonFocusRequester), - onShow = onShowFilterDropdown, - ) - } - ExpandableFaButton( - title = R.string.view_options, - iconStringRes = R.string.fa_sliders, - onClick = onClickShowViewOptions, - modifier = Modifier, + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier, + ) { + if (sortOptions.isNotEmpty()) { + SortByButton( + sortOptions = sortOptions, + current = sortAndDirection, + onSortChange = onSortChange, + modifier = Modifier.focusRequester(focusRequester), ) } + if (filterOptions.isNotEmpty()) { + FilterByButton( + filterOptions = filterOptions, + current = currentFilter, + onFilterChange = onFilterChange, + getPossibleValues = getPossibleFilterValues, + modifier = + Modifier + .focusRequester(filterButtonFocusRequester) + .ifElse( + sortOptions.isEmpty(), + Modifier.focusRequester(focusRequester), + ), + onShow = onShowFilterDropdown, + ) + } + ExpandableFaButton( + title = R.string.view_options, + iconStringRes = R.string.fa_sliders, + onClick = onClickShowViewOptions, + modifier = + Modifier.ifElse( + sortOptions.isEmpty() && filterOptions.isEmpty(), + Modifier.focusRequester(focusRequester), + ), + ) } - if (playEnabled) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.focusRestorer(), - ) { + + Spacer(Modifier.weight(1f)) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier, + ) { + ExpandableFaButton( + title = R.string.sort_by_random, + iconStringRes = R.string.fa_dice, + onClick = onClickRandom, + modifier = Modifier.focusRequester(randomButtonFocusRequester), + enabled = listIsNotEmpty, + ) + + if (playEnabled) { ExpandablePlayButton( title = R.string.play, resume = Duration.ZERO, icon = Icons.Default.PlayArrow, onClick = { onClickPlayAll.invoke(false) }, + enabled = listIsNotEmpty, ) ExpandableFaButton( title = R.string.shuffle, iconStringRes = R.string.fa_shuffle, onClick = { onClickPlayAll.invoke(true) }, + enabled = listIsNotEmpty, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt index c1566b61..732c531d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderView.kt @@ -65,6 +65,7 @@ import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetArtistsHandler @@ -78,6 +79,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch @@ -264,6 +266,24 @@ class CollectionFolderViewModel } } + override fun onClickRandom() { + viewModelScope.launchIO { + try { + val random = + (state.value.items.successValue as? BlockingList)?.randomBlocking() + Timber.d("Got random item: %s", random?.id) + random?.destination()?.let { + navigateTo(random.destination()) + } + } catch (ex: CancellationException) { + throw ex + } catch (ex: Exception) { + Timber.e(ex, "Error getting random") + showToast(context, "Error: ${ex.localizedMessage}}") + } + } + } + override fun onFilterChange( newFilter: GetItemsFilter, recursive: Boolean, @@ -767,6 +787,11 @@ interface CollectionFolderViewActions { suspend fun positionOfLetter(letter: Char): Int? fun saveViewOptions(viewOptions: ViewOptions) + + /** + * Navigate to a random item + */ + fun onClickRandom() } @Composable @@ -791,11 +816,15 @@ fun CollectionFolderViewContent( ) { var position by rememberInt(savedPosition) + // Track if leaving page due to clicking the random button so focus can be restored there + var clickedRandom by rememberSaveable { mutableStateOf(false) } + val contextMenu = rememberContextMenu(preferences, provider) var showViewOptions by rememberSaveable { mutableStateOf(false) } var filterDropdownShowing by remember { mutableStateOf(false) } val headerRowFocusRequester = remember { FocusRequester() } val filterButtonFocusRequester = remember { FocusRequester() } + val randomButtonFocusRequester = remember { FocusRequester() } val gridActions = remember(actions) { @@ -889,9 +918,10 @@ fun CollectionFolderViewContent( } } CollectionFolderHeader( + listIsNotEmpty = state.items.successValue?.isNotEmpty() == true, showHeader = showHeader || state.items !is DataLoadingState.Success, showTitle = showTitle, - playEnabled = playEnabled && state.items.successValue?.isNotEmpty() == true, + playEnabled = playEnabled, title = title, sortAndDirection = state.sortAndDirection, onSortChange = { @@ -908,9 +938,14 @@ fun CollectionFolderViewContent( filterOptions = filterOptions, onClickPlayAll = gridActions.onClickPlayAll!!, onClickShowViewOptions = { showViewOptions = true }, + onClickRandom = { + clickedRandom = true + viewActions.onClickRandom() + }, modifier = Modifier.focusRequester(headerRowFocusRequester), onShowFilterDropdown = { filterDropdownShowing = it }, filterButtonFocusRequester = filterButtonFocusRequester, + randomButtonFocusRequester = randomButtonFocusRequester, ) when (val pager = state.items) { @@ -933,10 +968,12 @@ fun CollectionFolderViewContent( when { contextMenu.isShowing -> null filterDropdownShowing -> filterButtonFocusRequester + clickedRandom -> randomButtonFocusRequester pager.data.isNotEmpty() -> gridFocusRequester else -> focusRequesterOnEmpty ?: headerRowFocusRequester } focusRequester?.tryRequestFocus() + clickedRandom = false } Box(Modifier.fillMaxSize()) { if (state.viewOptions.type == ViewOptionsType.GRID) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt index 734ac3d0..f67b322a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesViewModel.kt @@ -44,6 +44,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.BlockingList import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetArtistsHandler @@ -51,6 +52,7 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.WholphinDispatchers +import com.github.damontecres.wholphin.util.successValue import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException @@ -693,6 +695,26 @@ class FavoritesViewModel override fun saveViewOptions(viewOptions: ViewOptions) { saveViewOptions(type, viewOptions) } + + override fun onClickRandom() { + viewModelScope.launchIO { + try { + collectionStateFor(type)?.let { collectionState -> + val random = + (collectionState.items.successValue as? BlockingList)?.randomBlocking() + Timber.d("Got random item: %s", random?.id) + random?.destination()?.let { + navigateTo(random.destination()) + } + } + } catch (ex: CancellationException) { + throw ex + } catch (ex: Exception) { + Timber.e(ex, "Error getting random") + showToast(context, "Error: ${ex.localizedMessage}}") + } + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt b/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt index 08207e53..80272719 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt @@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.util import java.util.function.IntFunction import java.util.function.Predicate +import kotlin.random.Random /** * A [List] which has function that will wait for a result @@ -17,6 +18,11 @@ interface BlockingList : List { */ suspend fun indexOfBlocking(predicate: Predicate): Int + suspend fun randomBlocking(): T { + val index = Random.nextInt(size) + return getBlocking(index) + } + companion object { /** * Create a [BlockingList] over a regular [List] diff --git a/app/src/main/res/values/fa_strings.xml b/app/src/main/res/values/fa_strings.xml index 7e13f584..61c7af06 100644 --- a/app/src/main/res/values/fa_strings.xml +++ b/app/src/main/res/values/fa_strings.xml @@ -1,5 +1,8 @@ + + + @@ -57,4 +60,5 @@ +