Răsfoiți Sursa

refactor: 优化TXT目录规则预览页面

- 统一正则验证引擎为 Pattern.MULTILINE,与预览编译保持一致
- 通过 Koin 注入 TxtTocRuleRepository,替代 ViewModel 内部直接 new
- 批量计算章节预览后一次性更新 State,减少 recomposition
- 替换 MaterialTheme 为 LegadoTheme,兼容 Miuix 引擎
- 底部双按钮替换为 AppFAB(勾选图标)
- 顶栏接入 nestedScroll 滚动状态
- 内容全屏显示,padding 跟随 scaffold
HapeLee 4 luni în urmă
părinte
comite
7af07d9324

+ 3 - 1
app/src/main/java/io/legado/app/di/appModule.kt

@@ -45,6 +45,7 @@ import io.legado.app.data.repository.SearchContentRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchRepositoryImpl
 import io.legado.app.data.repository.SearchRepositoryImpl
 import io.legado.app.data.repository.SettingsRepository
 import io.legado.app.data.repository.SettingsRepository
+import io.legado.app.data.repository.TxtTocRuleRepository
 import io.legado.app.data.repository.TranslationCacheRepositoryImpl
 import io.legado.app.data.repository.TranslationCacheRepositoryImpl
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.WebDavBackupRepository
 import io.legado.app.data.repository.WebDavBackupRepository
@@ -187,6 +188,7 @@ val appModule = module {
     singleOf(::BookSourceRepository)
     singleOf(::BookSourceRepository)
     singleOf(::BookshelfRepository)
     singleOf(::BookshelfRepository)
     singleOf(::DictRuleRepository)
     singleOf(::DictRuleRepository)
+    singleOf(::TxtTocRuleRepository)
     singleOf(::SearchContentRepository)
     singleOf(::SearchContentRepository)
     singleOf(::RemoteBookRepository)
     singleOf(::RemoteBookRepository)
     singleOf(::SettingsRepository)
     singleOf(::SettingsRepository)
@@ -300,7 +302,7 @@ val appModule = module {
     viewModelOf(::ReplaceRuleViewModel)
     viewModelOf(::ReplaceRuleViewModel)
     viewModelOf(::AllBookmarkViewModel)
     viewModelOf(::AllBookmarkViewModel)
     viewModelOf(::TxtTocRuleViewModel)
     viewModelOf(::TxtTocRuleViewModel)
-    viewModel { TxtTocRulePreviewViewModel(app = get()) }
+    viewModel { TxtTocRulePreviewViewModel(app = get(), repository = get()) }
     viewModelOf(::OtherConfigViewModel)
     viewModelOf(::OtherConfigViewModel)
     viewModelOf(::ReadConfigViewModel)
     viewModelOf(::ReadConfigViewModel)
     viewModelOf(::CoverConfigViewModel)
     viewModelOf(::CoverConfigViewModel)

+ 49 - 75
app/src/main/java/io/legado/app/ui/book/toc/rule/preview/TxtTocRulePreviewScreen.kt

@@ -21,11 +21,11 @@ import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
 import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
+import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.GridView
 import androidx.compose.material.icons.filled.GridView
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
@@ -33,6 +33,7 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
@@ -41,19 +42,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.data.entities.TxtTocRule
 import io.legado.app.data.entities.TxtTocRule
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
+import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
-import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
+import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
+import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.rules.RuleEditFields
 import io.legado.app.ui.widget.components.rules.RuleEditFields
 import io.legado.app.ui.widget.components.rules.RuleEditSheet
 import io.legado.app.ui.widget.components.rules.RuleEditSheet
+import io.legado.app.ui.widget.components.text.AppText
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -151,12 +155,15 @@ fun TxtTocRulePreviewScreen(
         )
         )
     }
     }
 
 
