Explorar o código

feat: 阅读器相关优化与修复
允许书签角标大小为0

HapeLee hai 2 meses
pai
achega
fecd91fc50
Modificáronse 19 ficheiros con 332 adicións e 297 borrados
  1. 3 3
      app/src/main/java/io/legado/app/di/appModule.kt
  2. 1 1
      app/src/main/java/io/legado/app/domain/usecase/HomeDashboardUseCase.kt
  3. 1 0
      app/src/main/java/io/legado/app/feature/reader/ReaderCanvasSurface.kt
  4. 5 3
      app/src/main/java/io/legado/app/feature/reader/core/model/ReaderBookmarkBadge.kt
  5. 2 0
      app/src/main/java/io/legado/app/feature/reader/core/model/ReaderPage.kt
  6. 43 14
      app/src/main/java/io/legado/app/feature/reader/core/selection/ReaderSearchMatcher.kt
  7. 7 7
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  8. 4 2
      app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt
  9. 29 13
      app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt
  10. 30 9
      app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt
  11. 5 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt
  12. 48 70
      app/src/main/java/io/legado/app/ui/book/read/sheet/CharsetConfigSheet.kt
  13. 2 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/GlobalThemePage.kt
  14. 1 1
      app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt
  15. 70 104
      app/src/main/java/io/legado/app/ui/book/read/sheet/SimulatedReadingSheet.kt
  16. 22 41
      app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
  17. 13 8
      app/src/test/java/io/legado/app/feature/reader/core/model/ReaderBookmarkBadgeTest.kt
  18. 29 4
      app/src/test/java/io/legado/app/feature/reader/core/selection/ReaderSearchMatcherTest.kt
  19. 17 17
      gradle/libs.versions.toml

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

@@ -176,6 +176,7 @@ import io.legado.app.domain.usecase.BuildSpeechPlanUseCase
 import io.legado.app.domain.usecase.CacheBookChaptersUseCase
 import io.legado.app.domain.usecase.ChangeBookSourceUseCase
 import io.legado.app.domain.usecase.ChangeSourceSearchUseCase
+import io.legado.app.domain.usecase.CheckBookContentQualityUseCase
 import io.legado.app.domain.usecase.CleanSelectedTextUseCase
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.CoverAlbumUseCase
@@ -186,7 +187,6 @@ import io.legado.app.domain.usecase.ExportBookshelfUseCase
 import io.legado.app.domain.usecase.GenerateBookshelfAutoGroupPlanUseCase
 import io.legado.app.domain.usecase.GenerateChapterSummaryUseCase
 import io.legado.app.domain.usecase.GetChapterContentUseCase
-import io.legado.app.domain.usecase.CheckBookContentQualityUseCase
 import io.legado.app.domain.usecase.GetReadingProgressUseCase
 import io.legado.app.domain.usecase.HomeDashboardUseCase
 import io.legado.app.domain.usecase.IdentifyBookCharactersUseCase
@@ -211,6 +211,7 @@ import io.legado.app.domain.usecase.UploadReadingProgressUseCase
 import io.legado.app.domain.usecase.VerifyBookmarkTargetUseCase
 import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.domain.usecase.readRecord.GetReadRecordOverviewUseCase
+import io.legado.app.feature.onboarding.OnboardingViewModel
 import io.legado.app.help.coil.CoverFetcher
 import io.legado.app.help.coil.CoverInterceptor
 import io.legado.app.help.config.ThemePackageManager
@@ -289,7 +290,6 @@ import io.legado.app.ui.config.translation.TranslationConfigViewModel
 import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
-import io.legado.app.feature.onboarding.OnboardingViewModel
 import io.legado.app.ui.login.SourceLoginViewModel
 import io.legado.app.ui.main.MainRouteSearchContent
 import io.legado.app.ui.main.MainViewModel
@@ -315,12 +315,12 @@ import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
 import io.legado.app.utils.isNightMode
 import io.legado.app.utils.sysConfiguration
 import kotlinx.coroutines.Dispatchers
-import kotlinx.datetime.Clock
 import org.koin.core.module.dsl.factoryOf
 import org.koin.core.module.dsl.singleOf
 import org.koin.core.module.dsl.viewModel
 import org.koin.core.module.dsl.viewModelOf
 import org.koin.dsl.module
