Focus improvements

This commit is contained in:
Damontecres 2026-01-29 18:13:32 -05:00
parent 43fde88695
commit 7e53f95054
No known key found for this signature in database
4 changed files with 84 additions and 36 deletions

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@ -24,7 +23,6 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.tryRequestFocus
@Composable @Composable
fun HomeSettingsAddRow( fun HomeSettingsAddRow(
@ -35,7 +33,7 @@ fun HomeSettingsAddRow(
modifier: Modifier, modifier: Modifier,
firstFocus: FocusRequester = remember { FocusRequester() }, firstFocus: FocusRequester = remember { FocusRequester() },
) { ) {
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() } // LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
Column(modifier = modifier) { Column(modifier = modifier) {
Text( Text(
text = "Add row for...", text = "Add row for...",

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -65,7 +66,7 @@ fun HomeSettingsGlobal(
) )
}, },
onClick = { onClickResize.invoke(1) }, onClick = { onClickResize.invoke(1) },
modifier = Modifier, modifier = Modifier.focusRequester(firstFocus),
) )
} }
item { item {

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@ -17,7 +18,10 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusDirection
@ -36,7 +40,9 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.services.HomeRowConfigDisplay import com.github.damontecres.wholphin.services.HomeRowConfigDisplay
import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import kotlinx.coroutines.launch
enum class MoveDirection { enum class MoveDirection {
UP, UP,
@ -55,7 +61,16 @@ fun HomeSettingsRowList(
firstFocus: FocusRequester = remember { FocusRequester() }, firstFocus: FocusRequester = remember { FocusRequester() },
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() } val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
val itemsBeforeRows = 3
val focusRequesters =
remember(state.rows.size) { List(itemsBeforeRows + state.rows.size) { FocusRequester() } }
var position by rememberInt(0)
LaunchedEffect(Unit) { focusRequesters.getOrNull(position)?.tryRequestFocus() }
Column(modifier = modifier) { Column(modifier = modifier) {
Text( Text(
text = stringResource(R.string.customize_home), text = stringResource(R.string.customize_home),
@ -64,6 +79,7 @@ fun HomeSettingsRowList(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
LazyColumn( LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
modifier modifier
@ -84,8 +100,11 @@ fun HomeSettingsRowList(
contentDescription = null, contentDescription = null,
) )
}, },
onClick = onClickAdd, onClick = {
modifier = Modifier.focusRequester(firstFocus), position = 0
onClickAdd.invoke()
},
modifier = Modifier.focusRequester(focusRequesters[0]),
) )
} }
item { item {
@ -102,8 +121,11 @@ fun HomeSettingsRowList(
contentDescription = null, contentDescription = null,
) )
}, },
onClick = onClickSettings, onClick = {
modifier = Modifier, position = 1
onClickSettings.invoke()
},
modifier = Modifier.focusRequester(focusRequesters[1]),
) )
} }
item { item {
@ -121,7 +143,18 @@ fun HomeSettingsRowList(
moveUpAllowed = index > 0, moveUpAllowed = index > 0,
moveDownAllowed = index != state.rows.lastIndex, moveDownAllowed = index != state.rows.lastIndex,
deleteAllowed = state.rows.size > 1, deleteAllowed = state.rows.size > 1,
onClickMove = { onClickMove.invoke(it, index) }, onClickMove = {
onClickMove.invoke(it, index)
scope.launch {
val scrollIndex =
itemsBeforeRows + if (it == MoveDirection.UP) index - 1 else index + 1
if (scrollIndex < listState.firstVisibleItemIndex ||
scrollIndex > listState.layoutInfo.visibleItemsInfo.lastIndex
) {
listState.animateScrollToItem(scrollIndex)
}
}
},
onClickDelete = { onClickDelete = {
if (index != state.rows.lastIndex) { if (index != state.rows.lastIndex) {
focusManager.moveFocus(FocusDirection.Down) focusManager.moveFocus(FocusDirection.Down)
@ -130,10 +163,15 @@ fun HomeSettingsRowList(
} }
onClickDelete.invoke(index) onClickDelete.invoke(index)
}, },
onClick = { onClick.invoke(index, row) }, onClick = {
position = itemsBeforeRows + index
onClick.invoke(index, row)
},
modifier = modifier =
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth()
.animateItem()
.focusRequester(focusRequesters[itemsBeforeRows + index]),
) )
} }
} }

View file

@ -30,11 +30,16 @@ import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
@ -95,24 +100,29 @@ class HomeSettingsViewModel
private suspend fun fetchRowData() { private suspend fun fetchRowData() {
val limit = 6 val limit = 6
val semaphore = Semaphore(4)
val rows = val rows =
serverRepository.currentUserDto.value?.let { userDto -> serverRepository.currentUserDto.value?.let { userDto ->
val prefs = userPreferencesService.getCurrent().appPreferences.homePagePreferences val prefs = userPreferencesService.getCurrent().appPreferences.homePagePreferences
state.value.let { state -> state.value
state.rows .let { state ->
.map { it.config } state.rows
.map { row -> .map { it.config }
// TODO parallelize .map { row ->
homeSettingsService.fetchDataForRow( viewModelScope.async(Dispatchers.IO) {
row = row, semaphore.withPermit {
scope = viewModelScope, homeSettingsService.fetchDataForRow(
prefs = prefs, row = row,
userDto = userDto, scope = viewModelScope,
libraries = state.libraries, prefs = prefs,
limit = limit, userDto = userDto,
) libraries = state.libraries,
} limit = limit,
} )
}
}
}
}.awaitAll()
} }
rows?.let { rows -> rows?.let { rows ->
rows rows
@ -152,16 +162,17 @@ class HomeSettingsViewModel
direction: MoveDirection, direction: MoveDirection,
index: Int, index: Int,
) { ) {
updateState { viewModelScope.launchIO {
val rows = it.rows.move(direction, index) updateState {
// TODO would be more efficient to move rowData, but uncombined continue watching is two rows val rows = it.rows.move(direction, index)
// val rowData = it.rowData.move(direction, index) val rowData = it.rowData.move(direction, index)
it.copy( it.copy(
loading = LoadingState.Loading, rows = rows,
rows = rows, rowData = rowData,
) )
}
} }
viewModelScope.launchIO { fetchRowData() } // viewModelScope.launchIO { fetchRowData() }
} }
fun deleteRow(index: Int) { fun deleteRow(index: Int) {