Close, just a fwe linter issues

pull/2220/head
Dane Evans 2025-06-22 22:53:09 +10:00
rodzic a4a1a9bfad
commit 15ebeafdf5
2 zmienionych plików z 139 dodań i 24 usunięć

Wyświetl plik

@ -57,7 +57,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.graphics.Color import androidx.compose.ui.graphics.Color
@ -81,6 +83,9 @@ import com.geeksville.mesh.model.DebugViewModel.UiMeshLog
import com.geeksville.mesh.ui.common.theme.AppTheme import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.components.CopyIconButton import com.geeksville.mesh.ui.common.components.CopyIconButton
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -89,6 +94,9 @@ import com.geeksville.mesh.android.BuildUtils.warn
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.ui.zIndex
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE) private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
@ -112,10 +120,35 @@ internal fun DebugScreen(
}.toImmutableList() }.toImmutableList()
} }
// Track scroll direction and focus for header
var lastScrollOffset by remember { mutableStateOf(0) }
var headerVisible by remember { mutableStateOf(true) }
var headerHasFocus by remember { mutableStateOf(false) }
var programmaticScroll by remember { mutableStateOf(false) }
var ignoreNextScroll by remember { mutableStateOf(false) }
// header display
val headerHeightPx = remember { mutableStateOf(0) }
val density = LocalDensity.current
val headerHeightDp = with(density) { headerHeightPx.value.toDp() }
fun setHeaderVisible(newValue: Boolean) {
if (headerVisible != newValue) {
headerVisible = newValue
ignoreNextScroll = true // Ignore the next scroll event caused by this change (and search)
}
}
fun setHeaderHasFocus(newValue: Boolean) {
headerHasFocus = newValue
}
LaunchedEffect(filteredLogs) { LaunchedEffect(filteredLogs) {
viewModel.updateFilteredLogs(filteredLogs) viewModel.updateFilteredLogs(filteredLogs)
} }
// This code automatically scrolls the log list to the top (item 0) whenever the filteredLogs change,
// but only if the user is already near the top (within the first 3 items) and not currently scrolling.
// It uses a derived state to determine if auto-scroll should occur, and triggers the scroll in a LaunchedEffect.
val shouldAutoScroll by remember { derivedStateOf { listState.firstVisibleItemIndex < 3 } } val shouldAutoScroll by remember { derivedStateOf { listState.firstVisibleItemIndex < 3 } }
if (shouldAutoScroll) { if (shouldAutoScroll) {
LaunchedEffect(filteredLogs) { LaunchedEffect(filteredLogs) {
@ -124,35 +157,72 @@ internal fun DebugScreen(
} }
} }
} }
// Handle search result navigation
// Scrolls to the currently selected search match in the log list when searchState changes.
LaunchedEffect(searchState) { LaunchedEffect(searchState) {
if (searchState.currentMatchIndex >= 0 && searchState.currentMatchIndex < searchState.allMatches.size) { if (searchState.currentMatchIndex >= 0 && searchState.currentMatchIndex < searchState.allMatches.size) {
programmaticScroll = true
listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex) listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex)
programmaticScroll = false
} }
} }
LazyColumn( handleHeaderVisibilityOnScroll(
modifier = Modifier.fillMaxSize(), listState = listState,
state = listState, headerHasFocus = headerHasFocus,
) { programmaticScroll = programmaticScroll,
stickyHeader { ignoreNextScroll = ignoreNextScroll,
DebugSearchStateviewModelDefaults( setHeaderVisible = { setHeaderVisible(it) },
searchState = searchState, setIgnoreNextScroll = { ignoreNextScroll = it }
filterTexts = filterTexts, )
presetFilters = viewModel.presetFilters,
)
}
items(filteredLogs, key = { it.uuid }) { log -> LaunchedEffect(listState.isScrollInProgress) {
DebugItem( if (listState.isScrollInProgress) {
modifier = Modifier.animateItem(), setHeaderHasFocus(false)
log = log, }
searchText = searchState.searchText, }
isSelected = selectedLogId == log.uuid,
onLogClick = { Box(modifier = Modifier.fillMaxSize()) {
viewModel.setSelectedLogId(if (selectedLogId == log.uuid) null else log.uuid) LazyColumn(
} modifier = Modifier.fillMaxSize(),
) state = listState,
contentPadding = androidx.compose.foundation.layout.PaddingValues(0.dp)
) {
item {
Spacer(modifier = Modifier.height(headerHeightDp))
}
items(filteredLogs, key = { it.uuid }) { log ->
DebugItem(
modifier = Modifier.animateItem(),
log = log,
searchText = searchState.searchText,
isSelected = selectedLogId == log.uuid,
onLogClick = {
setHeaderHasFocus(false)
viewModel.setSelectedLogId(if (selectedLogId == log.uuid) null else log.uuid)
}
)
}
}
if (headerVisible) {
androidx.compose.material3.Surface(
tonalElevation = 4.dp,
shadowElevation = 4.dp,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.zIndex(1f)
.onGloballyPositioned { coordinates ->
headerHeightPx.value = coordinates.size.height
}
) {
DebugSearchStateviewModelDefaults(
searchState = searchState,
filterTexts = filterTexts,
presetFilters = viewModel.presetFilters,
onHeaderFocusChanged = { focused -> setHeaderHasFocus(focused) }
)
}
} }
} }
} }
@ -690,7 +760,7 @@ fun DebugMenuActions(
contentDescription = "Clear All" contentDescription = "Clear All"
) )
} }
} }
private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = withContext(Dispatchers.IO) { private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = withContext(Dispatchers.IO) {
try { try {
@ -738,3 +808,43 @@ private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = wit
warn("Error:IOException: " + e.toString()) warn("Error:IOException: " + e.toString())
} }
} }
@Composable
private fun handleHeaderVisibilityOnScroll(
listState: LazyListState,
headerHasFocus: Boolean,
programmaticScroll: Boolean,
ignoreNextScroll: Boolean,
setHeaderVisible: (Boolean) -> Unit,
setIgnoreNextScroll: (Boolean) -> Unit
) {
var lastScrollOffset by remember { mutableStateOf(0) }
LaunchedEffect(
listState.firstVisibleItemScrollOffset,
listState.firstVisibleItemIndex,
headerHasFocus,
programmaticScroll
) {
if (ignoreNextScroll) {
setIgnoreNextScroll(false)
return@LaunchedEffect
}
const itemIndexMultiplier = 10000
val currentOffset = listState.firstVisibleItemScrollOffset +
listState.firstVisibleItemIndex * itemIndexMultiplier
val scrollingUp = currentOffset < lastScrollOffset
val scrollingDown = currentOffset > lastScrollOffset
val idle = currentOffset == lastScrollOffset
when {
headerHasFocus -> setHeaderVisible(true)
programmaticScroll -> setHeaderVisible(true)
scrollingUp -> setHeaderVisible(true)
scrollingDown -> setHeaderVisible(false)
listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
-> setHeaderVisible(true)
idle -> { /* Do nothing */ }
}
lastScrollOffset = currentOffset
}
}

Wyświetl plik

@ -53,6 +53,7 @@ import com.geeksville.mesh.model.LogSearchManager.SearchState
import com.geeksville.mesh.ui.common.theme.AppTheme import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.model.DebugViewModel import com.geeksville.mesh.model.DebugViewModel
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.ui.focus.onFocusChanged
@Composable @Composable
internal fun DebugSearchNavigation( internal fun DebugSearchNavigation(
@ -158,6 +159,7 @@ internal fun DebugSearchState(
onPreviousMatch: () -> Unit, onPreviousMatch: () -> Unit,
onClearSearch: () -> Unit, onClearSearch: () -> Unit,
onFilterTextsChange: (List<String>) -> Unit, onFilterTextsChange: (List<String>) -> Unit,
onHeaderFocusChanged: (Boolean) -> Unit = {},
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
@ -179,7 +181,8 @@ internal fun DebugSearchState(
onSearchTextChange = onSearchTextChange, onSearchTextChange = onSearchTextChange,
onNextMatch = onNextMatch, onNextMatch = onNextMatch,
onPreviousMatch = onPreviousMatch, onPreviousMatch = onPreviousMatch,
onClearSearch = onClearSearch onClearSearch = onClearSearch,
modifier = Modifier.onFocusChanged { onHeaderFocusChanged(it.isFocused) }
) )
DebugFilterBar( DebugFilterBar(
filterTexts = filterTexts, filterTexts = filterTexts,
@ -203,6 +206,7 @@ fun DebugSearchStateviewModelDefaults(
searchState: SearchState, searchState: SearchState,
filterTexts: List<String>, filterTexts: List<String>,
presetFilters: List<String>, presetFilters: List<String>,
onHeaderFocusChanged: (Boolean) -> Unit = {},
) { ) {
val viewModel: DebugViewModel = hiltViewModel() val viewModel: DebugViewModel = hiltViewModel()
DebugSearchState( DebugSearchState(
@ -214,6 +218,7 @@ fun DebugSearchStateviewModelDefaults(
onPreviousMatch = viewModel.searchManager::goToPreviousMatch, onPreviousMatch = viewModel.searchManager::goToPreviousMatch,
onClearSearch = viewModel.searchManager::clearSearch, onClearSearch = viewModel.searchManager::clearSearch,
onFilterTextsChange = viewModel.filterManager::setFilterTexts, onFilterTextsChange = viewModel.filterManager::setFilterTexts,
onHeaderFocusChanged = onHeaderFocusChanged,
) )
} }