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

View file

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

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.ui.FontAwesome
import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus
import kotlinx.coroutines.launch
enum class MoveDirection {
UP,
@ -55,7 +61,16 @@ fun HomeSettingsRowList(
firstFocus: FocusRequester = remember { FocusRequester() },
) {
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) {
Text(
text = stringResource(R.string.customize_home),
@ -64,6 +79,7 @@ fun HomeSettingsRowList(
modifier = Modifier.fillMaxWidth(),
)
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
modifier
@ -84,8 +100,11 @@ fun HomeSettingsRowList(
contentDescription = null,
)
},
onClick = onClickAdd,
modifier = Modifier.focusRequester(firstFocus),
onClick = {
position = 0
onClickAdd.invoke()
},
modifier = Modifier.focusRequester(focusRequesters[0]),
)
}
item {
@ -102,8 +121,11 @@ fun HomeSettingsRowList(
contentDescription = null,
)
},
onClick = onClickSettings,
modifier = Modifier,
onClick = {
position = 1
onClickSettings.invoke()
},
modifier = Modifier.focusRequester(focusRequesters[1]),
)
}
item {
@ -121,7 +143,18 @@ fun HomeSettingsRowList(
moveUpAllowed = index > 0,
moveDownAllowed = index != state.rows.lastIndex,
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 = {
if (index != state.rows.lastIndex) {
focusManager.moveFocus(FocusDirection.Down)
@ -130,10 +163,15 @@ fun HomeSettingsRowList(
}
onClickDelete.invoke(index)
},
onClick = { onClick.invoke(index, row) },
onClick = {
position = itemsBeforeRows + index
onClick.invoke(index, row)
},
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.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.serialization.Serializable
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
@ -95,24 +100,29 @@ class HomeSettingsViewModel
private suspend fun fetchRowData() {
val limit = 6
val semaphore = Semaphore(4)
val rows =
serverRepository.currentUserDto.value?.let { userDto ->
val prefs = userPreferencesService.getCurrent().appPreferences.homePagePreferences
state.value.let { state ->
state.rows
.map { it.config }
.map { row ->
// TODO parallelize
homeSettingsService.fetchDataForRow(
row = row,
scope = viewModelScope,
prefs = prefs,
userDto = userDto,
libraries = state.libraries,
limit = limit,
)
}
}
state.value
.let { state ->
state.rows
.map { it.config }
.map { row ->
viewModelScope.async(Dispatchers.IO) {
semaphore.withPermit {
homeSettingsService.fetchDataForRow(
row = row,
scope = viewModelScope,
prefs = prefs,
userDto = userDto,
libraries = state.libraries,
limit = limit,
)
}
}
}
}.awaitAll()
}
rows?.let { rows ->
rows
@ -152,16 +162,17 @@ class HomeSettingsViewModel
direction: MoveDirection,
index: Int,
) {
updateState {
val rows = it.rows.move(direction, index)
// TODO would be more efficient to move rowData, but uncombined continue watching is two rows
// val rowData = it.rowData.move(direction, index)
it.copy(
loading = LoadingState.Loading,
rows = rows,
)
viewModelScope.launchIO {
updateState {
val rows = it.rows.move(direction, index)
val rowData = it.rowData.move(direction, index)
it.copy(
rows = rows,
rowData = rowData,
)
}
}
viewModelScope.launchIO { fetchRowData() }
// viewModelScope.launchIO { fetchRowData() }
}
fun deleteRow(index: Int) {