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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue