Просмотр исходного кода

Merge remote-tracking branch 'origin/main'

HapeLee 3 месяцев назад
Родитель
Сommit
3291dda012
100 измененных файлов с 1127 добавлено и 1501 удалено
  1. 2 1
      app/src/main/java/io/legado/app/constant/IntentAction.kt
  2. 82 17
      app/src/main/java/io/legado/app/model/CacheBookModel.kt
  3. 8 0
      app/src/main/java/io/legado/app/model/ReadAloud.kt
  4. 12 0
      app/src/main/java/io/legado/app/model/ReadAloudLayoutPolicy.kt
  5. 65 11
      app/src/main/java/io/legado/app/model/ReadBook.kt
  6. 71 0
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  7. 7 8
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  8. 22 9
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  9. 2 2
      app/src/main/java/io/legado/app/ui/ai/chat/AiGeneratedMessageContent.kt
  10. 1 1
      app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt
  11. 5 3
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt
  12. 3 2
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt
  13. 9 6
      app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt
  14. 10 4
      app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt
  15. 21 7
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt
  16. 27 8
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt
  17. 8 0
      app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt
  18. 43 0
      app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonFrame.kt
  19. 1 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt
  20. 16 14
      app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt
  21. 8 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt
  22. 6 4
      app/src/main/java/io/legado/app/ui/book/read/ReadBookSearchBar.kt
  23. 19 2
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  24. 0 7
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  25. 61 3
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  26. 1 1
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  27. 2 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/AiTextRewriteSheet.kt
  28. 9 4
      app/src/main/java/io/legado/app/ui/book/read/sheet/ChangeChapterSourceSheet.kt
  29. 2 1
      app/src/main/java/io/legado/app/ui/book/read/sheet/EffectiveReplacesSheet.kt
  30. 18 6
      app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt
  31. 1 1
      app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt
  32. 11 5
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt
  33. 46 12
      app/src/main/java/io/legado/app/ui/book/read/sheet/TextSelectMenuConfigSheet.kt
  34. 24 7
      app/src/main/java/io/legado/app/ui/book/read/sheet/TitleBarIconSheet.kt
  35. 1 1
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewScreen.kt
  36. 6 3
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt
  37. 7 2
      app/src/main/java/io/legado/app/ui/book/readRecord/component/ReadRecordHeatmap.kt
  38. 8 3
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt
  39. 2 6
      app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt
  40. 3 9
      app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
  41. 4 2
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt
  42. 4 2
      app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt
  43. 2 1
      app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt
  44. 8 4
      app/src/main/java/io/legado/app/ui/config/themeConfig/LabelColorManageSheet.kt
  45. 11 0
      app/src/main/java/io/legado/app/ui/config/themeConfig/MainNavigationSettingsSheet.kt
  46. 2 1
      app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt
  47. 1 7
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
  48. 1 1
      app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt
  49. 6 3
      app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt
  50. 3 9
      app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
  51. 5 9
      app/src/main/java/io/legado/app/ui/highlightTagRule/HighlightTagRuleScreen.kt
  52. 11 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  53. 9 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt
  54. 13 8
      app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt
  55. 16 4
      app/src/main/java/io/legado/app/ui/main/homepage/manage/SetDetailPage.kt
  56. 10 2
      app/src/main/java/io/legado/app/ui/main/homepage/manage/SetListPage.kt
  57. 18 5
      app/src/main/java/io/legado/app/ui/main/homepage/manage/SourceBrowseDetailPage.kt
  58. 3 9
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
  59. 3 0
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceScreen.kt
  60. 2 2
      app/src/main/java/io/legado/app/ui/tagGroupRule/TagGroupRuleEditSheet.kt
  61. 2 2
      app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt
  62. 3 0
      app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt
  63. 5 18
      app/src/main/java/io/legado/app/ui/widget/components/IconSwitch.kt
  64. 41 0
      app/src/main/java/io/legado/app/ui/widget/components/ReorderAccessibility.kt
  65. 4 0
      app/src/main/java/io/legado/app/ui/widget/components/ValueStepper.kt
  66. 10 1
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SeriesIconButton.kt
  67. 26 28
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallAnimatedButton.kt
  68. 19 21
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallOutlinedButton.kt
  69. 19 30
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallPlainButton.kt
  70. 39 41
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallToggleButton.kt
  71. 22 24
      app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallTonalButton.kt
  72. 21 10
      app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
  73. 8 4
      app/src/main/java/io/legado/app/ui/widget/components/changeSource/ChangeSourceSheet.kt
  74. 2 7
      app/src/main/java/io/legado/app/ui/widget/components/checkBox/AppCheckbox.kt
  75. 7 17
      app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt
  76. 50 39
      app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapCalendarComponents.kt
  77. 5 1
      app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapMode.kt
  78. 2 1
      app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt
  79. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/log/CrashLogSheet.kt
  80. 2 10
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt
  81. 6 0
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt
  82. 2 12
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt
  83. 7 5
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/TinySettingItems.kt
  84. 2 1
      app/src/main/res/layout/activity_audio_play.xml
  85. 0 190
      app/src/main/res/layout/activity_book_info_edit.xml
  86. 2 2
      app/src/main/res/layout/activity_book_source_edit.xml
  87. 0 42
      app/src/main/res/layout/activity_main.xml
  88. 15 13
      app/src/main/res/layout/activity_rss_source_edit.xml
  89. 1 0
      app/src/main/res/layout/activity_welcome.xml
  90. 0 318
      app/src/main/res/layout/dialog_read_book_style.xml
  91. 2 1
      app/src/main/res/layout/fragment_privacy.xml
  92. 10 9
      app/src/main/res/layout/item_book_source.xml
  93. 0 107
      app/src/main/res/layout/item_chapter_list.xml
  94. 0 85
      app/src/main/res/layout/item_read_record.xml
  95. 0 40
      app/src/main/res/layout/item_read_style.xml
  96. 0 83
      app/src/main/res/layout/item_replace_rule.xml
  97. 3 2
      app/src/main/res/layout/item_source_edit.xml
  98. 7 7
      app/src/main/res/layout/item_source_import.xml
  99. 0 56
      app/src/main/res/layout/item_tool_button.xml
  100. 0 32
      app/src/main/res/layout/navigation_rail_fab.xml

+ 2 - 1
app/src/main/java/io/legado/app/constant/IntentAction.kt

@@ -13,6 +13,7 @@ object IntentAction {
     const val prevParagraph = "prevParagraph"
     const val prevParagraph = "prevParagraph"
     const val nextParagraph = "nextParagraph"
     const val nextParagraph = "nextParagraph"
     const val upTtsSpeechRate = "upTtsSpeechRate"
     const val upTtsSpeechRate = "upTtsSpeechRate"
+    const val syncReadAloudLayout = "syncReadAloudLayout"
     const val upTtsProgress = "upTtsProgress"
     const val upTtsProgress = "upTtsProgress"
     const val adjustProgress = "adjustProgress"
     const val adjustProgress = "adjustProgress"
     const val adjustSpeed = "adjustSpeed"
     const val adjustSpeed = "adjustSpeed"
@@ -22,4 +23,4 @@ object IntentAction {
     const val init = "init"
     const val init = "init"
     const val remove = "remove"
     const val remove = "remove"
     const val stopPlay = "stopPlay"
     const val stopPlay = "stopPlay"
-}
+}

+ 82 - 17
app/src/main/java/io/legado/app/model/CacheBookModel.kt

@@ -62,7 +62,7 @@ class CacheBookModel(
     private val onDownloadSet = linkedSetOf<Int>()
     private val onDownloadSet = linkedSetOf<Int>()
     private val canceledDownloadSet = hashSetOf<Int>()
     private val canceledDownloadSet = hashSetOf<Int>()
     private val pausedChapterSet = linkedSetOf<Int>()
     private val pausedChapterSet = linkedSetOf<Int>()
-    private val pendingReadRequestMap = hashMapOf<Int, Boolean>()
+    private val pendingReadRequestMap = hashMapOf<Int, PendingReadRequest>()
     private val chapterTasks = hashMapOf<Int, Coroutine<*>>()
     private val chapterTasks = hashMapOf<Int, Coroutine<*>>()
     private val tasks = CompositeCoroutine()
     private val tasks = CompositeCoroutine()
     private val repository = CacheDownloadRepository()
     private val repository = CacheDownloadRepository()
@@ -671,7 +671,8 @@ class CacheBookModel(
         scope: CoroutineScope,
         scope: CoroutineScope,
         chapter: BookChapter,
         chapter: BookChapter,
         semaphore: Semaphore?,
         semaphore: Semaphore?,
-        resetPageOffset: Boolean = false
+        resetPageOffset: Boolean = false,
+        preserveReadAloudPosition: Boolean = false,
     ): Boolean {
     ): Boolean {
         if (!markChapterDownloadStarted(chapter.index)) {
         if (!markChapterDownloadStarted(chapter.index)) {
             // Chapter is already in onDownloadSet. Check if the task is actually alive.
             // Chapter is already in onDownloadSet. Check if the task is actually alive.
@@ -686,11 +687,19 @@ class CacheBookModel(
                 }
                 }
                 notifyDownloadSetChanged()
                 notifyDownloadSetChanged()
                 if (!markChapterDownloadStarted(chapter.index)) {
                 if (!markChapterDownloadStarted(chapter.index)) {
-                    markPendingReadRequest(chapter.index, resetPageOffset)
+                    markPendingReadRequest(
+                        chapter.index,
+                        resetPageOffset,
+                        preserveReadAloudPosition,
+                    )
                     return false
                     return false
                 }
                 }
             } else {
             } else {
-                markPendingReadRequest(chapter.index, resetPageOffset)
+                markPendingReadRequest(
+                    chapter.index,
+                    resetPageOffset,
+                    preserveReadAloudPosition,
+                )
                 return true
                 return true
             }
             }
         }
         }
@@ -712,17 +721,33 @@ class CacheBookModel(
             onSuccess(chapter)
             onSuccess(chapter)
             ReadBook.downloadedChapters.add(chapter.index)
             ReadBook.downloadedChapters.add(chapter.index)
             ReadBook.downloadFailChapters.remove(chapter.index)
             ReadBook.downloadFailChapters.remove(chapter.index)
-            downloadFinish(chapter, content, resetPageOffset)
+            downloadFinish(
+                chapter,
+                content,
+                resetPageOffset,
+                preserveReadAloudPosition = preserveReadAloudPosition,
+            )
             emitPendingReadContent(chapter, content)
             emitPendingReadContent(chapter, content)
         }.onError {
         }.onError {
             onError(chapter, it)
             onError(chapter, it)
             ReadBook.downloadFailChapters[chapter.index] =
             ReadBook.downloadFailChapters[chapter.index] =
                 (ReadBook.downloadFailChapters[chapter.index] ?: 0) + 1
                 (ReadBook.downloadFailChapters[chapter.index] ?: 0) + 1
-            downloadFinish(chapter, "获取正文失败\n${it.localizedMessage}", resetPageOffset)
+            downloadFinish(
+                chapter,
+                "获取正文失败\n${it.localizedMessage}",
+                resetPageOffset,
+                preserveReadAloudPosition = preserveReadAloudPosition,
+            )
             emitPendingReadError(chapter, it)
             emitPendingReadError(chapter, it)
         }.onCancel {
         }.onCancel {
             onCancel(chapter.index, requeue = false)
             onCancel(chapter.index, requeue = false)
-            downloadFinish(chapter, "download canceled", resetPageOffset, true)
+            downloadFinish(
+                chapter,
+                "download canceled",
+                resetPageOffset,
+                canceled = true,
+                preserveReadAloudPosition = preserveReadAloudPosition,
+            )
         }.onFinally {
         }.onFinally {
             chapterTasks.remove(chapter.index)
             chapterTasks.remove(chapter.index)
             host.onTaskQueuesChanged(book.bookUrl)
             host.onTaskQueuesChanged(book.bookUrl)
@@ -744,35 +769,59 @@ class CacheBookModel(
     }
     }
 
 
     @Synchronized
     @Synchronized
-    private fun markPendingReadRequest(index: Int, resetPageOffset: Boolean) {
-        pendingReadRequestMap[index] = pendingReadRequestMap[index] == true || resetPageOffset
+    private fun markPendingReadRequest(
+        index: Int,
+        resetPageOffset: Boolean,
+        preserveReadAloudPosition: Boolean,
+    ) {
+        pendingReadRequestMap[index] = mergePendingReadRequest(
+            previous = pendingReadRequestMap[index],
+            resetPageOffset = resetPageOffset,
+            preserveReadAloudPosition = preserveReadAloudPosition,
+        )
     }
     }
 
 
     @Synchronized
     @Synchronized
-    private fun consumePendingReadRequest(index: Int): Boolean? {
+    private fun consumePendingReadRequest(index: Int): PendingReadRequest? {
         return pendingReadRequestMap.remove(index)
         return pendingReadRequestMap.remove(index)
     }
     }
 
 
     private fun emitPendingReadContent(chapter: BookChapter, content: String) {
     private fun emitPendingReadContent(chapter: BookChapter, content: String) {
-        val resetPageOffset = consumePendingReadRequest(chapter.index) ?: return
-        downloadFinish(chapter, content, resetPageOffset)
+        val request = consumePendingReadRequest(chapter.index) ?: return
+        downloadFinish(
+            chapter,
+            content,
+            request.resetPageOffset,
+            preserveReadAloudPosition = request.preserveReadAloudPosition,
+        )
     }
     }
 
 
     private fun emitPendingReadError(chapter: BookChapter, error: Throwable) {
     private fun emitPendingReadError(chapter: BookChapter, error: Throwable) {
-        val resetPageOffset = consumePendingReadRequest(chapter.index) ?: return
-        downloadFinish(chapter, "获取正文失败\n${error.localizedMessage}", resetPageOffset)
+        val request = consumePendingReadRequest(chapter.index) ?: return
+        downloadFinish(
+            chapter,
+            "获取正文失败\n${error.localizedMessage}",
+            request.resetPageOffset,
+            preserveReadAloudPosition = request.preserveReadAloudPosition,
+        )
     }
     }
 
 
     private fun emitPendingReadCanceled(chapter: BookChapter) {
     private fun emitPendingReadCanceled(chapter: BookChapter) {
-        val resetPageOffset = consumePendingReadRequest(chapter.index) ?: return
-        downloadFinish(chapter, "download canceled", resetPageOffset)
+        val request = consumePendingReadRequest(chapter.index) ?: return
+        downloadFinish(
+            chapter,
+            "download canceled",
+            request.resetPageOffset,
+            preserveReadAloudPosition = request.preserveReadAloudPosition,
+        )
     }
     }
 
 
     private fun downloadFinish(
     private fun downloadFinish(
         chapter: BookChapter,
         chapter: BookChapter,
         content: String,
         content: String,
         resetPageOffset: Boolean = false,
         resetPageOffset: Boolean = false,
-        canceled: Boolean = false
+        canceled: Boolean = false,
+        preserveReadAloudPosition: Boolean = false,
     ) {
     ) {
         if (ReadBook.book?.bookUrl == book.bookUrl) {
         if (ReadBook.book?.bookUrl == book.bookUrl) {
             ReadBook.contentLoadFinish(
             ReadBook.contentLoadFinish(
@@ -781,7 +830,23 @@ class CacheBookModel(
                 content = content,
                 content = content,
                 resetPageOffset = resetPageOffset,
                 resetPageOffset = resetPageOffset,
                 canceled = canceled,
                 canceled = canceled,
+                preserveReadAloudPosition = preserveReadAloudPosition,
             )
             )
         }
         }
     }
     }
 }
 }
+
+internal data class PendingReadRequest(
+    val resetPageOffset: Boolean,
+    val preserveReadAloudPosition: Boolean,
+)
+
+internal fun mergePendingReadRequest(
+    previous: PendingReadRequest?,
+    resetPageOffset: Boolean,
+    preserveReadAloudPosition: Boolean,
+): PendingReadRequest = PendingReadRequest(
+    resetPageOffset = previous?.resetPageOffset == true || resetPageOffset,
+    preserveReadAloudPosition =
+        (previous?.preserveReadAloudPosition ?: true) && preserveReadAloudPosition,
+)

+ 8 - 0
app/src/main/java/io/legado/app/model/ReadAloud.kt

@@ -126,6 +126,14 @@ object ReadAloud {
         }
         }
     }
     }
 
 
+    fun syncLayout(context: Context = appCtx) {
+        if (BaseReadAloudService.isRun) {
+            val intent = Intent(context, aloudClass)
+            intent.action = IntentAction.syncReadAloudLayout
+            context.startForegroundServiceCompat(intent)
+        }
+    }
+
     fun setTimer(context: Context, minute: Int) {
     fun setTimer(context: Context, minute: Int) {
         if (BaseReadAloudService.isRun) {
         if (BaseReadAloudService.isRun) {
             val intent = Intent(context, aloudClass)
             val intent = Intent(context, aloudClass)

+ 12 - 0
app/src/main/java/io/legado/app/model/ReadAloudLayoutPolicy.kt

@@ -0,0 +1,12 @@
+package io.legado.app.model
+
+internal fun shouldRestartReadAloudAfterContentLoad(
+    preserveReadAloudPosition: Boolean,
+    serviceChapterIndex: Int,
+    loadedChapterIndex: Int,
+): Boolean = !preserveReadAloudPosition || serviceChapterIndex != loadedChapterIndex
+
+internal fun activeReadAloudProgress(
+    isPlaying: Boolean,
+    currentProgress: Int,
+): Int? = currentProgress.takeIf { isPlaying && it > 0 }

+ 65 - 11
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -627,15 +627,26 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
     /**
     /**
      * 当前页面变化
      * 当前页面变化
      */
      */
-    private fun curPageChanged(pageChanged: Boolean = false) {
+    private fun curPageChanged(
+        pageChanged: Boolean = false,
+        preserveReadAloudPosition: Boolean = false,
+    ) {
         callBack?.pageChanged()
         callBack?.pageChanged()
         curTextChapter?.let {
         curTextChapter?.let {
             if (BaseReadAloudService.isRun && it.isCompleted) {
             if (BaseReadAloudService.isRun && it.isCompleted) {
-                val scrollPageAnim = pageAnim() == 3
-                if (scrollPageAnim && pageChanged) {
-                    ReadAloud.pause(appCtx)
+                if (shouldRestartReadAloudAfterContentLoad(
+                        preserveReadAloudPosition = preserveReadAloudPosition,
+                        serviceChapterIndex = BaseReadAloudService.currentChapterIndex,
+                        loadedChapterIndex = it.chapter.index,
+                    )
+                ) {
+                    if (isScroll && pageChanged) {
+                        ReadAloud.pause(appCtx)
+                    } else {
+                        readAloud(!BaseReadAloudService.pause)
+                    }
                 } else {
                 } else {
-                    readAloud(!BaseReadAloudService.pause)
+                    ReadAloud.syncLayout()
                 }
                 }
             }
             }
         }
         }
@@ -654,6 +665,13 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         }
         }
     }
     }
 
 
+    fun syncReadAloudPage(chapterIndex: Int, chapterPos: Int) {
+        if (durChapterIndex != chapterIndex || durChapterPos == chapterPos) return
+        durChapterPos = chapterPos
+        callBack?.upContent(resetPageOffset = false)
+        saveRead(pageChanged = true)
+    }
+
     /**
     /**
      * 当前页数
      * 当前页数
      */
      */
@@ -694,22 +712,39 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
      */
      */
     fun loadContent(
     fun loadContent(
         resetPageOffset: Boolean,
         resetPageOffset: Boolean,
+        preserveReadAloudPosition: Boolean = false,
         success: (() -> Unit)? = null
         success: (() -> Unit)? = null
     ) {
     ) {
-        loadContent(durChapterIndex, resetPageOffset = resetPageOffset) {
+        loadContent(
+            durChapterIndex,
+            resetPageOffset = resetPageOffset,
+            preserveReadAloudPosition = preserveReadAloudPosition,
+        ) {
             success?.invoke()
             success?.invoke()
         }
         }
         loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset)
         loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset)
         loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
         loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset)
     }
     }
 
 
+    fun relayoutContent() {
+        loadContent(
+            resetPageOffset = false,
+            preserveReadAloudPosition = true,
+        )
+    }
+
     fun loadOrUpContent(success: (() -> Unit)? = null) {
     fun loadOrUpContent(success: (() -> Unit)? = null) {
         val curChapter = curTextChapter
         val curChapter = curTextChapter
         if (curChapter == null || !curChapter.isLayoutSizeMatch()) {
         if (curChapter == null || !curChapter.isLayoutSizeMatch()) {
+            val preserveReadAloudPosition = BaseReadAloudService.isRun &&
+                    BaseReadAloudService.currentChapterIndex == durChapterIndex
             curTextChapter = null
             curTextChapter = null
             nextTextChapter = null
             nextTextChapter = null
             prevTextChapter = null
             prevTextChapter = null
-            loadContent(durChapterIndex) {
+            loadContent(
+                durChapterIndex,
+                preserveReadAloudPosition = preserveReadAloudPosition,
+            ) {
                 success?.invoke()
                 success?.invoke()
             }
             }
         } else {
         } else {
@@ -738,6 +773,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         index: Int,
         index: Int,
         upContent: Boolean = true,
         upContent: Boolean = true,
         resetPageOffset: Boolean = false,
         resetPageOffset: Boolean = false,
+        preserveReadAloudPosition: Boolean = false,
         success: (() -> Unit)? = null
         success: (() -> Unit)? = null
     ) {
     ) {
         Coroutine.async {
         Coroutine.async {
@@ -767,12 +803,14 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                         it,
                         it,
                         upContent,
                         upContent,
                         resetPageOffset,
                         resetPageOffset,
+                        preserveReadAloudPosition,
                         success = success
                         success = success
                     )
                     )
                 } ?: download(
                 } ?: download(
                     downloadScope,
                     downloadScope,
                     chapter,
                     chapter,
-                    resetPageOffset
+                    resetPageOffset,
+                    preserveReadAloudPosition = preserveReadAloudPosition,
                 )
                 )
             }
             }
         }.onError {
         }.onError {
@@ -831,7 +869,12 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         } else {
         } else {
             delay(1000)
             delay(1000)
             if (addLoading(index)) {
             if (addLoading(index)) {
-                download(downloadScope, chapter, false, preDownloadSemaphore)
+                download(
+                    scope = downloadScope,
+                    chapter = chapter,
+                    resetPageOffset = false,
+                    semaphore = preDownloadSemaphore,
+                )
             }
             }
         }
         }
     }
     }
@@ -843,6 +886,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         scope: CoroutineScope,
         scope: CoroutineScope,
         chapter: BookChapter,
         chapter: BookChapter,
         resetPageOffset: Boolean,
         resetPageOffset: Boolean,
+        preserveReadAloudPosition: Boolean = false,
         semaphore: Semaphore? = null,
         semaphore: Semaphore? = null,
         success: (() -> Unit)? = null
         success: (() -> Unit)? = null
     ) {
     ) {
@@ -850,7 +894,13 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         val bookSource = bookSource
         val bookSource = bookSource
         if (bookSource != null) {
         if (bookSource != null) {
             val started =
             val started =
-                CacheBook.getOrCreate(bookSource, book).download(scope, chapter, semaphore)
+                CacheBook.getOrCreate(bookSource, book).download(
+                    scope = scope,
+                    chapter = chapter,
+                    semaphore = semaphore,
+                    resetPageOffset = resetPageOffset,
+                    preserveReadAloudPosition = preserveReadAloudPosition,
+                )
             if (!started) {
             if (!started) {
                 removeLoading(chapter.index)
                 removeLoading(chapter.index)
             }
             }
@@ -861,6 +911,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                 chapter,
                 chapter,
                 "加载正文失败\n$msg",
                 "加载正文失败\n$msg",
                 resetPageOffset = resetPageOffset,
                 resetPageOffset = resetPageOffset,
+                preserveReadAloudPosition = preserveReadAloudPosition,
                 success = success
                 success = success
             )
             )
         }
         }
@@ -927,6 +978,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         content: String,
         content: String,
         upContent: Boolean = true,
         upContent: Boolean = true,
         resetPageOffset: Boolean,
         resetPageOffset: Boolean,
+        preserveReadAloudPosition: Boolean = false,
         canceled: Boolean = false,
         canceled: Boolean = false,
         success: (() -> Unit)? = null
         success: (() -> Unit)? = null
     ) {
     ) {
@@ -971,7 +1023,9 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                         callBack?.onLayoutPageCompleted(index, page)
                         callBack?.onLayoutPageCompleted(index, page)
                     }
                     }
                     if (upContent) callBack?.upContent(offset, !available && resetPageOffset)
                     if (upContent) callBack?.upContent(offset, !available && resetPageOffset)
-                    curPageChanged()
+                    curPageChanged(
+                        preserveReadAloudPosition = preserveReadAloudPosition,
+                    )
                     callBack?.contentLoadFinish()
                     callBack?.contentLoadFinish()
                 }
                 }
 
 

+ 71 - 0
app/src/main/java/io/legado/app/service/BaseReadAloudService.kt

@@ -88,6 +88,16 @@ abstract class BaseReadAloudService : BaseService(),
                 field = PlaybackTimer.normalize(value)
                 field = PlaybackTimer.normalize(value)
             }
             }
 
 
+        @JvmStatic
+        @Volatile
+        var currentChapterIndex: Int = -1
+            private set
+
+        @JvmStatic
+        @Volatile
+        var currentProgress: Int = 0
+            private set
+
         fun isPlay(): Boolean {
         fun isPlay(): Boolean {
             return isRun && !pause
             return isRun && !pause
         }
         }
@@ -199,6 +209,8 @@ abstract class BaseReadAloudService : BaseService(),
         }
         }
         isRun = false
         isRun = false
         pause = true
         pause = true
+        currentChapterIndex = -1
+        currentProgress = 0
         abandonFocus()
         abandonFocus()
         unregisterReceiver(broadcastReceiver)
         unregisterReceiver(broadcastReceiver)
         postEvent(EventBus.ALOUD_STATE, Status.STOP)
         postEvent(EventBus.ALOUD_STATE, Status.STOP)
@@ -227,6 +239,7 @@ abstract class BaseReadAloudService : BaseService(),
             IntentAction.pause -> pauseReadAloud()
             IntentAction.pause -> pauseReadAloud()
             IntentAction.resume -> resumeReadAloud()
             IntentAction.resume -> resumeReadAloud()
             IntentAction.upTtsSpeechRate -> upSpeechRate(true)
             IntentAction.upTtsSpeechRate -> upSpeechRate(true)