+import kotlin.time.Clock
 
 val appModule = module {
 

+ 1 - 1
app/src/main/java/io/legado/app/domain/usecase/HomeDashboardUseCase.kt

@@ -12,12 +12,12 @@ import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
-import kotlinx.datetime.Clock
 import kotlinx.datetime.DatePeriod
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.atStartOfDayIn
 import kotlinx.datetime.plus
 import kotlinx.datetime.toLocalDateTime
+import kotlin.time.Clock
 
 class HomeDashboardUseCase(
     private val gateway: HomeDashboardGateway,

+ 1 - 0
app/src/main/java/io/legado/app/feature/reader/ReaderCanvasSurface.kt

@@ -1863,6 +1863,7 @@ private fun Drawable.isolatedCopy(): Drawable =
 private fun ReaderPage.isSearchResult(text: ReaderElement.Text): Boolean {
     val start = searchStart ?: return false
     val end = searchEndInclusive ?: return false
+    if (text.emphasized != searchIsTitle) return false
     val textEnd = text.chapterPosition + text.value.length - 1
     return text.chapterPosition <= maxOf(start, end) && textEnd >= minOf(start, end)
 }

+ 5 - 3
app/src/main/java/io/legado/app/feature/reader/core/model/ReaderBookmarkBadge.kt

@@ -24,11 +24,13 @@ data class ReaderBookmarkBadge(
             imageSource: String = "",
             imageVersion: String = "",
         ): ReaderBookmarkBadge? {
-            if (!hasBookmark || isScroll) return null
-            val width = (sizeDp.coerceAtLeast(1) * density).toInt().coerceAtLeast(1)
+            // A zero-size badge explicitly hides the decoration while preserving the bookmark.
+            if (!hasBookmark || isScroll || sizeDp <= 0) return null
+            val width = (sizeDp * density).toInt().coerceAtLeast(1)
             return ReaderBookmarkBadge(
                 leftPx = pageWidthPx - contentRightPaddingPx - 6 * density - width,
-                topPx = contentTopPx + 2 * density,
+                // Keep the page header clear: the badge starts at the body viewport's top edge.
+                topPx = contentTopPx,
                 widthPx = width,
                 heightPx = width * 2,
                 imageSource = imageSource,

+ 2 - 0
app/src/main/java/io/legado/app/feature/reader/core/model/ReaderPage.kt

@@ -167,6 +167,8 @@ data class ReaderPage(
     /** Dynamic search range, kept separate from immutable layout elements for draw-cache reuse. */
     val searchStart: Int? = null,
     val searchEndInclusive: Int? = null,
+    /** Whether the dynamic search range is in the independent title coordinate space. */
+    val searchIsTitle: Boolean = false,
     /** Dynamic read-aloud paragraph, likewise independent of the pagination layout. */
     val readAloudParagraphIndex: Int? = null,
     /** 邻章未装载时预置的"加载中"占位页,分页批次落地后被同 id 真实页替换。 */

+ 43 - 14
app/src/main/java/io/legado/app/feature/reader/core/selection/ReaderSearchMatcher.kt

@@ -5,42 +5,71 @@ data class ReaderSearchRequest(
     val directLength: Int,
     val occurrence: Int,
     val isRegex: Boolean,
-    /** Characters present in the search document but excluded from the renderer's body text. */
-    val leadingCharactersExcludedFromContent: Int = 0,
 )
 
-data class ReaderSearchMatch(val start: Int, val length: Int)
+data class ReaderSearchMatch(val start: Int, val length: Int, val isTitle: Boolean = false)
 
 /** Resolves a search result against the latest processed chapter text, without page coordinates. */
 object ReaderSearchMatcher {
-    fun find(content: String, query: String, request: ReaderSearchRequest): ReaderSearchMatch? {
+    /**
+     * [title] is the exact title prefix used by full-text search. It stays outside the
+     * renderer's body coordinate system, but is included while resolving occurrences.
+     */
+    fun find(
+        content: String,
+        query: String,
+        request: ReaderSearchRequest,
+        title: String? = null,
+    ): ReaderSearchMatch? {
         if (query.isEmpty()) return null
+        val titlePrefix = title?.let { "$it\n" }.orEmpty()
+        val searchContent = titlePrefix + content
         val directLength = request.directLength.takeIf { it > 0 } ?: query.length
-        val directIndex = request.directIndex - request.leadingCharactersExcludedFromContent
-        if (directIndex >= 0 && directIndex + directLength <= content.length) {
+        val directIndex = request.directIndex.takeIf { it >= 0 }
+        if (directIndex != null && directIndex + directLength <= searchContent.length) {
             val matches = if (request.isRegex) {
                 runCatching {
-                    Regex(query).matches(content.substring(directIndex, directIndex + directLength))
+                    Regex(query).matches(
+                        searchContent.substring(
+                            directIndex,
+                            directIndex + directLength
+                        )
+                    )
                 }.getOrDefault(false)
             } else {
-                content.regionMatches(directIndex, query, 0, query.length, ignoreCase = false)
+                searchContent.regionMatches(directIndex, query, 0, query.length, ignoreCase = false)
             }
-            if (matches) return ReaderSearchMatch(directIndex, directLength)
+            if (matches) return matchAt(directIndex, directLength, titlePrefix.length)
         }
         if (request.isRegex) {
             return runCatching {
-                Regex(query).findAll(content)
+                Regex(query).findAll(searchContent)
                     .drop(request.occurrence.coerceAtLeast(0))
                     .firstOrNull()
-                    ?.let { ReaderSearchMatch(it.range.first, it.value.length) }
+                    ?.let { matchAt(it.range.first, it.value.length, titlePrefix.length) }
             }.getOrNull()
         }
         var occurrence = 0
-        var index = content.indexOf(query)
+        var index = searchContent.indexOf(query)
         while (occurrence != request.occurrence.coerceAtLeast(0) && index >= 0) {
-            index = content.indexOf(query, index + query.length)
+            index = searchContent.indexOf(query, index + query.length)
             occurrence += 1
         }
-        return index.takeIf { it >= 0 }?.let { ReaderSearchMatch(it, query.length) }
+        return index.takeIf { it >= 0 }?.let { matchAt(it, query.length, titlePrefix.length) }
+    }
+
+    private fun matchAt(index: Int, length: Int, titlePrefixLength: Int): ReaderSearchMatch? =
+        when {
+            titlePrefixLength == 0 -> ReaderSearchMatch(index, length)
+            index + length <= titlePrefixLength - 1 -> ReaderSearchMatch(
+                index,
+                length,
+                isTitle = true
+            )
+
+            index >= titlePrefixLength -> ReaderSearchMatch(index - titlePrefixLength, length)
+            // A regular expression may straddle the title/body separator. It cannot be painted as
+            // one Canvas range, so never substitute an unrelated visible match.
+            else -> null
     }
 }

+ 7 - 7
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -19,12 +19,12 @@ import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.HighlightTagRule
 import io.legado.app.data.entities.TagGroupRule
-import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.domain.gateway.BookExportSettingsGateway
 import io.legado.app.domain.gateway.ImportBookSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.ReadSettingsGateway
+import io.legado.app.exception.NoStackTraceException
+import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.ui.book.info.HighlightedTag
 import io.legado.app.utils.FileDoc
@@ -37,17 +37,17 @@ import io.legado.app.utils.isUri
 import io.legado.app.utils.normalizeFileName
 import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.toastOnUi
-import splitties.init.appCtx
-import java.io.File
-import java.util.concurrent.ConcurrentHashMap
-import kotlinx.datetime.Clock
 import kotlinx.datetime.LocalDate
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.daysUntil
 import kotlinx.datetime.todayIn
+import org.koin.core.context.GlobalContext
+import splitties.init.appCtx
+import java.io.File
+import java.util.concurrent.ConcurrentHashMap
 import kotlin.math.max
 import kotlin.math.min
-import org.koin.core.context.GlobalContext
+import kotlin.time.Clock
 
 private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
 private val importBookGateway by lazy { GlobalContext.get().get<ImportBookSettingsGateway>() }

+ 4 - 2
app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt

@@ -393,10 +393,12 @@ abstract class AbsCallBack(
             val requestBuilder = userResponse.request.newBuilder()
             if (HttpMethod.permitsRequestBody(method)) {
                 val responseCode = userResponse.code
-                val maintainBody = HttpMethod.redirectsWithBody(method) ||
+                // OkHttp 5.5 folds redirect status handling into redirectsToGet().
+                // PROPFIND is the only method whose body is retained for 301/302/303.
+                val maintainBody = method == "PROPFIND" ||
                         responseCode == HTTP_PERM_REDIRECT ||
                         responseCode == HTTP_TEMP_REDIRECT
-                if (HttpMethod.redirectsToGet(method)
+                if (HttpMethod.redirectsToGet(method, responseCode)
                     && responseCode != HTTP_PERM_REDIRECT
                     && responseCode != HTTP_TEMP_REDIRECT
                 ) {

+ 29 - 13
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -612,6 +612,7 @@ class ReadBookController(
                 // resolves these compact dynamic ranges while drawing.
                 searchStart = selection?.anchor?.takeIf { pageHasSearchSelection },
                 searchEndInclusive = selection?.focus?.takeIf { pageHasSearchSelection },
+                searchIsTitle = selection?.anchorIsTitle == true,
                 readAloudParagraphIndex = aloudParagraphIndex.takeIf { pageHasAloudParagraph },
                 revision = decorated.revision xor (selection?.hashCode()?.toLong() ?: 0L) xor
                         (aloudPosition?.hashCode()?.toLong() ?: 0L),
@@ -666,10 +667,17 @@ class ReadBookController(
             chapterCount = ReadBook.simulatedChapterSize,
         )
         if (missingChapters.isEmpty()) return index
+        val cachedChapterIndexes = listOfNotNull(
+            ReadBook.readerChapterInputWindow.previous,
+            ReadBook.readerChapterInputWindow.current,
+            ReadBook.readerChapterInputWindow.next,
+        ).mapTo(mutableSetOf()) { it.chapter.index }
         val updated = pages.toMutableList()
         var added = 0
         missingChapters.forEach { chapterIndex ->
-            placeholderReaderPage(chapterIndex)?.let {
+            // Match the View reader's three-chapter hand-off: cached chapter content waits for
+            // its Canvas pagination rather than being presented as a network/content load.
+            if (chapterIndex !in cachedChapterIndexes) placeholderReaderPage(chapterIndex)?.let {
                 updated.add(it)
                 added++
             }
@@ -723,14 +731,11 @@ class ReadBookController(
     ) {
         val result = navigation.result
         val query = result.query.ifBlank { viewModel.uiState.value.searchContentQuery }
-        // 选区锚点、updateReadingPosition、locate 都以 semanticContent 为字符空间。
-        // 检索结果把显示标题也算入章节字符串,Canvas 的 semanticContent 则只保存正文;
-        // 不先扣掉标题前缀,偏移位置若恰好也是同一关键词会被当成有效直达结果,导致高亮落在别处。
-        val titlePrefixLength = input.source.blocks
-            .any { it is io.legado.app.feature.reader.core.source.ReaderChapterSourceBlock.Text && it.isTitle }
-            .takeIf { it }
-            ?.let { input.source.title.length + 1 }
-            ?: 0
+        // Full-text search uses the exact document “display title + newline + body” when the
+        // title is enabled. Resolve in that document, then map to title/body Canvas coordinates.
+        val searchTitle = input.displayTitle.takeIf {
+            ReadBookConfig.titleMode != 2 || input.chapter.isVolume || input.content.textList.isEmpty()
+        }
         val match = ReaderSearchMatcher.find(
             content = input.source.semanticContent,
             query = query,
@@ -739,8 +744,8 @@ class ReadBookController(
                 directLength = result.matchLength,
                 occurrence = result.resultCountWithinChapter,
                 isRegex = result.isRegex,
-                leadingCharactersExcludedFromContent = titlePrefixLength,
             ),
+            title = searchTitle,
         ) ?: run {
             pendingSearchNavigation = null
             return
@@ -750,13 +755,15 @@ class ReadBookController(
             chapterIndex = result.chapterIndex,
             anchor = match.start,
             focus = match.start + match.length - 1,
+            anchorIsTitle = match.isTitle,
         )
-        ReadBook.updateReadingPosition(match.start)
+        val bodyPosition = if (match.isTitle) 0 else match.start
+        ReadBook.updateReadingPosition(bodyPosition)
         directReaderPages.takeIf { pages ->
             pages.any { it.id.chapterIndex == result.chapterIndex }
         }?.let { pages ->
             publishDirectReaderWindow(
-                ReaderPageNavigator.locate(pages, result.chapterIndex, match.start)
+                ReaderPageNavigator.locate(pages, result.chapterIndex, bodyPosition)
             )
         }
     }
@@ -1688,7 +1695,9 @@ class ReadBookController(
                     // 避免挂起导航在用户之后主动进入同一章时劫持阅读位置
                     ReadBook.openChapter(
                         result.chapterIndex,
-                        result.queryIndexInChapter.coerceAtLeast(0),
+                        // Search offsets may include a title prefix. The body offset is resolved
+                        // after the cached chapter input has been published.
+                        0,
                     ) {
                         pendingSearchNavigation = null
                     }
@@ -2069,6 +2078,13 @@ class ReadBookController(
                 viewModel.startBackupJob()
                 return window
             }
+        // ReadBook has promoted a cached adjacent chapter input, but its Canvas pages may still
+        // be shaping in the background. Start that pagination and retain the completed source
+        // page until it publishes; only an actually missing chapter gets a “loading” page.
+        if (ReadBook.readerChapterInputWindow.current?.chapter?.index == targetChapterIndex) {
+            publishReaderPageWindow()
+            return null
+        }
         val placeholder = placeholderReaderPage(targetChapterIndex) ?: return null
         val pages = directReaderPages.toMutableList()
         pages.add(placeholder)

+ 30 - 9
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -42,6 +42,7 @@ import androidx.compose.runtime.withFrameNanos
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
@@ -587,14 +588,18 @@ fun ReadBookRouteScreen(
         }
     }
 
+    val fallbackReaderSurfaceColor = if (isDarkTheme) Color.Black else Color.White
     val readerSurfaceColor = Color(
-        readerBackground.meanColorArgb.takeIf { it != 0 } ?: runCatching {
-            android.graphics.Color.parseColor(
-                if (isDarkTheme) state.styleConfig.bgStrNight else state.styleConfig.bgStr
-            )
-        }.getOrDefault(
-            if (isDarkTheme) android.graphics.Color.BLACK else android.graphics.Color.WHITE
-        )
+        readerBackground.meanColorArgb.takeIf { it != 0 } ?: when {
+            // Before the route has a book, styleConfig only contains construction defaults.
+            // Keep the first opaque reader frame neutral instead of exposing an app-theme tint.
+            state.book == null -> fallbackReaderSurfaceColor.toArgb()
+            else -> runCatching {
+                android.graphics.Color.parseColor(
+                    if (isDarkTheme) state.styleConfig.bgStrNight else state.styleConfig.bgStr
+                )
+            }.getOrDefault(fallbackReaderSurfaceColor.toArgb())
+        }
     )
     val readerEntranceSettled = animatedVisibilityScope?.transition?.let { transition ->
         !transition.isRunning &&
@@ -605,7 +610,23 @@ fun ReadBookRouteScreen(
         controller.onReaderEntranceStateChanged(readerEntranceSettled)
         if (readerEntranceSettled) viewModel.onReaderEntranceSettled()
     }
-    val hasReadablePage = readerPageWindow.current != null && state.msg == null
+    // A chapter boundary can publish an empty window for one composition while the controller
+    // swaps a simulated-page turn to its cached/placeholder successor. Keeping the last complete
+    // window for that gap prevents the root reader background from becoming a visible fallback.
+    var lastReadablePageWindow by remember {
+        mutableStateOf<io.legado.app.feature.reader.core.model.ReaderPageWindow?>(
+            null
+        )
+    }
+    LaunchedEffect(readerPageWindow.current?.id, readerPageWindow.current?.layoutRevision) {
+        if (readerPageWindow.current != null) lastReadablePageWindow = readerPageWindow
+    }
+    val displayedReaderPageWindow =
+        readerPageWindow.takeIf { it.current != null } ?: lastReadablePageWindow
+    // A retained page bridges only a transient chapter-window gap. A real pagination failure
+    // must replace it with the retryable error state instead of leaving stale content visible.
+    val hasReadablePage = displayedReaderPageWindow?.current != null &&
+            state.msg == null && readerPaginationError == null
     var readerContentRevealAllowed by remember(sharedCoverKey) {
         mutableStateOf(sharedCoverKey == null || animatedVisibilityScope == null)
     }
@@ -676,7 +697,7 @@ fun ReadBookRouteScreen(
                 exit = fadeOut(animationSpec = tween(450)),
             ) {
                 ReaderCanvasSurface(
-                hostPages = readerPageWindow,
+                    hostPages = displayedReaderPageWindow ?: readerPageWindow,
                 transitionMode = ReaderTransitionMode.fromPageAnim(controller.pageAnim),
                 backgroundColor = readerSurfaceColor,
                 backgroundImage = readerBackground.drawable,

+ 5 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt

@@ -601,6 +601,9 @@ fun ReadBookScreen(
         )
     }
 
+    val showCharsetSheet = state.activeSheet is ReadBookSheet.Charset
+    val showSimulatedReadingSheet = state.activeSheet is ReadBookSheet.SimulatedReading
+
     // AlertDialog-based sheets and special cases — conditionally composed
     when (state.activeSheet) {
         is ReadBookSheet.ClickActionConfig -> {
@@ -636,12 +639,14 @@ fun ReadBookScreen(
 
         is ReadBookSheet.Charset -> {
             CharsetConfigSheet(
+                show = showCharsetSheet,
                 onDismissRequest = dismissSheet,
             )
         }
 
         is ReadBookSheet.SimulatedReading -> {
             SimulatedReadingSheet(
+                show = showSimulatedReadingSheet,
                 onDismissRequest = dismissSheet,
                 onApply = { onIntent(ReadBookIntent.ApplySimulatedReading) },
             )

+ 48 - 70
app/src/main/java/io/legado/app/ui/book/read/sheet/CharsetConfigSheet.kt

@@ -1,101 +1,79 @@
 package io.legado.app.ui.book.read.sheet
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.ExposedDropdownMenuAnchorType
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
 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.ui.Modifier
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.ui.res.stringResource
 import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.model.ReadBook
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.ProvideAppDensity
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun CharsetConfigSheet(
+    show: Boolean,
     onDismissRequest: () -> Unit,
 ) {
     var charset by remember { mutableStateOf(ReadBook.book?.charset ?: "UTF-8") }
     val charsetEntries = remember { AppConst.charsets }
     var expanded by remember { mutableStateOf(false) }
+    val title = stringResource(R.string.set_charset)
 
-    AlertDialog(
+    AppAlertDialog(
+        show = show,
         onDismissRequest = onDismissRequest,
-        containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { ProvideAppDensity { Text(stringResource(R.string.set_charset)) } },
-        text = {
-            ProvideAppDensity {
-                Column {
-                    ExposedDropdownMenuBox(
-                        expanded = expanded,
-                        onExpandedChange = { expanded = it },
-                    ) {
-                        OutlinedTextField(
-                            value = charset,
-                            onValueChange = { charset = it },
-                            modifier = Modifier
-                                .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable)
-                                .fillMaxWidth(),
-                            readOnly = false,
-                            label = { Text(stringResource(R.string.set_charset)) },
-                            singleLine = true,
-                            trailingIcon = {
-                                ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
-                            },
-                            colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
+        title = title,
+        content = {
+            Box {
+                AppTextField(
+                    value = charset,
+                    onValueChange = { charset = it },
+                    modifier = Modifier.fillMaxWidth(),
+                    label = title,
+                    singleLine = true,
+                    trailingIcon = {
+                        SmallPlainButton(
+                            onClick = { expanded = !expanded },
+                            selected = expanded,
+                            icon = Icons.Default.KeyboardArrowDown,
+                            contentDescription = title,
                         )
-                        ExposedDropdownMenu(
-                            expanded = expanded,
-                            onDismissRequest = { expanded = false },
-                        ) {
-                            ProvideAppDensity {
-                                charsetEntries.forEach { entry ->
-                                    DropdownMenuItem(
-                                        text = { Text(entry) },
-                                        onClick = {
-                                            charset = entry
-                                            expanded = false
-                                        },
-                                    )
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        },
-        confirmButton = {
-            ProvideAppDensity {
-                TextButton(
-                    onClick = {
-                        ReadBook.setCharset(charset)
-                        onDismissRequest()
                     },
+                )
+                RoundDropdownMenu(
+                    expanded = expanded,
+                    onDismissRequest = { expanded = false },
                 ) {
-                    Text(stringResource(R.string.ok))
+                    charsetEntries.forEach { entry ->
+                        RoundDropdownMenuItem(
+                            text = entry,
+                            isSelected = charset == entry,
+                            onClick = {
+                                charset = entry
+                                expanded = false
+                            },
+                        )
+                    }
                 }
             }
         },
-        dismissButton = {
-            ProvideAppDensity {
-                TextButton(onClick = onDismissRequest) {
-                    Text(stringResource(R.string.cancel))
-                }
-            }
+        confirmText = stringResource(R.string.ok),
+        onConfirm = {
+            ReadBook.setCharset(charset)
+            onDismissRequest()
         },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = onDismissRequest,
     )
 }

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/GlobalThemePage.kt

@@ -28,6 +28,7 @@ import androidx.compose.material.icons.filled.LightMode
 import androidx.compose.material.icons.filled.SpaceBar
 import androidx.compose.material.icons.filled.TextFields
 import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.PlainTooltip
 import androidx.compose.material3.TooltipAnchorPosition
@@ -72,6 +73,7 @@ import io.legado.app.ui.widget.components.text.AppText
 
 // ========== Page 0: Global & Theme ==========
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun GlobalThemePage(
     onToggleDayNight: () -> Unit,

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt

@@ -323,7 +323,7 @@ private fun PageControlSettings(
     TinySliderSettingItem(
         title = stringResource(R.string.bookmark_badge_size),
         value = preferences.bookmarkBadgeSize.toFloat(),
-        valueRange = 6f..50f,
+        valueRange = 0f..50f,
         valueFormat = { "${it.roundToInt()}dp" },
         onValueChange = { onBookmarkBadgeSizeChange(it.roundToInt()) },
     )

+ 70 - 104
app/src/main/java/io/legado/app/ui/book/read/sheet/SimulatedReadingSheet.kt

@@ -8,13 +8,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.DatePicker
-import androidx.compose.material3.DatePickerDialog
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
 import androidx.compose.material3.rememberDatePickerState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -27,8 +22,8 @@ import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.model.ReadBook
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.ProvideAppDensity
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
 import java.time.Instant
 import java.time.LocalDate
@@ -38,6 +33,7 @@ import java.time.format.DateTimeFormatter
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SimulatedReadingSheet(
+    show: Boolean,
     onDismissRequest: () -> Unit,
     onApply: () -> Unit,
 ) {
@@ -49,112 +45,82 @@ fun SimulatedReadingSheet(
     var showDatePicker by remember { mutableStateOf(false) }
     val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd") }
 
-    AlertDialog(
+    AppAlertDialog(
+        show = show,
         onDismissRequest = onDismissRequest,
-        containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { ProvideAppDensity { Text(stringResource(R.string.simulated_reading)) } },
-        text = {
-            ProvideAppDensity {
-                Column {
-                    OutlinedTextField(
-                        value = startDate.format(dateFormatter),
-                        onValueChange = {},
-                        label = { Text(stringResource(R.string.start_from)) },
-                        readOnly = true,
+        title = stringResource(R.string.simulated_reading),
+        content = {
+            Column {
+                AppTextField(
+                    value = startDate.format(dateFormatter),
+                    onValueChange = {},
+                    label = stringResource(R.string.start_from),
+                    readOnly = true,
+                    singleLine = true,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .clickable { showDatePicker = true },
+                )
+                Spacer(modifier = Modifier.height(8.dp))
+                TinySwitchSettingItem(
+                    title = stringResource(R.string.simulated_reading),
+                    checked = enabled,
+                    onCheckedChange = { enabled = it },
+                )
+                Spacer(modifier = Modifier.height(12.dp))
+                Row(modifier = Modifier.fillMaxWidth()) {
+                    AppTextField(
+                        value = startChapter,
+                        onValueChange = { startChapter = it },
+                        label = stringResource(R.string.start_chapter),
                         singleLine = true,
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .clickable { showDatePicker = true },
-                        enabled = false,
+                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                        modifier = Modifier.weight(1f),
                     )
-                    Spacer(modifier = Modifier.height(8.dp))
-                    TinySwitchSettingItem(
-                        title = stringResource(R.string.simulated_reading),
-                        checked = enabled,
-                        onCheckedChange = { enabled = it },
+                    Spacer(modifier = Modifier.width(8.dp))
+                    AppTextField(
+                        value = dailyChapters,
+                        onValueChange = { dailyChapters = it },
+                        label = stringResource(R.string.daily_chapters),
+                        singleLine = true,
+                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                        modifier = Modifier.weight(1f),
                     )
-                    Spacer(modifier = Modifier.height(12.dp))
-                    // Start chapter + daily chapters
-                    Row(modifier = Modifier.fillMaxWidth()) {
-                        OutlinedTextField(
-                            value = startChapter,
-                            onValueChange = { startChapter = it },
-                            label = { Text(stringResource(R.string.start_chapter)) },
-                            singleLine = true,
-                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
-                            modifier = Modifier.weight(1f),
-                        )
-                        Spacer(modifier = Modifier.width(8.dp))
-                        OutlinedTextField(
-                            value = dailyChapters,
-                            onValueChange = { dailyChapters = it },
-                            label = { Text(stringResource(R.string.daily_chapters)) },
-                            singleLine = true,
-                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
-                            modifier = Modifier.weight(1f),
-                        )
-                    }
                 }
             }
         },
-        confirmButton = {
-            ProvideAppDensity {
-                TextButton(
-                    onClick = {
-                        book.setStartDate(startDate)
-                        book.setDailyChapters(dailyChapters.toIntOrNull() ?: 0)
-                        book.setStartChapter(startChapter.toIntOrNull() ?: 0)
-                        book.setReadSimulating(enabled)
-                        book.save()
-                        onApply()
-                        onDismissRequest()
-                    },
-                ) {
-                    Text(stringResource(R.string.ok))
-                }
-            }
+        confirmText = stringResource(R.string.ok),
+        onConfirm = {
+            book.setStartDate(startDate)
+            book.setDailyChapters(dailyChapters.toIntOrNull() ?: 0)
+            book.setStartChapter(startChapter.toIntOrNull() ?: 0)
+            book.setReadSimulating(enabled)
+            book.save()
+            onApply()
+            onDismissRequest()
         },
-        dismissButton = {
-            ProvideAppDensity {
-                TextButton(onClick = onDismissRequest) {
-                    Text(stringResource(R.string.cancel))
-                }
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = onDismissRequest,
+    )
+
+    val datePickerState = rememberDatePickerState(
+        initialSelectedDateMillis = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
+            .toEpochMilli(),
+    )
+    AppAlertDialog(
+        show = showDatePicker,
+        onDismissRequest = { showDatePicker = false },
+        content = { DatePicker(state = datePickerState) },
+        confirmText = stringResource(R.string.ok),
+        onConfirm = {
+            datePickerState.selectedDateMillis?.let { millis ->
+                startDate = Instant.ofEpochMilli(millis)
+                    .atZone(ZoneId.systemDefault())
+                    .toLocalDate()
             }
+            showDatePicker = false
         },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = { showDatePicker = false },
     )
-
-    // Date picker dialog
-    if (showDatePicker) {
-        val datePickerState = rememberDatePickerState(
-            initialSelectedDateMillis = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
-                .toEpochMilli(),
-        )
-        DatePickerDialog(
-            onDismissRequest = { showDatePicker = false },
-            colors = androidx.compose.material3.DatePickerDefaults.colors(
-                containerColor = LegadoTheme.colorScheme.surfaceContainer,
-            ),
-            confirmButton = {
-                TextButton(
-                    onClick = {
-                        datePickerState.selectedDateMillis?.let { millis ->
-                            startDate = Instant.ofEpochMilli(millis)
-                                .atZone(ZoneId.systemDefault())
-                                .toLocalDate()
-                        }
-                        showDatePicker = false
-                    },
-                ) {
-                    Text(stringResource(R.string.ok))
-                }
-            },
-            dismissButton = {
-                TextButton(onClick = { showDatePicker = false }) {
-                    Text(stringResource(R.string.cancel))
-                }
-            },
-        ) {
-            DatePicker(state = datePickerState)
-        }
-    }
 }

+ 22 - 41
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -28,6 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.navigation3.runtime.NavKey
 import androidx.navigation3.runtime.entryProvider
+import androidx.navigation3.runtime.metadata
 import androidx.navigation3.ui.LocalNavAnimatedContentScope
 import androidx.navigation3.ui.NavDisplay
 import coil3.ImageLoader
@@ -598,18 +599,20 @@ fun MainActivity.mainEntryProvider(
     }
 
     entry<MainRouteReadBook>(
-        metadata = NavDisplay.transitionSpec {
-            fadeIn(animationSpec = tween(600)) togetherWith
-                fadeOut(animationSpec = tween(600))
-        } + NavDisplay.popTransitionSpec {
-            fadeIn(animationSpec = tween(600)) togetherWith
-                fadeOut(animationSpec = tween(600))
-        } + NavDisplay.predictivePopTransitionSpec { _ ->
-            if (configuration.appShell.predictiveBackEnabled) {
+        metadata = metadata {
+            put(NavDisplay.TransitionKey) {
+                fadeIn(animationSpec = tween(600)) togetherWith
+                        fadeOut(animationSpec = tween(600))
+            }
+            put(NavDisplay.PopTransitionKey) {
                 fadeIn(animationSpec = tween(600)) togetherWith
-                    fadeOut(animationSpec = tween(600))
-            } else {
-                null
+                        fadeOut(animationSpec = tween(600))
+            }
+            if (configuration.appShell.predictiveBackEnabled) {
+                put(NavDisplay.PredictivePopTransitionKey) { _ ->
+                    fadeIn(animationSpec = tween(600)) togetherWith
+                            fadeOut(animationSpec = tween(600))
+                }
             }
         }
     ) { route ->
@@ -1073,42 +1076,20 @@ fun MainActivity.mainEntryProvider(
     }
 
     entry<MainRouteBookInfo>(
-        metadata = NavDisplay.transitionSpec {
-            val from = initialState.key
-            val fromStr = from.toString()
-            if (from is MainRouteHome || from is MainRouteExploreShow || from is MainRouteSearch ||
-                fromStr.startsWith("MainRouteHome") || fromStr.startsWith("MainRouteExploreShow") || fromStr.startsWith(
-                    "MainRouteSearch"
-                )
-            ) {
+        metadata = metadata {
+            put(NavDisplay.TransitionKey) {
                 fadeIn(animationSpec = tween(300)) togetherWith
                         fadeOut(animationSpec = tween(300))
-            } else null
-        } + NavDisplay.popTransitionSpec {
-            val to = targetState.key
-            val toStr = to.toString()
-            if (to is MainRouteHome || to is MainRouteExploreShow || to is MainRouteSearch ||
-                toStr.startsWith("MainRouteHome") || toStr.startsWith("MainRouteExploreShow") || toStr.startsWith(
-                    "MainRouteSearch"
-                )
-            ) {
+            }
+            put(NavDisplay.PopTransitionKey) {
                 fadeIn(animationSpec = tween(300)) togetherWith
                         fadeOut(animationSpec = tween(300))
-            } else null
-        } + NavDisplay.predictivePopTransitionSpec { _ ->
-            if (!configuration.appShell.predictiveBackEnabled) {
-                null
-            } else {
-                val to = targetState.key
-                val toStr = to.toString()
-                if (to is MainRouteHome || to is MainRouteExploreShow || to is MainRouteSearch ||
-                    toStr.startsWith("MainRouteHome") || toStr.startsWith("MainRouteExploreShow") || toStr.startsWith(
-                        "MainRouteSearch"
-                    )
-                ) {
+            }
+            if (configuration.appShell.predictiveBackEnabled) {
+                put(NavDisplay.PredictivePopTransitionKey) { _ ->
                     fadeIn(animationSpec = tween(300)) togetherWith
                             fadeOut(animationSpec = tween(300))
-                } else null
+                }
             }
         }
     ) { route ->

+ 13 - 8
app/src/test/java/io/legado/app/feature/reader/core/model/ReaderBookmarkBadgeTest.kt

@@ -1,25 +1,30 @@
 package io.legado.app.feature.reader.core.model
 
-import org.junit.Assert.*
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
 import org.junit.Test
 
 class ReaderBookmarkBadgeTest {
     private fun badge(bookmarked: Boolean = true, scroll: Boolean = false, size: Int = 10) =
         ReaderBookmarkBadge.create(bookmarked, scroll, 600, 100f, 30, 2f, size)
 
-    @Test fun matchesViewContentAnchorMarginsAndRibbonRatio() {
-        assertEquals(ReaderBookmarkBadge(538f, 104f, 20, 40), badge())
-        assertEquals(ReaderBookmarkBadge(518f, 104f, 40, 80), badge(size = 20))
+    @Test
+    fun topEdgeAnchorsToBodyTopAndPreservesRibbonRatio() {
+        assertEquals(ReaderBookmarkBadge(538f, 100f, 20, 40), badge())
+        assertEquals(ReaderBookmarkBadge(518f, 100f, 40, 80), badge(size = 20))
     }
 
-    @Test fun hiddenWithoutBookmarkAndInScrollMode() {
+    @Test
+    fun hiddenWithoutBookmarkInScrollModeOrAtZeroSize() {
         assertNull(badge(bookmarked = false))
         assertNull(badge(scroll = true))
+        assertNull(badge(size = 0))
     }
 
-    @Test fun invalidSizeRetainsLegacyMinimum() {
-        assertEquals(2, badge(size = 0)?.widthPx)
-        assertEquals(4, badge(size = -10)?.heightPx)
+    @Test
+    fun nonPositiveSizeDoesNotCreateBadge() {
+        assertNull(badge(size = -10))
     }
 
     @Test fun customImageVersionParticipatesInPageStateEquality() {

+ 29 - 4
app/src/test/java/io/legado/app/feature/reader/core/selection/ReaderSearchMatcherTest.kt

@@ -10,13 +10,11 @@ class ReaderSearchMatcherTest {
         occurrence: Int = 0,
         directLength: Int = 0,
         isRegex: Boolean = false,
-        leadingCharactersExcludedFromContent: Int = 0,
     ) = ReaderSearchRequest(
         directIndex,
         directLength,
         occurrence,
         isRegex,
-        leadingCharactersExcludedFromContent,
     )
 
     @Test
@@ -47,7 +45,7 @@ class ReaderSearchMatcherTest {
     }
 
     @Test
-    fun `扣除标题前缀后的直达下标优先于同词出现次数`() {
+    fun `标题前缀后的直达下标映射到正文坐标`() {
         // 检索层的章节文本是“标题\\n正文”,Canvas 只以正文建立坐标。
         // 若不先扣标题长度,原下标可能碰巧指向正文中的另一个同词。
         val content = "灯塔在前,灯塔在后。"
@@ -59,8 +57,35 @@ class ReaderSearchMatcherTest {
                 request(
                     directIndex = 3,
                     occurrence = 1,
-                    leadingCharactersExcludedFromContent = 3,
                 ),
+                title = "标题",
+            ),
+        )
+    }
+
+    @Test
+    fun `按出现次数回退时保留检索标题中的匹配`() {
+        val content = "灯塔在前,灯塔在后。"
+        assertEquals(
+            ReaderSearchMatch(5, 2),
+            ReaderSearchMatcher.find(
+                content = content,
+                query = "灯塔",
+                request = request(directIndex = 99, occurrence = 2),
+                title = "灯塔章节",
+            ),
+        )
+    }
+
+    @Test
+    fun `标题命中使用标题坐标而不误标正文`() {
+        assertEquals(
+            ReaderSearchMatch(0, 2, isTitle = true),
+            ReaderSearchMatcher.find(
+                content = "正文也有灯塔。",
+                query = "灯塔",
+                request = request(directIndex = 0, directLength = 2),
+                title = "灯塔章节",
             ),
         )
     }

+ 17 - 17
gradle/libs.versions.toml

@@ -6,7 +6,7 @@ adaptiveLayout = "1.3.0"
 adaptiveNavigation = "1.3.0"
 animation = "1.12.0"
 biometric = "1.4.0-alpha07"
-coilCompose = "3.5.0"
+coilCompose = "3.6.2"
 composeBom = "2026.08.00"
 constraintlayoutCompose = "1.1.2"
 coreSplashscreen = "1.2.0"
@@ -16,48 +16,48 @@ kotlin = "2.4.10"
 kotlinxSerialization = "1.11.0"
 kotlinxCoroutinesAndroid = "1.11.0"
 kotlinxSerializationJson = "1.11.0"
-kotlinxCollectionsImmutable = "0.5.1"
+kotlinxCollectionsImmutable = "0.5.2"
 ksp = "2.3.6"
 agp = "9.2.1"
 appcompat = "1.8.0"
 colorpicker = "1.1.0"
 colorpicker-compose = "1.2.0"
 commonsText = "1.15.0"
-constraintlayout = "2.2.1"
+constraintlayout = "2.2.2"
 core = "1.19.0"
 firebaseBom = "34.6.0"
 flexbox = "3.0.0"
-fragment = "1.8.9"
+fragment = "1.9.0"
 
 libarchive = "1.1.6"
 lifecycle = "2.11.0"
-glide = "5.0.7"
+glide = "5.0.9"
 gson = "2.14.0"
 jsonPath = "3.0.0"
-ktor = "3.5.1"
+ktor = "3.5.2"
 # issue #3811,不要更新版本,新版引入了一个破坏性变更(详见https://github.com/jhy/jsoup/pull/2017)
 # 若要升级请确保相关代码不会受此变更影响(如AnalyzeByJSoup.kt、JsoupXpath库等)
-intellijMarkdown = "0.7.3"
+intellijMarkdown = "0.7.12"
 jsoup = "1.16.2"
 jsoupxpath = "2.5.5"
 coroutines = "1.11.0"
-kotlinxDatetime = "0.6.2"
+kotlinxDatetime = "0.8.0"
 lifecycleViewmodelCompose = "2.11.0"
 liveeventbus = "1.8.14"
-navigation3 = "1.1.7"
+navigation3 = "1.2.0-beta01"
 # 1.2.0-alpha02 修复了预测式返回手势中 NavigationEventInput.dispatchOnBackProgressed
 # 对已分离输入抛 IllegalStateException 的崩溃(1.1.2 仍会 checkNotNull 抛异常)。
 # 覆盖 navigation3 1.1.4 传递依赖的 1.1.2,等上游发布含该修复的稳定版后可移除。
 navigationevent = "1.2.0-alpha04"
 markwon = "4.6.2"
 material = "1.14.0"
-material3 = "1.5.0-alpha23"
+material3 = "1.5.0-alpha27"
 material3IconsExtended = "1.7.8"
-materialKolor = "4.1.1"
+materialKolor = "5.0.1"
 media = "1.8.0"
-media3 = "1.8.0"
-navigationCompose = "2.9.8"
-okhttp = "5.4.0"
+media3 = "1.11.0"
+navigationCompose = "2.10.0"
+okhttp = "5.5.0"
 palette = "1.0.0"
 preference = "1.2.1"
 protobufJavalite = "4.26.1"
@@ -80,7 +80,7 @@ recyclerview = "1.4.0"
 #noinspection GradleDependency
 uiViewbinding = "1.9.4"
 viewpager2 = "1.1.0"
-webkit = "1.16.0"
+webkit = "1.17.0"
 collection = "1.6.0"
 
 zxingLite = "3.4.1"
@@ -89,11 +89,11 @@ koin-bom = "4.2.2"
 biometricKtx = "1.1.0"
 
 reorderable = "3.1.0"
-haze = "1.7.2"
+haze = "1.7.3"
 timber = "5.0.1"
 telephoto = "0.19.0"
 miuix = "0.9.3"
-backdrop = "2.0.0"
+backdrop = "2.0.1"
 capsule = "2.1.3"
 lyricViewx = "1.3.2"
 uiautomator = "2.4.0"