+    val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
+
     AppScaffold(
     AppScaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = { _ ->
         topBar = { _ ->
             GlassMediumFlexibleTopAppBar(
             GlassMediumFlexibleTopAppBar(
                 title = stringResource(R.string.select_toc_rule),
                 title = stringResource(R.string.select_toc_rule),
                 subtitle = stringResource(R.string.select_rule_to_preview_chapters),
                 subtitle = stringResource(R.string.select_rule_to_preview_chapters),
-                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),
+                scrollBehavior = scrollBehavior,
                 navigationIcon = { TopBarNavigationButton(onBack) },
                 navigationIcon = { TopBarNavigationButton(onBack) },
                 actions = {
                 actions = {
                     TopBarActionButton(
                     TopBarActionButton(
@@ -173,23 +180,12 @@ fun TxtTocRulePreviewScreen(
                 },
                 },
             )
             )
         },
         },
-        bottomBar = {
-            Box(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(horizontal = 16.dp, vertical = 12.dp),
-            ) {
-                ConfirmDismissButtonsRow(
-                    onDismiss = onBack,
-                    onConfirm = {
-                        if (state.hasSelection) {
-                            onApplyRule(state.selectedRule)
-                        }
-                    },
-                    dismissText = stringResource(R.string.cancel),
-                    confirmText = stringResource(R.string.ok),
-                    confirmEnabled = state.hasSelection,
-                    modifier = Modifier.fillMaxWidth(),
+        floatingActionButton = {
+            if (state.hasSelection) {
+                AppFloatingActionButton(
+                    onClick = { onApplyRule(state.selectedRule) },
+                    icon = Icons.Default.Check,
+                    tooltipText = stringResource(R.string.ok),
                 )
                 )
             }
             }
         },
         },
@@ -206,12 +202,10 @@ fun TxtTocRulePreviewScreen(
         } else if (state.isGridLayout) {
         } else if (state.isGridLayout) {
             LazyVerticalGrid(
             LazyVerticalGrid(
                 columns = GridCells.Fixed(2),
                 columns = GridCells.Fixed(2),
-                modifier = Modifier
-                    .fillMaxSize()
-                    .padding(contentPadding),
+                modifier = Modifier.fillMaxSize(),
                 contentPadding = adaptiveContentPadding(
                 contentPadding = adaptiveContentPadding(
-                    top = 8.dp,
-                    bottom = 8.dp,
+                    top = contentPadding.calculateTopPadding(),
+                    bottom = contentPadding.calculateBottomPadding() + 80.dp,
                 ),
                 ),
                 horizontalArrangement = Arrangement.spacedBy(8.dp),
                 horizontalArrangement = Arrangement.spacedBy(8.dp),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -231,12 +225,10 @@ fun TxtTocRulePreviewScreen(
             }
             }
         } else {
         } else {
             LazyColumn(
             LazyColumn(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .padding(contentPadding),
+                modifier = Modifier.fillMaxSize(),
                 contentPadding = adaptiveContentPadding(
                 contentPadding = adaptiveContentPadding(
-                    top = 8.dp,
-                    bottom = 8.dp,
+                    top = contentPadding.calculateTopPadding(),
+                    bottom = contentPadding.calculateBottomPadding() + 80.dp,
                 ),
                 ),
                 verticalArrangement = Arrangement.spacedBy(4.dp),
                 verticalArrangement = Arrangement.spacedBy(4.dp),
             ) {
             ) {
@@ -266,68 +258,53 @@ private fun RulePreviewCard(
     NormalCard(
     NormalCard(
         onClick = onClick,
         onClick = onClick,
         border = if (isSelected) {
         border = if (isSelected) {
-            BorderStroke(
-                width = 1.5.dp,
-                color = MaterialTheme.colorScheme.primary,
-            )
+            BorderStroke(1.5.dp, LegadoTheme.colorScheme.primary)
         } else {
         } else {
             null
             null
         },
         },
         modifier = Modifier.fillMaxWidth(),
         modifier = Modifier.fillMaxWidth(),
     ) {
     ) {
-        Box(
-            modifier = Modifier.fillMaxWidth(),
-        ) {
+        Box(modifier = Modifier.fillMaxWidth()) {
             Column(
             Column(
                 modifier = Modifier
                 modifier = Modifier
                     .padding(12.dp)
                     .padding(12.dp)
                     .padding(bottom = 28.dp),
                     .padding(bottom = 28.dp),
             ) {
             ) {
-                // Rule name
-                Text(
+                AppText(
                     text = item.rule.name,
                     text = item.rule.name,
-                    style = MaterialTheme.typography.titleSmall,
+                    style = LegadoTheme.typography.titleSmall,
                     maxLines = 2,
                     maxLines = 2,
                     overflow = TextOverflow.Ellipsis,
                     overflow = TextOverflow.Ellipsis,
                 )
                 )
-
                 Spacer(modifier = Modifier.height(4.dp))
                 Spacer(modifier = Modifier.height(4.dp))
-
-                // Example text (subtitle)
                 item.rule.example?.let { example ->
                 item.rule.example?.let { example ->
-                    Text(
+                    AppText(
                         text = example,
                         text = example,
-                        style = MaterialTheme.typography.bodySmall,
-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
+                        style = LegadoTheme.typography.bodySmall,
+                        color = LegadoTheme.colorScheme.onSurfaceVariant,
                         maxLines = 2,
                         maxLines = 2,
                         overflow = TextOverflow.Ellipsis,
                         overflow = TextOverflow.Ellipsis,
                     )
                     )
                 }
                 }
             }
             }
-
-            // Chapter count badge - bottom right
             Surface(
             Surface(
                 shape = RoundedCornerShape(12.dp),
                 shape = RoundedCornerShape(12.dp),
-                color = MaterialTheme.colorScheme.primaryContainer,
+                color = LegadoTheme.colorScheme.primaryContainer,
                 modifier = Modifier
                 modifier = Modifier
                     .align(Alignment.BottomEnd)
                     .align(Alignment.BottomEnd)
                     .padding(8.dp),
                     .padding(8.dp),
             ) {
             ) {
                 if (item.totalCount < 0) {
                 if (item.totalCount < 0) {
-                    CircularProgressIndicator(
+                    AppCircularProgressIndicator(
                         modifier = Modifier
                         modifier = Modifier
                             .size(16.dp)
                             .size(16.dp)
-                            .padding(4.dp),
-                        strokeWidth = 2.dp,
+                            .padding(4.dp)
                     )
                     )
                 } else {
                 } else {
-                    Text(
-                        text = stringResource(
-                            R.string.chapter_count_format,
-                            item.totalCount
-                        ),
-                        style = MaterialTheme.typography.labelSmall,
-                        color = MaterialTheme.colorScheme.onPrimaryContainer,
+                    AppText(
+                        text = stringResource(R.string.chapter_count_format, item.totalCount),
+                        style = LegadoTheme.typography.labelSmall,
+                        color = LegadoTheme.colorScheme.onPrimaryContainer,
                         modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
                         modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
                     )
                     )
                 }
                 }
@@ -345,10 +322,7 @@ private fun RulePreviewListItem(
     NormalCard(
     NormalCard(
         onClick = onClick,
         onClick = onClick,
         border = if (isSelected) {
         border = if (isSelected) {
-            BorderStroke(
-                width = 1.5.dp,
-                color = MaterialTheme.colorScheme.primary,
-            )
+            BorderStroke(1.5.dp, LegadoTheme.colorScheme.primary)
         } else {
         } else {
             null
             null
         },
         },
@@ -366,7 +340,7 @@ private fun RulePreviewListItem(
             ) {
             ) {
                 Text(
                 Text(
                     text = item.rule.name,
                     text = item.rule.name,
-                    style = MaterialTheme.typography.bodyLarge,
+                    style = LegadoTheme.typography.bodyLarge,
                     maxLines = 1,
                     maxLines = 1,
                     overflow = TextOverflow.Ellipsis,
                     overflow = TextOverflow.Ellipsis,
                 )
                 )
@@ -374,8 +348,8 @@ private fun RulePreviewListItem(
                     Spacer(modifier = Modifier.height(2.dp))
                     Spacer(modifier = Modifier.height(2.dp))
                     Text(
                     Text(
                         text = example,
                         text = example,
-                        style = MaterialTheme.typography.bodySmall,
-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
+                        style = LegadoTheme.typography.bodySmall,
+                        color = LegadoTheme.colorScheme.onSurfaceVariant,
                         maxLines = 1,
                         maxLines = 1,
                         overflow = TextOverflow.Ellipsis,
                         overflow = TextOverflow.Ellipsis,
                     )
                     )
@@ -386,7 +360,7 @@ private fun RulePreviewListItem(
 
 
             Surface(
             Surface(
                 shape = RoundedCornerShape(12.dp),
                 shape = RoundedCornerShape(12.dp),
-                color = MaterialTheme.colorScheme.primaryContainer,
+                color = LegadoTheme.colorScheme.primaryContainer,
             ) {
             ) {
                 if (item.totalCount < 0) {
                 if (item.totalCount < 0) {
                     CircularProgressIndicator(
                     CircularProgressIndicator(
@@ -401,8 +375,8 @@ private fun RulePreviewListItem(
                             R.string.chapter_count_format,
                             R.string.chapter_count_format,
                             item.totalCount
                             item.totalCount
                         ),
                         ),
-                        style = MaterialTheme.typography.labelSmall,
-                        color = MaterialTheme.colorScheme.onPrimaryContainer,
+                        style = LegadoTheme.typography.labelSmall,
+                        color = LegadoTheme.colorScheme.onPrimaryContainer,
                         modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
                         modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
                     )
                     )
                 }
                 }
@@ -428,7 +402,7 @@ private fun ChapterListSheetContent(
         ) {
         ) {
             Text(
             Text(
                 text = item.rule.name,
                 text = item.rule.name,
-                style = MaterialTheme.typography.titleMedium,
+                style = LegadoTheme.typography.titleMedium,
                 modifier = Modifier.weight(1f),
                 modifier = Modifier.weight(1f),
                 maxLines = 1,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
@@ -449,8 +423,8 @@ private fun ChapterListSheetContent(
                     R.string.chapter_count_format,
                     R.string.chapter_count_format,
                     item.totalCount
                     item.totalCount
                 ),
                 ),
-                style = MaterialTheme.typography.bodyMedium,
-                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = LegadoTheme.typography.bodyMedium,
+                color = LegadoTheme.colorScheme.onSurfaceVariant,
             )
             )
         }
         }
 
 
@@ -465,7 +439,7 @@ private fun ChapterListSheetContent(
             itemsIndexed(item.chapters.toList()) { _, chapter ->
             itemsIndexed(item.chapters.toList()) { _, chapter ->
                 Text(
                 Text(
                     text = chapter,
                     text = chapter,
-                    style = MaterialTheme.typography.bodyMedium,
+                    style = LegadoTheme.typography.bodyMedium,
                     modifier = Modifier
                     modifier = Modifier
                         .fillMaxWidth()
                         .fillMaxWidth()
                         .padding(vertical = 8.dp, horizontal = 4.dp),
                         .padding(vertical = 8.dp, horizontal = 4.dp),
@@ -477,8 +451,8 @@ private fun ChapterListSheetContent(
                 item {
                 item {
                     Text(
                     Text(
                         text = stringResource(R.string.chapter_list_preview_limit),
                         text = stringResource(R.string.chapter_list_preview_limit),
-                        style = MaterialTheme.typography.bodySmall,
-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
+                        style = LegadoTheme.typography.bodySmall,
+                        color = LegadoTheme.colorScheme.onSurfaceVariant,
                         modifier = Modifier.padding(8.dp),
                         modifier = Modifier.padding(8.dp),
                     )
                     )
                 }
                 }

+ 11 - 11
app/src/main/java/io/legado/app/ui/book/toc/rule/preview/TxtTocRulePreviewViewModel.kt

@@ -24,10 +24,12 @@ import kotlinx.coroutines.launch
 import java.util.regex.Pattern
 import java.util.regex.Pattern
 import java.util.regex.PatternSyntaxException
 import java.util.regex.PatternSyntaxException
 
 
-class TxtTocRulePreviewViewModel(private val app: Application) : ViewModel() {
+class TxtTocRulePreviewViewModel(
+    private val app: Application,
+    private val repository: TxtTocRuleRepository,
+) : ViewModel() {
 
 
     private val context get() = app.applicationContext
     private val context get() = app.applicationContext
-    private val repository = TxtTocRuleRepository()
 
 
     private val _uiState = MutableStateFlow(TxtTocRulePreviewUiState())
     private val _uiState = MutableStateFlow(TxtTocRulePreviewUiState())
     val uiState = _uiState.asStateFlow()
     val uiState = _uiState.asStateFlow()
@@ -102,14 +104,12 @@ class TxtTocRulePreviewViewModel(private val app: Application) : ViewModel() {
 
 
     private fun computeChaptersLazy(book: Book, rules: List<TxtTocRule>) {
     private fun computeChaptersLazy(book: Book, rules: List<TxtTocRule>) {
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            for (tocRule in rules) {
-                val item = computePreview(book, tocRule)
-                _uiState.update { state ->
-                    val newRules = state.rules.map { existing ->
-                        if (existing.rule.id == item.rule.id) item else existing
-                    }.toImmutableList()
-                    state.copy(rules = newRules)
-                }
+            val results = rules.map { tocRule -> computePreview(book, tocRule) }
+            _uiState.update { state ->
+                val newRules = state.rules.map { existing ->
+                    results.find { it.rule.id == existing.rule.id } ?: existing
+                }.toImmutableList()
+                state.copy(rules = newRules)
             }
             }
         }
         }
     }
     }
@@ -141,7 +141,7 @@ class TxtTocRulePreviewViewModel(private val app: Application) : ViewModel() {
             _uiState.update { it.copy(editingRule = null) }
             _uiState.update { it.copy(editingRule = null) }
             return
             return
         }
         }
-        if (runCatching { Regex(updatedRule.rule) }.isFailure) {
+        if (runCatching { updatedRule.rule.toPattern(Pattern.MULTILINE) }.isFailure) {
             _effects.tryEmit(TxtTocRulePreviewEffect.ShowToast(context.getString(R.string.invalid_format)))
             _effects.tryEmit(TxtTocRulePreviewEffect.ShowToast(context.getString(R.string.invalid_format)))
             _uiState.update { it.copy(editingRule = null) }
             _uiState.update { it.copy(editingRule = null) }
             return
             return