+            IntentAction.syncReadAloudLayout -> syncTextChapterLayout()
             IntentAction.prevParagraph -> prevP()
             IntentAction.prevParagraph -> prevP()
             IntentAction.nextParagraph -> nextP()
             IntentAction.nextParagraph -> nextP()
             IntentAction.prev -> prevChapter()
             IntentAction.prev -> prevChapter()
@@ -274,6 +287,7 @@ abstract class BaseReadAloudService : BaseService(),
                             textChapter.paragraphs[nowSpeak].chapterPosition
                             textChapter.paragraphs[nowSpeak].chapterPosition
                 }
                 }
             }
             }
+            updateReadAloudProgressSnapshot(readAloudNumber + 1)
             paragraphStartPos = pos
             paragraphStartPos = pos
             launch(Main) {
             launch(Main) {
                 upMediaMetadata()
                 upMediaMetadata()
@@ -347,9 +361,50 @@ abstract class BaseReadAloudService : BaseService(),
 
 
     fun upTtsProgress(progress: Int) {
     fun upTtsProgress(progress: Int) {
         ReadBook.upReadTime()
         ReadBook.upReadTime()
+        updateReadAloudProgressSnapshot(progress)
         postEvent(EventBus.TTS_PROGRESS, progress)
         postEvent(EventBus.TTS_PROGRESS, progress)
     }
     }
 
 
+    protected fun updateReadAloudProgressSnapshot(progress: Int) {
+        currentChapterIndex = textChapter?.chapter?.index ?: currentChapterIndex
+        currentProgress = progress.coerceAtLeast(0)
+    }
+
+    protected fun moveToReadAloudPage(chapterPosition: Int): Boolean {
+        val chapter = textChapter ?: return false
+        val targetPageIndex = findReadAloudPageIndex(
+            currentPageIndex = pageIndex,
+            chapterPosition = chapterPosition,
+            pageCount = chapter.pageSize,
+            pageStart = chapter::getReadLength,
+        )
+        if (targetPageIndex == pageIndex) return false
+        repeat(targetPageIndex - pageIndex) {
+            pageIndex++
+            ReadBook.moveToNextPage()
+        }
+        return true
+    }
+
+    private fun syncTextChapterLayout() {
+        val latestChapter = ReadBook.curTextChapter ?: return
+        val serviceChapter = textChapter ?: return
+        if (!latestChapter.isCompleted || latestChapter.chapter.index != serviceChapter.chapter.index) {
+            return
+        }
+        val latestPageIndex = latestChapter.getPageIndexByCharIndex(
+            (currentProgress - 1).coerceAtLeast(0)
+        )
+        if (latestPageIndex < 0) return
+        textChapter = latestChapter
+        pageIndex = latestPageIndex
+        ReadBook.syncReadAloudPage(
+            chapterIndex = latestChapter.chapter.index,
+            chapterPos = latestChapter.getReadLength(latestPageIndex),
+        )
+        upTtsProgress(currentProgress)
+    }
+
     private fun prevP() {
     private fun prevP() {
         ReadBook.upReadTime()
         ReadBook.upReadTime()
         if (nowSpeak > 0) {
         if (nowSpeak > 0) {
@@ -816,3 +871,19 @@ abstract class BaseReadAloudService : BaseService(),
     }
     }
 
 
 }
 }
+
+internal inline fun findReadAloudPageIndex(
+    currentPageIndex: Int,
+    chapterPosition: Int,
+    pageCount: Int,
+    pageStart: (Int) -> Int,
+): Int {
+    var targetPageIndex = currentPageIndex
+    while (
+        targetPageIndex + 1 < pageCount &&
+        chapterPosition > pageStart(targetPageIndex + 1)
+    ) {
+        targetPageIndex++
+    }
+    return targetPageIndex
+}

+ 7 - 8
app/src/main/java/io/legado/app/service/HttpReadAloudService.kt

@@ -592,10 +592,10 @@ class HttpReadAloudService : BaseReadAloudService(),
 
 
     private fun upPlayPos() {
     private fun upPlayPos() {
         playIndexJob?.cancel()
         playIndexJob?.cancel()
-        val textChapter = textChapter ?: return
+        if (textChapter == null) return
         playIndexJob = lifecycleScope.launch {
         playIndexJob = lifecycleScope.launch {
-            upTtsProgress(readAloudNumber + 1)
             if (exoPlayer.duration <= 0) {
             if (exoPlayer.duration <= 0) {
+                upTtsProgress(readAloudNumber + 1)
                 return@launch
                 return@launch
             }
             }
             val speakTextLength = contentList[nowSpeak].length
             val speakTextLength = contentList[nowSpeak].length
@@ -604,13 +604,12 @@ class HttpReadAloudService : BaseReadAloudService(),
             }
             }
             val sleep = exoPlayer.duration / speakTextLength
             val sleep = exoPlayer.duration / speakTextLength
             val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration
             val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration
+            upTtsProgress(readAloudNumber + start.toInt() + 1)
             for (i in start..contentList[nowSpeak].length) {
             for (i in start..contentList[nowSpeak].length) {
-                if (pageIndex + 1 < textChapter.pageSize
-                    && readAloudNumber + i > textChapter.getReadLength(pageIndex + 1)
-                ) {
-                    pageIndex++
-                    ReadBook.moveToNextPage()
-                    upTtsProgress(readAloudNumber + i.toInt())
+                val chapterPosition = readAloudNumber + i.toInt()
+                updateReadAloudProgressSnapshot(chapterPosition + 1)
+                if (moveToReadAloudPage(chapterPosition)) {
+                    upTtsProgress(chapterPosition + 1)
                 }
                 }
                 delay(sleep)
                 delay(sleep)
             }
             }

+ 22 - 9
app/src/main/java/io/legado/app/service/TTSReadAloudService.kt

@@ -33,6 +33,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
     private val ttsUtteranceListener = TTSUtteranceListener()
     private val ttsUtteranceListener = TTSUtteranceListener()
     private var speakJob: Coroutine<*>? = null
     private var speakJob: Coroutine<*>? = null
     private var utteranceStartPos = 0
     private var utteranceStartPos = 0
+    private var utteranceStartReadAloudNumber = 0
     private var needParagraphInterval = false // 是否需要进行段落间隔延迟
     private var needParagraphInterval = false // 是否需要进行段落间隔延迟
     private val TAG = "TTSReadAloudService"
     private val TAG = "TTSReadAloudService"
 
 
@@ -243,6 +244,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         override fun onStart(s: String) {
         override fun onStart(s: String) {
             LogUtils.d(TAG, "onStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$s")
             LogUtils.d(TAG, "onStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$s")
             utteranceStartPos = paragraphStartPos
             utteranceStartPos = paragraphStartPos
+            utteranceStartReadAloudNumber = readAloudNumber
             textChapter?.let {
             textChapter?.let {
                 if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) {
                 if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) {
                     nextParagraph()
                     nextParagraph()
@@ -270,17 +272,13 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
         override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
             super.onRangeStart(utteranceId, start, end, frame)
             super.onRangeStart(utteranceId, start, end, frame)
             paragraphStartPos = utteranceStartPos + start
             paragraphStartPos = utteranceStartPos + start
+            readAloudNumber = currentRangePosition(utteranceStartReadAloudNumber, start)
+            updateReadAloudProgressSnapshot(readAloudNumber + 1)
             val msg =
             val msg =
                 "onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame"
                 "onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame"
             LogUtils.d(TAG, msg)
             LogUtils.d(TAG, msg)
-            textChapter?.let {
-                if (pageIndex + 1 < it.pageSize
-                    && readAloudNumber + start > it.getReadLength(pageIndex + 1)
-                ) {
-                    pageIndex++
-                    ReadBook.moveToNextPage()
-                    upTtsProgress(readAloudNumber + start)
-                }
+            if (moveToReadAloudPage(readAloudNumber)) {
+                upTtsProgress(readAloudNumber + 1)
             }
             }
         }
         }
 
 
@@ -299,7 +297,11 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         private fun nextParagraph() {
         private fun nextParagraph() {
             //跳过全标点段落
             //跳过全标点段落
             do {
             do {
-                readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos
+                readAloudNumber = nextParagraphPosition(
+                    currentPosition = readAloudNumber,
+                    paragraphLength = contentList[nowSpeak].length,
+                    paragraphStartPosition = paragraphStartPos,
+                )
                 paragraphStartPos = 0
                 paragraphStartPos = 0
                 nowSpeak++
                 nowSpeak++
                 if (nowSpeak >= contentList.size) {
                 if (nowSpeak >= contentList.size) {
@@ -326,3 +328,14 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
     }
     }
 
 
 }
 }
+
+internal fun nextParagraphPosition(
+    currentPosition: Int,
+    paragraphLength: Int,
+    paragraphStartPosition: Int,
+): Int = currentPosition + paragraphLength + 1 - paragraphStartPosition
+
+internal fun currentRangePosition(
+    utteranceStartPosition: Int,
+    rangeStart: Int,
+): Int = utteranceStartPosition + rangeStart

+ 2 - 2
app/src/main/java/io/legado/app/ui/ai/chat/AiGeneratedMessageContent.kt

@@ -197,7 +197,7 @@ fun AiGeneratedMessageContent(
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = { onSwitchBranch(-1) },
                         onClick = { onSwitchBranch(-1) },
                         icon = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
                         icon = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
-                        contentDescription = "Previous branch",
+                        contentDescription = stringResource(R.string.previous),
                         modifier = Modifier.size(28.dp)
                         modifier = Modifier.size(28.dp)
                     )
                     )
                     AppText(
                     AppText(
@@ -208,7 +208,7 @@ fun AiGeneratedMessageContent(
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = { onSwitchBranch(1) },
                         onClick = { onSwitchBranch(1) },
                         icon = Icons.AutoMirrored.Filled.KeyboardArrowRight,
                         icon = Icons.AutoMirrored.Filled.KeyboardArrowRight,
-                        contentDescription = "Next branch",
+                        contentDescription = stringResource(R.string.next),
                         modifier = Modifier.size(28.dp)
                         modifier = Modifier.size(28.dp)
                     )
                     )
                 }
                 }

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt

@@ -170,7 +170,7 @@ private fun BookCacheManageScreen(
                         TopBarActionButton(
                         TopBarActionButton(
                             onClick = { showGroupMenu = true },
                             onClick = { showGroupMenu = true },
                             imageVector = AppIcons.Filter,
                             imageVector = AppIcons.Filter,
-                            contentDescription = "分组筛选"
+                            contentDescription = stringResource(R.string.a11y_group_filter)
                         )
                         )
                         RoundDropdownMenu(
                         RoundDropdownMenu(
                             expanded = showGroupMenu,
                             expanded = showGroupMenu,

+ 5 - 3
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -42,12 +42,14 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import dev.chrisbanes.haze.HazeState
 import dev.chrisbanes.haze.HazeState
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
+import io.legado.app.R
 import io.legado.app.domain.model.BookShelfState
 import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.main.bookCoverSharedElementKey
 import io.legado.app.ui.main.bookCoverSharedElementKey
@@ -264,20 +266,20 @@ fun ExploreShowScreen(
                                 )
                                 )
                             },
                             },
                             imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
                             imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
-                            contentDescription = "列数设置"
+                            contentDescription = stringResource(R.string.a11y_grid_columns)
                         )
                         )
                     }
                     }
 
 
                     TopBarActionButton(
                     TopBarActionButton(
                         onClick = { viewModel.onIntent(ExploreShowIntent.ShowSheet(ExploreShowSheet.KindSelect)) },
                         onClick = { viewModel.onIntent(ExploreShowIntent.ShowSheet(ExploreShowSheet.KindSelect)) },
                         imageVector = Icons.Outlined.FilterAlt,
                         imageVector = Icons.Outlined.FilterAlt,
-                        contentDescription = "分类"
+                        contentDescription = stringResource(R.string.select_or_search_category)
                     )
                     )
 
 
                     TopBarActionButton(
                     TopBarActionButton(
                         onClick = { viewModel.onIntent(ExploreShowIntent.ToggleLayout) },
                         onClick = { viewModel.onIntent(ExploreShowIntent.ToggleLayout) },
                         imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView,
                         imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView,
-                        contentDescription = "切换布局"
+                        contentDescription = stringResource(R.string.a11y_switch_layout)
                     )
                     )
                 },
                 },
                 scrollBehavior = scrollBehavior
                 scrollBehavior = scrollBehavior

+ 3 - 2
app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt

@@ -455,7 +455,7 @@ private fun ImportPathNavigationBar(
             SmallTonalButton(
             SmallTonalButton(
                 onClick = onNavigateBack,
                 onClick = onNavigateBack,
                 icon = Icons.AutoMirrored.Filled.ArrowBack,
                 icon = Icons.AutoMirrored.Filled.ArrowBack,
-                contentDescription = "back"
+                contentDescription = stringResource(R.string.back)
             )
             )
         }
         }
     }
     }
@@ -538,7 +538,8 @@ private fun ImportBookItem(
                 Spacer(modifier = Modifier.width(8.dp))
                 Spacer(modifier = Modifier.width(8.dp))
                 SmallPlainButton(
                 SmallPlainButton(
                     onClick = onAddToBookshelf,
                     onClick = onAddToBookshelf,
-                    icon = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline
+                    icon = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline,
+                    contentDescription = stringResource(R.string.add_to_bookshelf)
                 )
                 )
             }
             }
         }
         }

+ 9 - 6
app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt

@@ -170,7 +170,8 @@ fun RemoteBookScreen(
             {
             {
                 MediumPlainButton(
                 MediumPlainButton(
                     onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
                     onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
-                    icon = Icons.Default.Add
+                    icon = Icons.Default.Add,
+                    contentDescription = stringResource(R.string.add)
                 )
                 )
             }
             }
         } else {
         } else {
@@ -264,7 +265,7 @@ fun RemoteBookScreen(
             TopBarActionButton(
             TopBarActionButton(
                 onClick = { showSheet = RemoteBookSheet.Servers },
                 onClick = { showSheet = RemoteBookSheet.Servers },
                 imageVector = Icons.Default.Storage,
                 imageVector = Icons.Default.Storage,
-                contentDescription = "服务器"
+                contentDescription = stringResource(R.string.a11y_server_list)
             )
             )
         },
         },
         dropDownMenuContent = { dismiss ->
         dropDownMenuContent = { dismiss ->
@@ -438,14 +439,14 @@ private fun ServerItem(
                         SmallPlainButton(
                         SmallPlainButton(
                             onClick = it,
                             onClick = it,
                             icon = Icons.Default.Edit,
                             icon = Icons.Default.Edit,
-                            contentDescription = "Edit"
+                            contentDescription = stringResource(R.string.edit)
                         )
                         )
                     }
                     }
                     onDelete?.let {
                     onDelete?.let {
                         SmallPlainButton(
                         SmallPlainButton(
                             onClick = it,
                             onClick = it,
                             icon = Icons.Default.Delete,
                             icon = Icons.Default.Delete,
-                            contentDescription = "Delete"
+                            contentDescription = stringResource(R.string.delete)
                         )
                         )
                     }
                     }
                 }
                 }
@@ -519,7 +520,9 @@ private fun ServerConfigSheetContent(
                 IconButton(onClick = { passwordVisible = !passwordVisible }) {
                 IconButton(onClick = { passwordVisible = !passwordVisible }) {
                     Icon(
                     Icon(
                         if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
                         if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
-                        contentDescription = null
+                        contentDescription = stringResource(
+                            if (passwordVisible) R.string.hide_password else R.string.show_password
+                        )
                     )
                     )
                 }
                 }
             }
             }
@@ -610,7 +613,7 @@ private fun PathNavigationBar(
             SmallTonalButton(
             SmallTonalButton(
                 onClick = onNavigateBack,
                 onClick = onNavigateBack,
                 icon = Icons.AutoMirrored.Filled.ArrowBack,
                 icon = Icons.AutoMirrored.Filled.ArrowBack,
-                contentDescription = "返回上级"
+                contentDescription = stringResource(R.string.a11y_parent_folder)
             )
             )
         }
         }
     }
     }

+ 10 - 4
app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt

@@ -148,13 +148,15 @@ fun GroupSelectSheet(
         startAction = {
         startAction = {
             MediumPlainButton(
             MediumPlainButton(
                 onClick = { showAddGroup = true },
                 onClick = { showAddGroup = true },
-                icon = Icons.Default.Add
+                icon = Icons.Default.Add,
+                contentDescription = stringResource(R.string.group_add)
             )
             )
         },
         },
         endAction = {
         endAction = {
             MediumPlainButton(
             MediumPlainButton(
                 onClick = { onConfirm(selectedGroupId) },
                 onClick = { onConfirm(selectedGroupId) },
-                icon = Icons.Default.Check
+                icon = Icons.Default.Check,
+                contentDescription = stringResource(R.string.confirm)
             )
             )
         }
         }
     ) {
     ) {
@@ -190,7 +192,8 @@ fun GroupSelectSheet(
                         trailingAction = {
                         trailingAction = {
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = { editingGroup = group },
                                 onClick = { editingGroup = group },
-                                icon = Icons.Default.Edit
+                                icon = Icons.Default.Edit,
+                                contentDescription = stringResource(R.string.edit)
                             )
                             )
                         },
                         },
                         containerColor = LegadoTheme.colorScheme.surfaceContainerLow
                         containerColor = LegadoTheme.colorScheme.surfaceContainerLow
@@ -239,7 +242,10 @@ fun ChangeCoverSheet(
         endAction = {
         endAction = {
             MediumPlainButton(
             MediumPlainButton(
                 onClick = { viewModel.startOrStopSearch() },
                 onClick = { viewModel.startOrStopSearch() },
-                icon = if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh
+                icon = if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh,
+                contentDescription = stringResource(
+                    if (isSearching) R.string.more_menu else R.string.refresh
+                )
             )
             )
         }
         }
     ) {
     ) {

+ 21 - 7
app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt

@@ -62,6 +62,7 @@ import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.image.cover.CoilBookCover
 import io.legado.app.ui.widget.components.image.cover.CoilBookCover
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 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.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.reorderAccessibility
 import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
@@ -165,15 +166,18 @@ fun BookInfoEditContent(
                                 )
                                 )
                             )
                             )
                         },
                         },
-                        icon = Icons.Default.ImageSearch
+                        icon = Icons.Default.ImageSearch,
+                        contentDescription = stringResource(R.string.refresh_cover)
                     )
                     )
                     MediumOutlinedButton(
                     MediumOutlinedButton(
                         onClick = { selectCover.launch() },
                         onClick = { selectCover.launch() },
-                        icon = Icons.Default.FolderOpen
+                        icon = Icons.Default.FolderOpen,
+                        contentDescription = stringResource(R.string.select_folder)
                     )
                     )
                     MediumOutlinedButton(
                     MediumOutlinedButton(
                         onClick = { viewModel.resetCover() },
                         onClick = { viewModel.resetCover() },
-                        icon = Icons.Default.Replay
+                        icon = Icons.Default.Replay,
+                        contentDescription = stringResource(R.string.cover_reset)
                     )
                     )
                 }
                 }
                 Spacer(modifier = Modifier.height(4.dp))
                 Spacer(modifier = Modifier.height(4.dp))
@@ -324,11 +328,14 @@ fun KindEditor(
     val hapticFeedback = LocalHapticFeedback.current
     val hapticFeedback = LocalHapticFeedback.current
 
 
     val listState = rememberLazyListState()
     val listState = rememberLazyListState()
-    val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
+    fun moveKind(from: Int, to: Int) {
         val mutable = kindList.toMutableList()
         val mutable = kindList.toMutableList()
-        mutable.add(to.index, mutable.removeAt(from.index))
+        mutable.add(to, mutable.removeAt(from))
         onKindListChange(mutable)
         onKindListChange(mutable)
     }
     }
+    val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
+        moveKind(from.index, to.index)
+    }
 
 
     Column(
     Column(
         modifier = Modifier.padding(vertical = 8.dp),
         modifier = Modifier.padding(vertical = 8.dp),
@@ -364,6 +371,11 @@ fun KindEditor(
                                 editText = kind
                                 editText = kind
                             },
                             },
                             modifier = Modifier
                             modifier = Modifier
+                                .reorderAccessibility(
+                                    index = index,
+                                    itemCount = kindList.size,
+                                    onMove = ::moveKind,
+                                )
                                 .longPressDraggableHandle(
                                 .longPressDraggableHandle(
                                     onDragStarted = {
                                     onDragStarted = {
                                         hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
                                         hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
@@ -382,7 +394,8 @@ fun KindEditor(
                 onClick = {
                 onClick = {
                     onReset()
                     onReset()
                 },
                 },
-                icon = Icons.Default.Replay
+                icon = Icons.Default.Replay,
+                contentDescription = stringResource(R.string.reset)
             )
             )
             Spacer(modifier = Modifier.width(8.dp))
             Spacer(modifier = Modifier.width(8.dp))
             MediumOutlinedButton(
             MediumOutlinedButton(
@@ -390,7 +403,8 @@ fun KindEditor(
                     editingIndex = -1
                     editingIndex = -1
                     editText = ""
                     editText = ""
                 },
                 },
-                icon = Icons.Default.Add
+                icon = Icons.Default.Add,
+                contentDescription = stringResource(R.string.add)
             )
             )
         }
         }
     }
     }

+ 27 - 8
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt

@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
@@ -98,6 +99,7 @@ import io.legado.app.ui.widget.components.log.AppLogSheet
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 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.menuItem.RoundDropdownMenuItem
 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.reorderAccessibility
 import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
 import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
 import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
