|
|
@@ -3,15 +3,19 @@ package io.legado.app.ui.widget.components.rules
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
+import androidx.compose.foundation.layout.Row
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
+import androidx.compose.foundation.layout.size
|
|
|
import androidx.compose.foundation.rememberScrollState
|
|
|
import androidx.compose.foundation.verticalScroll
|
|
|
import androidx.compose.material.icons.Icons
|
|
|
import androidx.compose.material.icons.automirrored.filled.NoteAdd
|
|
|
+import androidx.compose.material.icons.filled.Check
|
|
|
import androidx.compose.material.icons.filled.Close
|
|
|
import androidx.compose.material.icons.filled.ContentPaste
|
|
|
import androidx.compose.material.icons.filled.MoreVert
|
|
|
+import androidx.compose.material.icons.filled.RunningWithErrors
|
|
|
import androidx.compose.material.icons.filled.Save
|
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
|
import androidx.compose.material3.Icon
|
|
|
@@ -24,15 +28,18 @@ import androidx.compose.runtime.setValue
|
|
|
import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
import androidx.compose.ui.res.stringResource
|
|
|
+import androidx.compose.ui.text.style.TextOverflow
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
import io.legado.app.R
|
|
|
import io.legado.app.ui.theme.LegadoTheme
|
|
|
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
|
|
import io.legado.app.ui.widget.components.AppTextField
|
|
|
+import io.legado.app.ui.widget.components.text.AppText
|
|
|
import io.legado.app.ui.widget.components.button.series.MediumPlainButton
|
|
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
|
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
|
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
|
|
+import kotlinx.coroutines.Dispatchers
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
|
/**
|
|
|
@@ -45,6 +52,15 @@ data class RuleEditFields(
|
|
|
val extra: String = ""
|
|
|
)
|
|
|
|
|
|
+/**
|
|
|
+ * 测试结果:每一行的匹配状态
|
|
|
+ */
|
|
|
+data class TestLineResult(
|
|
|
+ val line: String,
|
|
|
+ val matched: Boolean,
|
|
|
+ val matchResult: String? = null,
|
|
|
+)
|
|
|
+
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
|
@Composable
|
|
|
fun <T> RuleEditSheet(
|
|
|
@@ -58,7 +74,9 @@ fun <T> RuleEditSheet(
|
|
|
onCopy: (T) -> Unit,
|
|
|
onPaste: () -> T?,
|
|
|
toFields: (T?) -> RuleEditFields,
|
|
|
- fromFields: (RuleEditFields, T?) -> T
|
|
|
+ fromFields: (RuleEditFields, T?) -> T,
|
|
|
+ showTestButton: Boolean = false,
|
|
|
+ onTest: (suspend (rule: String, example: String) -> List<TestLineResult>?)? = null,
|
|
|
) {
|
|
|
val scope = rememberCoroutineScope()
|
|
|
|
|
|
@@ -69,8 +87,58 @@ fun <T> RuleEditSheet(
|
|
|
|
|
|
var showMenu by remember(show, rule) { mutableStateOf(false) }
|
|
|
|
|
|
+ // Test results state
|
|
|
+ var testResults by remember(show, rule) { mutableStateOf<List<TestLineResult>?>(null) }
|
|
|
+ var testError by remember(show, rule) { mutableStateOf<String?>(null) }
|
|
|
+ var testRunning by remember(show, rule) { mutableStateOf(false) }
|
|
|
+
|
|
|
+ // Pre-resolve string resources at composable level
|
|
|
+ val regexIsEmptyStr = stringResource(R.string.regex_is_empty)
|
|
|
+ val exampleIsEmptyStr = stringResource(R.string.example_is_empty)
|
|
|
+ val invalidRegexStr = stringResource(R.string.invalid_regex)
|
|
|
+
|
|
|
fun getCurrentEntity() = fromFields(RuleEditFields(name, rule1, rule2), rule)
|
|
|
|
|
|
+ fun runTest() {
|
|
|
+ if (testRunning) return
|
|
|
+ if (rule1.isBlank()) {
|
|
|
+ testError = regexIsEmptyStr
|
|
|
+ testResults = null
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (rule2.isBlank()) {
|
|
|
+ testError = exampleIsEmptyStr
|
|
|
+ testResults = null
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ val testCallback = onTest
|
|
|
+ if (testCallback == null) {
|
|
|
+ testError = invalidRegexStr
|
|
|
+ testResults = null
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Capture inputs for background processing
|
|
|
+ val capturedRule1 = rule1
|
|
|
+ val capturedRule2 = rule2
|
|
|
+ testRunning = true
|
|
|
+ testError = null
|
|
|
+ testResults = null
|
|
|
+
|
|
|
+ scope.launch(Dispatchers.Default) {
|
|
|
+ try {
|
|
|
+ val results = testCallback(capturedRule1, capturedRule2)
|
|
|
+ testResults = results
|
|
|
+ } catch (_: Exception) {
|
|
|
+ testError = invalidRegexStr
|
|
|
+ testResults = null
|
|
|
+ } finally {
|
|
|
+ testRunning = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
AppModalBottomSheet(
|
|
|
title = title,
|
|
|
startAction = {
|
|
|
@@ -148,16 +216,83 @@ fun <T> RuleEditSheet(
|
|
|
label = label2,
|
|
|
minLines = 3
|
|
|
)
|
|
|
+
|
|
|
+ if (showTestButton) {
|
|
|
+ // Test error
|
|
|
+ testError?.let { error ->
|
|
|
+ AppText(
|
|
|
+ text = error,
|
|
|
+ color = LegadoTheme.colorScheme.error,
|
|
|
+ style = LegadoTheme.typography.bodySmall,
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test results
|
|
|
+ testResults?.let { results ->
|
|
|
+ val matchedCount = results.count { it.matched }
|
|
|
+ val totalCount = results.count { it.line.isNotBlank() }
|
|
|
+ AppText(
|
|
|
+ text = "$matchedCount / $totalCount",
|
|
|
+ style = LegadoTheme.typography.labelMedium,
|
|
|
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
+ )
|
|
|
+ results.forEach { result ->
|
|
|
+ if (result.line.isBlank()) return@forEach
|
|
|
+ Row(
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
+ verticalAlignment = Alignment.CenterVertically,
|
|
|
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
+ ) {
|
|
|
+ Icon(
|
|
|
+ imageVector = if (result.matched) Icons.Default.Check else Icons.Default.Close,
|
|
|
+ contentDescription = null,
|
|
|
+ tint = if (result.matched) {
|
|
|
+ LegadoTheme.colorScheme.primary
|
|
|
+ } else {
|
|
|
+ LegadoTheme.colorScheme.error
|
|
|
+ },
|
|
|
+ modifier = Modifier.size(18.dp),
|
|
|
+ )
|
|
|
+ AppText(
|
|
|
+ text = result.line,
|
|
|
+ style = LegadoTheme.typography.bodySmall,
|
|
|
+ color = if (result.matched) {
|
|
|
+ LegadoTheme.colorScheme.onSurface
|
|
|
+ } else {
|
|
|
+ LegadoTheme.colorScheme.onSurfaceVariant
|
|
|
+ },
|
|
|
+ maxLines = 1,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ modifier = Modifier.weight(1f),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- AppFloatingActionButton(
|
|
|
- onClick = { onSave(getCurrentEntity()) },
|
|
|
+ Row(
|
|
|
modifier = Modifier
|
|
|
.align(Alignment.BottomEnd)
|
|
|
.padding(16.dp),
|
|
|
- tooltipText = stringResource(R.string.action_save),
|
|
|
- icon = Icons.Default.Save
|
|
|
- )
|
|
|
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
|
+ ) {
|
|
|
+ if (showTestButton) {
|
|
|
+ AppFloatingActionButton(
|
|
|
+ onClick = { runTest() },
|
|
|
+ tooltipText = stringResource(R.string.test),
|
|
|
+ icon = Icons.Default.RunningWithErrors,
|
|
|
+ containerColor = LegadoTheme.colorScheme.secondaryContainer,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ AppFloatingActionButton(
|
|
|
+ onClick = { onSave(getCurrentEntity()) },
|
|
|
+ tooltipText = stringResource(R.string.action_save),
|
|
|
+ icon = Icons.Default.Save
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|