diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..fcb19bf 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index a8286ae..8859ca8 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/src/main/kotlin/com/jerryjeon/logjerry/Main.kt b/src/main/kotlin/com/jerryjeon/logjerry/Main.kt index 87aeb28..ba8cd4e 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/Main.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/Main.kt @@ -23,7 +23,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.* +import androidx.compose.ui.window.MenuBar +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.application import com.jerryjeon.logjerry.log.Log import com.jerryjeon.logjerry.parse.ParseStatus import com.jerryjeon.logjerry.preferences.ColorTheme @@ -106,6 +109,7 @@ private fun GettingStartedView(notStarted: ParseStatus.NotStarted, changeSource: ?.let { changeSource(Source.Text(it.toString())) } true } + else -> { false } @@ -296,16 +300,31 @@ private fun TabView(tabs: Tabs, activate: (Tab) -> Unit, close: (Tab) -> Unit) { val scrollState = rememberScrollState() Row(modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min).horizontalScroll(scrollState)) { tabList.forEach { tab -> - Row( - modifier = Modifier - .background(if (tab === activated) MaterialTheme.colors.secondary else Color.Transparent) - .clickable { activate(tab) } - .padding(8.dp) - ) { - Text(tab.name, modifier = Modifier.align(Alignment.CenterVertically), style = MaterialTheme.typography.body2) - Spacer(Modifier.width(8.dp)) - IconButton(modifier = Modifier.size(16.dp).align(Alignment.CenterVertically), onClick = { close(tab) }) { - Icon(Icons.Default.Close, "Close tab") + Column(modifier = Modifier.width(IntrinsicSize.Max).clickable { activate(tab) }) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, top = 12.dp) + ) { + Text( + tab.name, + modifier = Modifier.align(Alignment.CenterVertically), + style = MaterialTheme.typography.body2, + maxLines = 1 + ) + Spacer(Modifier.width(8.dp)) + IconButton( + modifier = Modifier.size(16.dp).align(Alignment.CenterVertically), + onClick = { close(tab) } + ) { + Icon(Icons.Default.Close, "Close tab") + } + } + if (tab === activated) { + Box(modifier = Modifier.fillMaxWidth().height(7.dp)) + Divider(modifier = Modifier.fillMaxWidth().height(5.dp), color = MaterialTheme.colors.primary) + } else { + Box(modifier = Modifier.fillMaxWidth().height(12.dp)) } } Divider(modifier = Modifier.fillMaxHeight().width(1.dp)) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt b/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt index 33e9897..4e185a1 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/log/Log.kt @@ -1,4 +1,11 @@ package com.jerryjeon.logjerry.log + +import java.time.Duration +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter + data class Log( val number: Int, val date: String?, @@ -12,6 +19,32 @@ data class Log( ) { val index = number - 1 val priority = Priority.find(priorityText) + + val localDateTime: LocalDateTime? + get() = try { + if (date != null && time != null) { + LocalDateTime.parse("$date $time", formatter) + } else if (time != null) { + // Assume date is today. + LocalTime.parse(time, timeFormatter).atDate(LocalDate.now()) + } else { + null + } + } catch (e: Exception) { + e.printStackTrace() + null + } + + fun durationBetween(other: Log): Duration? { + val thisLocalDateTime = localDateTime ?: return null + val otherLocalDateTime = other.localDateTime ?: return null + return Duration.between(thisLocalDateTime, otherLocalDateTime) + } + + companion object { + val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") + val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") + } } enum class Priority(val text: String, val fullText: String, val level: Int) { diff --git a/src/main/kotlin/com/jerryjeon/logjerry/log/ParseCompleted.kt b/src/main/kotlin/com/jerryjeon/logjerry/log/ParseCompleted.kt index 9581e37..25ad9b2 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/log/ParseCompleted.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/log/ParseCompleted.kt @@ -58,6 +58,7 @@ class ParseCompleted( detectionFinishedFlow ) { filteredLogs, detectionFinished -> val allDetections = mutableMapOf>() + var lastRefinedLog: RefinedLog? = null val refinedLogs = filteredLogs.map { log -> val detections = detectionFinished.detectionsByLog[log] ?: emptyMap() detections.forEach { (key, newValue) -> @@ -69,7 +70,10 @@ class ParseCompleted( // TODO don't want to repeat all annotate if just one log has changed. How can I achieve it val logContents = LogAnnotation.separateAnnotationStrings(log, detections.values.flatten()) - RefinedLog(log, detections, LogAnnotation.annotate(log, logContents, detectionFinished.detectors)) + val timeGap = lastRefinedLog?.log?.durationBetween(log)?.takeIf { it.toSeconds() >= 3 } + RefinedLog(log, detections, LogAnnotation.annotate(log, logContents, detectionFinished.detectors), timeGap).also { + lastRefinedLog = it + } } RefineResult(refinedLogs, allDetections) } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/logview/MarkInfo.kt b/src/main/kotlin/com/jerryjeon/logjerry/logview/MarkInfo.kt new file mode 100644 index 0000000..727b190 --- /dev/null +++ b/src/main/kotlin/com/jerryjeon/logjerry/logview/MarkInfo.kt @@ -0,0 +1,12 @@ +package com.jerryjeon.logjerry.logview + +sealed class MarkInfo { + class Marked( + val markedLog: RefinedLog, + ) : MarkInfo() + + class StatBetweenMarks( + val logCount: Int, + val duration: String?, // ex) 1h 2m 3s, and null if it's not able to calculate + ) : MarkInfo() +} diff --git a/src/main/kotlin/com/jerryjeon/logjerry/logview/RefineResult.kt b/src/main/kotlin/com/jerryjeon/logjerry/logview/RefineResult.kt index 5f1a3ad..0a68969 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/logview/RefineResult.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/logview/RefineResult.kt @@ -3,7 +3,6 @@ package com.jerryjeon.logjerry.logview import com.jerryjeon.logjerry.detector.Detection import com.jerryjeon.logjerry.detector.DetectionStatus import com.jerryjeon.logjerry.detector.DetectorKey -import com.jerryjeon.logjerry.detector.MarkDetection import com.jerryjeon.logjerry.ui.focus.DetectionFocus import com.jerryjeon.logjerry.ui.focus.LogFocus import kotlinx.coroutines.flow.MutableStateFlow @@ -13,8 +12,6 @@ data class RefineResult( val refinedLogs: List, val allDetections: Map> ) { - val markedRows = refinedLogs.filter { it.marked } - val currentFocus = MutableStateFlow(null) val statusByKey = MutableStateFlow( @@ -28,6 +25,41 @@ data class RefineResult( } ) + val markInfos: List + + init { + val markedLogs = refinedLogs.filter { it.marked } + markInfos = if (markedLogs.isEmpty()) { + emptyList() + } else { + val markInfos = mutableListOf() + markedLogs + .scan(refinedLogs.first()) { prevRefinedLog, refinedLog -> + val duration = prevRefinedLog.durationBetween(refinedLog) + markInfos.add( + MarkInfo.StatBetweenMarks( + logCount = refinedLogs.indexOf(refinedLog) - refinedLogs.indexOf(prevRefinedLog), + duration = duration?.toHumanReadable() + ) + ) + markInfos.add(MarkInfo.Marked(refinedLog)) + refinedLog + } + + val lastLog = refinedLogs.last() + val lastMarkedLogs = markedLogs.last() + val duration = lastMarkedLogs.durationBetween(lastLog) + + markInfos.add( + MarkInfo.StatBetweenMarks( + logCount = refinedLogs.indexOf(lastLog) - refinedLogs.indexOf(lastMarkedLogs), + duration = duration?.toHumanReadable() + ) + ) + markInfos + } + } + fun selectPreviousDetection(status: DetectionStatus) { val previousIndex = if (status.currentIndex <= 0) { status.allDetections.size - 1 @@ -68,7 +100,7 @@ data class RefineResult( statusByKey.value[key]?.let { selectNextDetection(it) } } - fun selectDetection(detection: MarkDetection) { + fun selectDetection(detection: Detection) { statusByKey.update { val status = it[detection.key] ?: return@update it val index = status.allDetections.indexOf(detection) diff --git a/src/main/kotlin/com/jerryjeon/logjerry/logview/RefinedLog.kt b/src/main/kotlin/com/jerryjeon/logjerry/logview/RefinedLog.kt index dd27488..1beb613 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/logview/RefinedLog.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/logview/RefinedLog.kt @@ -5,12 +5,36 @@ import com.jerryjeon.logjerry.detector.DetectorKey import com.jerryjeon.logjerry.detector.MarkDetection import com.jerryjeon.logjerry.log.Log import com.jerryjeon.logjerry.log.LogContentView +import java.time.Duration class RefinedLog( val log: Log, val detections: Map>, - val logContentViews: List + val logContentViews: List, + val timeGap: Duration? ) { val mark = detections[DetectorKey.Mark]?.firstOrNull() as? MarkDetection val marked = mark != null + + fun durationBetween(other: RefinedLog): Duration? { + return this.log.durationBetween(other.log) + } +} + +fun Duration.toHumanReadable(): String { + val hours: Long = toHours() + val minutes: Long = toMinutes() % 60 + val seconds: Long = seconds % 60 + + return when { + hours > 0 -> { + "${hours}h ${minutes}m ${seconds}s" + } + minutes > 0 -> { + "${minutes}m ${seconds}s" + } + else -> { + "${toMillis()}ms" + } + } } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/AppliedTextFilter.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/AppliedTextFilter.kt new file mode 100644 index 0000000..039ee50 --- /dev/null +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/AppliedTextFilter.kt @@ -0,0 +1,53 @@ +package com.jerryjeon.logjerry.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.jerryjeon.logjerry.filter.TextFilter + +@Composable +fun AppliedTextFilter(textFilter: TextFilter, removeFilter: (TextFilter) -> Unit) { + Box( + Modifier + .padding(horizontal = 4.dp, vertical = 8.dp) + .border(1.dp, Color.LightGray, RoundedCornerShape(8.dp)) + ) { + Row(modifier = Modifier.height(30.dp)) { + Spacer(Modifier.width(8.dp)) + Text( + textFilter.columnType.text, + modifier = Modifier.align(Alignment.CenterVertically), + ) + Spacer(Modifier.width(8.dp)) + Divider(Modifier.width(1.dp).fillMaxHeight()) + Spacer(Modifier.width(8.dp)) + Text(textFilter.text, modifier = Modifier.align(Alignment.CenterVertically)) + Spacer(Modifier.width(8.dp)) + Box( + Modifier + .clickable { removeFilter(textFilter) } + .align(Alignment.CenterVertically) + .fillMaxHeight() + .aspectRatio(1f) + ) { + Icon( + Icons.Default.Close, + contentDescription = "Remove a filter", + modifier = Modifier.size(ButtonDefaults.IconSize).align(Alignment.Center) + ) + } + } + } +} diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/DetectionView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/DetectionView.kt deleted file mode 100644 index 1336ec9..0000000 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/DetectionView.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.jerryjeon.logjerry.ui - -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.jerryjeon.logjerry.detector.DetectionStatus -import com.jerryjeon.logjerry.detector.DetectorKey -import com.jerryjeon.logjerry.detector.DetectorManager -import com.jerryjeon.logjerry.log.Log -import com.jerryjeon.logjerry.preferences.Preferences -import kotlinx.coroutines.flow.StateFlow - -@Composable -fun RowScope.DetectionView( - preferences: Preferences, - detectorManager: DetectorManager, - statusByKey: Map, - openNewTab: (StateFlow>) -> Unit, - selectPreviousDetection: (status: DetectionStatus) -> Unit, - selectNextDetection: (status: DetectionStatus) -> Unit, -) { - Box(modifier = Modifier.weight(0.5f).border(1.dp, Color.LightGray, RoundedCornerShape(4.dp))) { - Column { - Text("Auto-detection", modifier = Modifier.padding(8.dp)) - Divider() - Row { - JsonDetectionView( - Modifier.width(200.dp).wrapContentHeight(), - statusByKey[DetectorKey.Json], - selectPreviousDetection, - selectNextDetection - ) - - Spacer(Modifier.width(8.dp)) - Divider(Modifier.width(1.dp).height(70.dp).align(Alignment.CenterVertically)) - Spacer(Modifier.width(8.dp)) - - MarkDetectionView( - Modifier.width(200.dp).wrapContentHeight(), - statusByKey[DetectorKey.Mark], - selectPreviousDetection, - selectNextDetection, - openMarkedRowsTab = { openNewTab(detectorManager.markedRowsFlow) } - ) - - Spacer(Modifier.width(8.dp)) - Divider(Modifier.width(1.dp).height(70.dp).align(Alignment.CenterVertically)) - } - } - } -} diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/FilterView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/FilterView.kt deleted file mode 100644 index b0116e4..0000000 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/FilterView.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.jerryjeon.logjerry.ui - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.jerryjeon.logjerry.filter.FilterManager - -@Composable -fun FilterView( - filterManager: FilterManager, -) { - val textFilters by filterManager.textFiltersFlow.collectAsState() - val priorityFilters by filterManager.priorityFilterFlow.collectAsState() - - TextFilterView(textFilters, filterManager::addTextFilter, filterManager::removeTextFilter) - Spacer(Modifier.width(16.dp)) - PriorityFilterView(priorityFilters, filterManager::setPriorityFilter) - Spacer(Modifier.width(16.dp)) -} diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/JsonDetectionView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/JsonDetectionView.kt index ee1f140..d50b768 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/JsonDetectionView.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/JsonDetectionView.kt @@ -1,10 +1,8 @@ package com.jerryjeon.logjerry.ui +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp @@ -12,37 +10,31 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.jerryjeon.logjerry.detector.DetectionStatus -import com.jerryjeon.logjerry.detector.JsonDetection @Composable fun JsonDetectionView( - modifier: Modifier, - detectionStatus: DetectionStatus?, + modifier: Modifier = Modifier, + detectionStatus: DetectionStatus, moveToPreviousOccurrence: (DetectionStatus) -> Unit, moveToNextOccurrence: (DetectionStatus) -> Unit, ) { CompositionLocalProvider( LocalTextStyle provides LocalTextStyle.current.copy(fontSize = 12.sp), ) { - Box(modifier = modifier) { - Column(Modifier.height(IntrinsicSize.Min).padding(8.dp)) { - val title = buildAnnotatedString { - append("Json") - } - Text(title) - if (detectionStatus == null) { - Spacer(Modifier.height(16.dp)) - Text("No results", textAlign = TextAlign.Center) - } else { - JsonDetectionSelectionExist(detectionStatus, moveToPreviousOccurrence, moveToNextOccurrence) - } - } + Row( + modifier + .wrapContentWidth() + .border(ButtonDefaults.outlinedBorder, MaterialTheme.shapes.small) + .padding(start = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Json") + Spacer(modifier = Modifier.width(8.dp)) + JsonDetectionSelectionExist(detectionStatus, moveToPreviousOccurrence, moveToNextOccurrence) } } } @@ -53,34 +45,25 @@ fun JsonDetectionSelectionExist( moveToPreviousOccurrence: (DetectionStatus) -> Unit, moveToNextOccurrence: (DetectionStatus) -> Unit ) { - Column { - Row { - Row(modifier = Modifier.weight(1f).fillMaxHeight()) { - if (selection.selected == null) { - Text( - "${selection.allDetections.size} results", - modifier = Modifier.align(Alignment.CenterVertically) - ) - } else { - Text( - "${selection.currentIndexInView} / ${selection.totalCount}", - modifier = Modifier.align(Alignment.CenterVertically) - ) - } - } - IconButton(onClick = { moveToPreviousOccurrence(selection) }) { - Icon(Icons.Default.KeyboardArrowUp, "Previous Occurrence") - } - IconButton(onClick = { moveToNextOccurrence(selection) }) { - Icon(Icons.Default.KeyboardArrowDown, "Next Occurrence") + Row(verticalAlignment = Alignment.CenterVertically) { + Row(modifier = Modifier) { + if (selection.selected == null) { + Text( + " ${selection.allDetections.size}", + modifier = Modifier.align(Alignment.CenterVertically) + ) + } else { + Text( + "${selection.currentIndexInView} / ${selection.totalCount}", + modifier = Modifier.align(Alignment.CenterVertically) + ) } } - - // TODO cleanup ; don't cast - /* - (it.focusing as? JsonDetectionResult)?.let { - Text(it.jsonList) // TODO show summary of json - } - */ + IconButton(onClick = { moveToPreviousOccurrence(selection) }) { + Icon(Icons.Default.KeyboardArrowUp, "Previous Occurrence") + } + IconButton(onClick = { moveToNextOccurrence(selection) }) { + Icon(Icons.Default.KeyboardArrowDown, "Next Occurrence") + } } } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt index 46439bc..47e369a 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/LogRow.kt @@ -7,8 +7,6 @@ import androidx.compose.foundation.PointerMatcher import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.onClick -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ContentCopy @@ -17,28 +15,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.type import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogState -import com.jerryjeon.logjerry.detector.JsonDetection import com.jerryjeon.logjerry.log.Log import com.jerryjeon.logjerry.log.LogContentView import com.jerryjeon.logjerry.logview.RefinedLog +import com.jerryjeon.logjerry.logview.toHumanReadable import com.jerryjeon.logjerry.mark.LogMark import com.jerryjeon.logjerry.preferences.Preferences import com.jerryjeon.logjerry.table.ColumnInfo import com.jerryjeon.logjerry.table.ColumnType import com.jerryjeon.logjerry.table.Header import com.jerryjeon.logjerry.util.copyToClipboard -import com.jerryjeon.logjerry.util.isCtrlOrMetaPressed import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject val json = Json { prettyPrint = true } @@ -80,34 +69,52 @@ fun LogRow( MarkDialog(showMarkDialog, setMark) - Row( - Modifier - .background( - when { - selected -> Color(0x20CCCCCC) - else -> Color.Transparent - } - ) - .onClick { selectLog(refinedLog) } - .onClick( - matcher = PointerMatcher.mouse(PointerButton.Secondary), - onClick = { - showContextMenu = refinedLog - } - ) + Column { + // This should be separated as a different item of LazyColumn + if (refinedLog.timeGap != null) { + Box( + Modifier + .fillMaxWidth() + .height(40.dp) + .background(Color(0x20CCCCCC)) + ) { + Text( + text = "Large time gap: ${refinedLog.timeGap.toHumanReadable()}", + style = MaterialTheme.typography.body2, + modifier = Modifier.align(Alignment.Center) + ) + } + } - ) { - Spacer(Modifier.width(8.dp)) - header.asColumnList.forEach { columnInfo -> - if (columnInfo.visible) { - CellByColumnType(preferences, columnInfo, refinedLog) - if (columnInfo.columnType.showDivider) { - divider() + Row( + Modifier + .background( + when { + selected -> Color(0x20CCCCCC) + else -> Color.Transparent + } + ) + .onClick { selectLog(refinedLog) } + .onClick( + matcher = PointerMatcher.mouse(PointerButton.Secondary), + onClick = { + showContextMenu = refinedLog + } + ) + + ) { + Spacer(Modifier.width(8.dp)) + header.asColumnList.forEach { columnInfo -> + if (columnInfo.visible) { + CellByColumnType(preferences, columnInfo, refinedLog) + if (columnInfo.columnType.showDivider) { + divider() + } } } - } - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(8.dp)) + } } } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/LogsView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/LogsView.kt index 068fc57..3ba7c8f 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/LogsView.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/LogsView.kt @@ -2,18 +2,12 @@ package com.jerryjeon.logjerry.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.* import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* @@ -23,16 +17,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.key.* import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.jerryjeon.logjerry.ColumnDivider import com.jerryjeon.logjerry.HeaderDivider +import com.jerryjeon.logjerry.detector.Detection import com.jerryjeon.logjerry.detector.DetectorManager import com.jerryjeon.logjerry.log.ParseCompleted import com.jerryjeon.logjerry.logview.LogSelection +import com.jerryjeon.logjerry.logview.MarkInfo import com.jerryjeon.logjerry.logview.RefineResult import com.jerryjeon.logjerry.logview.RefinedLog import com.jerryjeon.logjerry.mark.LogMark @@ -41,10 +38,7 @@ import com.jerryjeon.logjerry.table.Header import com.jerryjeon.logjerry.ui.focus.DetectionFocus import com.jerryjeon.logjerry.ui.focus.KeyboardFocus import com.jerryjeon.logjerry.ui.focus.LogFocus -import com.jerryjeon.logjerry.ui.focus.MarkFocus import com.jerryjeon.logjerry.util.isCtrlOrMetaPressed -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -102,34 +96,15 @@ fun LogsView( detectorManager = detectorManager, header = header, listState = listState, - markedRows = refineResult.markedRows, + markInfos = refineResult.markInfos, setMark = detectorManager::setMark, deleteMark = detectorManager::deleteMark, hide = hide, changeFocus = { refineResult.currentFocus.value = it }, moveToPreviousMark = moveToPreviousMark, moveToNextMark = moveToNextMark, + selectDetection = refineResult::selectDetection, ) - LazyColumn(modifier = Modifier.width(120.dp).fillMaxHeight().align(Alignment.CenterEnd)) { - items(refineResult.markedRows) { - val mark = it.mark!! - Box( - modifier = Modifier.fillMaxWidth().height(60.dp).background(mark.color) - .clickable { - refineResult.selectDetection(mark) - }, - ) { - Text( - mark.note, - modifier = Modifier.align(Alignment.Center), - textAlign = TextAlign.Center, - color = Color.Black, - ) - } - Divider() - } - } - } } @@ -142,13 +117,14 @@ fun LogsView( preferences: Preferences, header: Header, listState: LazyListState, - markedRows: List, + markInfos: List, setMark: (logMark: LogMark) -> Unit, deleteMark: (logIndex: Int) -> Unit, hide: (logIndex: Int) -> Unit, changeFocus: (LogFocus?) -> Unit, moveToPreviousMark: () -> Unit, moveToNextMark: () -> Unit, + selectDetection: (Detection) -> Unit, ) { val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } @@ -295,7 +271,6 @@ fun LogsView( } else { logRow() } - Divider() } } } @@ -307,48 +282,147 @@ fun LogsView( adapter = adapter, ) - var isScrolling by remember { mutableStateOf(false) } - LaunchedEffect(listState.firstVisibleItemScrollOffset) { - isScrolling = true - delay(1000) - isScrolling = false + Box( + modifier = Modifier + .width(120.dp) + .fillMaxHeight() + .padding(end = LocalScrollbarStyle.current.thickness) + .align(Alignment.TopEnd) + ) { + MarkView(markInfos, listState.layoutInfo.viewportSize.height, refinedLogs.size, selectDetection) } + } + } - // TODO why this should be annotated - this@Column.AnimatedVisibility( - visible = isScrolling, - enter = fadeIn(animationSpec = tween(500)), - exit = fadeOut(animationSpec = tween(500)) - ) { - val width = listState.layoutInfo.viewportSize.width - Box( - modifier = Modifier - .fillMaxSize() - .padding(start = (width - 120).dp) - ) { - markedRows.forEach { - val y = - it.log.index.toFloat() / listState.layoutInfo.totalItemsCount.toFloat() * listState.layoutInfo.viewportSize.height - Box( - modifier = Modifier.fillMaxWidth().height(30.dp) - .offset(y = y.toInt().dp) - .background(it.mark!!.color), - contentAlignment = Alignment.Center, - ) { - Text( - text = it.mark.note, - color = Color.Black, - fontSize = 12.sp, - maxLines = 1 - ) - } + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } +} + +@Composable +private fun MarkView( + markInfos: List, + viewportHeight: Int, + refinedLogsSize: Int, + selectDetection: (Detection) -> Unit, +) { + val minHeight = 40 + val minRatio = minHeight.toFloat() / viewportHeight.toFloat() + + Column(modifier = Modifier.fillMaxSize()) { + markInfos.forEach { + when (it) { + is MarkInfo.Marked -> { + val mark = it.markedLog.mark!! + Box( + modifier = Modifier.fillMaxWidth().height(60.dp).background(mark.color) + .clickable { selectDetection(mark) }, + ) { + Text( + mark.note, + modifier = Modifier.align(Alignment.Center), + textAlign = TextAlign.Center, + color = Color.Black, + ) + } + } + + is MarkInfo.StatBetweenMarks -> { + val ratio = it.logCount.toFloat() / refinedLogsSize.toFloat() / markInfos.size + val baseModifier = if (ratio < minRatio) { + Modifier.height(minHeight.dp) + } else { + Modifier.weight(ratio) + } + Box( + modifier = baseModifier.fillMaxWidth() + ) { + DashedDivider( + modifier = Modifier.fillMaxHeight().align(Alignment.Center), + thickness = 4.dp, + color = Color(0x44888888) + ) + Text( + text = "${it.logCount} logs, ${it.duration}", + modifier = Modifier.padding(vertical = 12.dp).align(Alignment.Center).background(MaterialTheme.colors.background), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2 + ) } } } } } - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() + /* TODO it seems not useful.. + var isScrolling by remember { mutableStateOf(false) } + LaunchedEffect(listState.firstVisibleItemScrollOffset) { + isScrolling = true + delay(1000) + isScrolling = false + } + + Column(modifier = Modifier.fillMaxSize()) { + AnimatedVisibility( + visible = isScrolling, + enter = fadeIn(animationSpec = tween(500)), + exit = fadeOut(animationSpec = tween(500)) + ) { + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) + } + } + + Column(modifier = Modifier.fillMaxSize()) { + AnimatedVisibility( + visible = isScrolling, + enter = fadeIn(animationSpec = tween(500)), + exit = fadeOut(animationSpec = tween(500)) + ) { + Column(modifier = Modifier.fillMaxSize()) { + markedRows.forEach { + val y = + it.log.index.toFloat() / listState.layoutInfo.totalItemsCount.toFloat() * listState.layoutInfo.viewportSize.height + Box( + modifier = Modifier.fillMaxWidth().height(30.dp) + .offset(y = y.toInt().dp) + .background(it.mark!!.color), + contentAlignment = Alignment.Center, + ) { + Text( + text = it.mark.note, + color = Color.Black, + fontSize = 12.sp, + maxLines = 1 + ) + } + } + } + } + } + */ +} + +@Composable +fun DashedDivider( + thickness: Dp, + color: Color = MaterialTheme.colors.onSurface, + phase: Float = 10f, + intervals: FloatArray = floatArrayOf(20f, 25f), + modifier: Modifier = Modifier +) { + Canvas( + modifier = modifier + ) { + val dividerHeight = thickness.toPx() + drawRoundRect( + color = color, + style = Stroke( + width = dividerHeight, + pathEffect = PathEffect.dashPathEffect( + intervals = intervals, + phase = phase + ) + ) + ) } } diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/ParseCompletedView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/ParseCompletedView.kt index fbc6068..ef7f3e2 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/ParseCompletedView.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/ParseCompletedView.kt @@ -3,15 +3,21 @@ package com.jerryjeon.logjerry.ui import androidx.compose.foundation.layout.* +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.unit.dp +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider import com.jerryjeon.logjerry.detector.DetectorKey import com.jerryjeon.logjerry.detector.KeywordDetectionView +import com.jerryjeon.logjerry.filter.FilterManager +import com.jerryjeon.logjerry.filter.PriorityFilter import com.jerryjeon.logjerry.log.Log import com.jerryjeon.logjerry.log.ParseCompleted import com.jerryjeon.logjerry.preferences.Preferences @@ -31,31 +37,47 @@ fun ParseCompletedView( Column( modifier = Modifier ) { - Column { - val keywordDetectionRequest by detectorManager.keywordDetectionRequestFlow.collectAsState() + Row(modifier = Modifier.height(IntrinsicSize.Min)) { val statusByKey by refineResult.statusByKey.collectAsState() - Row(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier + .padding(12.dp) + .height(IntrinsicSize.Min) + ) { FilterView(filterManager) - DetectionView( - preferences, - detectorManager, - statusByKey, - openNewTab, - refineResult::selectPreviousDetection, - refineResult::selectNextDetection, - ) + Spacer(Modifier.width(8.dp)) + statusByKey[DetectorKey.Json]?.let { + JsonDetectionView( + modifier = Modifier.fillMaxHeight(), + detectionStatus = it, + moveToPreviousOccurrence = refineResult::selectPreviousDetection, + moveToNextOccurrence = refineResult::selectNextDetection, + ) + } } - Box(modifier = Modifier.fillMaxWidth()) { - KeywordDetectionView( - Modifier.align(Alignment.BottomEnd), - keywordDetectionRequest, - statusByKey[DetectorKey.Keyword], - detectorManager::findKeyword, - detectorManager::setKeywordDetectionEnabled, - refineResult::selectPreviousDetection, - refineResult::selectNextDetection, - ) + Spacer(modifier = Modifier.weight(1f)) + + val keywordDetectionRequest by detectorManager.keywordDetectionRequestFlow.collectAsState() + KeywordDetectionView( + keywordDetectionRequest = keywordDetectionRequest, + detectionStatus = statusByKey[DetectorKey.Keyword], + find = detectorManager::findKeyword, + setFindEnabled = detectorManager::setKeywordDetectionEnabled, + moveToPreviousOccurrence = refineResult::selectPreviousDetection, + moveToNextOccurrence = refineResult::selectNextDetection, + ) + } + + val textFilters by filterManager.textFiltersFlow.collectAsState() + Column { + textFilters.chunked(2).forEach { + Row { + it.forEach { filter -> + AppliedTextFilter(filter, filterManager::removeTextFilter) + Spacer(Modifier.width(8.dp)) + } + } } } @@ -72,3 +94,96 @@ fun ParseCompletedView( } } +@Composable +private fun FilterView(filterManager: FilterManager) { + var showTextFilterPopup by remember { mutableStateOf(false) } + var textFilterAnchor by remember { mutableStateOf(Offset.Zero) } + var showLogLevelPopup by remember { mutableStateOf(false) } + var logLevelAnchor by remember { mutableStateOf(Offset.Zero) } + val priorityFilter by filterManager.priorityFilterFlow.collectAsState() + + OutlinedButton( + onClick = { + showTextFilterPopup = true + }, + modifier = Modifier + .height(48.dp) + .onGloballyPositioned { coordinates -> + textFilterAnchor = coordinates.positionInRoot() + }, + ) { + Text("Add Filter") + } + + Spacer(Modifier.width(8.dp)) + + OutlinedButton( + onClick = { + showLogLevelPopup = true + }, + modifier = Modifier + .height(48.dp) + .onGloballyPositioned { coordinates -> + logLevelAnchor = coordinates.positionInRoot() + }, + ) { + Text("Log Level | ${priorityFilter.priority.name}") + } + + if (showTextFilterPopup) { + Popup( + onDismissRequest = { showTextFilterPopup = false }, + focusable = true, + popupPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return IntOffset(textFilterAnchor.x.toInt(), (textFilterAnchor.y + anchorBounds.height).toInt()) + } + } + ) { + TextFilterView( + filterManager::addTextFilter + ) { showTextFilterPopup = false } + } + } + + PriorityFilterPopup( + priorityFilter, + logLevelAnchor, + showLogLevelPopup, + dismissPopup = { showLogLevelPopup = false }, + setPriorityFilter = filterManager::setPriorityFilter + ) +} + +@Composable +private fun PriorityFilterPopup( + priorityFilter: PriorityFilter, + anchor: Offset, + showPopup: Boolean, + dismissPopup: () -> Unit, + setPriorityFilter: (PriorityFilter) -> Unit +) { + if (showPopup) { + Popup( + onDismissRequest = dismissPopup, + focusable = true, + popupPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return IntOffset(anchor.x.toInt(), (anchor.y + anchorBounds.height).toInt()) + } + } + ) { + PriorityFilterView(priorityFilter, setPriorityFilter) + } + } +} diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/PriorityFilterView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/PriorityFilterView.kt index 5e39989..cae887f 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/PriorityFilterView.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/PriorityFilterView.kt @@ -1,24 +1,14 @@ package com.jerryjeon.logjerry.ui +import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme import androidx.compose.material.Slider import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign @@ -33,7 +23,12 @@ fun PriorityFilterView( changePriorityFilter: (PriorityFilter) -> Unit ) { var value by remember(priorityFilter) { mutableStateOf(priorityFilter.priority.ordinal.toFloat()) } - Column(Modifier.width(IntrinsicSize.Min).border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)).padding(8.dp)) { + Column( + Modifier.width(IntrinsicSize.Min) + .border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)) + .background(MaterialTheme.colors.background) + .padding(8.dp) + ) { Text("Log level") Spacer(Modifier.height(8.dp)) val priorities = Priority.values() diff --git a/src/main/kotlin/com/jerryjeon/logjerry/ui/TextFilterView.kt b/src/main/kotlin/com/jerryjeon/logjerry/ui/TextFilterView.kt index 843ced9..dd6d1e6 100644 --- a/src/main/kotlin/com/jerryjeon/logjerry/ui/TextFilterView.kt +++ b/src/main/kotlin/com/jerryjeon/logjerry/ui/TextFilterView.kt @@ -2,51 +2,20 @@ package com.jerryjeon.logjerry.ui +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Divider -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.Close -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.key.* import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -55,43 +24,29 @@ import com.jerryjeon.logjerry.table.ColumnType @Composable fun TextFilterView( - textFilters: List, addFilter: (TextFilter) -> Unit, - removeFilter: (TextFilter) -> Unit + dismiss: () -> Unit ) { - Box(Modifier.border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)).padding(8.dp)) { - Column { - Text("Filter") - CompositionLocalProvider( - LocalTextStyle provides LocalTextStyle.current.copy(fontSize = 12.sp) - ) { - Spacer(Modifier.height(8.dp)) - AddTextFilterView(addFilter) - - Spacer(Modifier.height(8.dp)) - // FlowRow must be better, but it works strangely.. - Column { - textFilters.chunked(2).forEach { - Row { - it.forEach { filter -> - AppliedTextFilter(filter, removeFilter) - Spacer(Modifier.width(8.dp)) - } - } - } - } - } + Column( + Modifier.border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)).background(MaterialTheme.colors.background) + .padding(8.dp) + ) { + CompositionLocalProvider( + LocalTextStyle provides LocalTextStyle.current.copy(fontSize = 12.sp) + ) { + AddTextFilterView(addFilter, dismiss) } } } @Composable private fun AddTextFilterView( - addFilter: (TextFilter) -> Unit + addFilter: (TextFilter) -> Unit, + dismiss: () -> Unit, ) { var text by remember { mutableStateOf("") } val columnTypeState = remember { mutableStateOf(ColumnType.Log) } - Row( + Column( modifier = Modifier.height(IntrinsicSize.Min).onPreviewKeyEvent { when { it.key == Key.Enter && it.type == KeyEventType.KeyDown -> { @@ -115,18 +70,30 @@ private fun AddTextFilterView( leadingIcon = { SelectColumnTypeView(columnTypeState) }, - trailingIcon = { - IconButton(onClick = { - addFilter(TextFilter(columnTypeState.value, text)) - text = "" - }) { - Icon(Icons.Default.Add, "Add a filter") - } - }, colors = TextFieldDefaults.textFieldColors( backgroundColor = Color.Transparent ) ) + + Row(modifier = Modifier.align(Alignment.End)) { + TextButton( + onClick = { + dismiss() + text = "" + } + ) { + Text("Cancel") + } + TextButton( + onClick = { + addFilter(TextFilter(columnTypeState.value, text)) + dismiss() + text = "" + } + ) { + Text("Ok") + } + } } } @@ -163,38 +130,3 @@ private fun SelectColumnTypeView( } } } - -@Composable -private fun AppliedTextFilter(textFilter: TextFilter, removeFilter: (TextFilter) -> Unit) { - Box( - Modifier - .padding(horizontal = 4.dp, vertical = 8.dp) - .border(1.dp, Color.LightGray, RoundedCornerShape(8.dp)) - ) { - Row(modifier = Modifier.height(30.dp)) { - Spacer(Modifier.width(8.dp)) - Text( - textFilter.columnType.text, - modifier = Modifier.align(Alignment.CenterVertically), - ) - Spacer(Modifier.width(8.dp)) - Divider(Modifier.width(1.dp).fillMaxHeight()) - Spacer(Modifier.width(8.dp)) - Text(textFilter.text, modifier = Modifier.align(Alignment.CenterVertically)) - Spacer(Modifier.width(8.dp)) - Box( - Modifier - .clickable { removeFilter(textFilter) } - .align(Alignment.CenterVertically) - .fillMaxHeight() - .aspectRatio(1f) - ) { - Icon( - Icons.Default.Close, - contentDescription = "Remove a filter", - modifier = Modifier.size(ButtonDefaults.IconSize).align(Alignment.Center) - ) - } - } - } -}