@@ -478,7 +480,7 @@ private fun BookshelfManageScreen(
                 TopBarActionButton(
                 TopBarActionButton(
                     onClick = { showGroupMenu = true },
                     onClick = { showGroupMenu = true },
                     imageVector = AppIcons.Filter,
                     imageVector = AppIcons.Filter,
-                    contentDescription = null
+                    contentDescription = stringResource(R.string.a11y_group_filter)
                 )
                 )
                 RoundDropdownMenu(
                 RoundDropdownMenu(
                     expanded = showGroupMenu,
                     expanded = showGroupMenu,
@@ -656,7 +658,7 @@ private fun BookshelfManageScreen(
             ),
             ),
             verticalArrangement = Arrangement.spacedBy(8.dp)
             verticalArrangement = Arrangement.spacedBy(8.dp)
         ) {
         ) {
-            items(filteredBooks, key = { it.bookUrl }) { book ->
+            itemsIndexed(filteredBooks, key = { _, item -> item.bookUrl }) { index, book ->
                 val cacheCount = remember(renderVersion, book.bookUrl) {
                 val cacheCount = remember(renderVersion, book.bookUrl) {
                     viewModel.getCacheCount(book.bookUrl) ?: 0
                     viewModel.getCacheCount(book.bookUrl) ?: 0
                 }
                 }
@@ -687,6 +689,15 @@ private fun BookshelfManageScreen(
                     GlassCard(
                     GlassCard(
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
+                            .reorderAccessibility(
+                                index = index,
+                                itemCount = filteredBooks.size,
+                                enabled = canReorderBooks,
+                            ) { from, to ->
+                                viewModel.dispatch(
+                                    BookshelfManageScreenIntent.MoveBookOrder(from, to)
+                                )
+                            }
                             .then(
                             .then(
                                 if (canReorderBooks) {
                                 if (canReorderBooks) {
                                     Modifier.longPressDraggableHandle()
                                     Modifier.longPressDraggableHandle()
@@ -801,12 +812,12 @@ private fun BookshelfManageScreen(
                                             }
                                             }
                                         },
                                         },
                                         icon = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
                                         icon = if (isDownloading) Icons.Default.Stop else Icons.Default.Download,
-                                        contentDescription = "download"
+                                        contentDescription = stringResource(R.string.action_download)
                                     )
                                     )
                                     SmallTonalButton(
                                     SmallTonalButton(
                                         onClick = { exportBook(book) },
                                         onClick = { exportBook(book) },
                                         icon = Icons.Default.Upload,
                                         icon = Icons.Default.Upload,
-                                        contentDescription = "upload"
+                                        contentDescription = stringResource(R.string.upload_to_remote)
                                     )
                                     )
                                     SmallTonalButton(
                                     SmallTonalButton(
                                         onClick = {
                                         onClick = {
@@ -815,12 +826,12 @@ private fun BookshelfManageScreen(
                                             showGroupSelectSheet = true
                                             showGroupSelectSheet = true
                                         },
                                         },
                                         icon = Icons.Default.Bookmarks,
                                         icon = Icons.Default.Bookmarks,
-                                        contentDescription = "group"
+                                        contentDescription = stringResource(R.string.group)
                                     )
                                     )
                                     SmallTonalButton(
                                     SmallTonalButton(
                                         onClick = { moreMenuBookUrl = book.bookUrl },
                                         onClick = { moreMenuBookUrl = book.bookUrl },
                                         icon = Icons.Default.MoreVert,
                                         icon = Icons.Default.MoreVert,
-                                        contentDescription = "more"
+                                        contentDescription = stringResource(R.string.more_menu)
                                     )
                                     )
                                 }
                                 }
                             }
                             }
@@ -1349,6 +1360,13 @@ private fun BookSourcePickerSheet(
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = source.bookSourceUrl,
                         key = source.bookSourceUrl,
+                        reorderIndex = selectedSources.indexOf(source),
+                        reorderItemCount = selectedSources.size,
+                        onMoveItem = { from, to ->
+                            selectedSources = selectedSources.toMutableList().apply {
+                                move(from, to)
+                            }
+                        },
                         title = source.bookSourceName,
                         title = source.bookSourceName,
                         subtitle = source.bookSourceGroup,
                         subtitle = source.bookSourceGroup,
                         isSelected = true,
                         isSelected = true,
@@ -1526,7 +1544,8 @@ private fun BatchChangePreviewRow(
             ) {
             ) {
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = { onManualSearch(item.oldBook) },
                     onClick = { onManualSearch(item.oldBook) },
-                    icon = Icons.Default.Search
+                    icon = Icons.Default.Search,
+                    contentDescription = stringResource(R.string.search)
                 )
                 )
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = { onSkip(item.oldBook.bookUrl) },
                     onClick = { onSkip(item.oldBook.bookUrl) },
@@ -1634,7 +1653,7 @@ private fun OtherSourceOptionsSheet(
                         SmallTonalButton(
                         SmallTonalButton(
                             onClick = { onOpenBook(candidate.book) },
                             onClick = { onOpenBook(candidate.book) },
                             icon = Icons.Default.Info,
                             icon = Icons.Default.Info,
-                            contentDescription = null,
+                            contentDescription = stringResource(R.string.details),
                         )
                         )
                     },
                     },
                     containerColor = LegadoTheme.colorScheme.onSheetContent,
                     containerColor = LegadoTheme.colorScheme.onSheetContent,

+ 8 - 0
app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt

@@ -445,6 +445,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
 
 
     private fun upInfoBar(page: Any?) {
     private fun upInfoBar(page: Any?) {
         if (page !is MangaPage) {
         if (page !is MangaPage) {
+            binding.webtoonFrame.updateAccessibilityState(getString(R.string.loading))
             return
             return
         }
         }
         val chapterIndex = page.chapterIndex
         val chapterIndex = page.chapterIndex
@@ -452,6 +453,13 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         val chapterPos = page.index
         val chapterPos = page.index
         val imageCount = page.imageCount
         val imageCount = page.imageCount
         val chapterName = page.mChapterName
         val chapterName = page.mChapterName
+        binding.webtoonFrame.updateAccessibilityState(
+            listOfNotNull(
+                chapterName.takeIf(String::isNotBlank),
+                getString(R.string.a11y_read_progress_page, chapterPos + 1, imageCount),
+                getString(R.string.a11y_read_progress_chapter, chapterIndex + 1, chapterSize),
+            ).joinToString(", ")
+        )
         mMangaFooterConfig.run {
         mMangaFooterConfig.run {
             mLabelBuilder.clear()
             mLabelBuilder.clear()
             binding.infobar.isGone = hideFooter
             binding.infobar.isGone = hideFooter

+ 43 - 0
app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonFrame.kt

@@ -7,8 +7,12 @@ import android.util.AttributeSet
 import android.view.GestureDetector
 import android.view.GestureDetector
 import android.view.MotionEvent
 import android.view.MotionEvent
 import android.view.ScaleGestureDetector
 import android.view.ScaleGestureDetector
+import android.view.View
 import android.widget.FrameLayout
 import android.widget.FrameLayout
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView
+import io.legado.app.R
 import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.utils.findCenterViewPosition
 import io.legado.app.utils.findCenterViewPosition
 
 
@@ -22,6 +26,22 @@ class WebtoonFrame : FrameLayout {
         defStyle
         defStyle
     )
     )
 
 
+    init {
+        contentDescription = context.getString(R.string.manga)
+        isFocusable = true
+        importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+
+        ViewCompat.replaceAccessibilityAction(
+            this,
+            AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+            context.getString(R.string.menu),
+        ) { _, _ -> performClick() }
+        addAccessibilityAction(R.string.prev_page) { it.prevPage() }
+        addAccessibilityAction(R.string.next_page) { it.nextPage() }
+        addAccessibilityAction(R.string.a11y_previous_chapter) { it.prevChapter() }
+        addAccessibilityAction(R.string.a11y_next_chapter) { it.nextChapter() }
+    }
+
     /** 长按回调,返回手指坐标 */
     /** 长按回调,返回手指坐标 */
     var longPressListener: ((centerPosition: Int) -> Unit)? = null
     var longPressListener: ((centerPosition: Int) -> Unit)? = null
 
 
@@ -125,6 +145,29 @@ class WebtoonFrame : FrameLayout {
         return super.dispatchTouchEvent(ev)
         return super.dispatchTouchEvent(ev)
     }
     }
 
 
+    override fun performClick(): Boolean {
+        val handler = actionHandler ?: return super.performClick()
+        super.performClick()
+        handler.showMenu()
+        return true
+    }
+
+    fun updateAccessibilityState(description: String) {
+        ViewCompat.setStateDescription(this, description)
+    }
+
+    private fun addAccessibilityAction(
+        labelRes: Int,
+        action: (ClickActionHandler) -> Unit,
+    ) {
+        ViewCompat.addAccessibilityAction(this, context.getString(labelRes)) { _, _ ->
+            actionHandler?.let {
+                action(it)
+                true
+            } ?: false
+        }
+    }
+
     private fun performClickAction(action: Int) {
     private fun performClickAction(action: Int) {
         when (action) {
         when (action) {
             -1 -> return
             -1 -> return

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

@@ -800,7 +800,6 @@ sealed interface ReadBookEffect {
     data class UpTextSelectAble(val enabled: Boolean) : ReadBookEffect
     data class UpTextSelectAble(val enabled: Boolean) : ReadBookEffect
 
 
     // TTS
     // TTS
-    data class UpTtsAloudSpan(val chapterStart: Int) : ReadBookEffect
 
 
     // Dialogs (Activity-driven)
     // Dialogs (Activity-driven)
     data object ShowConfirmSkipToChapter : ReadBookEffect
     data object ShowConfirmSkipToChapter : ReadBookEffect
@@ -914,6 +913,7 @@ sealed interface ConfigUpdateAction {
     data object UpdateBackgroundAlpha : ConfigUpdateAction
     data object UpdateBackgroundAlpha : ConfigUpdateAction
     data object UpdatePageSlopSquare : ConfigUpdateAction
     data object UpdatePageSlopSquare : ConfigUpdateAction
     data object ReloadContent : ConfigUpdateAction
     data object ReloadContent : ConfigUpdateAction
+    data object RelayoutContent : ConfigUpdateAction
     data object UpdateContent : ConfigUpdateAction
     data object UpdateContent : ConfigUpdateAction
     data object UpdateChapterStyle : ConfigUpdateAction
     data object UpdateChapterStyle : ConfigUpdateAction
     data object InvalidateTextPage : ConfigUpdateAction
     data object InvalidateTextPage : ConfigUpdateAction

+ 16 - 14
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -577,6 +577,9 @@ class ReadBookController(
 
 
             MotionEvent.ACTION_UP,
             MotionEvent.ACTION_UP,
             MotionEvent.ACTION_CANCEL -> {
             MotionEvent.ACTION_CANCEL -> {
+                if (action == MotionEvent.ACTION_UP) {
+                    v.performClick()
+                }
                 r.readView.curPage.resetReverseCursor()
                 r.readView.curPage.resetReverseCursor()
                 if (r.readView.isTextSelected) {
                 if (r.readView.isTextSelected) {
                     showTextActionMenu()
                     showTextActionMenu()
@@ -813,6 +816,7 @@ class ReadBookController(
                         ConfigUpdateAction.UpdateBackgroundAlpha -> r.readView.upBgAlpha()
                         ConfigUpdateAction.UpdateBackgroundAlpha -> r.readView.upBgAlpha()
                         ConfigUpdateAction.UpdatePageSlopSquare -> r.readView.upPageSlopSquare()
                         ConfigUpdateAction.UpdatePageSlopSquare -> r.readView.upPageSlopSquare()
                         ConfigUpdateAction.ReloadContent -> if (viewModel.isInitFinish) ReadBook.loadContent(resetPageOffset = false)
                         ConfigUpdateAction.ReloadContent -> if (viewModel.isInitFinish) ReadBook.loadContent(resetPageOffset = false)
+                        ConfigUpdateAction.RelayoutContent -> if (viewModel.isInitFinish) ReadBook.relayoutContent()
                         ConfigUpdateAction.UpdateContent -> r.readView.upContent(resetPageOffset = false)
                         ConfigUpdateAction.UpdateContent -> r.readView.upContent(resetPageOffset = false)
                         ConfigUpdateAction.UpdateChapterStyle -> ChapterProvider.upStyle()
                         ConfigUpdateAction.UpdateChapterStyle -> ChapterProvider.upStyle()
                         ConfigUpdateAction.InvalidateTextPage -> r.readView.invalidateTextPage()
                         ConfigUpdateAction.InvalidateTextPage -> r.readView.invalidateTextPage()
@@ -879,20 +883,6 @@ class ReadBookController(
                 }
                 }
             }
             }
 
 
-            is ReadBookEffect.UpTtsAloudSpan -> {
-                activity.lifecycleScope.launch(IO) {
-                    if (BaseReadAloudService.isPlay()) {
-                        ReadBook.curTextChapter?.let { textChapter ->
-                            val pageIndex = ReadBook.durPageIndex
-                            val aloudSpanStart =
-                                effect.chapterStart - textChapter.getReadLength(pageIndex)
-                            textChapter.getPage(pageIndex)?.upPageAloudSpan(aloudSpanStart)
-                            refs?.readView?.upContent()
-                        }
-                    }
-                }
-            }
-
             is ReadBookEffect.RefreshBookContent -> {
             is ReadBookEffect.RefreshBookContent -> {
                 ReadBook.curTextChapter = null
                 ReadBook.curTextChapter = null
                 refs?.readView?.upContent()
                 refs?.readView?.upContent()
@@ -911,6 +901,7 @@ class ReadBookController(
             }
             }
 
 
             is ReadBookEffect.ContentLoadFinish -> {
             is ReadBookEffect.ContentLoadFinish -> {
+                viewModel.readAloudProgress.value?.let(::updateReadAloudProgress)
                 onStartContentLoadFinish?.invoke()
                 onStartContentLoadFinish?.invoke()
             }
             }
 
 
@@ -1061,6 +1052,17 @@ class ReadBookController(
         }
         }
     }
     }
 
 
+    fun updateReadAloudProgress(chapterStart: Int) {
+        if (!BaseReadAloudService.isPlay()) return
+        val textChapter = ReadBook.curTextChapter ?: return
+        if (textChapter.chapter.index != BaseReadAloudService.currentChapterIndex) return
+        val pageIndex = textChapter.getPageIndexByCharIndex(chapterStart)
+        if (pageIndex < 0 || pageIndex != ReadBook.durPageIndex) return
+        val aloudSpanStart = chapterStart - textChapter.getReadLength(pageIndex)
+        textChapter.getPage(pageIndex)?.upPageAloudSpan(aloudSpanStart)
+        refs?.readView?.upContent(resetPageOffset = false)
+    }
+
     // ── Key handling ──
     // ── Key handling ──
 
 
     private fun toggleReadAloud() {
     private fun toggleReadAloud() {

+ 8 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -137,6 +137,14 @@ fun ReadBookRouteScreen(
         controller.onMenuVisibilityChanged(state.menuVisible)
         controller.onMenuVisibilityChanged(state.menuVisible)
     }
     }
 
 
+    LaunchedEffect(viewModel, controller, lifecycleOwner) {
+        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+            viewModel.readAloudProgress.collect { chapterStart ->
+                chapterStart?.let(controller::updateReadAloudProgress)
+            }
+        }
+    }
+
     // ── ActivityResult Launchers ──────────────────────────────────────
     // ── ActivityResult Launchers ──────────────────────────────────────
 
 
     val tocLauncher = rememberLauncherForActivityResult(TocActivityResult()) { result ->
     val tocLauncher = rememberLauncherForActivityResult(TocActivityResult()) { result ->

+ 6 - 4
app/src/main/java/io/legado/app/ui/book/read/ReadBookSearchBar.kt

@@ -14,7 +14,9 @@ import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 
 
@@ -46,13 +48,13 @@ fun ReadBookSearchBar(
                 onClick = {
                 onClick = {
                     onIntent(ReadBookIntent.NavigateSearchResultByOffset(-1))
                     onIntent(ReadBookIntent.NavigateSearchResultByOffset(-1))
                 },
                 },
-                tooltipText = "上一个结果",
+                tooltipText = stringResource(R.string.a11y_previous_result),
                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                 contentColor = LegadoTheme.colorScheme.onSurfaceVariant,
                 contentColor = LegadoTheme.colorScheme.onSurfaceVariant,
             ) {
             ) {
                 Icon(
                 Icon(
                     Icons.AutoMirrored.Filled.ArrowBack,
                     Icons.AutoMirrored.Filled.ArrowBack,
-                    contentDescription = "上一个结果",
+                    contentDescription = stringResource(R.string.a11y_previous_result),
                     modifier = Modifier.size(20.dp),
                     modifier = Modifier.size(20.dp),
                 )
                 )
             }
             }
@@ -71,13 +73,13 @@ fun ReadBookSearchBar(
                 onClick = {
                 onClick = {
                     onIntent(ReadBookIntent.NavigateSearchResultByOffset(1))
                     onIntent(ReadBookIntent.NavigateSearchResultByOffset(1))
                 },
                 },
-                tooltipText = "下一个结果",
+                tooltipText = stringResource(R.string.a11y_next_result),
                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                 contentColor = LegadoTheme.colorScheme.onSurfaceVariant,
                 contentColor = LegadoTheme.colorScheme.onSurfaceVariant,
             ) {
             ) {
                 Icon(
                 Icon(
                     Icons.AutoMirrored.Filled.ArrowForward,
                     Icons.AutoMirrored.Filled.ArrowForward,
-                    contentDescription = "下一个结果",
+                    contentDescription = stringResource(R.string.a11y_next_result),
                     modifier = Modifier.size(20.dp),
                     modifier = Modifier.size(20.dp),
                 )
                 )
             }
             }

+ 19 - 2
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -75,6 +75,7 @@ import io.legado.app.model.ImageProvider
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadBook
 import io.legado.app.model.SourceCallBack
 import io.legado.app.model.SourceCallBack
+import io.legado.app.model.activeReadAloudProgress
 import io.legado.app.model.analyzeRule.AnalyzeRule
 import io.legado.app.model.analyzeRule.AnalyzeRule
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
@@ -180,6 +181,14 @@ class ReadBookViewModel(
     private val _effects = MutableSharedFlow<ReadBookEffect>(extraBufferCapacity = 16)
     private val _effects = MutableSharedFlow<ReadBookEffect>(extraBufferCapacity = 16)
     val effects = _effects.asSharedFlow()
     val effects = _effects.asSharedFlow()
 
 
+    private val _readAloudProgress = MutableStateFlow(
+        activeReadAloudProgress(
+            isPlaying = BaseReadAloudService.isPlay(),
+            currentProgress = BaseReadAloudService.currentProgress,
+        )
+    )
+    val readAloudProgress = _readAloudProgress.asStateFlow()
+
     private suspend fun emitEffectWhenSubscribed(effect: ReadBookEffect) {
     private suspend fun emitEffectWhenSubscribed(effect: ReadBookEffect) {
         _effects.subscriptionCount.first { it > 0 }
         _effects.subscriptionCount.first { it > 0 }
         _effects.emit(effect)
         _effects.emit(effect)
@@ -869,7 +878,7 @@ class ReadBookViewModel(
                 }
                 }
             }
             }
 
 
-            is ReadBookIntent.TtsProgress -> _effects.tryEmit(ReadBookEffect.UpTtsAloudSpan(intent.chapterStart))
+            is ReadBookIntent.TtsProgress -> updateReadAloudProgress(intent.chapterStart)
             is ReadBookIntent.ReadAloudAction -> {
             is ReadBookIntent.ReadAloudAction -> {
                 openReadMenuRoute(ReadBookMenuRoute.ReadAloud)
                 openReadMenuRoute(ReadBookMenuRoute.ReadAloud)
             }
             }
@@ -1827,6 +1836,7 @@ class ReadBookViewModel(
                         9 -> ConfigUpdateAction.InvalidateTextPage
                         9 -> ConfigUpdateAction.InvalidateTextPage
                         10 -> ConfigUpdateAction.UpdateLayout
                         10 -> ConfigUpdateAction.UpdateLayout
                         11 -> ConfigUpdateAction.SubmitRenderTask
                         11 -> ConfigUpdateAction.SubmitRenderTask
+                        12 -> ConfigUpdateAction.RelayoutContent
                         else -> null
                         else -> null
                     }
                     }
                 }.toSet()
                 }.toSet()
@@ -1844,6 +1854,7 @@ class ReadBookViewModel(
                     )
                     )
                 }
                 }
                 if (state == Status.STOP || state == Status.PAUSE) {
                 if (state == Status.STOP || state == Status.PAUSE) {
+                    _readAloudProgress.value = null
                     _effects.tryEmit(ReadBookEffect.UpAloudState)
                     _effects.tryEmit(ReadBookEffect.UpAloudState)
                 }
                 }
                 if (state == Status.PAUSE) {
                 if (state == Status.PAUSE) {
@@ -1886,11 +1897,17 @@ class ReadBookViewModel(
         }
         }
         viewModelScope.launch {
         viewModelScope.launch {
             eventFlowSticky<Int>(EventBus.TTS_PROGRESS).collect { chapterStart ->
             eventFlowSticky<Int>(EventBus.TTS_PROGRESS).collect { chapterStart ->
-                _effects.tryEmit(ReadBookEffect.UpTtsAloudSpan(chapterStart))
+                updateReadAloudProgress(chapterStart)
             }
             }
         }
         }
     }
     }
 
 
+    private fun updateReadAloudProgress(chapterStart: Int) {
+        if (BaseReadAloudService.isPlay() && chapterStart > 0) {
+            _readAloudProgress.value = chapterStart
+        }
+    }
+
     private fun collectReadPreferences() {
     private fun collectReadPreferences() {
         viewModelScope.launch {
         viewModelScope.launch {
             var previous: ReadPreferences? = null
             var previous: ReadPreferences? = null

+ 0 - 7
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -455,13 +455,6 @@ class PageView(
         binding.contentTextView.invalidate()
         binding.contentTextView.invalidate()
     }
     }
 
 
-    /**
-     * 设置无障碍文本
-     */
-    fun setContentDescription(content: String) {
-        binding.contentTextView.contentDescription = content
-    }
-
     /**
     /**
      * 重置滚动位置
      * 重置滚动位置
      */
      */

+ 61 - 3
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -1,14 +1,16 @@
 package io.legado.app.ui.book.read.page
 package io.legado.app.ui.book.read.page
 
 
-import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Canvas
 import android.graphics.RectF
 import android.graphics.RectF
 import android.os.Build
 import android.os.Build
+import android.os.Bundle
 import android.util.AttributeSet
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.view.MotionEvent
+import android.view.View
 import android.view.ViewConfiguration
 import android.view.ViewConfiguration
 import android.view.WindowInsets
 import android.view.WindowInsets
+import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
 import android.widget.FrameLayout
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.PageAnim
 import io.legado.app.constant.PageAnim
@@ -123,6 +125,10 @@ class ReadView(
         addView(nextPage)
         addView(nextPage)
         addView(curPage)
         addView(curPage)
         addView(prevPage)
         addView(prevPage)
+        descendantFocusability = FOCUS_BLOCK_DESCENDANTS
+        importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+        isClickable = true
+        isFocusable = true
         prevPage.invisible()
         prevPage.invisible()
         nextPage.invisible()
         nextPage.invisible()
         curPage.markAsMainView()
         curPage.markAsMainView()
@@ -175,7 +181,6 @@ class ReadView(
     /**
     /**
      * 触摸事件
      * 触摸事件
      */
      */
-    @SuppressLint("ClickableViewAccessibility")
     override fun onTouchEvent(event: MotionEvent): Boolean {
     override fun onTouchEvent(event: MotionEvent): Boolean {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             val insets = this.rootWindowInsets.getInsetsIgnoringVisibility(
             val insets = this.rootWindowInsets.getInsetsIgnoringVisibility(
@@ -240,6 +245,7 @@ class ReadView(
                 pressDown = false
                 pressDown = false
                 if (pageDelegate?.isMoved == false && !isMove) {
                 if (pageDelegate?.isMoved == false && !isMove) {
                     if (!longPressed && !pressOnTextSelected) {
                     if (!longPressed && !pressOnTextSelected) {
+                        performClick()
                         val handled = curPage.onClick(startX, startY)
                         val handled = curPage.onClick(startX, startY)
                         if (!handled) {
                         if (!handled) {
                             onSingleTapUp()
                             onSingleTapUp()
@@ -271,6 +277,58 @@ class ReadView(
         return true
         return true
     }
     }
 
 
+    override fun performClick(): Boolean {
+        super.performClick()
+        return true
+    }
+
+    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
+        super.onInitializeAccessibilityNodeInfo(info)
+        info.className = ReadView::class.java.name
+        info.isScrollable = true
+        info.addAction(
+            AccessibilityNodeInfo.AccessibilityAction(
+                AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD,
+                context.getString(R.string.prev_page)
+            )
+        )
+        info.addAction(
+            AccessibilityNodeInfo.AccessibilityAction(
+                AccessibilityNodeInfo.ACTION_SCROLL_FORWARD,
+                context.getString(R.string.next_page)
+            )
+        )
+        info.addAction(
+            AccessibilityNodeInfo.AccessibilityAction(
+                AccessibilityNodeInfo.ACTION_CLICK,
+                context.getString(R.string.menu)
+            )
+        )
+    }
+
+    override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
+        return when (action) {
+            AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> {
+                pageDelegate?.prevPageByAnim(defaultAnimationSpeed)
+                true
+            }
+
+            AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> {
+                pageDelegate?.nextPageByAnim(defaultAnimationSpeed)
+                true
+            }
+
+            AccessibilityNodeInfo.ACTION_CLICK -> {
+                performClick()
+                pageDelegate?.dismissSnackBar()
+                callBack.showActionMenu()
+                true
+            }
+
+            else -> super.performAccessibilityAction(action, arguments)
+        }
+    }
+
     fun cancelSelect(clearSearchResult: Boolean = false) {
     fun cancelSelect(clearSearchResult: Boolean = false) {
         if (isTextSelected || clearSearchResult) {
         if (isTextSelected || clearSearchResult) {
             curPage.cancelSelect(clearSearchResult)
             curPage.cancelSelect(clearSearchResult)
@@ -573,7 +631,7 @@ class ReadView(
      */
      */
     override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
     override fun upContent(relativePosition: Int, resetPageOffset: Boolean) {
         post {
         post {
-            curPage.setContentDescription(pageFactory.curPage.text)
+            contentDescription = pageFactory.curPage.text
         }
         }
         if (isScroll && !isAutoPage) {
         if (isScroll && !isAutoPage) {
             if (relativePosition == 0) {
             if (relativePosition == 0) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -1082,7 +1082,7 @@ object ChapterProvider {
         viewWidth = width
         viewWidth = width
         viewHeight = height
         viewHeight = height
         upLayout()
         upLayout()
-        postEvent(EventBus.UP_CONFIG, arrayListOf(5))
+        postEvent(EventBus.UP_CONFIG, arrayListOf(12))
     }
     }
 
 
     /**
     /**

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

@@ -74,6 +74,7 @@ fun AiTextRewriteSheet(
                     Icons.Default.AutoAwesome
                     Icons.Default.AutoAwesome
                 else
                 else
                     Icons.Default.Autorenew,
                     Icons.Default.Autorenew,
+                contentDescription = stringResource(R.string.ai_generate),
                 enabled = state.presets.isNotEmpty() &&
                 enabled = state.presets.isNotEmpty() &&
                         !state.isLoading &&
                         !state.isLoading &&
                         !state.isApplying
                         !state.isApplying
@@ -83,6 +84,7 @@ fun AiTextRewriteSheet(
             MediumTonalButton(
             MediumTonalButton(
                 onClick = { onIntent(ReadBookIntent.ConfirmAiTextRewrite) },
                 onClick = { onIntent(ReadBookIntent.ConfirmAiTextRewrite) },
                 icon = Icons.Default.Save,
                 icon = Icons.Default.Save,
+                contentDescription = stringResource(R.string.save),
                 enabled = !state.isLoading &&
                 enabled = !state.isLoading &&
                         !state.isApplying &&
                         !state.isApplying &&
                         state.errorMessage == null &&
                         state.errorMessage == null &&

+ 9 - 4
app/src/main/java/io/legado/app/ui/book/read/sheet/ChangeChapterSourceSheet.kt

@@ -70,7 +70,8 @@ fun ChangeChapterSourceSheet(
                 Box {
                 Box {
                     MediumPlainButton(
                     MediumPlainButton(
                         onClick = { showOptionsMenu = true },
                         onClick = { showOptionsMenu = true },
-                        icon = Icons.Default.MoreVert
+                icon = Icons.Default.MoreVert,
+                contentDescription = stringResource(R.string.more_menu)
                     )
                     )
                     RoundDropdownMenu(
                     RoundDropdownMenu(
                         expanded = showOptionsMenu,
                         expanded = showOptionsMenu,
@@ -117,11 +118,15 @@ fun ChangeChapterSourceSheet(
                 Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                 Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                     MediumPlainButton(
                     MediumPlainButton(
                         onClick = { onIntent(ChangeChapterSourceIntent.StartStopSearch) },
                         onClick = { onIntent(ChangeChapterSourceIntent.StartStopSearch) },
-                        icon = if (state.isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
+                icon = if (state.isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
+                contentDescription = stringResource(
+                    if (state.isSearching) R.string.pause else R.string.refresh
+                ),
                     )
                     )
                     MediumPlainButton(
                     MediumPlainButton(
                         onClick = { showFilterSheet = true },
                         onClick = { showFilterSheet = true },
-                        icon = Icons.Default.FilterList
+                icon = Icons.Default.FilterList,
+                contentDescription = stringResource(R.string.screen)
                     )
                     )
                 }
                 }
             }
             }
@@ -261,7 +266,7 @@ private fun SearchContent(
                             onClick = { onBookScoreClick(item) },
                             onClick = { onBookScoreClick(item) },
                             icon = Icons.Default.PushPin,
                             icon = Icons.Default.PushPin,
                             tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
                             tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
-                            contentDescription = null
+                            contentDescription = stringResource(R.string.a11y_pin_source)
                         )
                         )
                     },
                     },
                     supportingContent = {
                     supportingContent = {

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

@@ -166,7 +166,8 @@ fun EffectiveReplacesSheet(
                                         }
                                         }
                                     }
                                     }
                                 },
                                 },
-                                icon = Icons.Default.Close
+                icon = Icons.Default.Close,
+                contentDescription = stringResource(R.string.close)
                             )
                             )
                         }
                         }
                     }
                     }

+ 18 - 6
app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt

@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
@@ -42,6 +42,7 @@ import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 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.menuItem.RoundDropdownMenuItem
 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.reorderAccessibility
 import io.legado.app.ui.widget.components.settingItem.TinySettingItem
 import io.legado.app.ui.widget.components.settingItem.TinySettingItem
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
 import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -121,7 +122,8 @@ fun HighlightRuleConfigSheet(
         startAction = {
         startAction = {
             SmallTonalButton(
             SmallTonalButton(
                 onClick = { onIntent(ReadBookIntent.AddHighlightRule) },
                 onClick = { onIntent(ReadBookIntent.AddHighlightRule) },
-                icon = Icons.Default.Add
+                icon = Icons.Default.Add,
+                contentDescription = stringResource(R.string.add)
             )
             )
         },
         },
         endAction = {
         endAction = {
@@ -129,7 +131,8 @@ fun HighlightRuleConfigSheet(
             Box {
             Box {
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = { expanded = true },
                     onClick = { expanded = true },
-                    icon = Icons.Default.MoreVert
+                    icon = Icons.Default.MoreVert,
+                    contentDescription = stringResource(R.string.more_menu)
                 )
                 )
                 RoundDropdownMenu(
                 RoundDropdownMenu(
                     expanded = expanded,
                     expanded = expanded,
@@ -170,7 +173,7 @@ fun HighlightRuleConfigSheet(
                 verticalArrangement = Arrangement.spacedBy(8.dp),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
                 modifier = Modifier.weight(1f, fill = false),
                 modifier = Modifier.weight(1f, fill = false),
             ) {
             ) {
-                items(state.rules, key = { it.id }) { rule ->
+                itemsIndexed(state.rules, key = { _, rule -> rule.id }) { index, rule ->
                     ReorderableItem(reorderableState, key = rule.id) { isDragging ->
                     ReorderableItem(reorderableState, key = rule.id) { isDragging ->
                         HighlightRuleItem(
                         HighlightRuleItem(
                             rule = rule,
                             rule = rule,
@@ -184,6 +187,13 @@ fun HighlightRuleConfigSheet(
                                 onIntent(ReadBookIntent.RequestDeleteHighlightRule(rule))
                                 onIntent(ReadBookIntent.RequestDeleteHighlightRule(rule))
                             },
                             },
                             modifier = Modifier
                             modifier = Modifier
+                                .reorderAccessibility(
+                                    index = index,
+                                    itemCount = state.rules.size,
+                                ) { from, to ->
+                                    onIntent(ReadBookIntent.MoveHighlightRule(from, to))
+                                    onIntent(ReadBookIntent.SaveHighlightRuleOrder)
+                                }
                                 .longPressDraggableHandle()
                                 .longPressDraggableHandle()
                                 .zIndex(if (isDragging) 1f else 0f)
                                 .zIndex(if (isDragging) 1f else 0f)
                                 .animateItem(),
                                 .animateItem(),
@@ -251,11 +261,13 @@ private fun HighlightRuleItem(
                 )
                 )
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = onEditClick,
                     onClick = onEditClick,
-                    icon = Icons.Default.Edit
+                    icon = Icons.Default.Edit,
+                    contentDescription = stringResource(R.string.edit)
                 )
                 )
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = onDeleteClick,
                     onClick = onDeleteClick,
-                    icon = Icons.Default.Delete
+                    icon = Icons.Default.Delete,
+                    contentDescription = stringResource(R.string.delete)
                 )
                 )
             }
             }
         },
         },

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

@@ -231,7 +231,7 @@ fun HighlightRuleEditSheet(
             }) {
             }) {
                 androidx.compose.material3.Icon(
                 androidx.compose.material3.Icon(
                     Icons.Default.Done,
                     Icons.Default.Done,
-                    contentDescription = null,
+                    contentDescription = stringResource(R.string.save),
                 )
                 )
             }
             }
         },
         },

+ 11 - 5
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt

@@ -225,7 +225,8 @@ fun SpeakEngineConfigSheet(
         startAction = {
         startAction = {
             SmallTonalButton(
             SmallTonalButton(
                 onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
                 onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
-                icon = Icons.Default.Add
+                        icon = Icons.Default.Add,
+                        contentDescription = stringResource(R.string.add)
             )
             )
         },
         },
         endAction = {
         endAction = {
@@ -233,7 +234,8 @@ fun SpeakEngineConfigSheet(
             Box {
             Box {
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = { expanded = true },
                     onClick = { expanded = true },
-                    icon = Icons.Default.MoreVert
+                        icon = Icons.Default.MoreVert,
+                        contentDescription = stringResource(R.string.more_menu)
                 )
                 )
                 RoundDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
                 RoundDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
                     RoundDropdownMenuItem(
                     RoundDropdownMenuItem(
@@ -293,11 +295,13 @@ fun SpeakEngineConfigSheet(
                             Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
                             Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
                                 SmallTonalButton(
                                 SmallTonalButton(
                                     onClick = { onIntent(ReadBookIntent.EditHttpTts(httpTtsId)) },
                                     onClick = { onIntent(ReadBookIntent.EditHttpTts(httpTtsId)) },
-                                    icon = Icons.Default.Edit,
+                        icon = Icons.Default.Edit,
+                        contentDescription = stringResource(R.string.edit),
                                 )
                                 )
                                 SmallTonalButton(
                                 SmallTonalButton(
                                     onClick = { onIntent(ReadBookIntent.DeleteHttpTts(httpTtsId)) },
                                     onClick = { onIntent(ReadBookIntent.DeleteHttpTts(httpTtsId)) },
-                                    icon = Icons.Default.Delete,
+                        icon = Icons.Default.Delete,
+                        contentDescription = stringResource(R.string.delete),
                                 )
                                 )
                             }
                             }
                         }
                         }
@@ -338,7 +342,8 @@ fun HttpTtsEditSheet(
             Box {
             Box {
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = { expanded = true },
                     onClick = { expanded = true },
-                    icon = Icons.Default.MoreVert
+                        icon = Icons.Default.MoreVert,
+                        contentDescription = stringResource(R.string.more_menu)
                 )
                 )
                 RoundDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
                 RoundDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
                     RoundDropdownMenuItem(
                     RoundDropdownMenuItem(
@@ -381,6 +386,7 @@ fun HttpTtsEditSheet(
         endAction = {
         endAction = {
             SmallTonalButton(
             SmallTonalButton(
                 icon = Icons.Default.Save,
                 icon = Icons.Default.Save,
+                contentDescription = stringResource(R.string.save),
                 onClick = {
                 onClick = {
                     onIntent(
                     onIntent(
                         ReadBookIntent.SaveHttpTts(
                         ReadBookIntent.SaveHttpTts(

+ 46 - 12
app/src/main/java/io/legado/app/ui/book/read/sheet/TextSelectMenuConfigSheet.kt

@@ -17,7 +17,7 @@ import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyItemScope
 import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
@@ -49,6 +49,7 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 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.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.reorderAccessibility
 import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinySettingItem
 import io.legado.app.ui.widget.components.settingItem.TinySettingItem
 import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
@@ -83,6 +84,16 @@ fun TextSelectMenuConfigSheet(
     val group2Items = remember(draftItems) { draftItems.filter { it.showState == 1 } }
     val group2Items = remember(draftItems) { draftItems.filter { it.showState == 1 } }
     val group3Items = remember(draftItems) { draftItems.filter { it.showState == 2 } }
     val group3Items = remember(draftItems) { draftItems.filter { it.showState == 2 } }
 
 
+    fun moveWithinGroup(groupItems: List<ActionMenuItem>, from: Int, to: Int) {
+        val fromIndex = draftItems.indexOfFirst { it.uniqueId == groupItems[from].uniqueId }
+        val toIndex = draftItems.indexOfFirst { it.uniqueId == groupItems[to].uniqueId }
+        if (fromIndex >= 0 && toIndex >= 0) {
+            draftItems = draftItems.toMutableList().apply {
+                add(toIndex, removeAt(fromIndex))
+            }
+        }
+    }
+
     val lazyListState = rememberLazyListState()
     val lazyListState = rememberLazyListState()
     val coroutineScope = rememberCoroutineScope()
     val coroutineScope = rememberCoroutineScope()
 
 
@@ -119,7 +130,8 @@ fun TextSelectMenuConfigSheet(
                     onSaved(draftItems)
                     onSaved(draftItems)
                     onDismissRequest()
                     onDismissRequest()
                 },
                 },
-                icon = Icons.Default.Save
+                icon = Icons.Default.Save,
+                contentDescription = stringResource(R.string.save)
             )
             )
         }
         }
     ) {
     ) {
@@ -164,7 +176,7 @@ fun TextSelectMenuConfigSheet(
                 }
                 }
 
 
                 if (group1Expanded) {
                 if (group1Expanded) {
-                    items(group1Items, key = { it.uniqueId }) { item ->
+                    itemsIndexed(group1Items, key = { _, item -> item.uniqueId }) { index, item ->
                         ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
                         ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
                             val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                             val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                             NormalCard(
                             NormalCard(
@@ -186,7 +198,18 @@ fun TextSelectMenuConfigSheet(
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 2) else it
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 2) else it
                                         }
                                         }
                                     },
                                     },
-                                    dragHandleModifier = Modifier.draggableHandle()
+                                    dragHandleModifier = Modifier
+                                        .reorderAccessibility(
+                                            index = index,
+                                            itemCount = group1Items.size,
+                                            description = stringResource(
+                                                R.string.a11y_reorder_named,
+                                                item.title,
+                                            ),
+                                        ) { from, to ->
+                                            moveWithinGroup(group1Items, from, to)
+                                        }
+                                        .draggableHandle()
                                 )
                                 )
                             }
                             }
                         }
                         }
@@ -205,7 +228,7 @@ fun TextSelectMenuConfigSheet(
                 }
                 }
 
 
                 if (group2Expanded) {
                 if (group2Expanded) {
-                    items(group2Items, key = { it.uniqueId }) { item ->
+                    itemsIndexed(group2Items, key = { _, item -> item.uniqueId }) { index, item ->
                         ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
                         ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
                             val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                             val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                             NormalCard(
                             NormalCard(
@@ -227,7 +250,18 @@ fun TextSelectMenuConfigSheet(
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 2) else it
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 2) else it
                                         }
                                         }
                                     },
                                     },
-                                    dragHandleModifier = Modifier.draggableHandle()
+                                    dragHandleModifier = Modifier
+                                        .reorderAccessibility(
+                                            index = index,
+                                            itemCount = group2Items.size,
+                                            description = stringResource(
+                                                R.string.a11y_reorder_named,
+                                                item.title,
+                                            ),
+                                        ) { from, to ->
+                                            moveWithinGroup(group2Items, from, to)
+                                        }
+                                        .draggableHandle()
                                 )
                                 )
                             }
                             }
                         }
                         }
@@ -328,7 +362,7 @@ private fun PrimaryMenuItemRow(
         ) {
         ) {
             Icon(
             Icon(
                 imageVector = Icons.Default.Remove,
                 imageVector = Icons.Default.Remove,
-                contentDescription = null,
+                contentDescription = stringResource(R.string.collapsed_menu),
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.size(20.dp),
                 modifier = Modifier.size(20.dp),
             )
             )
@@ -340,14 +374,14 @@ private fun PrimaryMenuItemRow(
         ) {
         ) {
             Icon(
             Icon(
                 imageVector = Icons.Default.VisibilityOff,
                 imageVector = Icons.Default.VisibilityOff,
-                contentDescription = null,
+                contentDescription = stringResource(R.string.hidden_items),
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.size(20.dp),
                 modifier = Modifier.size(20.dp),
             )
             )
         }
         }
 
 
         Box(
         Box(
-            modifier = dragHandleModifier.size(36.dp),
+            modifier = dragHandleModifier.size(48.dp),
             contentAlignment = Alignment.Center
             contentAlignment = Alignment.Center
         ) {
         ) {
             Icon(
             Icon(
@@ -403,7 +437,7 @@ private fun FoldedMenuItemRow(
         ) {
         ) {
             Icon(
             Icon(
                 imageVector = Icons.Default.Add,
                 imageVector = Icons.Default.Add,
-                contentDescription = null,
+                contentDescription = stringResource(R.string.primary_menu),
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.size(20.dp),
                 modifier = Modifier.size(20.dp),
             )
             )
@@ -415,14 +449,14 @@ private fun FoldedMenuItemRow(
         ) {
         ) {
             Icon(
             Icon(
                 imageVector = Icons.Default.VisibilityOff,
                 imageVector = Icons.Default.VisibilityOff,
-                contentDescription = null,
+                contentDescription = stringResource(R.string.hidden_items),
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.size(20.dp),
                 modifier = Modifier.size(20.dp),
             )
             )
         }
         }
 
 
         Box(
         Box(
-            modifier = dragHandleModifier.size(36.dp),
+            modifier = dragHandleModifier.size(48.dp),
             contentAlignment = Alignment.Center
             contentAlignment = Alignment.Center
         ) {
         ) {
             Icon(
             Icon(

+ 24 - 7
app/src/main/java/io/legado/app/ui/book/read/sheet/TitleBarIconSheet.kt

@@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
@@ -45,6 +45,7 @@ import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 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.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.reorderAccessibility
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
 import sh.calvin.reorderable.rememberReorderableLazyListState
 
 
@@ -130,7 +131,7 @@ private fun ButtonIconConfigSheet(
                 verticalArrangement = Arrangement.spacedBy(4.dp),
                 verticalArrangement = Arrangement.spacedBy(4.dp),
                 modifier = Modifier.weight(1f, fill = false),
                 modifier = Modifier.weight(1f, fill = false),
             ) {
             ) {
-                items(draftItems, key = { it.id }) { item ->
+                itemsIndexed(draftItems, key = { _, item -> item.id }) { index, item ->
                     ReorderableItem(reorderableState, key = item.id) { isDragging ->
                     ReorderableItem(reorderableState, key = item.id) { isDragging ->
                         val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                         val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                         NormalCard(
                         NormalCard(
@@ -146,7 +147,20 @@ private fun ButtonIconConfigSheet(
                                 },
                                 },
                                 onSelectIcon = { onSelectIcon(item.id) },
                                 onSelectIcon = { onSelectIcon(item.id) },
                                 onClearIcon = { onClearIcon(item.id) },
                                 onClearIcon = { onClearIcon(item.id) },
-                                dragHandleModifier = Modifier.draggableHandle(),
+                                dragHandleModifier = Modifier
+                                    .reorderAccessibility(
+                                        index = index,
+                                        itemCount = draftItems.size,
+                                        description = stringResource(
+                                            R.string.a11y_reorder_named,
+                                            item.label,
+                                        ),
+                                    ) { from, to ->
+                                        draftItems = draftItems.toMutableList().apply {
+                                            add(to, removeAt(from))
+                                        }
+                                    }
+                                    .draggableHandle(),
                             )
                             )
                         }
                         }
                     }
                     }
@@ -236,6 +250,7 @@ private fun TitleBarIconItem(
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = onSelectIcon,
                     onClick = onSelectIcon,
                     icon = Icons.Default.Add,
                     icon = Icons.Default.Add,
+                    contentDescription = stringResource(R.string.add),
                     modifier = Modifier.size(36.dp),
                     modifier = Modifier.size(36.dp),
                 )
                 )
             }
             }
@@ -252,7 +267,9 @@ private fun TitleBarIconItem(
                 } else {
                 } else {
                     Icons.Default.VisibilityOff
                     Icons.Default.VisibilityOff
                 },
                 },
-                contentDescription = null,
+                contentDescription = stringResource(
+                    if (item.enabled) R.string.disable_selection else R.string.enable_selection
+                ),
                 tint = if (item.enabled) {
                 tint = if (item.enabled) {
                     LegadoTheme.colorScheme.onSurface
                     LegadoTheme.colorScheme.onSurface
                 } else {
                 } else {
@@ -263,9 +280,9 @@ private fun TitleBarIconItem(
         }
         }
 
 
         // Drag handle
         // Drag handle
-        IconButton(
-            modifier = dragHandleModifier.size(36.dp),
-            onClick = {},
+        Box(
+            modifier = dragHandleModifier.size(48.dp),
+            contentAlignment = Alignment.Center,
         ) {
         ) {
             Icon(
             Icon(
                 Icons.Default.Menu,
                 Icons.Default.Menu,

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewScreen.kt

@@ -263,7 +263,7 @@ fun HeatmapCard(state: ReadRecordOverviewUiState) {
                 dailyReadTimes = state.allReadTimes,
                 dailyReadTimes = state.allReadTimes,
                 currentMode = HeatmapMode.TIME,
                 currentMode = HeatmapMode.TIME,
                 selectedDate = null,
                 selectedDate = null,
-                onDateSelected = {}
+                onDateSelected = null
             )
             )
         }
         }
     }
     }

+ 6 - 3
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt

@@ -729,7 +729,7 @@ fun HeatmapCalendarSection(
     dailyReadTimes: Map<LocalDate, Long>,
     dailyReadTimes: Map<LocalDate, Long>,
     currentMode: HeatmapMode,
     currentMode: HeatmapMode,
     selectedDate: LocalDate?,
     selectedDate: LocalDate?,
-    onDateSelected: (LocalDate) -> Unit,
+    onDateSelected: ((LocalDate) -> Unit)?,
     config: HeatmapConfig = HeatmapConfig()
     config: HeatmapConfig = HeatmapConfig()
 ) {
 ) {
     val (startDate, endDate) = rememberDateRange(dailyReadCounts, dailyReadTimes)
     val (startDate, endDate) = rememberDateRange(dailyReadCounts, dailyReadTimes)
@@ -752,7 +752,7 @@ fun HeatmapCalendarSection(
     ) {
     ) {
         Row(modifier = Modifier.fillMaxWidth()) {
         Row(modifier = Modifier.fillMaxWidth()) {
             WeekdayLabelsColumn(
             WeekdayLabelsColumn(
-                cellSize = config.cellSize,
+                cellSize = config.interactiveCellSize,
                 cellSpacing = config.cellSpacing
                 cellSpacing = config.cellSpacing
             )
             )
 
 
@@ -770,7 +770,10 @@ fun HeatmapCalendarSection(
 
 
                 if (firstReadDate != null) {
                 if (firstReadDate != null) {
                     item {
                     item {
-                        NoEarlierDataIndicator(cellSize = config.cellSize)
+                        NoEarlierDataIndicator(
+                            cellSize = config.cellSize,
+                            touchTargetSize = config.interactiveCellSize,
+                        )
                     }
                     }
                 }
                 }
 
 

+ 7 - 2
app/src/main/java/io/legado/app/ui/book/readRecord/component/ReadRecordHeatmap.kt

@@ -47,7 +47,7 @@ fun HeatmapCalendarSection(
 
 
         Row(modifier = Modifier.fillMaxWidth()) {
         Row(modifier = Modifier.fillMaxWidth()) {
             WeekdayLabelsColumn(
             WeekdayLabelsColumn(
-                cellSize = config.cellSize,
+                cellSize = config.interactiveCellSize,
                 cellSpacing = config.cellSpacing
                 cellSpacing = config.cellSpacing
             )
             )
             Spacer(modifier = Modifier.width(4.dp))
             Spacer(modifier = Modifier.width(4.dp))
@@ -78,7 +78,12 @@ fun HeatmapCalendarSection(
                         onModeChanged = {}
                         onModeChanged = {}
                     )
                     )
                 }
                 }
-                item { NoEarlierDataIndicator(cellSize = config.cellSize) }
+                item {
+                    NoEarlierDataIndicator(
+                        cellSize = config.cellSize,
+                        touchTargetSize = config.interactiveCellSize,
+                    )
+                }
             }
             }
         }
         }
     }
     }

+ 8 - 3
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt

@@ -47,9 +47,11 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.data.entities.SearchContentHistory
 import io.legado.app.data.entities.SearchContentHistory
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
@@ -201,9 +203,12 @@ fun SearchContentScreen(
                     label = "FabIconTransition"
                     label = "FabIconTransition"
                 ) { searching ->
                 ) { searching ->
                     if (searching) {
                     if (searching) {
-                        AppIcon(Icons.Default.Stop, contentDescription = "停止搜索")
+                        AppIcon(Icons.Default.Stop, contentDescription = stringResource(R.string.stop))
                     } else {
                     } else {
-                        AppIcon(Icons.Default.MyLocation, contentDescription = "定位当前章节")
+                        AppIcon(
+                            Icons.Default.MyLocation,
+                            contentDescription = stringResource(R.string.a11y_locate_current_chapter)
+                        )
                     }
                     }
                 }
                 }
             }
             }
@@ -337,7 +342,7 @@ fun SearchHistoryList(
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = { onDeleteHistory(item) },
                                 onClick = { onDeleteHistory(item) },
                                 icon = Icons.Default.Close,
                                 icon = Icons.Default.Close,
-                                contentDescription = "删除"
+                                contentDescription = stringResource(R.string.delete)
                             )
                             )
                         },
                         },
                         colors = ListItemDefaults.colors(
                         colors = ListItemDefaults.colors(

+ 2 - 6
app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt

@@ -77,7 +77,7 @@ import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.selected
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -762,7 +762,6 @@ fun ChapterItem(
             else -> LegadoTheme.colorScheme.onSurfaceVariant
             else -> LegadoTheme.colorScheme.onSurfaceVariant
         }, label = "BgColor"
         }, label = "BgColor"
     )
     )
-    val selectedDescription = stringResource(R.string.a11y_selected)
     val currentReadingDescription = stringResource(R.string.a11y_current_reading)
     val currentReadingDescription = stringResource(R.string.a11y_current_reading)
     val lockedDescription = stringResource(R.string.a11y_vip_locked)
     val lockedDescription = stringResource(R.string.a11y_vip_locked)
     val downloadedDescription = stringResource(R.string.a11y_downloaded)
     val downloadedDescription = stringResource(R.string.a11y_downloaded)
@@ -783,7 +782,6 @@ fun ChapterItem(
         add(item.title)
         add(item.title)
         item.tag?.takeIf { it.isNotBlank() }?.let(::add)
         item.tag?.takeIf { it.isNotBlank() }?.let(::add)
         if (item.isDur) add(currentReadingDescription)
         if (item.isDur) add(currentReadingDescription)
-        if (item.isSelected) add(selectedDescription)
         if (item.isVip && !item.isPay) add(lockedDescription)
         if (item.isVip && !item.isPay) add(lockedDescription)
         if (showWordCount) wordCountDescription?.let(::add)
         if (showWordCount) wordCountDescription?.let(::add)
         downloadStateDescription?.let(::add)
         downloadStateDescription?.let(::add)
@@ -805,9 +803,7 @@ fun ChapterItem(
             .semantics {
             .semantics {
                 role = Role.Button
                 role = Role.Button
                 contentDescription = chapterContentDescription
                 contentDescription = chapterContentDescription
-                if (item.isSelected) {
-                    stateDescription = selectedDescription
-                }
+                selected = item.isSelected
             }
             }
             .combinedClickable(
             .combinedClickable(
                 onClick = onClick,
                 onClick = onClick,

+ 3 - 9
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt

@@ -390,18 +390,10 @@ fun TxtRuleScreen(
                     val enabledState = stringResource(
                     val enabledState = stringResource(
                         if (item.isEnabled) R.string.enabled else R.string.disabled
                         if (item.isEnabled) R.string.enabled else R.string.disabled
                     )
                     )
-                    val selectedState = stringResource(
-                        if (isItemHighLighted) {
-                            R.string.a11y_selected
-                        } else {
-                            R.string.a11y_not_selected
-                        }
-                    )
                     val itemDescription = listOfNotNull(
                     val itemDescription = listOfNotNull(
                         item.name,
                         item.name,
                         item.example.takeIf { it.isNotBlank() },
                         item.example.takeIf { it.isNotBlank() },
                         enabledState,
                         enabledState,
-                        selectedState,
                         if (!isPickMode && !inSelectionMode) {
                         if (!isPickMode && !inSelectionMode) {
                             stringResource(R.string.a11y_long_press_reorder)
                             stringResource(R.string.a11y_long_press_reorder)
                         } else {
                         } else {
@@ -412,6 +404,9 @@ fun TxtRuleScreen(
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = item.id,
                         key = item.id,
+                        reorderIndex = rules.indexOf(item),
+                        reorderItemCount = rules.size,
+                        onMoveItem = { from, to -> onIntent(TxtTocRuleIntent.MoveItem(from, to)) },
                         title = item.name,
                         title = item.name,
                         subtitle = item.example,
                         subtitle = item.example,
                         isEnabled = item.isEnabled,
                         isEnabled = item.isEnabled,
@@ -429,7 +424,6 @@ fun TxtRuleScreen(
                             onIntent(TxtTocRuleIntent.SetRuleEnabled(item.rule, enabled))
                             onIntent(TxtTocRuleIntent.SetRuleEnabled(item.rule, enabled))
                         },
                         },
                         contentDescription = itemDescription,
                         contentDescription = itemDescription,
-                        stateDescription = selectedState,
                         enableSwitchContentDescription = stringResource(
                         enableSwitchContentDescription = stringResource(
                             R.string.a11y_rule_enabled_switch,
                             R.string.a11y_rule_enabled_switch,
                             item.name
                             item.name

+ 4 - 2
app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt

@@ -60,7 +60,8 @@ fun CoverRuleConfigSheet(
         title = stringResource(R.string.cover_rule),
         title = stringResource(R.string.cover_rule),
         startAction = {
         startAction = {
             MediumPlainButton(
             MediumPlainButton(
-                icon = Icons.Default.SettingsBackupRestore,
+            icon = Icons.Default.SettingsBackupRestore,
+            contentDescription = stringResource(R.string.restore_default),
                 onClick = {
                 onClick = {
                     DefaultData.coverRule.let {
                     DefaultData.coverRule.let {
                         enable = it.enable
                         enable = it.enable
@@ -73,7 +74,8 @@ fun CoverRuleConfigSheet(
         },
         },
         endAction = {
         endAction = {
             MediumPlainButton(
             MediumPlainButton(
-                icon = Icons.Default.Save,
+            icon = Icons.Default.Save,
+            contentDescription = stringResource(R.string.save),
                 onClick = {
                 onClick = {
                     if (searchUrl.isBlank() || coverRule.isBlank()) {
                     if (searchUrl.isBlank() || coverRule.isBlank()) {
                         appCtx.toastOnUi(R.string.cover_rule_fields_required)
                         appCtx.toastOnUi(R.string.cover_rule_fields_required)

+ 4 - 2
app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt

@@ -69,14 +69,16 @@ fun DirectLinkUploadBottomSheet(
                 onClick = {
                 onClick = {
                     viewModel.testRule { result -> showTestResult = result }
                     viewModel.testRule { result -> showTestResult = result }
                 },
                 },
-                icon = Icons.Default.Checklist
+                icon = Icons.Default.Checklist,
+                contentDescription = stringResource(R.string.test)
             )
             )
         },
         },
         endAction = {
         endAction = {
             Box {
             Box {
                 MediumPlainButton(
                 MediumPlainButton(
                     onClick = { showMenu = true },
                     onClick = { showMenu = true },
-                    icon = Icons.Default.MoreVert
+                icon = Icons.Default.MoreVert,
+                contentDescription = stringResource(R.string.more_menu)
                 )
                 )
                 RoundDropdownMenu(
                 RoundDropdownMenu(
                     expanded = showMenu,
                     expanded = showMenu,

+ 2 - 1
app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt

@@ -120,7 +120,8 @@ fun BackgroundImageManageSheet(
                             .align(Alignment.TopEnd)
                             .align(Alignment.TopEnd)
                             .padding(8.dp)
                             .padding(8.dp)
                             .size(32.dp),
                             .size(32.dp),
-                        icon = Icons.Default.Close
+                        icon = Icons.Default.Close,
+                        contentDescription = stringResource(R.string.close)
                     )
                     )
                 }
                 }
             }
             }

+ 8 - 4
app/src/main/java/io/legado/app/ui/config/themeConfig/LabelColorManageSheet.kt

@@ -63,7 +63,8 @@ fun LabelColorManageSheet(
                     tagColors.addAll(generatedColors)
                     tagColors.addAll(generatedColors)
                     ThemeConfig.saveCustomTagColors(tagColors)
                     ThemeConfig.saveCustomTagColors(tagColors)
                 },
                 },
-                icon = Icons.Default.AutoAwesome
+                icon = Icons.Default.AutoAwesome,
+                contentDescription = stringResource(R.string.ai_generate)
             )
             )
         },
         },
         endAction = {
         endAction = {
@@ -74,7 +75,8 @@ fun LabelColorManageSheet(
                     editingTextColor = 0
                     editingTextColor = 0
                     showColorPicker = true
                     showColorPicker = true
                 },
                 },
-                icon = Icons.Default.Add
+                icon = Icons.Default.Add,
+                contentDescription = stringResource(R.string.add)
             )
             )
         }
         }
     ) {
     ) {
@@ -116,14 +118,16 @@ fun LabelColorManageSheet(
                                     editingTextColor = colorPair.textColor
                                     editingTextColor = colorPair.textColor
                                     showColorPicker = true
                                     showColorPicker = true
                                 },
                                 },
-                                icon = Icons.Default.Edit
+                                icon = Icons.Default.Edit,
+                                contentDescription = stringResource(R.string.edit)
                             )
                             )
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = {
                                 onClick = {
                                     tagColors.removeAt(index)
                                     tagColors.removeAt(index)
                                     ThemeConfig.saveCustomTagColors(tagColors)
                                     ThemeConfig.saveCustomTagColors(tagColors)
                                 },
                                 },
-                                icon = Icons.Default.Delete
+                                icon = Icons.Default.Delete,
+                                contentDescription = stringResource(R.string.delete)
                             )
                             )
                         }
                         }
                     }
                     }

+ 11 - 0
app/src/main/java/io/legado/app/ui/config/themeConfig/MainNavigationSettingsSheet.kt

@@ -122,6 +122,17 @@ fun MainNavigationSettingsSheet(
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = destination.route,
                         key = destination.route,
+                        reorderIndex = visibleItems.indexOf(destination),
+                        reorderItemCount = visibleItems.size,
+                        onMoveItem = { from, to ->
+                            val fromItem = visibleItems[from]
+                            val toItem = visibleItems[to]
+                            navigationItems = navigationItems.toMutableList().apply {
+                                move(indexOf(fromItem), indexOf(toItem))
+                            }
+                            ThemeConfig.mainNavigationOrder =
+                                navigationItems.joinToString(",") { it.route }
+                        },
                         title = stringResource(destination.labelId),
                         title = stringResource(destination.labelId),
                         isEnabled = true,
                         isEnabled = true,
                         containerColor = LegadoTheme.colorScheme.onSheetContent,
                         containerColor = LegadoTheme.colorScheme.onSheetContent,

+ 2 - 1
app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt

@@ -129,7 +129,8 @@ fun NavIconManageSheet(
                                             .align(Alignment.TopEnd)
                                             .align(Alignment.TopEnd)
                                             .padding(4.dp)
                                             .padding(4.dp)
                                             .size(24.dp),
                                             .size(24.dp),
-                                        icon = Icons.Default.Close
+                                        icon = Icons.Default.Close,
+                                        contentDescription = stringResource(R.string.close)
                                     )
                                     )
                                 }
                                 }
                             } else {
                             } else {

+ 1 - 7
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -66,7 +66,6 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.constraintlayout.compose.ConstraintLayout
 import androidx.constraintlayout.compose.ConstraintLayout
@@ -220,7 +219,7 @@ fun ThemeConfigScreen(
                             )
                             )
                             SmallPlainButton(
                             SmallPlainButton(
                                 icon = AppIcons.Close,
                                 icon = AppIcons.Close,
-                                contentDescription = "关闭",
+                                contentDescription = stringResource(R.string.close),
                                 onClick = {
                                 onClick = {
                                     viewModel.setShowThemeRefactorTip(false)
                                     viewModel.setShowThemeRefactorTip(false)
                                 }
                                 }
@@ -928,17 +927,12 @@ fun ThemeModeSelector(
 
 
         modes.forEachIndexed { index, (value, label, icon) ->
         modes.forEachIndexed { index, (value, label, icon) ->
             val checked = selectedIndex == index
             val checked = selectedIndex == index
-            val modeStateDescription = stringResource(
-                if (checked) R.string.a11y_selected else R.string.a11y_not_selected
-            )
-
             ToggleButton(
             ToggleButton(
                 checked = checked,
                 checked = checked,
                 onCheckedChange = { onModeSelected(value) },
                 onCheckedChange = { onModeSelected(value) },
                 modifier = modifiers[index]
                 modifier = modifiers[index]
                     .semantics {
                     .semantics {
                         role = Role.RadioButton
                         role = Role.RadioButton
-                        stateDescription = modeStateDescription
                     },
                     },
 
 
                 shapes = when (index) {
                 shapes = when (index) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt

@@ -67,7 +67,7 @@ fun EditThemeSheet(
                     }
                     }
                 },
                 },
                 icon = Icons.Default.Done,
                 icon = Icons.Default.Done,
-                contentDescription = "Save"
+                contentDescription = stringResource(R.string.save)
             )
             )
         }
         }
     ) {
     ) {

+ 6 - 3
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt

@@ -486,15 +486,18 @@ private fun SavedThemeItem(
                 ) {
                 ) {
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = onEdit,
                         onClick = onEdit,
-                        icon = Icons.Default.Edit
+            icon = Icons.Default.Edit,
+            contentDescription = stringResource(R.string.edit)
                     )
                     )
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = onExport,
                         onClick = onExport,
-                        icon = Icons.Default.Share
+            icon = Icons.Default.Share,
+            contentDescription = stringResource(R.string.share)
                     )
                     )
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = onDelete,
                         onClick = onDelete,
-                        icon = Icons.Default.Delete
+            icon = Icons.Default.Delete,
+            contentDescription = stringResource(R.string.delete)
                     )
                     )
                 }
                 }
             }
             }

+ 3 - 9
app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt

@@ -328,23 +328,18 @@ fun DictRuleScreen(
                     val enabledState = stringResource(
                     val enabledState = stringResource(
                         if (item.isEnabled) R.string.enabled else R.string.disabled
                         if (item.isEnabled) R.string.enabled else R.string.disabled
                     )
                     )
-                    val selectedState = stringResource(
-                        if (selectedIds.contains(item.id)) {
-                            R.string.a11y_selected
-                        } else {
-                            R.string.a11y_not_selected
-                        }
-                    )
                     val itemDescription = listOfNotNull(
                     val itemDescription = listOfNotNull(
                         item.id,
                         item.id,
                         item.urlRule.takeIf { it.isNotBlank() },
                         item.urlRule.takeIf { it.isNotBlank() },
                         enabledState,
                         enabledState,
-                        selectedState,
                         stringResource(R.string.a11y_long_press_reorder)
                         stringResource(R.string.a11y_long_press_reorder)
                     ).joinToString()
                     ).joinToString()
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = item.id,
                         key = item.id,
+                        reorderIndex = rules.indexOf(item),
+                        reorderItemCount = rules.size,
+                        onMoveItem = { from, to -> onIntent(DictRuleIntent.MoveItem(from, to)) },
                         title = item.id,
                         title = item.id,
                         isEnabled = item.isEnabled,
                         isEnabled = item.isEnabled,
                         isSelected = selectedIds.contains(item.id),
                         isSelected = selectedIds.contains(item.id),
@@ -354,7 +349,6 @@ fun DictRuleScreen(
                             onIntent(DictRuleIntent.SetRuleEnabled(item.rule, enabled))
                             onIntent(DictRuleIntent.SetRuleEnabled(item.rule, enabled))
                         },
                         },
                         contentDescription = itemDescription,
                         contentDescription = itemDescription,
-                        stateDescription = selectedState,
                         enableSwitchContentDescription = stringResource(
                         enableSwitchContentDescription = stringResource(
                             R.string.a11y_rule_enabled_switch,
                             R.string.a11y_rule_enabled_switch,
                             item.id
                             item.id

+ 5 - 9
app/src/main/java/io/legado/app/ui/highlightTagRule/HighlightTagRuleScreen.kt

@@ -297,18 +297,10 @@ private fun HighlightTagRuleContent(
                     val enabledState = stringResource(
                     val enabledState = stringResource(
                         if (item.isEnabled) R.string.enabled else R.string.disabled
                         if (item.isEnabled) R.string.enabled else R.string.disabled
                     )
                     )
-                    val selectedState = stringResource(
-                        if (selectedIds.contains(item.id)) {
-                            R.string.a11y_selected
-                        } else {
-                            R.string.a11y_not_selected
-                        }
-                    )
                     val itemDescription = listOfNotNull(
                     val itemDescription = listOfNotNull(
                         item.displayName,
                         item.displayName,
                         item.pattern.takeIf { it.isNotBlank() },
                         item.pattern.takeIf { it.isNotBlank() },
                         enabledState,
                         enabledState,
-                        selectedState,
                         if (!inSelectionMode) {
                         if (!inSelectionMode) {
                             stringResource(R.string.a11y_long_press_reorder)
                             stringResource(R.string.a11y_long_press_reorder)
                         } else {
                         } else {
@@ -318,6 +310,11 @@ private fun HighlightTagRuleContent(
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = item.id,
                         key = item.id,
+                        reorderIndex = rules.indexOf(item),
+                        reorderItemCount = rules.size,
+                        onMoveItem = { from, to ->
+                            onIntent(HighlightTagRuleIntent.MoveItem(from, to))
+                        },
                         title = item.displayName,
                         title = item.displayName,
                         subtitle = item.pattern,
                         subtitle = item.pattern,
                         isEnabled = item.isEnabled,
                         isEnabled = item.isEnabled,
@@ -328,7 +325,6 @@ private fun HighlightTagRuleContent(
                             onIntent(HighlightTagRuleIntent.SetRuleEnabled(item.rule, enabled))
                             onIntent(HighlightTagRuleIntent.SetRuleEnabled(item.rule, enabled))
                         },
                         },
                         contentDescription = itemDescription,
                         contentDescription = itemDescription,
-                        stateDescription = selectedState,
                         enableSwitchContentDescription = stringResource(
                         enableSwitchContentDescription = stringResource(
                             R.string.a11y_rule_enabled_switch,
                             R.string.a11y_rule_enabled_switch,
                             item.displayName
                             item.displayName

+ 11 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -136,6 +136,7 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.components.reorderAccessibility
 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.topbar.GlassTopAppBarScrollBehavior
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarScrollBehavior
@@ -1382,7 +1383,7 @@ fun BookshelfPage(
             horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
             horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
             showFastScroll = showFastScroll
             showFastScroll = showFastScroll
         ) {
         ) {
-            items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
+            itemsIndexed(displayBooks, key = { _, item -> item.book.bookUrl }) { index, bookUi ->
                 val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
                 val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
                 val sharedCoverKey = bookCoverSharedElementKey(
                 val sharedCoverKey = bookCoverSharedElementKey(
                     bookUi.book.bookUrl,
                     bookUi.book.bookUrl,
@@ -1396,6 +1397,15 @@ fun BookshelfPage(
                     BookItem(
                     BookItem(
                         bookUi = bookUi,
                         bookUi = bookUi,
                         modifier = Modifier
                         modifier = Modifier
+                            .reorderAccessibility(
+                                index = index,
+                                itemCount = displayBooks.size,
+                                enabled = canReorderBooks,
+                            ) { from, to ->
+                                onDragStarted(displayBooks)
+                                onMoveBook(from, to, displayBooks)
+                                onDragFinished()
+                            }
                             .then(
                             .then(
                                 if (canReorderBooks) {
                                 if (canReorderBooks) {
                                     Modifier.longPressDraggableHandle(
                                     Modifier.longPressDraggableHandle(

+ 9 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt

@@ -193,6 +193,15 @@ fun GroupManageSheet(
                         ReorderableSelectionItem(
                         ReorderableSelectionItem(
                             state = reorderableState,
                             state = reorderableState,
                             key = group.groupId,
                             key = group.groupId,
+                            reorderIndex = listData.indexOf(group),
+                            reorderItemCount = listData.size,
+                            onMoveItem = { from, to ->
+                                listData = listData.toMutableList().apply { move(from, to) }
+                                val updatedGroups = listData.mapIndexed { index, item ->
+                                    item.copy(order = index)
+                                }
+                                viewModel.upGroup(*updatedGroups.toTypedArray())
+                            },
                             title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
                             title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
                             subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
                             subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
                             isEnabled = group.show,
                             isEnabled = group.show,

+ 13 - 8
app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt

@@ -132,8 +132,9 @@ fun <T> HomepageModuleManageSheet(
         startAction = {
         startAction = {
             if (showCustomSetAddModules) {
             if (showCustomSetAddModules) {
                 SmallPlainButton(
                 SmallPlainButton(
-                    onClick = { showCustomSetAddModules = false },
-                    icon = Icons.AutoMirrored.Filled.ArrowBack
+                        onClick = { showCustomSetAddModules = false },
+                        icon = Icons.AutoMirrored.Filled.ArrowBack,
+                        contentDescription = stringResource(R.string.back)
                 )
                 )
             } else if (browsingSourceUrl != null || showSourceBrowser) {
             } else if (browsingSourceUrl != null || showSourceBrowser) {
                 SmallPlainButton(
                 SmallPlainButton(
@@ -142,12 +143,14 @@ fun <T> HomepageModuleManageSheet(
                         else if (showSourceBrowser) showSourceBrowser = false
                         else if (showSourceBrowser) showSourceBrowser = false
                         else browsingSourceUrl = null
                         else browsingSourceUrl = null
                     },
                     },
-                    icon = Icons.AutoMirrored.Filled.ArrowBack
+                        icon = Icons.AutoMirrored.Filled.ArrowBack,
+                        contentDescription = stringResource(R.string.back)
                 )
                 )
             } else if (selectingSetUrl != null) {
             } else if (selectingSetUrl != null) {
                 SmallPlainButton(
                 SmallPlainButton(
-                    onClick = { selectingSetUrl = null },
-                    icon = Icons.AutoMirrored.Filled.ArrowBack
+                        onClick = { selectingSetUrl = null },
+                        icon = Icons.AutoMirrored.Filled.ArrowBack,
+                        contentDescription = stringResource(R.string.back)
                 )
                 )
             }
             }
         },
         },
@@ -159,15 +162,17 @@ fun <T> HomepageModuleManageSheet(
                 selectedKindTitles.isNotEmpty()
                 selectedKindTitles.isNotEmpty()
             ) {
             ) {
                 SmallPlainButton(
                 SmallPlainButton(
-                    onClick = { showAddKindGroupDialog = true },
-                    icon = Icons.Default.Check
+                        onClick = { showAddKindGroupDialog = true },
+                        icon = Icons.Default.Check,
+                        contentDescription = stringResource(R.string.confirm)
                 )
                 )
             } else if ((showSourceBrowser || browsingSourceUrl != null) && !browsingDetail) {
             } else if ((showSourceBrowser || browsingSourceUrl != null) && !browsingDetail) {
                 var expanded by remember { mutableStateOf(false) }
                 var expanded by remember { mutableStateOf(false) }
                 Box {
                 Box {
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = { expanded = true },
                         onClick = { expanded = true },
-                        icon = Icons.Default.FilterList
+                        icon = Icons.Default.FilterList,
+                        contentDescription = stringResource(R.string.screen)
                     )
                     )
                     RoundDropdownMenu(
                     RoundDropdownMenu(
                         expanded = expanded,
                         expanded = expanded,

+ 16 - 4
app/src/main/java/io/legado/app/ui/main/homepage/manage/SetDetailPage.kt

@@ -118,6 +118,14 @@ fun SetDetailPage(
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = module.id,
                         key = module.id,
+                        reorderIndex = listData.indexOf(module),
+                        reorderItemCount = listData.size,
+                        onMoveItem = { from, to ->
+                            listData = listData.toMutableList().apply { move(from, to) }
+                            onReorderModules(
+                                (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
+                            )
+                        },
                         title = module.title,
                         title = module.title,
                         subtitle = HomepageModuleType.fromKey(module.type).title,
                         subtitle = HomepageModuleType.fromKey(module.type).title,
                         isEnabled = module.isVisible,
                         isEnabled = module.isVisible,
@@ -131,11 +139,13 @@ fun SetDetailPage(
                         trailingAction = {
                         trailingAction = {
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = { onEditModule(module) },
                                 onClick = { onEditModule(module) },
-                                icon = Icons.Default.Edit
+                                icon = Icons.Default.Edit,
+                                contentDescription = stringResource(R.string.edit)
                             )
                             )
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = { onRequestDeleteModule(module.id) },
                                 onClick = { onRequestDeleteModule(module.id) },
-                                icon = Icons.Default.Delete
+                                icon = Icons.Default.Delete,
+                                contentDescription = stringResource(R.string.delete)
                             )
                             )
                         }
                         }
                     )
                     )
@@ -162,11 +172,13 @@ fun SetDetailPage(
                         trailingAction = {
                         trailingAction = {
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = { onEditModule(module) },
                                 onClick = { onEditModule(module) },
-                                icon = Icons.Default.Edit
+                                icon = Icons.Default.Edit,
+                                contentDescription = stringResource(R.string.edit)
                             )
                             )
                             SmallPlainButton(
                             SmallPlainButton(
                                 onClick = { onRequestDeleteModule(module.id) },
                                 onClick = { onRequestDeleteModule(module.id) },
-                                icon = Icons.Default.Delete
+                                icon = Icons.Default.Delete,
+                                contentDescription = stringResource(R.string.delete)
                             )
                             )
                         }
                         }
                     )
                     )

+ 10 - 2
app/src/main/java/io/legado/app/ui/main/homepage/manage/SetListPage.kt

@@ -73,6 +73,12 @@ fun SetListPage(
             ReorderableSelectionItem(
             ReorderableSelectionItem(
                 state = setsReorderableState,
                 state = setsReorderableState,
                 key = set.sourceUrl,
                 key = set.sourceUrl,
+                reorderIndex = localSets.indexOf(set),
+                reorderItemCount = localSets.size,
+                onMoveItem = { from, to ->
+                    localSets = localSets.toMutableList().apply { move(from, to) }
+                    onReorderSets(localSets.map { it.sourceUrl })
+                },
                 title = set.sourceName,
                 title = set.sourceName,
                 subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount),
                 subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount),
                 containerColor = LegadoTheme.colorScheme.onSheetContent,
                 containerColor = LegadoTheme.colorScheme.onSheetContent,
@@ -87,11 +93,13 @@ fun SetListPage(
                 trailingAction = {
                 trailingAction = {
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = { onRenameSet(set.sourceUrl) },
                         onClick = { onRenameSet(set.sourceUrl) },
-                        icon = Icons.Default.DriveFileRenameOutline
+                        icon = Icons.Default.DriveFileRenameOutline,
+                        contentDescription = stringResource(R.string.homepage_rename_custom_set)
                     )
                     )
                     SmallPlainButton(
                     SmallPlainButton(
                         onClick = { onDeleteSet(set.sourceUrl) },
                         onClick = { onDeleteSet(set.sourceUrl) },
-                        icon = Icons.Default.Delete
+                        icon = Icons.Default.Delete,
+                        contentDescription = stringResource(R.string.delete)
                     )
                     )
                 }
                 }
             )
             )

+ 18 - 5
app/src/main/java/io/legado/app/ui/main/homepage/manage/SourceBrowseDetailPage.kt

@@ -159,6 +159,14 @@ fun SourceBrowseDetailPage(
                             ReorderableSelectionItem(
                             ReorderableSelectionItem(
                                 state = reorderableState,
                                 state = reorderableState,
                                 key = module.id,
                                 key = module.id,
+                                reorderIndex = listData.indexOf(module),
+                                reorderItemCount = listData.size,
+                                onMoveItem = { from, to ->
+                                    listData = listData.toMutableList().apply { move(from, to) }
+                                    onReorderModules(
+                                        (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
+                                    )
+                                },
                                 title = module.title,
                                 title = module.title,
                                 subtitle = HomepageModuleType.fromKey(module.type).title,
                                 subtitle = HomepageModuleType.fromKey(module.type).title,
                                 isEnabled = module.isVisible,
                                 isEnabled = module.isVisible,
@@ -172,11 +180,13 @@ fun SourceBrowseDetailPage(
                                 trailingAction = {
                                 trailingAction = {
                                     SmallPlainButton(
                                     SmallPlainButton(
                                         onClick = { onEditModule(module) },
                                         onClick = { onEditModule(module) },
-                                        icon = Icons.Default.Edit
+                                        icon = Icons.Default.Edit,
+                                        contentDescription = stringResource(R.string.edit)
                                     )
                                     )
                                     SmallPlainButton(
                                     SmallPlainButton(
                                         onClick = { onRequestDeleteModule(module.id) },
                                         onClick = { onRequestDeleteModule(module.id) },
-                                        icon = Icons.Default.Delete
+                                        icon = Icons.Default.Delete,
+                                        contentDescription = stringResource(R.string.delete)
                                     )
                                     )
                                 }
                                 }
                             )
                             )
@@ -214,11 +224,13 @@ fun SourceBrowseDetailPage(
                                     trailingAction = {
                                     trailingAction = {
                                         SmallPlainButton(
                                         SmallPlainButton(
                                             onClick = { onEditModule(module) },
                                             onClick = { onEditModule(module) },
-                                            icon = Icons.Default.Edit
+                                            icon = Icons.Default.Edit,
+                                            contentDescription = stringResource(R.string.edit)
                                         )
                                         )
                                         SmallPlainButton(
                                         SmallPlainButton(
                                             onClick = { onRequestDeleteModule(module.id) },
                                             onClick = { onRequestDeleteModule(module.id) },
-                                            icon = Icons.Default.Delete
+                                            icon = Icons.Default.Delete,
+                                            contentDescription = stringResource(R.string.delete)
                                         )
                                         )
                                     }
                                     }
                                 )
                                 )
@@ -339,7 +351,8 @@ fun SourceBrowseDetailPage(
                             if (supportsMultipleKinds && selectedKindTitles.isNotEmpty()) {
                             if (supportsMultipleKinds && selectedKindTitles.isNotEmpty()) {
                                 SmallPlainButton(
                                 SmallPlainButton(
                                     onClick = { onShowAddKindGroupDialog() },
                                     onClick = { onShowAddKindGroupDialog() },
-                                    icon = Icons.Default.Check
+                                    icon = Icons.Default.Check,
+                                    contentDescription = stringResource(R.string.confirm)
                                 )
                                 )
                             }
                             }
                         }
                         }

+ 3 - 9
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt

@@ -412,13 +412,6 @@ fun ReplaceRuleScreen(
                     val enabledState = stringResource(
                     val enabledState = stringResource(
                         if (ui.isEnabled) R.string.enabled else R.string.disabled
                         if (ui.isEnabled) R.string.enabled else R.string.disabled
                     )
                     )
-                    val selectedState = stringResource(
-                        if (selectedIds.contains(ui.id)) {
-                            R.string.a11y_selected
-                        } else {
-                            R.string.a11y_not_selected
-                        }
-                    )
                     val reorderHint = if (canReorder && !inSelectionMode) {
                     val reorderHint = if (canReorder && !inSelectionMode) {
                         stringResource(R.string.a11y_long_press_reorder)
                         stringResource(R.string.a11y_long_press_reorder)
                     } else {
                     } else {
@@ -428,12 +421,14 @@ fun ReplaceRuleScreen(
                         ui.name,
                         ui.name,
                         ui.pattern.takeIf { it.isNotBlank() },
                         ui.pattern.takeIf { it.isNotBlank() },
                         enabledState,
                         enabledState,
-                        selectedState,
                         reorderHint
                         reorderHint
                     ).joinToString()
                     ).joinToString()
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = ui.id,
                         key = ui.id,
+                        reorderIndex = rules.indexOf(ui),
+                        reorderItemCount = rules.size,
+                        onMoveItem = { from, to -> onIntent(ReplaceRuleIntent.MoveItem(from, to)) },
                         title = ui.name,
                         title = ui.name,
                         isEnabled = ui.isEnabled,
                         isEnabled = ui.isEnabled,
                         isSelected = selectedIds.contains(ui.id),
                         isSelected = selectedIds.contains(ui.id),
@@ -446,7 +441,6 @@ fun ReplaceRuleScreen(
                             onIntent(ReplaceRuleIntent.SetRuleEnabled(ui.id, enabled))
                             onIntent(ReplaceRuleIntent.SetRuleEnabled(ui.id, enabled))
                         },
                         },
                         contentDescription = itemDescription,
                         contentDescription = itemDescription,
-                        stateDescription = selectedState,
                         enableSwitchContentDescription = stringResource(
                         enableSwitchContentDescription = stringResource(
                             R.string.a11y_rule_enabled_switch,
                             R.string.a11y_rule_enabled_switch,
                             ui.name
                             ui.name

+ 3 - 0
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceScreen.kt

@@ -351,6 +351,9 @@ fun RssSourceScreen(
                     ReorderableSelectionItem(
                     ReorderableSelectionItem(
                         state = reorderableState,
                         state = reorderableState,
                         key = item.id,
                         key = item.id,
+                        reorderIndex = rules.indexOf(item),
+                        reorderItemCount = rules.size,
+                        onMoveItem = viewModel::moveItemInList,
                         title = item.name,
                         title = item.name,
                         subtitle = item.group,
                         subtitle = item.group,
                         isEnabled = item.isEnabled,
                         isEnabled = item.isEnabled,

+ 2 - 2
app/src/main/java/io/legado/app/ui/tagGroupRule/TagGroupRuleEditSheet.kt

@@ -74,7 +74,7 @@ fun TagGroupRuleEditSheet(
             MediumPlainButton(
             MediumPlainButton(
                 onClick = onDismissRequest,
                 onClick = onDismissRequest,
                 icon = Icons.Default.Close,
                 icon = Icons.Default.Close,
-                contentDescription = "Close",
+                contentDescription = stringResource(R.string.close),
             )
             )
         },
         },
         endAction = {
         endAction = {
@@ -82,7 +82,7 @@ fun TagGroupRuleEditSheet(
                 MediumPlainButton(
                 MediumPlainButton(
                     onClick = { showMenu = true },
                     onClick = { showMenu = true },
                     icon = Icons.Default.MoreVert,
                     icon = Icons.Default.MoreVert,
-                    contentDescription = "More"
+                    contentDescription = stringResource(R.string.more_menu)
                 )
                 )
                 RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
                 RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
                     RoundDropdownMenuItem(
                     RoundDropdownMenuItem(

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt

@@ -199,7 +199,7 @@ fun AppFloatingActionButtonMenu(
                         MiuixIcon(
                         MiuixIcon(
                             imageVector = if (expanded) Icons.Filled.Close
                             imageVector = if (expanded) Icons.Filled.Close
                             else Icons.AutoMirrored.Filled.MenuOpen,
                             else Icons.AutoMirrored.Filled.MenuOpen,
-                            contentDescription = "Menu",
+                            contentDescription = stringResource(R.string.menu),
                             tint = Color.White
                             tint = Color.White
                         )
                         )
                         Spacer(modifier = Modifier.width(8.dp))
                         Spacer(modifier = Modifier.width(8.dp))
@@ -231,7 +231,7 @@ fun AppFloatingActionButtonMenu(
                     }
                     }
                     Icon(
                     Icon(
                         imageVector = imageVector,
                         imageVector = imageVector,
-                        contentDescription = "Menu",
+                        contentDescription = stringResource(R.string.menu),
                         modifier = Modifier.animateIcon({ checkedProgress }),
                         modifier = Modifier.animateIcon({ checkedProgress }),
                     )
                     )
                 }
                 }

+ 3 - 0
app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt

@@ -13,8 +13,10 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import androidx.core.net.toUri
 import androidx.core.net.toUri
 import androidx.documentfile.provider.DocumentFile
 import androidx.documentfile.provider.DocumentFile
+import io.legado.app.R
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileDoc
@@ -70,6 +72,7 @@ fun FontSelectSheet(
                 SmallPlainButton(
                 SmallPlainButton(
                     onClick = { showTypefaceMenu = true },
                     onClick = { showTypefaceMenu = true },
                     icon = Icons.Default.TextFields,
                     icon = Icons.Default.TextFields,
+                    contentDescription = stringResource(R.string.select_font),
                 )
                 )
             }
             }
         },
         },

+ 5 - 18
app/src/main/java/io/legado/app/ui/widget/components/IconSwitch.kt

@@ -11,10 +11,9 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
-import io.legado.app.R
+import androidx.compose.ui.state.ToggleableState
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import top.yukonga.miuix.kmp.basic.Switch as MiuixSwitch
 import top.yukonga.miuix.kmp.basic.Switch as MiuixSwitch
@@ -31,17 +30,13 @@ fun AdaptiveSwitch(
     includeStateSemantics: Boolean = true
     includeStateSemantics: Boolean = true
 ) {
 ) {
     val composeEngine = LegadoTheme.composeEngine
     val composeEngine = LegadoTheme.composeEngine
-    val switchStateDescription = stringResource(
-        if (checked) R.string.a11y_on else R.string.a11y_off
-    )
-
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         MiuixSwitch(
         MiuixSwitch(
             checked = checked,
             checked = checked,
             onCheckedChange = onCheckedChange,
             onCheckedChange = onCheckedChange,
             modifier = if (includeStateSemantics) {
             modifier = if (includeStateSemantics) {
                 modifier.semantics {
                 modifier.semantics {
-                    stateDescription = switchStateDescription
+                    toggleableState = if (checked) ToggleableState.On else ToggleableState.Off
                 }
                 }
             } else {
             } else {
                 modifier
                 modifier
@@ -74,10 +69,6 @@ fun TinySwitch(
     includeStateSemantics: Boolean = true
     includeStateSemantics: Boolean = true
 ) {
 ) {
     val composeEngine = LegadoTheme.composeEngine
     val composeEngine = LegadoTheme.composeEngine
-    val switchStateDescription = stringResource(
-        if (checked) R.string.a11y_on else R.string.a11y_off
-    )
-
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         MiuixSwitch(
         MiuixSwitch(
             checked = checked,
             checked = checked,
@@ -86,7 +77,7 @@ fun TinySwitch(
                 modifier
                 modifier
                     .scale(0.9f)
                     .scale(0.9f)
                     .semantics {
                     .semantics {
-                        stateDescription = switchStateDescription
+                        toggleableState = if (checked) ToggleableState.On else ToggleableState.Off
                     }
                     }
             } else {
             } else {
                 modifier.scale(0.9f)
                 modifier.scale(0.9f)
@@ -119,14 +110,10 @@ fun IconSwitch(
     colors: SwitchColors = SwitchDefaults.colors(),
     colors: SwitchColors = SwitchDefaults.colors(),
     includeStateSemantics: Boolean = true
     includeStateSemantics: Boolean = true
 ) {
 ) {
-    val switchStateDescription = stringResource(
-        if (checked) R.string.a11y_on else R.string.a11y_off
-    )
-
     Switch(
     Switch(
         modifier = if (includeStateSemantics) {
         modifier = if (includeStateSemantics) {
             modifier.semantics {
             modifier.semantics {
-                stateDescription = switchStateDescription
+                toggleableState = if (checked) ToggleableState.On else ToggleableState.Off
             }
             }
         } else {
         } else {
             modifier
             modifier

+ 41 - 0
app/src/main/java/io/legado/app/ui/widget/components/ReorderAccessibility.kt

@@ -0,0 +1,41 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
+import io.legado.app.R
+
+@Composable
+fun Modifier.reorderAccessibility(
+    index: Int,
+    itemCount: Int,
+    enabled: Boolean = true,
+    description: String? = null,
+    onMove: (from: Int, to: Int) -> Unit,
+): Modifier {
+    if (!enabled || itemCount < 2 || index !in 0 until itemCount) return this
+
+    val moveUpLabel = stringResource(R.string.a11y_move_up)
+    val moveDownLabel = stringResource(R.string.a11y_move_down)
+    return semantics {
+        description?.let { contentDescription = it }
+        customActions = buildList {
+            if (index > 0) {
+                add(CustomAccessibilityAction(moveUpLabel) {
+                    onMove(index, index - 1)
+                    true
+                })
+            }
+            if (index < itemCount - 1) {
+                add(CustomAccessibilityAction(moveDownLabel) {
+                    onMove(index, index + 1)
+                    true
+                })
+            }
+        }
+    }
+}

+ 4 - 0
app/src/main/java/io/legado/app/ui/widget/components/ValueStepper.kt

@@ -9,7 +9,9 @@ import androidx.compose.material.icons.filled.Remove
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.series.SmallOutlinedButton
 import io.legado.app.ui.widget.components.button.series.SmallOutlinedButton
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.card.TextCard
@@ -39,6 +41,7 @@ fun ValueStepper(
             },
             },
             enabled = enabled,
             enabled = enabled,
             icon = Icons.Default.Remove,
             icon = Icons.Default.Remove,
+            contentDescription = stringResource(R.string.a11y_decrease),
         )
         )
         if (content != null) {
         if (content != null) {
             content()
             content()
@@ -64,6 +67,7 @@ fun ValueStepper(
             },
             },
             enabled = enabled,
             enabled = enabled,
             icon = Icons.Default.Add,
             icon = Icons.Default.Add,
+            contentDescription = stringResource(R.string.a11y_increase),
         )
         )
     }
     }
 }
 }

+ 10 - 1
app/src/main/java/io/legado/app/ui/widget/components/button/series/SeriesIconButton.kt

@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.minimumInteractiveComponentSize
 import androidx.compose.material3.ripple
 import androidx.compose.material3.ripple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.SideEffect
@@ -103,7 +104,9 @@ internal fun SeriesButton(
     val interactionSource = remember { MutableInteractionSource() }
     val interactionSource = remember { MutableInteractionSource() }
 
 
     Box(
     Box(
-        modifier = modifier
+        modifier = Modifier
+            .minimumInteractiveComponentSize()
+            .then(modifier)
             .then(if (size != null) Modifier.size(size) else Modifier)
             .then(if (size != null) Modifier.size(size) else Modifier)
             .clip(shape)
             .clip(shape)
             .background(containerColor, shape)
             .background(containerColor, shape)
@@ -144,6 +147,9 @@ internal fun SeriesIconButton(
     selectedContainerColor: Color = LegadoTheme.colorScheme.primaryContainer,
     selectedContainerColor: Color = LegadoTheme.colorScheme.primaryContainer,
     selectedContentColor: Color = LegadoTheme.colorScheme.onPrimaryContainer,
     selectedContentColor: Color = LegadoTheme.colorScheme.onPrimaryContainer,
 ) {
 ) {
+    require(!contentDescription.isNullOrBlank()) {
+        "Icon-only buttons must provide a contentDescription"
+    }
     SeriesButton(
     SeriesButton(
         onClick = onClick,
         onClick = onClick,
         modifier = modifier,
         modifier = modifier,
@@ -178,6 +184,9 @@ internal fun SeriesButtonContent(
     spacing: Dp
     spacing: Dp
 ) {
 ) {
     val hasText = text != null
     val hasText = text != null
+    require(hasText || !contentDescription.isNullOrBlank()) {
+        "Icon-only buttons must provide a contentDescription"
+    }
     Row(
     Row(
         modifier = Modifier.padding(if (hasText) padding else PaddingValues(0.dp)),
         modifier = Modifier.padding(if (hasText) padding else PaddingValues(0.dp)),
         horizontalArrangement = Arrangement.spacedBy(
         horizontalArrangement = Arrangement.spacedBy(

+ 26 - 28
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallAnimatedButton.kt

@@ -44,34 +44,32 @@ fun SmallAnimatedButton(
         }
         }
     }
     }
 
 
-    SmallNoMinTouchTarget {
-        SeriesButton(
-            onClick = {
-                onCheckedChange(!checked)
-                showText = true
-            },
-            modifier = if (text == null) modifier else modifier.height(36.dp),
-            enabled = enabled,
-            selected = checked,
-            onLongClick = onLongClick,
-            size = if (text == null) smallContainerSize() else null,
-            style = SeriesIconButtonStyle.Tonal,
+    SeriesButton(
+        onClick = {
+            onCheckedChange(!checked)
+            showText = true
+        },
+        modifier = if (text == null) modifier else modifier.height(36.dp),
+        enabled = enabled,
+        selected = checked,
+        onLongClick = onLongClick,
+        size = if (text == null) smallContainerSize() else null,
+        style = SeriesIconButtonStyle.Tonal,
+        contentColor = contentColor,
+        containerColor = containerColor,
+        selectedContainerColor = selectedContainerColor,
+        selectedContentColor = selectedContentColor
+    ) { contentColor ->
+        SeriesAnimatedButtonContent(
+            icon = currentIcon,
+            text = text,
+            contentDescription = if (text == null) contentDescription else null,
+            showText = showText,
+            iconSize = smallIconSize,
+            textStyle = LegadoTheme.typography.labelSmall,
             contentColor = contentColor,
             contentColor = contentColor,
-            containerColor = containerColor,
-            selectedContainerColor = selectedContainerColor,
-            selectedContentColor = selectedContentColor
-        ) { contentColor ->
-            SeriesAnimatedButtonContent(
-                icon = currentIcon,
-                text = text,
-                contentDescription = if (text == null) contentDescription else null,
-                showText = showText,
-                iconSize = smallIconSize,
-                textStyle = LegadoTheme.typography.labelSmall,
-                contentColor = contentColor,
-                padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
-                spacing = 6.dp
-            )
-        }
+            padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
+            spacing = 6.dp
+        )
     }
     }
 }
 }

+ 19 - 21
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallOutlinedButton.kt

@@ -20,26 +20,24 @@ fun SmallOutlinedButton(
     text: String? = null,
     text: String? = null,
     contentDescription: String? = null
     contentDescription: String? = null
 ) {
 ) {
-    SmallNoMinTouchTarget {
-        SeriesButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            selected = selected,
-            onLongClick = onLongClick,
-            size = if (text == null) smallContainerSize() else null,
-            style = SeriesIconButtonStyle.Outlined
-        ) { contentColor ->
-            SeriesButtonContent(
-                icon = icon,
-                text = text,
-                contentDescription = contentDescription,
-                iconSize = smallIconSize,
-                textStyle = LegadoTheme.typography.labelMedium,
-                contentColor = contentColor,
-                padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
-                spacing = 4.dp
-            )
-        }
+    SeriesButton(
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        selected = selected,
+        onLongClick = onLongClick,
+        size = if (text == null) smallContainerSize() else null,
+        style = SeriesIconButtonStyle.Outlined
+    ) { contentColor ->
+        SeriesButtonContent(
+            icon = icon,
+            text = text,
+            contentDescription = contentDescription,
+            iconSize = smallIconSize,
+            textStyle = LegadoTheme.typography.labelMedium,
+            contentColor = contentColor,
+            padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
+            spacing = 4.dp
+        )
     }
     }
 }
 }

+ 19 - 30
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallPlainButton.kt

@@ -3,9 +3,7 @@ package io.legado.app.ui.widget.components.button.series
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.LocalMinimumInteractiveComponentSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.Dp
@@ -26,13 +24,6 @@ internal val smallIconSize: Dp
 internal val SmallMiuixIconSize: Dp
 internal val SmallMiuixIconSize: Dp
     get() = smallIconSize
     get() = smallIconSize
 
 
-@Composable
-internal fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
-    CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
-        content()
-    }
-}
-
 @Composable
 @Composable
 fun SmallPlainButton(
 fun SmallPlainButton(
     onClick: () -> Unit,
     onClick: () -> Unit,
@@ -44,26 +35,24 @@ fun SmallPlainButton(
     text: String? = null,
     text: String? = null,
     contentDescription: String? = null
     contentDescription: String? = null
 ) {
 ) {
-    SmallNoMinTouchTarget {
-        SeriesButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            selected = selected,
-            onLongClick = onLongClick,
-            size = if (text == null) smallContainerSize() else null,
-            contentColor = LegadoTheme.colorScheme.onSurfaceVariant
-        ) { contentColor ->
-            SeriesButtonContent(
-                icon = icon,
-                text = text,
-                contentDescription = contentDescription,
-                iconSize = smallIconSize,
-                textStyle = LegadoTheme.typography.labelMedium,
-                contentColor = contentColor,
-                padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
-                spacing = 4.dp
-            )
-        }
+    SeriesButton(
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        selected = selected,
+        onLongClick = onLongClick,
+        size = if (text == null) smallContainerSize() else null,
+        contentColor = LegadoTheme.colorScheme.onSurfaceVariant
+    ) { contentColor ->
+        SeriesButtonContent(
+            icon = icon,
+            text = text,
+            contentDescription = contentDescription,
+            iconSize = smallIconSize,
+            textStyle = LegadoTheme.typography.labelMedium,
+            contentColor = contentColor,
+            padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
+            spacing = 4.dp
+        )
     }
     }
 }
 }

+ 39 - 41
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallToggleButton.kt

@@ -101,50 +101,48 @@ fun SmallToggleButton(
             )
             )
         }
         }
     } else {
     } else {
-        SmallNoMinTouchTarget {
-            if (text != null) {
-                TonalToggleButton(
-                    checked = checked,
-                    onCheckedChange = onCheckedChange,
-                    modifier = modifier,
-                    enabled = enabled,
-                    contentPadding = PaddingValues(horizontal = 8.dp)
+        if (text != null) {
+            TonalToggleButton(
+                checked = checked,
+                onCheckedChange = onCheckedChange,
+                modifier = modifier,
+                enabled = enabled,
+                contentPadding = PaddingValues(horizontal = 8.dp)
+            ) {
+                Row(
+                    verticalAlignment = Alignment.CenterVertically,
+                    horizontalArrangement = Arrangement.Center
                 ) {
                 ) {
-                    Row(
-                        verticalAlignment = Alignment.CenterVertically,
-                        horizontalArrangement = Arrangement.Center
-                    ) {
-                        Icon(
-                            imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
-                            contentDescription = null,
-                            modifier = Modifier.size(16.dp)
-                        )
-                        Text(
-                            text = text,
-                            style = LegadoTheme.typography.labelSmall,
-                            modifier = Modifier.padding(start = 6.dp),
-                            maxLines = 1,
-                            softWrap = false
-                        )
-                    }
+                    Icon(
+                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
+                        contentDescription = null,
+                        modifier = Modifier.size(16.dp)
+                    )
+                    Text(
+                        text = text,
+                        style = LegadoTheme.typography.labelSmall,
+                        modifier = Modifier.padding(start = 6.dp),
+                        maxLines = 1,
+                        softWrap = false
+                    )
                 }
                 }
-            } else {
-                SeriesIconButton(
-                    icon = if (checked) (iconChecked ?: icon)!! else icon!!,
-                    contentDescription = contentDescription,
-                    onClick = { onCheckedChange(!checked) },
-                    modifier = modifier,
-                    enabled = enabled,
-                    selected = checked,
-                    onLongClick = onLongClick,
-                    size = smallContainerSize(),
-                    iconSize = smallIconSize,
-                    style = when (style) {
-                        ToggleStyle.Outlined -> SeriesIconButtonStyle.Outlined
-                        ToggleStyle.Tonal -> SeriesIconButtonStyle.Tonal
-                    }
-                )
             }
             }
+        } else {
+            SeriesIconButton(
+                icon = if (checked) (iconChecked ?: icon)!! else icon!!,
+                contentDescription = contentDescription,
+                onClick = { onCheckedChange(!checked) },
+                modifier = modifier,
+                enabled = enabled,
+                selected = checked,
+                onLongClick = onLongClick,
+                size = smallContainerSize(),
+                iconSize = smallIconSize,
+                style = when (style) {
+                    ToggleStyle.Outlined -> SeriesIconButtonStyle.Outlined
+                    ToggleStyle.Tonal -> SeriesIconButtonStyle.Tonal
+                }
+            )
         }
         }
     }
     }
 }
 }

+ 22 - 24
app/src/main/java/io/legado/app/ui/widget/components/button/series/SmallTonalButton.kt

@@ -25,30 +25,28 @@ fun SmallTonalButton(
     selectedContentColor: Color = LegadoTheme.colorScheme.onPrimaryContainer,
     selectedContentColor: Color = LegadoTheme.colorScheme.onPrimaryContainer,
     contentDescription: String? = null
     contentDescription: String? = null
 ) {
 ) {
-    SmallNoMinTouchTarget {
-        SeriesButton(
-            onClick = onClick,
-            modifier = modifier,
-            enabled = enabled,
-            selected = selected,
-            onLongClick = onLongClick,
-            size = if (text == null) smallContainerSize() else null,
-            style = SeriesIconButtonStyle.Tonal,
+    SeriesButton(
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        selected = selected,
+        onLongClick = onLongClick,
+        size = if (text == null) smallContainerSize() else null,
+        style = SeriesIconButtonStyle.Tonal,
+        contentColor = contentColor,
+        containerColor = containerColor,
+        selectedContainerColor = selectedContainerColor,
+        selectedContentColor = selectedContentColor
+    ) { contentColor ->
+        SeriesButtonContent(
+            icon = icon,
+            text = text,
+            contentDescription = contentDescription,
+            iconSize = smallIconSize,
+            textStyle = LegadoTheme.typography.labelMedium,
             contentColor = contentColor,
             contentColor = contentColor,
-            containerColor = containerColor,
-            selectedContainerColor = selectedContainerColor,
-            selectedContentColor = selectedContentColor
-        ) { contentColor ->
-            SeriesButtonContent(
-                icon = icon,
-                text = text,
-                contentDescription = contentDescription,
-                iconSize = smallIconSize,
-                textStyle = LegadoTheme.typography.labelMedium,
-                contentColor = contentColor,
-                padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
-                spacing = 4.dp
-            )
-        }
+            padding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
+            spacing = 4.dp
+        )
     }
     }
 }
 }

+ 21 - 10
app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt

@@ -32,10 +32,10 @@ import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.selected
 import androidx.compose.ui.semantics.selected
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -45,6 +45,7 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.AdaptiveSwitch
 import io.legado.app.ui.widget.components.AdaptiveSwitch
+import io.legado.app.ui.widget.components.reorderAccessibility
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.checkBox.AppCheckbox
 import io.legado.app.ui.widget.components.checkBox.AppCheckbox
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.icon.AppIcons
@@ -73,7 +74,6 @@ fun SelectionItemCard(
     containerColor: Color? = null,
     containerColor: Color? = null,
     selectedContainerColor: Color? = null,
     selectedContainerColor: Color? = null,
     contentDescription: String? = null,
     contentDescription: String? = null,
-    stateDescription: String? = null,
     enableSwitchContentDescription: String? = null,
     enableSwitchContentDescription: String? = null,
     editContentDescription: String? = null,
     editContentDescription: String? = null,
     moreContentDescription: String? = null
     moreContentDescription: String? = null
@@ -95,12 +95,11 @@ fun SelectionItemCard(
         modifier = modifier
         modifier = modifier
             .fillMaxWidth()
             .fillMaxWidth()
             .then(
             .then(
-                if (contentDescription != null || stateDescription != null || isSelected) {
+                if (contentDescription != null || inSelectionMode) {
                     Modifier.semantics {
                     Modifier.semantics {
                         contentDescription?.let { this.contentDescription = it }
                         contentDescription?.let { this.contentDescription = it }
-                        stateDescription?.let { this.stateDescription = it }
-                        if (isSelected) {
-                            this.selected = true
+                        if (inSelectionMode) {
+                            selected = isSelected
                         }
                         }
                         role = Role.Button
                         role = Role.Button
                     }
                     }
@@ -172,7 +171,9 @@ fun SelectionItemCardContent(
                     if (selectionMode) {
                     if (selectionMode) {
                         AppCheckbox(
                         AppCheckbox(
                             checked = isSelected,
                             checked = isSelected,
-                            onCheckedChange = null
+                            onCheckedChange = null,
+                            includeStateSemantics = false,
+                            modifier = Modifier.clearAndSetSemantics { }
                         )
                         )
                     } else {
                     } else {
                         leadingContent?.invoke()
                         leadingContent?.invoke()
@@ -307,12 +308,22 @@ fun LazyItemScope.ReorderableSelectionItem(
     containerColor: Color? = null,
     containerColor: Color? = null,
     selectedContainerColor: Color? = null,
     selectedContainerColor: Color? = null,
     contentDescription: String? = null,
     contentDescription: String? = null,
-    stateDescription: String? = null,
     enableSwitchContentDescription: String? = null,
     enableSwitchContentDescription: String? = null,
     editContentDescription: String? = null,
     editContentDescription: String? = null,
-    moreContentDescription: String? = null
+    moreContentDescription: String? = null,
+    reorderIndex: Int? = null,
+    reorderItemCount: Int = 0,
+    onMoveItem: ((from: Int, to: Int) -> Unit)? = null,
 ) {
 ) {
     val hapticFeedback = LocalHapticFeedback.current
     val hapticFeedback = LocalHapticFeedback.current
+    val reorderAccessibilityModifier = if (reorderIndex != null && onMoveItem != null) {
+        Modifier.reorderAccessibility(
+            index = reorderIndex,
+            itemCount = reorderItemCount,
+            enabled = canReorder && !inSelectionMode,
+            onMove = onMoveItem,
+        )
+    } else Modifier
 
 
     ReorderableItem(state, key = key) { isDragging ->
     ReorderableItem(state, key = key) { isDragging ->
         val elevation by animateDpAsState(
         val elevation by animateDpAsState(
@@ -337,11 +348,11 @@ fun LazyItemScope.ReorderableSelectionItem(
             containerColor = containerColor,
             containerColor = containerColor,
             selectedContainerColor = selectedContainerColor,
             selectedContainerColor = selectedContainerColor,
             contentDescription = contentDescription,
             contentDescription = contentDescription,
-            stateDescription = stateDescription,
             enableSwitchContentDescription = enableSwitchContentDescription,
             enableSwitchContentDescription = enableSwitchContentDescription,
             editContentDescription = editContentDescription,
             editContentDescription = editContentDescription,
             moreContentDescription = moreContentDescription,
             moreContentDescription = moreContentDescription,
             modifier = modifier
             modifier = modifier
+                .then(reorderAccessibilityModifier)
                 .zIndex(if (isDragging) 1f else 0f)
                 .zIndex(if (isDragging) 1f else 0f)
                 .then(
                 .then(
                     if (canReorder && !inSelectionMode) {
                     if (canReorder && !inSelectionMode) {

+ 8 - 4
app/src/main/java/io/legado/app/ui/widget/components/changeSource/ChangeSourceSheet.kt

@@ -221,7 +221,8 @@ fun ChangeSourceSheet(
                 Box {
                 Box {
                     MediumPlainButton(
                     MediumPlainButton(
                         onClick = { showOptionsMenu = true },
                         onClick = { showOptionsMenu = true },
-                        icon = Icons.Default.MoreVert
+                icon = Icons.Default.MoreVert,
+                contentDescription = stringResource(R.string.more_menu)
                     )
                     )
                     RoundDropdownMenu(
                     RoundDropdownMenu(
                         expanded = showOptionsMenu,
                         expanded = showOptionsMenu,
@@ -270,7 +271,8 @@ fun ChangeSourceSheet(
                 }
                 }
                 MediumPlainButton(
                 MediumPlainButton(
                     onClick = { showMigrationOptions = true },
                     onClick = { showMigrationOptions = true },
-                    icon = Icons.Outlined.Settings
+                    icon = Icons.Outlined.Settings,
+                    contentDescription = stringResource(R.string.setting)
                 )
                 )
             }
             }
         },
         },
@@ -279,10 +281,12 @@ fun ChangeSourceSheet(
                 MediumPlainButton(
                 MediumPlainButton(
                     onClick = { viewModel.startOrStopSearch() },
                     onClick = { viewModel.startOrStopSearch() },
                     icon = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
                     icon = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
+                    contentDescription = stringResource(if (isSearching) R.string.pause else R.string.refresh),
                 )
                 )
                 MediumPlainButton(
                 MediumPlainButton(
                     onClick = { showFilterSheet = true },
                     onClick = { showFilterSheet = true },
-                    icon = Icons.Default.FilterList
+                    icon = Icons.Default.FilterList,
+                    contentDescription = stringResource(R.string.screen)
                 )
                 )
             }
             }
         }
         }
@@ -339,7 +343,7 @@ fun ChangeSourceSheet(
                                 },
                                 },
                                 icon = Icons.Default.PushPin,
                                 icon = Icons.Default.PushPin,
                                 tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
                                 tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
-                                contentDescription = null
+                                contentDescription = stringResource(R.string.a11y_pin_source)
                             )
                             )
                         },
                         },
                         supportingContent = {
                         supportingContent = {

+ 2 - 7
app/src/main/java/io/legado/app/ui/widget/components/checkBox/AppCheckbox.kt

@@ -4,11 +4,9 @@ import androidx.compose.material3.Checkbox
 import androidx.compose.material3.TriStateCheckbox
 import androidx.compose.material3.TriStateCheckbox
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.state.ToggleableState
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
-import io.legado.app.R
+import androidx.compose.ui.semantics.toggleableState
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import top.yukonga.miuix.kmp.basic.Checkbox as MiuixCheckbox
 import top.yukonga.miuix.kmp.basic.Checkbox as MiuixCheckbox
@@ -22,12 +20,9 @@ fun AppCheckbox(
     includeStateSemantics: Boolean = true
     includeStateSemantics: Boolean = true
 ) {
 ) {
     val state = if (checked) ToggleableState.On else ToggleableState.Off
     val state = if (checked) ToggleableState.On else ToggleableState.Off
-    val checkboxStateDescription = stringResource(
-        if (checked) R.string.a11y_selected else R.string.a11y_not_selected
-    )
     val semanticModifier = if (includeStateSemantics) {
     val semanticModifier = if (includeStateSemantics) {
         modifier.semantics {
         modifier.semantics {
-            stateDescription = checkboxStateDescription
+            toggleableState = state
         }
         }
     } else {
     } else {
         modifier
         modifier

+ 7 - 17
app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt

@@ -1,7 +1,7 @@
 package io.legado.app.ui.widget.components.checkBox
 package io.legado.app.ui.widget.components.checkBox
 
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.selection.toggleable
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
@@ -12,15 +12,9 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.clearAndSetSemantics
-import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
-import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
@@ -34,10 +28,6 @@ fun CheckboxItem(
     onCheckedChange: (Boolean) -> Unit
     onCheckedChange: (Boolean) -> Unit
 ) {
 ) {
     val alpha = if (enabled) 1f else 0.5f
     val alpha = if (enabled) 1f else 0.5f
-    val checkboxStateDescription = stringResource(
-        if (checked) R.string.a11y_selected else R.string.a11y_not_selected
-    )
-
     GlassCard(
     GlassCard(
         cornerRadius = 12.dp,
         cornerRadius = 12.dp,
         containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color,
         containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color,
@@ -45,12 +35,12 @@ fun CheckboxItem(
         Row(
         Row(
             modifier = Modifier
             modifier = Modifier
                 .fillMaxWidth()
                 .fillMaxWidth()
-                .semantics(mergeDescendants = true) {
-                    role = Role.Checkbox
-                    stateDescription = checkboxStateDescription
-                    if (!enabled) disabled()
-                }
-                .clickable(enabled = enabled) { onCheckedChange(!checked) }
+                .toggleable(
+                    value = checked,
+                    enabled = enabled,
+                    role = Role.Checkbox,
+                    onValueChange = onCheckedChange
+                )
                 .padding(vertical = 12.dp, horizontal = 12.dp),
                 .padding(vertical = 12.dp, horizontal = 12.dp),
             verticalAlignment = Alignment.CenterVertically
             verticalAlignment = Alignment.CenterVertically
         ) {
         ) {

+ 50 - 39
app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapCalendarComponents.kt

@@ -83,6 +83,7 @@ fun HeatmapCalendarEndAction(
     MediumOutlinedButton(
     MediumOutlinedButton(
         onClick = onClearDate,
         onClick = onClearDate,
         icon = Icons.Outlined.Delete,
         icon = Icons.Outlined.Delete,
+        contentDescription = stringResource(R.string.clear),
     )
     )
 }
 }
 
 
@@ -132,6 +133,7 @@ fun WeekdayLabelsColumn(
 @Composable
 @Composable
 fun NoEarlierDataIndicator(
 fun NoEarlierDataIndicator(
     cellSize: Dp,
     cellSize: Dp,
+    touchTargetSize: Dp = cellSize,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
     val outlineColor = MaterialTheme.colorScheme.outlineVariant
     val outlineColor = MaterialTheme.colorScheme.outlineVariant
@@ -147,23 +149,29 @@ fun NoEarlierDataIndicator(
             repeat(7) {
             repeat(7) {
                 Box(
                 Box(
                     modifier = Modifier
                     modifier = Modifier
-                        .size(cellSize)
-                        .drawBehind {
-                            val strokeWidth = 1.dp.toPx()
-                            val stroke = Stroke(
-                                width = strokeWidth,
-                                pathEffect = PathEffect.dashPathEffect(floatArrayOf(5f, 5f), 0f)
-                            )
-                            val inset = strokeWidth / 2
-                            drawRoundRect(
-                                color = outlineColor,
-                                style = stroke,
-                                topLeft = Offset(inset, inset),
-                                size = Size(size.width - strokeWidth, size.height - strokeWidth),
-                                cornerRadius = CornerRadius(4.dp.toPx())
-                            )
-                        }
-                )
+                        .size(maxOf(cellSize, touchTargetSize)),
+                    contentAlignment = Alignment.Center,
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .size(cellSize)
+                            .drawBehind {
+                                val strokeWidth = 1.dp.toPx()
+                                val stroke = Stroke(
+                                    width = strokeWidth,
+                                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(5f, 5f), 0f)
+                                )
+                                val inset = strokeWidth / 2
+                                drawRoundRect(
+                                    color = outlineColor,
+                                    style = stroke,
+                                    topLeft = Offset(inset, inset),
+                                    size = Size(size.width - strokeWidth, size.height - strokeWidth),
+                                    cornerRadius = CornerRadius(4.dp.toPx())
+                                )
+                            }
+                    )
+                }
             }
             }
         }
         }
 
 
@@ -196,7 +204,7 @@ fun HeatmapCalendarCell(
     dailyReadTimes: Map<LocalDate, Long>,
     dailyReadTimes: Map<LocalDate, Long>,
     isSelected: Boolean,
     isSelected: Boolean,
     config: HeatmapConfig,
     config: HeatmapConfig,
-    onDateSelected: (LocalDate) -> Unit,
+    onDateSelected: ((LocalDate) -> Unit)?,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
     val level = rememberHeatmapLevel(day, mode, dailyReadCounts, dailyReadTimes)
     val level = rememberHeatmapLevel(day, mode, dailyReadCounts, dailyReadTimes)
@@ -208,31 +216,34 @@ fun HeatmapCalendarCell(
     } else {
     } else {
         stringResource(R.string.a11y_heatmap_day_duration, day.toString(), readDuration)
         stringResource(R.string.a11y_heatmap_day_duration, day.toString(), readDuration)
     }
     }
-    val selectedStateDescription = if (isSelected) {
-        stringResource(R.string.a11y_selected)
-    } else {
-        stringResource(R.string.a11y_not_selected)
-    }
-
     Box(
     Box(
         modifier = modifier
         modifier = modifier
-            .size(config.cellSize)
+            .size(config.interactiveCellSize)
+            .then(
+                onDateSelected?.let { onSelect ->
+                    Modifier.clickable { onSelect(day) }
+                } ?: Modifier
+            )
             .semantics {
             .semantics {
                 contentDescription = cellDescription
                 contentDescription = cellDescription
-                stateDescription = selectedStateDescription
                 if (isSelected) {
                 if (isSelected) {
                     selected = true
                     selected = true
                 }
                 }
-            }
-            .clip(RoundedCornerShape(config.cornerRadius))
-            .background(cellColor)
-            .border(
-                width = if (isSelected) 2.dp else 0.dp,
-                color = if (isSelected) MaterialTheme.colorScheme.onSurface else Color.Transparent,
-                shape = RoundedCornerShape(config.cornerRadius)
-            )
-            .clickable { onDateSelected(day) }
-    )
+            },
+        contentAlignment = Alignment.Center,
+    ) {
+        Box(
+            modifier = Modifier
+                .size(config.cellSize)
+                .clip(RoundedCornerShape(config.cornerRadius))
+                .background(cellColor)
+                .border(
+                    width = if (isSelected) 2.dp else 0.dp,
+                    color = if (isSelected) MaterialTheme.colorScheme.onSurface else Color.Transparent,
+                    shape = RoundedCornerShape(config.cornerRadius)
+                )
+        )
+    }
 }
 }
 
 
 /**
 /**
@@ -246,19 +257,19 @@ fun HeatmapWeekColumn(
     dailyReadTimes: Map<LocalDate, Long>,
     dailyReadTimes: Map<LocalDate, Long>,
     selectedDate: LocalDate?,
     selectedDate: LocalDate?,
     config: HeatmapConfig,
     config: HeatmapConfig,
-    onDateSelected: (LocalDate) -> Unit,
+    onDateSelected: ((LocalDate) -> Unit)?,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
     val firstDayOfMonth = week.firstOrNull { it?.dayOfMonth == 1 }
     val firstDayOfMonth = week.firstOrNull { it?.dayOfMonth == 1 }
 
 
-    Box(modifier = modifier.width(config.cellSize)) {
+    Box(modifier = modifier.width(config.interactiveCellSize)) {
         Column(
         Column(
             modifier = Modifier.padding(top = 24.dp),
             modifier = Modifier.padding(top = 24.dp),
             verticalArrangement = Arrangement.spacedBy(config.cellSpacing)
             verticalArrangement = Arrangement.spacedBy(config.cellSpacing)
         ) {
         ) {
             week.forEach { day ->
             week.forEach { day ->
                 if (day == null) {
                 if (day == null) {
-                    Spacer(modifier = Modifier.size(config.cellSize))
+                    Spacer(modifier = Modifier.size(config.interactiveCellSize))
                 } else {
                 } else {
                     HeatmapCalendarCell(
                     HeatmapCalendarCell(
                         day = day,
                         day = day,

+ 5 - 1
app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapMode.kt

@@ -9,8 +9,12 @@ enum class HeatmapMode {
 
 
 data class HeatmapConfig(
 data class HeatmapConfig(
     val cellSize: Dp = 16.dp,
     val cellSize: Dp = 16.dp,
+    val touchTargetSize: Dp = 48.dp,
     val cellSpacing: Dp = 4.dp,
     val cellSpacing: Dp = 4.dp,
     val cornerRadius: Dp = 4.dp,
     val cornerRadius: Dp = 4.dp,
     val gradientWidth: Dp = 16.dp,
     val gradientWidth: Dp = 16.dp,
     val legendSize: Dp = 12.dp
     val legendSize: Dp = 12.dp
-)
+) {
+    val interactiveCellSize: Dp
+        get() = maxOf(cellSize, touchTargetSize)
+}

+ 2 - 1
app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt

@@ -54,7 +54,8 @@ fun AppLogSheet(
                     clearAllLogs()
                     clearAllLogs()
                     logs = emptyList()
                     logs = emptyList()
                 },
                 },
-                icon = Icons.Default.DeleteSweep
+                icon = Icons.Default.DeleteSweep,
+                contentDescription = stringResource(R.string.clear)
             )
             )
         }
         }
     ) {
     ) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/log/CrashLogSheet.kt

@@ -36,7 +36,7 @@ fun CrashLogSheet(
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
         endAction = {
         endAction = {
             IconButton(onClick = onClear) {
             IconButton(onClick = onClear) {
-                Icon(Icons.Default.DeleteSweep, contentDescription = "Clear")
+                Icon(Icons.Default.DeleteSweep, contentDescription = stringResource(R.string.clear))
             }
             }
         },
         },
         title = stringResource(R.string.crash_log),
         title = stringResource(R.string.crash_log),

+ 2 - 10
app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt

@@ -247,21 +247,13 @@ fun CompactSwitchSettingItem(
     enabled: Boolean = true,
     enabled: Boolean = true,
     onCheckedChange: (Boolean) -> Unit
     onCheckedChange: (Boolean) -> Unit
 ) {
 ) {
-    val switchStateDescription = stringResource(
-        if (checked) R.string.a11y_on else R.string.a11y_off
-    )
-
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         SwitchPreference(
         SwitchPreference(
             title = title,
             title = title,
             summary = description,
             summary = description,
             checked = checked,
             checked = checked,
             onCheckedChange = onCheckedChange,
             onCheckedChange = onCheckedChange,
-            modifier = Modifier.semantics(mergeDescendants = true) {
-                role = Role.Switch
-                stateDescription = switchStateDescription
-                if (!enabled) disabled()
-            },
+            modifier = Modifier,
             enabled = enabled,
             enabled = enabled,
         )
         )
     } else {
     } else {
@@ -273,7 +265,7 @@ fun CompactSwitchSettingItem(
             shape = shape,
             shape = shape,
             enabled = enabled,
             enabled = enabled,
             semanticRole = Role.Switch,
             semanticRole = Role.Switch,
-            semanticStateDescription = switchStateDescription,
+            semanticToggleState = checked,
             onClick = { if (enabled) onCheckedChange(!checked) },
             onClick = { if (enabled) onCheckedChange(!checked) },
             trailingContent = {
             trailingContent = {
                 Switch(
                 Switch(

+ 6 - 0
app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt

@@ -36,6 +36,8 @@ import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.card.SettingCard
 import io.legado.app.ui.widget.components.card.SettingCard
@@ -60,6 +62,7 @@ fun SettingItem(
     enabled: Boolean = true,
     enabled: Boolean = true,
     semanticRole: Role? = null,
     semanticRole: Role? = null,
     semanticStateDescription: String? = null,
     semanticStateDescription: String? = null,
+    semanticToggleState: Boolean? = null,
     expanded: Boolean = false,
     expanded: Boolean = false,
     onExpandChange: ((Boolean) -> Unit)? = null,
     onExpandChange: ((Boolean) -> Unit)? = null,
     expandContent: (@Composable ColumnScope.() -> Unit)? = null
     expandContent: (@Composable ColumnScope.() -> Unit)? = null
@@ -82,6 +85,9 @@ fun SettingItem(
                     .semantics(mergeDescendants = true) {
                     .semantics(mergeDescendants = true) {
                         semanticRole?.let { role = it }
                         semanticRole?.let { role = it }
                         semanticStateDescription?.let { stateDescription = it }
                         semanticStateDescription?.let { stateDescription = it }
+                        semanticToggleState?.let {
+                            toggleableState = if (it) ToggleableState.On else ToggleableState.Off
+                        }
                         if (!enabled) disabled()
                         if (!enabled) disabled()
                     }
                     }
                     .combinedClickable(
                     .combinedClickable(

+ 2 - 12
app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt

@@ -7,11 +7,8 @@ import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.stringResource
-import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.AdaptiveSwitch
 import io.legado.app.ui.widget.components.AdaptiveSwitch
@@ -30,9 +27,6 @@ fun SwitchSettingItem(
     onCheckedChange: (Boolean) -> Unit
     onCheckedChange: (Boolean) -> Unit
 ) {
 ) {
     val composeEngine = LegadoTheme.composeEngine
     val composeEngine = LegadoTheme.composeEngine
-    val switchStateDescription = stringResource(
-        if (checked) R.string.a11y_on else R.string.a11y_off
-    )
     SplicedColumnDivider()
     SplicedColumnDivider()
 
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
@@ -41,11 +35,7 @@ fun SwitchSettingItem(
             summary = description,
             summary = description,
             checked = checked,
             checked = checked,
             onCheckedChange = onCheckedChange,
             onCheckedChange = onCheckedChange,
-            modifier = Modifier.semantics(mergeDescendants = true) {
-                role = Role.Switch
-                stateDescription = switchStateDescription
-                if (!enabled) disabled()
-            },
+            modifier = Modifier,
             enabled = enabled,
             enabled = enabled,
         )
         )
     } else {
     } else {
@@ -56,7 +46,7 @@ fun SwitchSettingItem(
             color = color,
             color = color,
             enabled = enabled,
             enabled = enabled,
             semanticRole = Role.Switch,
             semanticRole = Role.Switch,
-            semanticStateDescription = switchStateDescription,
+            semanticToggleState = checked,
             onClick = { if (enabled) onCheckedChange(!checked) },
             onClick = { if (enabled) onCheckedChange(!checked) },
             trailingContent = {
             trailingContent = {
                 AdaptiveSwitch(
                 AdaptiveSwitch(

+ 7 - 5
app/src/main/java/io/legado/app/ui/widget/components/settingItem/TinySettingItems.kt

@@ -52,6 +52,8 @@ import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -82,6 +84,7 @@ fun TinySettingItem(
     enabled: Boolean = true,
     enabled: Boolean = true,
     semanticRole: Role? = null,
     semanticRole: Role? = null,
     semanticStateDescription: String? = null,
     semanticStateDescription: String? = null,
+    semanticToggleState: Boolean? = null,
     onClick: (() -> Unit)? = null,
     onClick: (() -> Unit)? = null,
     onLongClick: (() -> Unit)? = null,
     onLongClick: (() -> Unit)? = null,
 ) {
 ) {
@@ -105,6 +108,9 @@ fun TinySettingItem(
             .semantics(mergeDescendants = true) {
             .semantics(mergeDescendants = true) {
                 semanticRole?.let { role = it }
                 semanticRole?.let { role = it }
                 semanticStateDescription?.let { stateDescription = it }
                 semanticStateDescription?.let { stateDescription = it }
+                semanticToggleState?.let {
+                    toggleableState = if (it) ToggleableState.On else ToggleableState.Off
+                }
                 if (!enabled) disabled()
                 if (!enabled) disabled()
             },
             },
         cornerRadius = 12.dp,
         cornerRadius = 12.dp,
@@ -325,10 +331,6 @@ fun TinySwitchSettingItem(
     enabled: Boolean = true,
     enabled: Boolean = true,
     onCheckedChange: (Boolean) -> Unit,
     onCheckedChange: (Boolean) -> Unit,
 ) {
 ) {
-    val switchStateDescription = stringResource(
-        if (checked) R.string.a11y_on else R.string.a11y_off
-    )
-
     TinySettingItem(
     TinySettingItem(
         title = title,
         title = title,
         description = description,
         description = description,
@@ -337,7 +339,7 @@ fun TinySwitchSettingItem(
         color = color,
         color = color,
         enabled = enabled,
         enabled = enabled,
         semanticRole = Role.Switch,
         semanticRole = Role.Switch,
-        semanticStateDescription = switchStateDescription,
+        semanticToggleState = checked,
         trailingContent = {
         trailingContent = {
             TinySwitch(
             TinySwitch(
                 modifier = Modifier.clearAndSetSemantics { },
                 modifier = Modifier.clearAndSetSemantics { },

+ 2 - 1
app/src/main/res/layout/activity_audio_play.xml

@@ -83,6 +83,7 @@
                 android:layout_width="12dp"
                 android:layout_width="12dp"
                 android:layout_height="12dp"
                 android:layout_height="12dp"
                 android:layout_marginEnd="4dp"
                 android:layout_marginEnd="4dp"
+                android:importantForAccessibility="no"
                 android:src="@drawable/ic_timer_black_24dp" />
                 android:src="@drawable/ic_timer_black_24dp" />
 
 
             <TextView
             <TextView
@@ -413,4 +414,4 @@
 
 
     </LinearLayout>
     </LinearLayout>
 
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 190
app/src/main/res/layout/activity_book_info_edit.xml

@@ -1,190 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <io.legado.app.ui.widget.TitleBar
-        android:id="@+id/title_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:title="@string/book_info_edit" />
-
-    <androidx.core.widget.NestedScrollView
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"
-        android:fillViewport="true">
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:gravity="center_horizontal"
-            android:layout_marginHorizontal="16dp"
-            android:layout_marginVertical="8dp">
-
-            <io.legado.app.ui.widget.image.CoverImageView
-                android:id="@+id/iv_cover"
-                android:layout_width="120dp"
-                android:layout_height="172dp"
-                android:contentDescription="@string/img_cover"
-                android:scaleType="centerCrop"
-                android:layout_marginBottom="8dp"
-                android:src="@drawable/image_cover_default"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent"/>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/til_book_name"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
-                android:layout_marginBottom="12dp"
-                android:hint="@string/book_name"
-                app:layout_constraintTop_toBottomOf="@id/iv_cover">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/tie_book_name"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/til_book_author"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
-                android:layout_marginBottom="16dp"
-                android:hint="@string/author"
-                app:layout_constraintTop_toBottomOf="@id/til_book_name">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/tie_book_author"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/til_cover_url"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
-                android:layout_marginBottom="16dp"
-                android:hint="@string/cover_path"
-                app:layout_constraintTop_toBottomOf="@id/til_book_author">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/tie_cover_url"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/til_type"
-                style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_marginStart="8dp"
-                android:hint="@string/book_type"
-                app:layout_constraintStart_toEndOf="@id/tv_refresh_cover"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintBottom_toBottomOf="@id/iv_cover">
-
-                <AutoCompleteTextView
-                    android:id="@+id/actv_type"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:inputType="none" />
-
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <com.google.android.material.button.MaterialButtonGroup
-                android:id="@+id/bot"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="vertical"
-                android:layout_marginStart="8dp"
-                android:layout_marginBottom="12dp"
-                app:layout_constraintStart_toEndOf="@id/iv_cover"
-                app:layout_constraintTop_toTopOf="@id/iv_cover"
-                app:layout_constraintBottom_toTopOf="@id/tv_refresh_cover">
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/tv_select_cover"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:textSize="14sp"
-                    app:iconPadding="4dp"
-                    android:text="@string/select_local_image"
-                    app:icon="@drawable/ic_save"
-                    style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                    android:padding="8dp" />
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/tv_change_cover"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    app:icon="@drawable/ic_web_outline"
-                    android:textSize="14sp"
-                    app:iconPadding="4dp"
-                    style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                    android:layout_marginStart="8dp"
-                    android:padding="8dp"
-                    android:text="@string/change_cover_source" />
-
-            </com.google.android.material.button.MaterialButtonGroup>
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/tv_refresh_cover"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                app:icon="@drawable/ic_change_source"
-                style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                android:layout_marginStart="8dp"
-
-                android:padding="8dp"
-                android:contentDescription="@string/refresh_cover"
-                app:layout_constraintBottom_toBottomOf="@id/iv_cover"
-                app:layout_constraintStart_toEndOf="@id/iv_cover"/>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/til_book_jj"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
-                android:hint="@string/book_intro"
-                app:layout_constraintTop_toBottomOf="@id/til_cover_url">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/tie_book_intro"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-            <com.google.android.material.textfield.TextInputLayout
-                android:id="@+id/til_remark"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
-                android:hint="@string/book_remark"
-                app:layout_constraintTop_toBottomOf="@id/til_book_jj">
-
-                <com.google.android.material.textfield.TextInputEditText
-                    android:id="@+id/tie_remark"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content" />
-            </com.google.android.material.textfield.TextInputLayout>
-
-        </androidx.constraintlayout.widget.ConstraintLayout>
-    </androidx.core.widget.NestedScrollView>
-
-</LinearLayout>

+ 2 - 2
app/src/main/res/layout/activity_book_source_edit.xml

@@ -39,10 +39,10 @@
                 android:id="@+id/sp_type"
                 android:id="@+id/sp_type"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:contentDescription="@string/book_type"
                 android:contentDescription="@string/book_type"
                 android:entries="@array/book_type"
                 android:entries="@array/book_type"
-                android:prompt="@string/book_type"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:prompt="@string/book_type" />
 
 
             <io.legado.app.lib.theme.view.ThemeCheckBox
             <io.legado.app.lib.theme.view.ThemeCheckBox
                 android:id="@+id/cb_is_enable"
                 android:id="@+id/cb_is_enable"

+ 0 - 42
app/src/main/res/layout/activity_main.xml

@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <androidx.viewpager2.widget.ViewPager2
-        android:id="@+id/view_pager_main"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@id/navigation_rail_view"
-        app:layout_constraintTop_toTopOf="parent"
-
-        tools:ignore="SpeakableTextPresentCheck" />
-
-    <!-- 底部导航栏 -->
-    <com.google.android.material.bottomnavigation.BottomNavigationView
-        android:id="@+id/bottom_navigation_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:elevation="0dp"
-        android:minHeight="56dp"
-        android:visibility="visible"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:menu="@menu/main_bnv" />
-
-    <com.google.android.material.navigationrail.NavigationRailView
-        android:id="@+id/navigation_rail_view"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:background="?attr/colorSurfaceContainer"
-        android:visibility="visible"
-        app:headerLayout="@layout/navigation_rail_fab"
-        app:expandedMinWidth="0dp"
-        app:layout_constraintStart_toStartOf="parent"
-        app:menu="@menu/main_bnv"
-        app:menuGravity="center" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 15 - 13
app/src/main/res/layout/activity_rss_source_edit.xml

@@ -31,33 +31,33 @@
                 android:id="@+id/cb_is_enable"
                 android:id="@+id/cb_is_enable"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:checked="true"
                 android:checked="true"
-                android:text="@string/is_enable"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:text="@string/is_enable" />
 
 
             <io.legado.app.lib.theme.view.ThemeCheckBox
             <io.legado.app.lib.theme.view.ThemeCheckBox
                 android:id="@+id/cb_single_url"
                 android:id="@+id/cb_single_url"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:checked="false"
                 android:checked="false"
-                android:text="@string/single_url"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:text="@string/single_url" />
 
 
             <io.legado.app.lib.theme.view.ThemeCheckBox
             <io.legado.app.lib.theme.view.ThemeCheckBox
                 android:id="@+id/cb_is_enable_cookie"
                 android:id="@+id/cb_is_enable_cookie"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:checked="true"
                 android:checked="true"
-                android:text="@string/auto_save_cookie"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:text="@string/auto_save_cookie" />
 
 
             <io.legado.app.lib.theme.view.ThemeCheckBox
             <io.legado.app.lib.theme.view.ThemeCheckBox
                 android:id="@+id/cb_is_enable_preload"
                 android:id="@+id/cb_is_enable_preload"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:checked="false"
                 android:checked="false"
-                android:text="@string/enable_preload"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:text="@string/enable_preload" />
 
 
         </LinearLayout>
         </LinearLayout>
 
 
@@ -79,6 +79,7 @@
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:layout_height="match_parent"
                 android:gravity="center"
                 android:gravity="center"
+                android:labelFor="@id/sp_type"
                 android:text="@string/book_type"
                 android:text="@string/book_type"
                 tools:ignore="RtlHardcoded" />
                 tools:ignore="RtlHardcoded" />
 
 
@@ -86,14 +87,15 @@
                 android:id="@+id/sp_type"
                 android:id="@+id/sp_type"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:entries="@array/rss_type"
                 android:entries="@array/rss_type"
-                android:theme="@style/Spinner"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:theme="@style/Spinner" />
 
 
             <TextView
             <TextView
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:layout_height="match_parent"
                 android:gravity="center"
                 android:gravity="center"
+                android:labelFor="@id/ly_type"
                 android:text="@string/layout_type"
                 android:text="@string/layout_type"
                 tools:ignore="RtlHardcoded,RtlSymmetry" />
                 tools:ignore="RtlHardcoded,RtlSymmetry" />
 
 
@@ -101,9 +103,9 @@
                 android:id="@+id/ly_type"
                 android:id="@+id/ly_type"
                 android:layout_width="wrap_content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:minHeight="48dp"
                 android:entries="@array/layout_type"
                 android:entries="@array/layout_type"
-                android:theme="@style/Spinner"
-                tools:ignore="TouchTargetSizeCheck" />
+                android:theme="@style/Spinner" />
 
 
         </LinearLayout>
         </LinearLayout>
 
 
@@ -124,4 +126,4 @@
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         tools:listitem="@layout/item_source_edit" />
         tools:listitem="@layout/item_source_edit" />
 
 
-</LinearLayout>
+</LinearLayout>

+ 1 - 0
app/src/main/res/layout/activity_welcome.xml

@@ -20,6 +20,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginStart="16dp"
             android:layout_marginStart="16dp"
             android:layout_marginTop="48dp"
             android:layout_marginTop="48dp"
+            android:importantForAccessibility="no"
             android:scaleType="centerCrop"
             android:scaleType="centerCrop"
             android:src="@drawable/ic_launcher_foreground" />
             android:src="@drawable/ic_launcher_foreground" />
 
 

+ 0 - 318
app/src/main/res/layout/dialog_read_book_style.xml

@@ -1,318 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.core.widget.NestedScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tool="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-    <LinearLayout
-        android:id="@+id/root_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:gravity="center"
-            android:paddingBottom="8dp"
-            android:paddingHorizontal="16dp">
-
-            <com.google.android.material.bottomsheet.BottomSheetDragHandleView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:paddingVertical="8dp"
-                android:minHeight="0dp"
-                android:layout_gravity="center_horizontal" />
-
-            <androidx.constraintlayout.widget.ConstraintLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:paddingTop="8dp"
-                android:orientation="horizontal">
-
-                <TextView
-                    android:id="@+id/bottom_sheet_title"
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    app:layout_constraintBottom_toBottomOf="parent"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toTopOf="parent"
-                    android:text="@string/read_config"
-                    android:textSize="16sp"
-                    android:textStyle="bold" />
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/tv_day_night"
-                    style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                    android:layout_height="36dp"
-                    android:layout_width="wrap_content"
-                    android:layout_marginStart="8dp"
-                    android:textColor="?attr/colorTertiary"
-                    app:iconSize="16dp"
-                    app:icon="@drawable/ic_brightness"
-                    app:backgroundTint="?attr/colorTertiaryContainer"
-                    app:iconGravity="textStart"
-                    app:layout_constraintBottom_toBottomOf="parent"
-                    app:layout_constraintStart_toEndOf="@id/bottom_sheet_title"
-                    app:layout_constraintTop_toTopOf="parent" />
-            </androidx.constraintlayout.widget.ConstraintLayout>
-            
-        </LinearLayout>
-
-        <LinearLayout
-            android:id="@+id/set_bar"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical">
-
-            <com.google.android.material.button.MaterialButtonGroup
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:gravity="center_vertical"
-                android:orientation="horizontal"
-                android:layout_marginHorizontal="12dp" >
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/tv_text_font"
-                    style="@style/Widget.Material3Expressive.Button.OutlinedButton"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_weight="3"
-                    android:text="@string/text_and_padding"
-                    app:icon="@drawable/ic_cfg_txt"
-                    tool:ignore="SmallSp" />
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/tv_padding"
-                    style="@style/Widget.Material3Expressive.Button.OutlinedButton"
-                    android:layout_width="wrap_content"
-                    android:layout_weight="2"
-                    android:layout_height="wrap_content"
-                    android:text="@string/padding"
-                    app:icon="@drawable/ic_padding"
-                    tool:ignore="SmallSp" />
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/tv_tip"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_weight="2"
-                    style="@style/Widget.Material3Expressive.Button.OutlinedButton"
-                    android:text="@string/information"
-                    app:icon="@drawable/ic_header"
-                    tool:ignore="SmallSp" />
-
-            </com.google.android.material.button.MaterialButtonGroup>
-
-        </LinearLayout>
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="4dp"
-            android:orientation="horizontal"
-            android:gravity="bottom"
-            android:layout_marginHorizontal="16dp">
-
-            <io.legado.app.ui.widget.SimpleCounterView
-                android:id="@+id/dsb_text_size"
-                android:layout_width="128dp"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="2dp"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:max="45"
-                app:layout_constraintStart_toStartOf="parent"
-                app:orientation="vertical"
-                app:title="@string/text_size" />
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/btnChineseConverter"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                style="@style/Widget.Material3Expressive.Button.OutlinedButton"
-                android:minWidth="0dp"
-                android:minHeight="0dp"
-                app:iconPadding="0dp"
-                app:icon="@drawable/ic_cn_convert"
-                app:iconGravity="textStart"
-                app:iconSize="16dp"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintEnd_toStartOf="@id/tv_more"
-                app:layout_constraintStart_toEndOf="@id/dsb_text_size" />
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/tv_more"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                style="@style/Widget.Material3Expressive.Button.TonalButton"
-                app:icon="@drawable/ic_arrow_right"
-                android:text="@string/more_setting"
-                app:iconGravity="end"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintEnd_toEndOf="parent" />
-
-        </androidx.constraintlayout.widget.ConstraintLayout>
-
-
-        <LinearLayout
-            android:id="@+id/set_bottom_bar"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical">
-
-            <TextView
-                android:id="@+id/tv_page_anim"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginHorizontal="16dp"
-                android:layout_marginTop="8dp"
-                android:text="@string/page_anim"
-                android:textStyle="bold"
-                android:textSize="12sp" />
-
-            <HorizontalScrollView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:scrollbars="none">
-
-                <com.google.android.material.chip.ChipGroup
-                    android:id="@+id/rg_page_anim"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:paddingHorizontal="16dp"
-                    android:layout_marginVertical="8dp"
-                    app:singleLine="true"
-                    app:singleSelection="true"
-                    app:selectionRequired="true">
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/rb_anim0"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/page_anim_cover" />
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/rb_anim1"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/page_anim_slide" />
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/rb_simulation_anim"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/page_anim_simulation" />
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/rb_scroll_anim"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/page_anim_scroll" />
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/rb_fade_anim"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/page_anim_fade" />
-
-                    <com.google.android.material.chip.Chip
-                        android:id="@+id/rb_no_anim"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:checkable="true"
-                        android:text="@string/page_anim_none" />
-
-                </com.google.android.material.chip.ChipGroup>
-            </HorizontalScrollView>
-
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:paddingTop="8dp"
-                android:orientation="horizontal">
-
-                <TextView
-                    android:id="@+id/tv_bg_ts"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginStart="16dp"
-                    android:text="@string/text_bg_style"
-                    android:textSize="12sp"
-                    android:textStyle="bold" />
-
-                <TextView
-                    android:layout_width="0dp"
-                    android:layout_height="wrap_content"
-                    android:alpha="0.6"
-                    android:layout_weight="1"
-                    android:layout_marginStart="4dp"
-                    android:textColor="?attr/colorOnSurfaceVariant"
-                    android:text="@string/long_click_to_custom"
-                    android:textSize="10sp"
-                    android:textStyle="bold"
-                    tool:ignore="SmallSp" />
-
-            </LinearLayout>
-
-            <androidx.constraintlayout.widget.ConstraintLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:paddingBottom="16dp">
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/cb_share_layout"
-                    android:layout_width="48dp"
-                    android:layout_height="44dp"
-                    app:cornerRadius="8dp"
-                    style="@style/Widget.Material3Expressive.Button.TonalButton"
-                    android:checkable="true"
-                    android:clickable="true"
-                    android:layout_marginVertical="2dp"
-                    app:icon="@drawable/ic_share_layout"
-                    app:iconGravity="textStart"
-                    app:iconPadding="0dp"
-                    android:layout_marginStart="16dp"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toTopOf="parent"
-                    app:layout_constraintBottom_toBottomOf="parent"/>
-
-                <androidx.recyclerview.widget.RecyclerView
-                    android:id="@+id/rv_style"
-                    android:layout_width="0dp"
-                    android:layout_height="wrap_content"
-                    android:paddingHorizontal="8dp"
-                    android:clipToPadding="false"
-                    android:orientation="horizontal"
-                    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
-                    tool:listitem="@layout/item_read_style"
-                    app:layout_constraintStart_toEndOf="@id/cb_share_layout"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintTop_toTopOf="parent"
-                    app:layout_constraintBottom_toBottomOf="parent"/>
-
-                <ImageView
-                    android:layout_width="16dp"
-                    android:layout_height="48dp"
-                    android:importantForAccessibility="no"
-                    app:layout_constraintStart_toEndOf="@id/cb_share_layout"
-                    app:layout_constraintTop_toTopOf="parent"
-                    app:layout_constraintBottom_toBottomOf="parent"
-                    android:src="@drawable/left_fade"/>
-
-            </androidx.constraintlayout.widget.ConstraintLayout>
-
-
-
-        </LinearLayout>
-
-    </LinearLayout>
-</androidx.core.widget.NestedScrollView>

+ 2 - 1
app/src/main/res/layout/fragment_privacy.xml

@@ -58,9 +58,10 @@
     <ImageView
     <ImageView
         android:layout_width="match_parent"
         android:layout_width="match_parent"
         android:layout_height="60dp"
         android:layout_height="60dp"
+        android:importantForAccessibility="no"
         android:src="@drawable/bg_gradient_mask"
         android:src="@drawable/bg_gradient_mask"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent" />
         app:layout_constraintStart_toStartOf="parent" />
 
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 10 - 9
app/src/main/res/layout/item_book_source.xml

@@ -35,6 +35,7 @@
         android:id="@+id/cb_book_source"
         android:id="@+id/cb_book_source"
         android:layout_width="0dp"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
+        android:minHeight="48dp"
         android:layout_marginStart="4dp"
         android:layout_marginStart="4dp"
         android:layout_marginEnd="12dp"
         android:layout_marginEnd="12dp"
         android:scrollbars="none"
         android:scrollbars="none"
@@ -43,7 +44,6 @@
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toLeftOf="@id/swt_enabled"
         app:layout_constraintRight_toLeftOf="@id/swt_enabled"
         app:layout_constraintTop_toBottomOf="@id/barrier"
         app:layout_constraintTop_toBottomOf="@id/barrier"
-        tools:ignore="TouchTargetSizeCheck"
         tools:text="@string/book_source" />
         tools:text="@string/book_source" />
 
 
     <io.legado.app.lib.theme.view.ThemeSwitch
     <io.legado.app.lib.theme.view.ThemeSwitch
@@ -51,21 +51,22 @@
         android:name="@string/enable"
         android:name="@string/enable"
         android:layout_width="wrap_content"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
+        android:minHeight="48dp"
         android:layout_marginEnd="12dp"
         android:layout_marginEnd="12dp"
         android:scrollbars="none"
         android:scrollbars="none"
         app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
         app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
         app:layout_constraintRight_toLeftOf="@id/iv_edit"
         app:layout_constraintRight_toLeftOf="@id/iv_edit"
         app:layout_constraintTop_toBottomOf="@id/barrier"
         app:layout_constraintTop_toBottomOf="@id/barrier"
-        tools:ignore="RtlHardcoded,RtlSymmetry,TouchTargetSizeCheck" />
+        tools:ignore="RtlHardcoded,RtlSymmetry" />
 
 
     <androidx.appcompat.widget.AppCompatImageView
     <androidx.appcompat.widget.AppCompatImageView
         android:id="@+id/iv_edit"
         android:id="@+id/iv_edit"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
         android:layout_marginEnd="8dp"
         android:layout_marginEnd="8dp"
         android:background="?attr/selectableItemBackgroundBorderless"
         android:background="?attr/selectableItemBackgroundBorderless"
         android:layout_gravity="center"
         android:layout_gravity="center"
-        android:padding="4dp"
+        android:padding="12dp"
         android:contentDescription="@string/edit"
         android:contentDescription="@string/edit"
         android:scrollbars="none"
         android:scrollbars="none"
         android:src="@drawable/ic_edit"
         android:src="@drawable/ic_edit"
@@ -75,9 +76,9 @@
 
 
     <androidx.appcompat.widget.AppCompatImageView
     <androidx.appcompat.widget.AppCompatImageView
         android:id="@+id/iv_menu_more"
         android:id="@+id/iv_menu_more"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:padding="4dp"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:padding="12dp"
         android:background="?attr/selectableItemBackgroundBorderless"
         android:background="?attr/selectableItemBackgroundBorderless"
         android:contentDescription="@string/more_menu"
         android:contentDescription="@string/more_menu"
         android:scrollbars="none"
         android:scrollbars="none"
@@ -127,4 +128,4 @@
         app:layout_constraintTop_toTopOf="@id/iv_debug_text"
         app:layout_constraintTop_toTopOf="@id/iv_debug_text"
         tools:visibility="visible" />
         tools:visibility="visible" />
 
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 107
app/src/main/res/layout/item_chapter_list.xml

@@ -1,107 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/tv_chapter_item"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?attr/selectableItemBackground"
-    android:orientation="vertical"
-    android:paddingHorizontal="16dp"
-    android:paddingVertical="12dp">
-
-    <LinearLayout
-        android:id="@+id/ll_info"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="center"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent">
-
-        <ImageView
-            android:id="@+id/iv_locked"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:padding="4dp"
-            android:src="@drawable/ic_lock_outline"
-            android:visibility="visible" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/iv_volume"
-            android:layout_width="18dp"
-            android:layout_height="26dp"
-            android:layout_marginEnd="8dp"
-            android:checkable="true"
-            android:clickable="false"
-            app:iconPadding="0dp"
-            app:iconGravity="textStart"
-            app:iconSize="16dp"
-            style="@style/Widget.Material3Expressive.Button.TonalButton"
-            android:visibility="visible"
-            app:icon="@drawable/m3_split_button_chevron_avd" />
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/ll_chap"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/iv_checked"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toEndOf="@id/ll_info">
-
-        <TextView
-            android:id="@+id/tv_chapter_name"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:maxLines="2"
-            android:textSize="14sp"
-            android:textStyle="bold"
-            app:layout_constraintEnd_toStartOf="@+id/iv_checked"
-            app:layout_constraintStart_toEndOf="@+id/iv_locked"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <TextView
-            android:id="@+id/tv_word_count"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="4dp"
-            android:paddingEnd="18dp"
-            android:singleLine="true"
-            android:textSize="12sp"
-            android:visibility="gone"
-            app:layout_constraintEnd_toStartOf="@+id/iv_checked"
-            app:layout_constraintStart_toEndOf="@+id/iv_locked"
-            app:layout_constraintTop_toBottomOf="@id/tv_chapter_name"
-            tools:ignore="RtlSymmetry" />
-
-        <TextView
-            android:id="@+id/tv_tag"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="4dp"
-            android:singleLine="true"
-            android:textSize="12sp"
-            android:visibility="gone"
-            app:layout_constraintEnd_toStartOf="@+id/iv_checked"
-            app:layout_constraintStart_toEndOf="@+id/iv_locked"
-            app:layout_constraintTop_toBottomOf="@id/tv_word_count" />
-    </LinearLayout>
-
-    <ImageView
-        android:id="@+id/iv_checked"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:contentDescription="@string/success"
-        app:layout_constraintEnd_toEndOf="parent"
-        android:padding="4dp"
-        android:src="@drawable/ic_check"
-        android:visibility="invisible"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:tint="?attr/colorSecondary"
-        app:layout_constraintTop_toTopOf="parent" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 85
app/src/main/res/layout/item_read_record.xml

@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?attr/selectableItemBackground"
-    android:paddingStart="16dp"
-    android:paddingEnd="10dp"
-    android:paddingVertical="8dp">
-
-    <com.google.android.material.card.MaterialCardView
-        android:id="@+id/cd_cover"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        app:cardBackgroundColor="?attr/colorSurfaceContainer"
-        app:cardCornerRadius="6dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:strokeWidth="0dp">
-
-        <ImageView
-            android:id="@+id/iv_cover"
-            android:layout_width="60dp"
-            android:layout_height="80dp" />
-    </com.google.android.material.card.MaterialCardView>
-
-    <TextView
-        android:id="@+id/tv_book_name"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        style="@style/ChapterNameText"
-        android:layout_marginStart="8dp"
-        android:singleLine="true"
-        app:layout_constraintEnd_toEndOf="@+id/tv_remove"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toEndOf="@id/cd_cover"
-        tools:text="name"
-        tools:ignore="RtlHardcoded" />
-
-    <TextView
-        android:id="@+id/tv_reading_time_tag"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="8dp"
-        android:layout_marginTop="2dp"
-        android:singleLine="true"
-        android:text="@string/reading_time_tag"
-        app:layout_constraintStart_toEndOf="@id/cd_cover"
-        app:layout_constraintTop_toBottomOf="@id/tv_book_name" />
-
-    <TextView
-        android:id="@+id/tv_reading_time"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:singleLine="true"
-        app:layout_constraintBottom_toTopOf="@id/tv_reading_time_tag"
-        app:layout_constraintEnd_toStartOf="@+id/tv_remove"
-        app:layout_constraintStart_toEndOf="@+id/tv_reading_time_tag"
-        app:layout_constraintTop_toBottomOf="@id/tv_reading_time_tag"
-        tools:text="readingTime" />
-
-    <TextView
-        android:id="@+id/tv_last_read_time"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="8dp"
-        android:layout_marginTop="2dp"
-        android:singleLine="true"
-        app:layout_constraintLeft_toRightOf="@+id/cd_cover"
-        app:layout_constraintTop_toBottomOf="@id/tv_reading_time_tag"
-        app:layout_constraintRight_toLeftOf="@+id/tv_remove"
-        tools:text="readingTime" />
-
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/tv_remove"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-        app:icon="@drawable/ic_clear_all"
-        app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/tv_book_name" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 40
app/src/main/res/layout/item_read_style.xml

@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/ll_style"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:gravity="center">
-
-    <com.google.android.material.card.MaterialCardView
-        android:id="@+id/cd_style"
-        android:layout_width="48dp"
-        android:layout_height="36dp"
-        app:cardCornerRadius="8dp"
-        app:strokeColor="?attr/colorOutlineVariant"
-        app:strokeWidth="2dp"
-        android:layout_margin="4dp"
-        android:foreground="?android:attr/selectableItemBackground">
-
-        <ImageView
-            android:id="@+id/iv_style"
-            android:scaleType="centerCrop"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
-
-        <TextView
-            android:id="@+id/tv_style"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:textStyle="bold"
-            android:paddingHorizontal="4dp"
-            android:autoSizeMaxTextSize="10sp"
-            android:autoSizeMinTextSize="6sp"
-            android:autoSizeStepGranularity="1sp"
-            android:autoSizeTextType="uniform"
-            android:gravity="center"
-            android:singleLine="true"
-            android:text="@string/text" />
-    </com.google.android.material.card.MaterialCardView>
-
-</LinearLayout>

+ 0 - 83
app/src/main/res/layout/item_replace_rule.xml

@@ -1,83 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center"
-    android:orientation="horizontal"
-    android:paddingHorizontal="8dp"
-    android:paddingVertical="12dp"
-    android:background="?attr/selectableItemBackground">
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:gravity="center_vertical"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/iv_pin">
-
-        <com.google.android.material.checkbox.MaterialCheckBox
-            android:id="@+id/cb_name"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="4dp"
-            android:layout_marginEnd="8dp"
-            android:scrollbars="none"
-            android:maxLines="2"
-            android:ellipsize="end"
-            tools:ignore="RtlSymmetry" />
-
-    </LinearLayout>
-
-    <ImageView
-        android:id="@+id/iv_pin"
-        android:layout_width="16dp"
-        android:layout_height="16dp"
-        android:layout_marginEnd="16dp"
-        android:src="@drawable/ic_praise"
-        app:layout_constraintEnd_toStartOf="@id/swt_enabled"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"/>
-
-    <com.google.android.material.materialswitch.MaterialSwitch
-        android:id="@+id/swt_enabled"
-        android:name="@string/enable"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="8dp"
-        android:layout_marginEnd="12dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintRight_toLeftOf="@id/iv_edit"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:ignore="RtlSymmetry" />
-
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_edit"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:layout_gravity="center"
-        android:padding="4dp"
-        android:contentDescription="@string/edit"
-        android:layout_marginEnd="8dp"
-        android:src="@drawable/ic_edit"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintRight_toLeftOf="@id/iv_menu_more"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_menu_more"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:padding="4dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:contentDescription="@string/more_menu"
-        android:src="@drawable/ic_more_vert"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:ignore="RtlHardcoded" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 3 - 2
app/src/main/res/layout/item_source_edit.xml

@@ -11,9 +11,10 @@
         android:id="@+id/editText"
         android:id="@+id/editText"
         android:layout_width="match_parent"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
+        android:minHeight="48dp"
         style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
         style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
         android:padding="16dp"
         android:padding="16dp"
         android:inputType="textMultiLine"
         android:inputType="textMultiLine"
-        tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
+        tools:ignore="SpeakableTextPresentCheck" />
 
 
-</com.google.android.material.textfield.TextInputLayout>
+</com.google.android.material.textfield.TextInputLayout>

+ 7 - 7
app/src/main/res/layout/item_source_import.xml

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
     android:background="?attr/selectableItemBackground"
     android:background="?attr/selectableItemBackground"
@@ -11,17 +10,17 @@
         android:id="@+id/cb_source_name"
         android:id="@+id/cb_source_name"
         android:layout_width="0dp"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_height="wrap_content"
+        android:minHeight="48dp"
         android:maxLines="2"
         android:maxLines="2"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@+id/tv_source_state"
         app:layout_constraintEnd_toStartOf="@+id/tv_source_state"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        tools:ignore="TouchTargetSizeCheck" />
+        app:layout_constraintBottom_toBottomOf="parent" />
 
 
     <com.google.android.material.button.MaterialButton
     <com.google.android.material.button.MaterialButton
         android:id="@+id/tv_source_state"
         android:id="@+id/tv_source_state"
         android:layout_width="wrap_content"
         android:layout_width="wrap_content"
-        android:layout_height="40dp"
+        android:layout_height="48dp"
         style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"
         style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"
         android:minWidth="0dp"
         android:minWidth="0dp"
         android:textSize="11sp"
         android:textSize="11sp"
@@ -33,13 +32,14 @@
 
 
     <com.google.android.material.button.MaterialButton
     <com.google.android.material.button.MaterialButton
         android:id="@+id/tv_open"
         android:id="@+id/tv_open"
-        android:layout_width="40dp"
-        android:layout_height="40dp"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
         style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
         style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
+        android:contentDescription="@string/edit"
         app:icon="@drawable/ic_edit"
         app:icon="@drawable/ic_edit"
         app:iconGravity="textStart"
         app:iconGravity="textStart"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
         app:layout_constraintTop_toTopOf="parent" />
 
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 56
app/src/main/res/layout/item_tool_button.xml

@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<com.google.android.material.card.MaterialCardView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    app:strokeWidth="0dp"
-    android:layout_marginHorizontal="16dp"
-    android:layout_marginVertical="8dp">
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:clickable="true"
-        android:background="?attr/colorSurface"
-        android:foreground="?attr/selectableItemBackground"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        android:paddingHorizontal="16dp"
-        android:paddingVertical="8dp">
-
-        <!-- 图标 -->
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/ivIcon"
-            style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:clickable="false" />
-
-        <!-- 按钮名称 -->
-        <TextView
-            android:id="@+id/tvName"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="12dp"
-            android:layout_weight="1"
-            android:text=""
-            android:textColor="?attr/colorOnSurface"
-            android:textSize="16sp" />
-
-        <!-- 禁用/隐藏按钮 -->
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/btnDisable"
-            style="@style/Widget.Material3Expressive.Button.IconButton.Outlined"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="8dp"
-            app:icon="@drawable/ic_visibility_on" />
-
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="4dp"
-            android:src="@drawable/ic_drag_handle" />
-
-    </LinearLayout>
-</com.google.android.material.card.MaterialCardView>

+ 0 - 32
app/src/main/res/layout/navigation_rail_fab.xml

@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/navigation_rail_fab"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:layout_gravity="top|start"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:orientation="vertical">
-
-    <ImageView
-        android:id="@+id/nav_botton"
-        android:layout_width="56dp"
-        android:layout_height="56dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:scaleType="center"
-        android:src="@drawable/ic_menu" />
-
-    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
-        android:id="@+id/nav_fab"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:backgroundTint="?attr/colorTertiaryContainer"
-        app:iconTint="?attr/colorOnTertiaryContainer"
-        android:textColor="?attr/colorOnTertiaryContainer"
-        android:minWidth="0dp"
-        android:scrollbars="none"
-        android:text="@string/search"
-        app:icon="@drawable/ic_search" />
-
-</LinearLayout>

Некоторые файлы не были показаны из-за большого количества измененных файлов