kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Close, just a fwe linter issues
rodzic
a4a1a9bfad
commit
15ebeafdf5
|
@ -57,7 +57,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.components.CopyIconButton
|
||||
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.ui.platform.LocalContext
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
@ -89,6 +94,9 @@ import com.geeksville.mesh.android.BuildUtils.warn
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
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)
|
||||
|
||||
|
@ -112,10 +120,35 @@ internal fun DebugScreen(
|
|||
}.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) {
|
||||
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 } }
|
||||
if (shouldAutoScroll) {
|
||||
LaunchedEffect(filteredLogs) {
|
||||
|
@ -124,25 +157,40 @@ internal fun DebugScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
// Handle search result navigation
|
||||
|
||||
// Scrolls to the currently selected search match in the log list when searchState changes.
|
||||
LaunchedEffect(searchState) {
|
||||
if (searchState.currentMatchIndex >= 0 && searchState.currentMatchIndex < searchState.allMatches.size) {
|
||||
programmaticScroll = true
|
||||
listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex)
|
||||
programmaticScroll = false
|
||||
}
|
||||
}
|
||||
|
||||
handleHeaderVisibilityOnScroll(
|
||||
listState = listState,
|
||||
headerHasFocus = headerHasFocus,
|
||||
programmaticScroll = programmaticScroll,
|
||||
ignoreNextScroll = ignoreNextScroll,
|
||||
setHeaderVisible = { setHeaderVisible(it) },
|
||||
setIgnoreNextScroll = { ignoreNextScroll = it }
|
||||
)
|
||||
|
||||
LaunchedEffect(listState.isScrollInProgress) {
|
||||
if (listState.isScrollInProgress) {
|
||||
setHeaderHasFocus(false)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(0.dp)
|
||||
) {
|
||||
stickyHeader {
|
||||
DebugSearchStateviewModelDefaults(
|
||||
searchState = searchState,
|
||||
filterTexts = filterTexts,
|
||||
presetFilters = viewModel.presetFilters,
|
||||
)
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(headerHeightDp))
|
||||
}
|
||||
|
||||
items(filteredLogs, key = { it.uuid }) { log ->
|
||||
DebugItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
|
@ -150,11 +198,33 @@ internal fun DebugScreen(
|
|||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -690,7 +760,7 @@ fun DebugMenuActions(
|
|||
contentDescription = "Clear All"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
@ -738,3 +808,43 @@ private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = wit
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import com.geeksville.mesh.model.LogSearchManager.SearchState
|
|||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.model.DebugViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
|
||||
@Composable
|
||||
internal fun DebugSearchNavigation(
|
||||
|
@ -158,6 +159,7 @@ internal fun DebugSearchState(
|
|||
onPreviousMatch: () -> Unit,
|
||||
onClearSearch: () -> Unit,
|
||||
onFilterTextsChange: (List<String>) -> Unit,
|
||||
onHeaderFocusChanged: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
|
@ -179,7 +181,8 @@ internal fun DebugSearchState(
|
|||
onSearchTextChange = onSearchTextChange,
|
||||
onNextMatch = onNextMatch,
|
||||
onPreviousMatch = onPreviousMatch,
|
||||
onClearSearch = onClearSearch
|
||||
onClearSearch = onClearSearch,
|
||||
modifier = Modifier.onFocusChanged { onHeaderFocusChanged(it.isFocused) }
|
||||
)
|
||||
DebugFilterBar(
|
||||
filterTexts = filterTexts,
|
||||
|
@ -203,6 +206,7 @@ fun DebugSearchStateviewModelDefaults(
|
|||
searchState: SearchState,
|
||||
filterTexts: List<String>,
|
||||
presetFilters: List<String>,
|
||||
onHeaderFocusChanged: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val viewModel: DebugViewModel = hiltViewModel()
|
||||
DebugSearchState(
|
||||
|
@ -214,6 +218,7 @@ fun DebugSearchStateviewModelDefaults(
|
|||
onPreviousMatch = viewModel.searchManager::goToPreviousMatch,
|
||||
onClearSearch = viewModel.searchManager::clearSearch,
|
||||
onFilterTextsChange = viewModel.filterManager::setFilterTexts,
|
||||
onHeaderFocusChanged = onHeaderFocusChanged,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue