Jelajahi Sumber

增强所有界面无障碍体验

a328929 4 bulan lalu
induk
melakukan
517ec0b2cd
86 mengubah file dengan 2022 tambahan dan 282 penghapusan
  1. 2 0
      app/src/main/java/io/legado/app/lib/prefs/Preference.kt
  2. 12 2
      app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt
  3. 6 1
      app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt
  4. 17 1
      app/src/main/java/io/legado/app/lib/prefs/ThemeModePreference.kt
  5. 3 3
      app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
  6. 37 10
      app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt
  7. 115 27
      app/src/main/java/io/legado/app/ui/book/cache/manage/BookCacheManageScreen.kt
  8. 4 2
      app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
  9. 23 6
      app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
  10. 30 0
      app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt
  11. 145 23
      app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt
  12. 46 14
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewScreen.kt
  13. 131 41
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt
  14. 3 1
      app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt
  15. 2 1
      app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt
  16. 4 1
      app/src/main/java/io/legado/app/ui/book/search/SearchSourceSection.kt
  17. 19 1
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt
  18. 2 1
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt
  19. 64 7
      app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt
  20. 34 2
      app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
  21. 6 1
      app/src/main/java/io/legado/app/ui/config/ai/AiProviderEditScreen.kt
  22. 15 1
      app/src/main/java/io/legado/app/ui/config/readConfig/TextSelectMenuFilterSheet.kt
  23. 10 3
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
  24. 32 4
      app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
  25. 3 2
      app/src/main/java/io/legado/app/ui/highlightTagRule/HighlightTagRuleEditSheet.kt
  26. 38 3
      app/src/main/java/io/legado/app/ui/highlightTagRule/HighlightTagRuleScreen.kt
  27. 9 4
      app/src/main/java/io/legado/app/ui/main/MainScreen.kt
  28. 77 4
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
  29. 16 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  30. 2 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt
  31. 22 2
      app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt
  32. 32 4
      app/src/main/java/io/legado/app/ui/main/home/HomeScreen.kt
  33. 7 2
      app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleFeed.kt
  34. 17 1
      app/src/main/java/io/legado/app/ui/main/homepage/modules/BannerModule.kt
  35. 2 1
      app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt
  36. 1 2
      app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
  37. 29 4
      app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt
  38. 33 0
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
  39. 2 1
      app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt
  40. 14 1
      app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt
  41. 1 1
      app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt
  42. 1 2
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt
  43. 1 1
      app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt
  44. 2 2
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceScreen.kt
  45. 1 1
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt
  46. 12 6
      app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt
  47. 14 1
      app/src/main/java/io/legado/app/ui/widget/SimpleSliderView.kt
  48. 22 2
      app/src/main/java/io/legado/app/ui/widget/components/AppSlider.kt
  49. 6 0
      app/src/main/java/io/legado/app/ui/widget/components/AppVerticalSlider.kt
  50. 49 10
      app/src/main/java/io/legado/app/ui/widget/components/IconSwitch.kt
  51. 16 8
      app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt
  52. 10 8
      app/src/main/java/io/legado/app/ui/widget/components/bookmark/BookmarkEditSheet.kt
  53. 24 2
      app/src/main/java/io/legado/app/ui/widget/components/bookmark/BookmarkItem.kt
  54. 61 9
      app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
  55. 19 4
      app/src/main/java/io/legado/app/ui/widget/components/checkBox/AppCheckbox.kt
  56. 21 2
      app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt
  57. 18 2
      app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt
  58. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindSelectSheet.kt
  59. 6 1
      app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindTextField.kt
  60. 24 0
      app/src/main/java/io/legado/app/ui/widget/components/heatmap/HeatmapCalendarComponents.kt
  61. 3 2
      app/src/main/java/io/legado/app/ui/widget/components/rules/RuleEditSheet.kt
  62. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt
  63. 37 3
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt
  64. 15 0
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt
  65. 15 3
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
  66. 23 1
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt
  67. 24 1
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/TinySettingItems.kt
  68. 2 1
      app/src/main/java/io/legado/app/ui/widget/components/swipe/SwipeAction.kt
  69. 35 2
      app/src/main/java/io/legado/app/ui/widget/components/swipe/SwipeActionContainer.kt
  70. 13 6
      app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt
  71. 2 1
      app/src/main/java/io/legado/app/ui/widget/components/topbar/TopBarButton.kt
  72. 5 1
      app/src/main/res/layout/activity_book_source_edit.xml
  73. 2 1
      app/src/main/res/layout/activity_source_debug.xml
  74. 2 1
      app/src/main/res/layout/dialog_manga_auto_read.xml
  75. 1 0
      app/src/main/res/layout/dialog_manga_footer_setting.xml
  76. 1 0
      app/src/main/res/layout/item_search_history.xml
  77. 6 2
      app/src/main/res/layout/item_theme_card.xml
  78. 2 1
      app/src/main/res/layout/preference_widget_material_switch.xml
  79. 2 1
      app/src/main/res/layout/view_icon.xml
  80. 7 2
      app/src/main/res/layout/view_manga_menu.xml
  81. 4 2
      app/src/main/res/layout/view_preference.xml
  82. 1 1
      app/src/main/res/layout/view_preference_tip.xml
  83. 119 0
      app/src/main/res/values-zh-rCN/strings.xml
  84. 103 0
      app/src/main/res/values-zh-rHK/strings.xml
  85. 103 0
      app/src/main/res/values-zh-rTW/strings.xml
  86. 119 0
      app/src/main/res/values/strings.xml

+ 2 - 0
app/src/main/java/io/legado/app/lib/prefs/Preference.kt

@@ -10,6 +10,7 @@ import android.widget.ImageView
 import android.widget.TextView
 import androidx.core.view.isGone
 import androidx.core.view.isVisible
+import androidx.core.view.ViewCompat
 import androidx.preference.PreferenceViewHolder
 import io.legado.app.R
 //import io.legado.app.lib.theme.accentColor
@@ -47,6 +48,7 @@ open class Preference(context: Context, attrs: AttributeSet?) :
             isBottomBackground: Boolean = false
         ): T? {
             if (viewHolder == null) return null
+            ViewCompat.setScreenReaderFocusable(viewHolder.itemView, true)
             val tvTitle = viewHolder.findViewById(R.id.preference_title) as? TextView
             tvTitle?.let {
                 tvTitle.text = title

+ 12 - 2
app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt

@@ -2,11 +2,13 @@ package io.legado.app.lib.prefs
 
 import android.content.Context
 import android.util.AttributeSet
+import android.view.View
 import androidx.appcompat.widget.SwitchCompat
 import androidx.preference.PreferenceViewHolder
 import androidx.preference.SwitchPreferenceCompat
-import io.legado.app.R
 import androidx.core.content.withStyledAttributes
+import androidx.core.view.ViewCompat
+import io.legado.app.R
 
 //import io.legado.app.lib.theme.accentColor
 
@@ -25,7 +27,7 @@ class SwitchPreference(context: Context, attrs: AttributeSet) :
     }
 
     override fun onBindViewHolder(holder: PreferenceViewHolder) {
-        Preference.bindView<SwitchCompat>(
+        val switchView = Preference.bindView<SwitchCompat>(
             context, holder, icon, title, summary,
             widgetLayoutResource,
             androidx.preference.R.id.switchWidget,
@@ -33,6 +35,14 @@ class SwitchPreference(context: Context, attrs: AttributeSet) :
         )
 
         super.onBindViewHolder(holder)
+        val stateDescription = context.getString(
+            if (isChecked) R.string.a11y_on else R.string.a11y_off
+        )
+        ViewCompat.setScreenReaderFocusable(holder.itemView, true)
+        ViewCompat.setStateDescription(holder.itemView, stateDescription)
+        switchView?.let {
+            it.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+        }
         onLongClick?.let { listener ->
             holder.itemView.setOnLongClickListener {
                 listener.invoke(this)

+ 6 - 1
app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt

@@ -12,6 +12,7 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
+import androidx.core.view.ViewCompat
 import androidx.preference.PreferenceViewHolder
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -66,6 +67,7 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co
 
             holder.label.text = label
             holder.card.isChecked = (value == currentValue)
+            holder.card.contentDescription = label
 
             val colors = getThemeColors(value)
             holder.colorTop.setCardBackgroundColor(colors[0])
@@ -78,6 +80,10 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co
             val isSelected = (value == currentValue)
             holder.background.strokeColor = if (isSelected) colors[4] else colors[7]
             holder.card.checkedIconTint = ColorStateList.valueOf(colors[2])
+            ViewCompat.setStateDescription(
+                holder.card,
+                context.getString(if (isSelected) R.string.a11y_selected else R.string.a11y_not_selected)
+            )
 
 
             holder.card.setOnClickListener {
@@ -173,4 +179,3 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co
     }
 }
 
-

+ 17 - 1
app/src/main/java/io/legado/app/lib/prefs/ThemeModePreference.kt

@@ -4,6 +4,8 @@ import android.content.Context
 import android.os.Handler
 import android.os.Looper
 import android.util.AttributeSet
+import android.view.View
+import androidx.core.view.ViewCompat
 import androidx.preference.PreferenceViewHolder
 import com.google.android.material.button.MaterialButtonToggleGroup
 import io.legado.app.R
@@ -34,6 +36,7 @@ class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(co
         }
 
         setupToggleGroup(toggleGroup)
+        updateButtonStateDescriptions(toggleGroup)
     }
 
 
@@ -58,6 +61,7 @@ class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(co
                     currentValue = newValue
                     persistString(newValue)
                     callChangeListener(newValue)
+                    updateButtonStateDescriptions(group)
                     Handler(Looper.getMainLooper()).postDelayed({
                         ThemeConfigStore.applyDayNight(context)
                     }, 300)
@@ -70,4 +74,16 @@ class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(co
         currentValue = getPersistedString(defaultValue as? String ?: "0")
     }
 
-}
+    private fun updateButtonStateDescriptions(group: MaterialButtonToggleGroup) {
+        for (index in 0 until group.childCount) {
+            val child = group.getChildAt(index)
+            val selected = child.id == group.checkedButtonId
+            ViewCompat.setStateDescription(
+                child,
+                context.getString(if (selected) R.string.a11y_selected else R.string.a11y_not_selected)
+            )
+            child.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+        }
+    }
+
+}

+ 3 - 3
app/src/main/java/io/legado/app/ui/about/AboutScreen.kt

@@ -148,19 +148,19 @@ private fun MaterialAboutScreen(
                 FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
                     Icon(
                         painter = painterResource(R.drawable.ic_web_outline),
-                        contentDescription = stringResource(R.string.back)
+                        contentDescription = stringResource(R.string.about_open_project_homepage)
                     )
                 }
                 FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
                     Icon(
                         painter = painterResource(R.drawable.ic_github),
-                        contentDescription = stringResource(R.string.back)
+                        contentDescription = stringResource(R.string.about_open_github)
                     )
                 }
                 FilledTonalIconButton(onClick = { onIntent(AboutIntent.CheckUpdate) }) {
                     Icon(
                         painter = painterResource(R.drawable.ic_import),
-                        contentDescription = stringResource(R.string.back)
+                        contentDescription = stringResource(R.string.check_update)
                     )
                 }
             }

+ 37 - 10
app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt

@@ -45,7 +45,15 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.onClick
+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 io.legado.app.R
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
@@ -123,7 +131,7 @@ fun AllBookmarkScreen(
     ) { uri: Uri? ->
         uri?.let {
             viewModel.exportBookmark(it, pendingExportIsMd)
-            Toast.makeText(context, "开始导出...", Toast.LENGTH_SHORT).show()
+            Toast.makeText(context, context.getString(R.string.export_started), Toast.LENGTH_SHORT).show()
         }
     }
 
@@ -132,7 +140,7 @@ fun AllBookmarkScreen(
         topBar = {
             Column {
                 GlassMediumFlexibleTopAppBar(
-                    title = "所有书签",
+                    title = stringResource(R.string.all_bookmark),
                     scrollBehavior = scrollBehavior,
                     navigationIcon = {
                         TopBarNavigationButton(onClick = onBack)
@@ -142,7 +150,13 @@ fun AllBookmarkScreen(
                             TopBarActionButton(
                                 onClick = { viewModel.toggleAllCollapse(allKeys) },
                                 imageVector = if (isAllCollapsed) Icons.Default.UnfoldMore else Icons.Default.UnfoldLess,
-                                contentDescription = null
+                                contentDescription = stringResource(
+                                    if (isAllCollapsed) {
+                                        R.string.a11y_expand_all_bookmark_groups
+                                    } else {
+                                        R.string.a11y_collapse_all_bookmark_groups
+                                    }
+                                )
                             )
                         }
                         TopBarActionButton(
@@ -153,19 +167,19 @@ fun AllBookmarkScreen(
                                 }
                             },
                             imageVector = Icons.Default.Search,
-                            contentDescription = null
+                            contentDescription = stringResource(R.string.search)
                         )
                         TopBarActionButton(
                             onClick = { showMenu = true },
                             imageVector = Icons.Default.MoreVert,
-                            contentDescription = "Menu"
+                            contentDescription = stringResource(R.string.more_menu)
                         )
                         RoundDropdownMenu(
                             expanded = showMenu,
                             onDismissRequest = { showMenu = false }
                         ) {
                             RoundDropdownMenuItem(
-                                text = "导出 JSON",
+                                text = stringResource(R.string.export_bookmarks_json),
                                 onClick = {
                                     showMenu = false
                                     pendingExportIsMd = false
@@ -173,7 +187,7 @@ fun AllBookmarkScreen(
                                 }
                             )
                             RoundDropdownMenuItem(
-                                text = "导出 Markdown",
+                                text = stringResource(R.string.export_bookmarks_markdown),
                                 onClick = {
                                     showMenu = false
                                     pendingExportIsMd = true
@@ -193,7 +207,7 @@ fun AllBookmarkScreen(
                     SearchBar(
                         query = searchText,
                         onQueryChange = { viewModel.onSearchQueryChanged(it) },
-                        placeholder = "搜索...",
+                        placeholder = stringResource(R.string.search),
                         scrollState = listState,
                         scope = scope
                     )
@@ -212,7 +226,7 @@ fun AllBookmarkScreen(
                 when (state) {
                     "LOADING" -> {
                         EmptyMessage(
-                            message = "加载中...",
+                            message = stringResource(R.string.loading),
                             isLoading = true,
                             modifier = Modifier
                                 .fillMaxSize()
@@ -225,7 +239,7 @@ fun AllBookmarkScreen(
 
                     "EMPTY" -> {
                         EmptyMessage(
-                            message = "没有书签!",
+                            message = stringResource(R.string.no_bookmark),
                             modifier = Modifier
                                 .fillMaxSize()
                                 .padding(
@@ -362,10 +376,23 @@ private fun BookmarkGroupHeaderContent(
         animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
         label = "CardColor"
     )
+    val headerDescription = listOfNotNull(title, subtitle).joinToString()
+    val headerStateDescription = stringResource(
+        if (isCollapsed) R.string.a11y_collapsed else R.string.a11y_expanded
+    )
+    val clickLabel = stringResource(
+        if (isCollapsed) R.string.expand else R.string.collapse
+    )
 
     ListItem(
         modifier = modifier
             .fillMaxWidth()
+            .semantics {
+                role = Role.Button
+                contentDescription = headerDescription
+                stateDescription = headerStateDescription
+                onClick(label = clickLabel, action = null)
+            }
             .combinedClickable(onClick = onToggle),
         colors = ListItemDefaults.colors(
             containerColor = Color.Transparent,

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

@@ -31,6 +31,13 @@ import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -91,12 +98,16 @@ private fun BookCacheManageScreen(
     val allBooks = state.shelfBooks + state.notShelfBooks
     val hasRunningDownload = allBooks.any { it.hasActiveDownload }
     val hasDownloadTarget = allBooks.any { it.cachedCount < it.totalCount }
+    val bookshelfSectionTitle = stringResource(R.string.cache_section_bookshelf)
+    val bookshelfSectionEmptyText = stringResource(R.string.cache_empty_bookshelf)
+    val notBookshelfSectionTitle = stringResource(R.string.cache_section_not_in_bookshelf)
+    val notBookshelfSectionEmptyText = stringResource(R.string.cache_empty_not_in_bookshelf)
 
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
             GlassMediumFlexibleTopAppBar(
-                title = "缓存管理",
+                title = stringResource(R.string.cache_management),
                 subtitle = state.downloadSummary.takeIf { it.isNotBlank() },
                 navigationIcon = {
                     TopBarNavigationButton(onClick = onBackClick)
@@ -105,7 +116,7 @@ private fun BookCacheManageScreen(
                     TopBarActionButton(
                         onClick = { onIntent(BookCacheManageIntent.Refresh) },
                         imageVector = Icons.Default.Refresh,
-                        contentDescription = "刷新"
+                        contentDescription = stringResource(R.string.refresh)
                     )
                 },
                 scrollBehavior = scrollBehavior
@@ -122,7 +133,13 @@ private fun BookCacheManageScreen(
                         }
                     },
                     icon = if (hasRunningDownload) Icons.Default.Stop else Icons.Default.Download,
-                    tooltipText = if (hasRunningDownload) "停止下载" else "开始下载"
+                    tooltipText = stringResource(
+                        if (hasRunningDownload) {
+                            R.string.stop_download
+                        } else {
+                            R.string.start_download
+                        }
+                    )
                 )
             }
         }
@@ -147,8 +164,8 @@ private fun BookCacheManageScreen(
                 verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {
                 cacheSection(
-                    title = "书架书籍",
-                    emptyText = "没有书架内书籍缓存或下载任务",
+                    title = bookshelfSectionTitle,
+                    emptyText = bookshelfSectionEmptyText,
                     books = state.shelfBooks,
                     expandedBookUrls = state.expandedBookUrls,
                     chaptersByBookUrl = state.chaptersByBookUrl,
@@ -160,8 +177,8 @@ private fun BookCacheManageScreen(
                     onDeleteChapter = { book, chapter -> pendingDeleteChapter = book to chapter }
                 )
                 cacheSection(
-                    title = "未在书架",
-                    emptyText = "没有未在书架的书籍下载状态",
+                    title = notBookshelfSectionTitle,
+                    emptyText = notBookshelfSectionEmptyText,
                     books = state.notShelfBooks,
                     expandedBookUrls = state.expandedBookUrls,
                     chaptersByBookUrl = state.chaptersByBookUrl,
@@ -288,8 +305,38 @@ private fun BookCacheBookCard(
         targetValue = if (expanded) 90f else 0f,
         label = "BookCacheExpandArrow"
     )
+    val progressPercent = (item.progress * 100).toInt().coerceIn(0, 100)
+    val statusSummary = stringResource(
+        R.string.cache_download_status_summary,
+        item.downloadingCount,
+        item.waitingCount,
+        item.pausedCount,
+        item.errorCount
+    )
+    val progressDescription = stringResource(
+        R.string.cache_progress_description,
+        item.cachedCount,
+        item.totalCount,
+        progressPercent
+    )
+    val expandedState = stringResource(
+        if (expanded) R.string.a11y_expanded else R.string.a11y_collapsed
+    )
+    val bookDescription = stringResource(
+        R.string.a11y_cache_book_item,
+        item.name,
+        item.author,
+        progressDescription,
+        statusSummary
+    )
     NormalCard(
-        modifier = modifier.fillMaxWidth(),
+        modifier = modifier
+            .fillMaxWidth()
+            .semantics {
+                contentDescription = bookDescription
+                stateDescription = expandedState
+                role = Role.Button
+            },
         onClick = onToggleExpanded,
         containerColor = LegadoTheme.colorScheme.surfaceContainer
     ) {
@@ -333,7 +380,12 @@ private fun BookCacheBookCard(
             }
             AppLinearProgressIndicator(
                 progress = item.progress,
-                modifier = Modifier.fillMaxWidth()
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .semantics {
+                        contentDescription = progressDescription
+                        progressBarRangeInfo = ProgressBarRangeInfo(item.progress, 0f..1f)
+                    }
             )
             Row(
                 modifier = Modifier.fillMaxWidth(),
@@ -341,7 +393,7 @@ private fun BookCacheBookCard(
                 verticalAlignment = Alignment.CenterVertically
             ) {
                 AppText(
-                    text = "下载中 ${item.downloadingCount} · 等待 ${item.waitingCount} · 暂停 ${item.pausedCount} · 失败 ${item.errorCount}",
+                    text = statusSummary,
                     modifier = Modifier.weight(1f),
                     style = LegadoTheme.typography.labelMediumEmphasized,
                     color = LegadoTheme.colorScheme.onSurfaceVariant
@@ -357,16 +409,22 @@ private fun BookCacheBookCard(
                         },
                         icon = if (item.hasActiveDownload) Icons.Default.Stop else Icons.Default.PlayArrow,
                         contentDescription = when {
-                            item.hasActiveDownload -> "暂停本书下载"
-                            item.isPaused -> "继续本书下载"
-                            else -> "开始本书下载"
+                            item.hasActiveDownload -> stringResource(
+                                R.string.pause_book_download,
+                                item.name
+                            )
+                            item.isPaused -> stringResource(
+                                R.string.resume_book_download,
+                                item.name
+                            )
+                            else -> stringResource(R.string.start_book_download, item.name)
                         }
                     )
                 }
                 SmallTonalButton(
                     onClick = { onDeleteBook(item) },
                     icon = Icons.Default.Delete,
-                    contentDescription = null
+                    contentDescription = stringResource(R.string.delete_book_cache, item.name)
                 )
             }
         }
@@ -381,6 +439,18 @@ private fun BookCacheChapterRow(
     onStop: () -> Unit,
     onDelete: () -> Unit,
 ) {
+    val statusText = chapterStatusText(item)
+    val chapterDescription = stringResource(
+        R.string.a11y_cache_chapter_item,
+        item.title,
+        statusText
+    )
+    val chapterDownloadProgress = item.downloadProgress ?: 0f
+    val downloadProgressDescription = item.progressLabel?.takeIf { it.isNotBlank() }
+        ?: stringResource(
+            R.string.cache_chapter_progress_description,
+            (chapterDownloadProgress * 100).toInt().coerceIn(0, 100)
+        )
     Row(
         modifier = modifier
             .fillMaxWidth()
@@ -391,7 +461,13 @@ private fun BookCacheChapterRow(
         horizontalArrangement = Arrangement.spacedBy(8.dp),
         verticalAlignment = Alignment.CenterVertically
     ) {
-        Column(modifier = Modifier.weight(1f)) {
+        Column(
+            modifier = Modifier
+                .weight(1f)
+                .semantics(mergeDescendants = true) {
+                    contentDescription = chapterDescription
+                }
+        ) {
             AppText(
                 text = item.title,
                 maxLines = 1,
@@ -399,7 +475,7 @@ private fun BookCacheChapterRow(
                 style = LegadoTheme.typography.titleSmallEmphasized
             )
             AppText(
-                text = chapterStatusText(item),
+                text = statusText,
                 maxLines = 1,
                 style = LegadoTheme.typography.labelSmall,
                 color = if (item.isError) {
@@ -416,6 +492,13 @@ private fun BookCacheChapterRow(
                     modifier = Modifier
                         .fillMaxWidth()
                         .padding(top = 4.dp)
+                        .semantics {
+                            contentDescription = downloadProgressDescription
+                            progressBarRangeInfo = ProgressBarRangeInfo(
+                                chapterDownloadProgress,
+                                0f..1f
+                            )
+                        }
                 )
             }
         }
@@ -423,34 +506,39 @@ private fun BookCacheChapterRow(
             SmallTonalButton(
                 onClick = onStop,
                 icon = Icons.Default.Stop,
-                contentDescription = "暂停章节下载"
+                contentDescription = stringResource(R.string.pause_chapter_download, item.title)
             )
         } else if (item.isPaused || !item.isCached) {
             SmallTonalButton(
                 onClick = onDownload,
                 icon = Icons.Default.Download,
-                contentDescription = if (item.isPaused) "继续章节下载" else "下载章节"
+                contentDescription = if (item.isPaused) {
+                    stringResource(R.string.resume_chapter_download, item.title)
+                } else {
+                    stringResource(R.string.download_chapter, item.title)
+                }
             )
         }
         SmallTonalButton(
             onClick = onDelete,
             icon = Icons.Default.Delete,
-            contentDescription = null
+            contentDescription = stringResource(R.string.delete_chapter_cache, item.title)
         )
     }
 }
 
+@Composable
 private fun chapterStatusText(item: BookCacheChapterItem): String {
     if (item.isDownloading && !item.progressLabel.isNullOrBlank()) {
         return item.progressLabel
     }
     return when {
-        item.isDownloading -> "下载中"
-        item.isWaiting -> "等待下载"
-        item.isPaused -> "已暂停"
-        item.isError -> "下载失败"
-        item.isCached -> "已缓存"
-        else -> "未缓存"
+        item.isDownloading -> stringResource(R.string.downloading)
+        item.isWaiting -> stringResource(R.string.wait_download)
+        item.isPaused -> stringResource(R.string.download_paused)
+        item.isError -> stringResource(R.string.download_error)
+        item.isCached -> stringResource(R.string.download_success)
+        else -> stringResource(R.string.not_cached)
     }
 }
 
@@ -464,7 +552,7 @@ private fun DeleteBookCacheDialog(
         show = item != null,
         onDismissRequest = onDismiss,
         title = stringResource(R.string.delete),
-        text = "删除《${item?.name.orEmpty()}》的全部缓存,并从下载队列移除?",
+        text = stringResource(R.string.delete_book_cache_message, item?.name.orEmpty()),
         confirmText = stringResource(android.R.string.ok),
         onConfirm = { item?.let(onConfirm) },
         dismissText = stringResource(android.R.string.cancel),
@@ -482,7 +570,7 @@ private fun DeleteChapterCacheDialog(
         show = item != null,
         onDismissRequest = onDismiss,
         title = stringResource(R.string.delete),
-        text = "删除章节缓存:${item?.second?.title.orEmpty()}?",
+        text = stringResource(R.string.delete_chapter_cache_message, item?.second?.title.orEmpty()),
         confirmText = stringResource(android.R.string.ok),
         onConfirm = { item?.let { onConfirm(it.first, it.second) } },
         dismissText = stringResource(android.R.string.cancel),

+ 4 - 2
app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt

@@ -322,7 +322,8 @@ fun GroupDeleteAction(
         onClick = {
             showDeleteDialog = true
         },
-        icon = Icons.Default.Delete
+        icon = Icons.Default.Delete,
+        contentDescription = stringResource(R.string.delete),
     )
 
     AppAlertDialog(
@@ -361,6 +362,7 @@ fun GroupResetCoverAction(
                 onCoverPathChange(null)
             }
         },
-        icon = Icons.Default.Restore
+        icon = Icons.Default.Restore,
+        contentDescription = stringResource(R.string.reset),
     )
 }

+ 23 - 6
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -63,6 +63,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -509,18 +514,18 @@ private fun BookInfoTopBarActions(
         TopBarActionButton(
             onClick = { onMenuAction(BookInfoMenuAction.Edit) },
             imageVector = Icons.Default.Edit,
-            contentDescription = "编辑"
+            contentDescription = stringResource(R.string.edit)
         )
     }
     TopBarActionButton(
         onClick = { onMenuAction(BookInfoMenuAction.Share) },
         imageVector = Icons.Default.Share,
-        contentDescription = "分享"
+        contentDescription = stringResource(R.string.share)
     )
     TopBarActionButton(
         onClick = { onShowMenuChange(true) },
         imageVector = Icons.Default.MoreVert,
-        contentDescription = "更多"
+        contentDescription = stringResource(R.string.more_actions)
     )
     BookInfoOverflowMenu(
         expanded = showMenu,
@@ -568,7 +573,11 @@ private fun BookInfoBackdrop(
         LegadoTheme.seedColor,
         0.42f
     )
-    Box(modifier = Modifier.fillMaxSize()) {
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .clearAndSetSemantics { }
+    ) {
         Crossfade(
             targetState = backdropState,
             animationSpec = tween(800),
@@ -735,6 +744,7 @@ private fun BookInfoHeader(
     animatedVisibilityScope: AnimatedVisibilityScope?,
     sharedCoverKey: String?,
 ) {
+    val coverDescription = stringResource(R.string.a11y_book_cover_actions, book.name)
     Column(
         modifier = Modifier
             .fillMaxWidth()
@@ -764,6 +774,10 @@ private fun BookInfoHeader(
                     modifier = Modifier
                         .width(112.dp)
                         .combinedClickable(onClick = onCoverClick, onLongClick = onCoverLongClick)
+                        .semantics {
+                            role = Role.Button
+                            contentDescription = coverDescription
+                        }
                 ) {
                     CoilBookCover(
                         name = book.name,
@@ -964,7 +978,10 @@ private fun BookInfoActionCard(
     onClick: () -> Unit
 ) {
     GlassCard(
-        modifier = modifier,
+        modifier = modifier.semantics(mergeDescendants = true) {
+            role = Role.Button
+            contentDescription = label
+        },
         onLongClick = onLongClick,
         onClick = onClick,
         containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
@@ -1201,7 +1218,7 @@ private fun RelatedBooksBanner(
                 SmallTonalButton(
                     onClick = onMoreClick,
                     icon = Icons.AutoMirrored.Filled.ArrowForward,
-                    contentDescription = "more",
+                    contentDescription = stringResource(R.string.a11y_related_books_more, title),
                 )
             }
         }

+ 30 - 0
app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt

@@ -8,6 +8,7 @@ import android.view.LayoutInflater
 import android.view.animation.Animation
 import android.widget.FrameLayout
 import androidx.appcompat.widget.PopupMenu
+import androidx.core.view.ViewCompat
 import androidx.core.view.HapticFeedbackConstantsCompat
 import androidx.core.view.isGone
 import androidx.core.view.isVisible
@@ -88,6 +89,7 @@ class MangaMenu @JvmOverloads constructor(
         override fun onAnimationStart(animation: Animation) {
             binding.tvSourceAction.text =
                 ReadManga.bookSource?.bookSourceName ?: context.getString(R.string.book_source)
+            updateSourceActionDescription()
             callBack.upSystemUiVisibility(true)
             binding.tvSourceAction.isGone = false
         }
@@ -165,6 +167,7 @@ class MangaMenu @JvmOverloads constructor(
     fun upBookView() {
         binding.titleBar.title = " "
         binding.tvBookName.text = ReadManga.book?.name
+        updateSourceActionDescription()
         ReadManga.curMangaChapter?.let {
             binding.tvChapterName.text = ReadManga.book?.durChapterTitle
             binding.tvPre.isEnabled = ReadManga.durChapterIndex != 0
@@ -230,6 +233,10 @@ class MangaMenu @JvmOverloads constructor(
         seekReadPage.addOnChangeListener { _, value, fromUser ->
             if (fromUser) {
                 HapticFeedbackConstantsCompat.TEXT_HANDLE_MOVE
+                updateSeekAccessibility(
+                    value = value.toInt(),
+                    count = seekReadPage.valueTo.toInt().coerceAtLeast(1),
+                )
                 callBack.skipToPage(value.toInt() - 1)
             }
         }
@@ -279,9 +286,32 @@ class MangaMenu @JvmOverloads constructor(
                 stepSize = 1f
                 this.value = value.toFloat().coerceIn(valueFrom, valueTo)
             }
+            updateSeekAccessibility(
+                value = this.value.toInt(),
+                count = if (count <= 1) 1 else count,
+            )
         }
     }
 
+    private fun updateSeekAccessibility(value: Int, count: Int) {
+        binding.seekReadPage.contentDescription = context.getString(R.string.progress)
+        ViewCompat.setStateDescription(
+            binding.seekReadPage,
+            context.getString(
+                R.string.a11y_read_progress_page,
+                value.coerceAtLeast(1),
+                count.coerceAtLeast(1),
+            )
+        )
+    }
+
+    private fun updateSourceActionDescription() {
+        binding.tvSourceAction.contentDescription = context.getString(
+            R.string.a11y_book_source_action,
+            binding.tvSourceAction.text,
+        )
+    }
+
     interface CallBack {
         fun openBookInfoActivity()
         fun upSystemUiVisibility(menuIsVisible: Boolean)

+ 145 - 23
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt

@@ -122,7 +122,16 @@ import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
 import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
@@ -799,7 +808,8 @@ private fun ReadBookMenuRoutePage(
         ) {
             SmallTonalButton(
                 onClick = onBack,
-                icon = Icons.AutoMirrored.Filled.ArrowBack
+                icon = Icons.AutoMirrored.Filled.ArrowBack,
+                contentDescription = stringResource(R.string.back),
             )
             Text(
                 text = title,
@@ -934,7 +944,7 @@ private fun MenuTitleBar(
                 MenuTitleGlassButton(
                     onClick = { onIntent(ReadBookIntent.CloseReadBook()) },
                     icon = Icons.AutoMirrored.Filled.ArrowBack,
-                    contentDescription = "Back",
+                    contentDescription = stringResource(R.string.back),
                     state = state,
                     colors = colors,
                     backdrop = backdrop,
@@ -1022,6 +1032,7 @@ private fun MenuTitleBar(
                         MenuTitleGlassButton(
                             onClick = { expanded = true },
                             icon = Icons.Default.MoreVert,
+                            contentDescription = stringResource(R.string.more_actions),
                             state = state,
                             colors = colors,
                             backdrop = backdrop,
@@ -1198,10 +1209,11 @@ private fun ReadMenuGlassIconButton(
         modifier = modifier,
         onLongClick = onLongClick,
         selected = selected,
+        contentDescription = contentDescription,
     ) { tint ->
         Icon(
             imageVector = icon,
-            contentDescription = contentDescription,
+            contentDescription = null,
             tint = tint,
             modifier = Modifier.size(20.dp),
         )
@@ -1219,6 +1231,7 @@ private fun ReadMenuGlassButtonSurface(
     modifier: Modifier = Modifier,
     onLongClick: (() -> Unit)? = null,
     selected: Boolean = false,
+    contentDescription: String? = null,
     content: @Composable (Color) -> Unit,
 ) {
     val shape = CircleShape
@@ -1272,6 +1285,16 @@ private fun ReadMenuGlassButtonSurface(
                     role = Role.Button,
                     onLongClick = onLongClick,
                     onClick = onClick,
+                )
+                .then(
+                    if (contentDescription != null) {
+                        Modifier.semantics {
+                            this.contentDescription = contentDescription
+                            role = Role.Button
+                        }
+                    } else {
+                        Modifier
+                    }
                 ),
         ) {
             content(tint)
@@ -1379,6 +1402,7 @@ private fun MenuTitleBarMergedGlassButton(
             ) {
             // SwapHoriz - change source
             if (!state.isLocalBook) {
+                val changeSourceDescription = stringResource(R.string.change_origin)
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
@@ -1389,11 +1413,15 @@ private fun MenuTitleBarMergedGlassButton(
                             role = Role.Button,
                             onClick = { onIntent(ReadBookIntent.MenuChangeSource) },
                             onLongClick = { sourceExpanded = true },
-                        ),
+                        )
+                        .semantics {
+                            contentDescription = changeSourceDescription
+                            role = Role.Button
+                        },
                 ) {
                     Icon(
                         imageVector = Icons.Default.SwapHoriz,
-                        contentDescription = stringResource(R.string.change_origin),
+                        contentDescription = null,
                         tint = tint,
                         modifier = Modifier.size(20.dp),
                     )
@@ -1404,9 +1432,11 @@ private fun MenuTitleBarMergedGlassButton(
                         .width(1.dp)
                         .height(20.dp)
                         .background(tint.copy(alpha = 0.15f))
+                        .clearAndSetSemantics { }
                 )
 
                 // Refresh
+                val refreshDescription = stringResource(R.string.menu_refresh_after)
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
@@ -1417,11 +1447,15 @@ private fun MenuTitleBarMergedGlassButton(
                             role = Role.Button,
                             onClick = { onIntent(ReadBookIntent.MenuRefreshAfter) },
                             onLongClick = { refreshExpanded = true },
-                        ),
+                        )
+                        .semantics {
+                            contentDescription = refreshDescription
+                            role = Role.Button
+                        },
                 ) {
                     Icon(
                         imageVector = Icons.Default.Refresh,
-                        contentDescription = stringResource(R.string.menu_refresh_after),
+                        contentDescription = null,
                         tint = tint,
                         modifier = Modifier.size(20.dp),
                     )
@@ -1432,9 +1466,11 @@ private fun MenuTitleBarMergedGlassButton(
                         .width(1.dp)
                         .height(20.dp)
                         .background(tint.copy(alpha = 0.15f))
+                        .clearAndSetSemantics { }
                 )
 
                 // Download
+                val downloadDescription = stringResource(R.string.offline_cache)
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
@@ -1444,11 +1480,15 @@ private fun MenuTitleBarMergedGlassButton(
                             interactionSource = remember { MutableInteractionSource() },
                             role = Role.Button,
                             onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
-                        ),
+                        )
+                        .semantics {
+                            contentDescription = downloadDescription
+                            role = Role.Button
+                        },
                 ) {
                     Icon(
                         imageVector = Icons.Default.CloudDownload,
-                        contentDescription = stringResource(R.string.offline_cache),
+                        contentDescription = null,
                         tint = tint,
                         modifier = Modifier.size(20.dp),
                     )
@@ -1459,10 +1499,12 @@ private fun MenuTitleBarMergedGlassButton(
                         .width(1.dp)
                         .height(20.dp)
                         .background(tint.copy(alpha = 0.15f))
+                        .clearAndSetSemantics { }
                 )
             } else {
                 // TXT directory rule
                 if (state.isLocalTxt) {
+                    val tocRuleDescription = stringResource(R.string.txt_toc_rule)
                     Box(
                         contentAlignment = Alignment.Center,
                         modifier = Modifier
@@ -1472,11 +1514,15 @@ private fun MenuTitleBarMergedGlassButton(
                                 interactionSource = remember { MutableInteractionSource() },
                                 role = Role.Button,
                                 onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
-                            ),
+                            )
+                            .semantics {
+                                contentDescription = tocRuleDescription
+                                role = Role.Button
+                            },
                     ) {
                         Icon(
                             imageVector = Icons.Default.Toc,
-                            contentDescription = stringResource(R.string.txt_toc_rule),
+                            contentDescription = null,
                             tint = tint,
                             modifier = Modifier.size(20.dp),
                         )
@@ -1487,10 +1533,12 @@ private fun MenuTitleBarMergedGlassButton(
                             .width(1.dp)
                             .height(20.dp)
                             .background(tint.copy(alpha = 0.15f))
+                            .clearAndSetSemantics { }
                     )
                 }
 
                 // Text encoding
+                val charsetDescription = stringResource(R.string.set_charset)
                 Box(
                     contentAlignment = Alignment.Center,
                     modifier = Modifier
@@ -1500,11 +1548,15 @@ private fun MenuTitleBarMergedGlassButton(
                             interactionSource = remember { MutableInteractionSource() },
                             role = Role.Button,
                             onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
-                        ),
+                        )
+                        .semantics {
+                            contentDescription = charsetDescription
+                            role = Role.Button
+                        },
                 ) {
                     Icon(
                         imageVector = Icons.Default.Translate,
-                        contentDescription = stringResource(R.string.set_charset),
+                        contentDescription = null,
                         tint = tint,
                         modifier = Modifier.size(20.dp),
                     )
@@ -1515,10 +1567,12 @@ private fun MenuTitleBarMergedGlassButton(
                         .width(1.dp)
                         .height(20.dp)
                         .background(tint.copy(alpha = 0.15f))
+                        .clearAndSetSemantics { }
                 )
             }
 
             // MoreVert - overflow menu
+            val moreActionsDescription = stringResource(R.string.more_actions)
             Box(
                 contentAlignment = Alignment.Center,
                 modifier = Modifier
@@ -1528,7 +1582,11 @@ private fun MenuTitleBarMergedGlassButton(
                         interactionSource = remember { MutableInteractionSource() },
                         role = Role.Button,
                         onClick = { overflowExpanded = true },
-                    ),
+                    )
+                    .semantics {
+                        contentDescription = moreActionsDescription
+                        role = Role.Button
+                    },
             ) {
                 Icon(
                     imageVector = Icons.Default.MoreVert,
@@ -1750,11 +1808,12 @@ private fun FloatingIconRow(
                 selected = iconDef.isActive,
                 modifier = Modifier.padding(horizontal = 4.dp),
                 onLongClick = iconDef.onLongClick,
+                contentDescription = iconDef.label,
             ) {
                 if (isCustom) {
                     AsyncImage(
                         model = customPath,
-                        contentDescription = iconDef.label,
+                        contentDescription = null,
                         contentScale = ContentScale.Crop,
                         modifier = Modifier
                             .size(36.dp)
@@ -1763,7 +1822,7 @@ private fun FloatingIconRow(
                 } else {
                     Icon(
                         imageVector = iconDef.icon,
-                        contentDescription = iconDef.label,
+                        contentDescription = null,
                         tint = if (iconDef.isActive) LegadoTheme.colorScheme.primary else colors.content,
                         modifier = Modifier.size(20.dp),
                     )
@@ -2052,13 +2111,13 @@ private fun SearchBottomMenuContent(
             SearchMenuActionButton(
                 modifier = Modifier.weight(1f),
                 icon = Icons.Default.Search,
-                text = "搜索内容",
+                text = stringResource(R.string.search),
                 onClick = { onIntent(ReadBookIntent.OpenSearch(null)) },
             )
             SearchMenuActionButton(
                 modifier = Modifier.weight(1f),
                 icon = Icons.Default.Menu,
-                text = "主菜单",
+                text = stringResource(R.string.main_menu),
                 onClick = {
                     onIntent(ReadBookIntent.HideSearchMenu)
                     onIntent(ReadBookIntent.ShowMenu)
@@ -2067,7 +2126,7 @@ private fun SearchBottomMenuContent(
             SearchMenuActionButton(
                 modifier = Modifier.weight(0.55f),
                 icon = Icons.Default.Close,
-                text = "退出",
+                text = stringResource(R.string.exit),
                 onClick = { onIntent(ReadBookIntent.ExitSearch) },
             )
         }
@@ -2149,6 +2208,17 @@ private fun MenuBottomBar(
         0.dp
     }
     val progressBarBehavior = ReadConfig.progressBarBehavior
+    val progressCurrent = sliderValue.roundToInt().coerceIn(0, seekMax) + 1
+    val progressTotal = seekMax + 1
+    val progressValueDescription = stringResource(
+        if (progressBarBehavior == "page") {
+            R.string.a11y_read_progress_page
+        } else {
+            R.string.a11y_read_progress_chapter
+        },
+        progressCurrent,
+        progressTotal,
+    )
 
     fun commitSliderValue(value: Float) {
         val target = value.roundToInt().coerceIn(0, seekMax)
@@ -2230,6 +2300,7 @@ private fun MenuBottomBar(
                     backdrop = backdrop,
                     menuConfig = state.menuConfig,
                     glassEnabled = buttonGlassEnabled,
+                    contentDescription = stringResource(R.string.previous_chapter),
                 )
 
                 ReadMenuSlider(
@@ -2244,6 +2315,8 @@ private fun MenuBottomBar(
                     enabled = seekMax > 0,
                     backdrop = backdrop,
                     glassThumbEnabled = buttonGlassEnabled,
+                    accessibilityLabel = stringResource(R.string.progress),
+                    accessibilityValue = progressValueDescription,
                     modifier = Modifier
                         .weight(1f)
                         .padding(horizontal = 8.dp)
@@ -2256,6 +2329,7 @@ private fun MenuBottomBar(
                     backdrop = backdrop,
                     menuConfig = state.menuConfig,
                     glassEnabled = buttonGlassEnabled,
+                    contentDescription = stringResource(R.string.next_chapter),
                 )
             }
         }
@@ -2352,18 +2426,23 @@ private fun ReadMenuSlider(
     onValueCommit: ((Float) -> Unit)? = null,
     backdrop: Backdrop?,
     glassThumbEnabled: Boolean,
+    accessibilityLabel: String? = null,
+    accessibilityValue: String? = null,
 ) {
     if (glassThumbEnabled && backdrop != null) {
         ReadMenuLiquidSlider(
             value = value,
             onValueChange = onValueChange,
             valueRange = valueRange,
+            steps = steps,
             visibilityThreshold = 0.001f,
             backdrop = backdrop,
             modifier = modifier,
             enabled = enabled,
             onValueChangeFinished = onValueChangeFinished,
             onValueCommit = onValueCommit,
+            accessibilityLabel = accessibilityLabel,
+            accessibilityValue = accessibilityValue,
         )
         return
     }
@@ -2378,6 +2457,8 @@ private fun ReadMenuSlider(
         valueRange = valueRange,
         steps = steps,
         onValueChangeFinished = commitAction,
+        accessibilityLabel = accessibilityLabel,
+        accessibilityValue = accessibilityValue,
     )
 }
 
@@ -2386,12 +2467,15 @@ private fun ReadMenuLiquidSlider(
     value: Float,
     onValueChange: (Float) -> Unit,
     valueRange: ClosedFloatingPointRange<Float>,
+    steps: Int = 0,
     visibilityThreshold: Float,
     backdrop: Backdrop,
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     onValueChangeFinished: (() -> Unit)? = null,
     onValueCommit: ((Float) -> Unit)? = null,
+    accessibilityLabel: String? = null,
+    accessibilityValue: String? = null,
 ) {
     val accentColor = LegadoTheme.colorScheme.secondary
     val trackColor = LegadoTheme.colorScheme.surfaceContainerLow
@@ -2401,7 +2485,30 @@ private fun ReadMenuLiquidSlider(
     val trackBackdrop = rememberLayerBackdrop()
 
     BoxWithConstraints(
-        modifier.fillMaxWidth(),
+        modifier
+            .fillMaxWidth()
+            .semantics {
+                accessibilityLabel?.let { contentDescription = it }
+                accessibilityValue?.let { stateDescription = it }
+                progressBarRangeInfo = ProgressBarRangeInfo(
+                    current = value.coerceIn(valueRange),
+                    range = valueRange,
+                    steps = steps,
+                )
+                if (!enabled) {
+                    disabled()
+                }
+                setProgress { target ->
+                    if (!enabled) {
+                        false
+                    } else {
+                        val nextValue = target.coerceIn(valueRange)
+                        onValueChange(nextValue)
+                        onValueCommit?.invoke(nextValue) ?: onValueChangeFinished?.invoke()
+                        true
+                    }
+                }
+            },
         contentAlignment = Alignment.CenterStart,
     ) {
         val trackWidth = constraints.maxWidth
@@ -2604,6 +2711,7 @@ private fun ToolButtonItem(
                 glassEnabled = true,
                 selected = button.isActive,
                 onLongClick = button.onLongClick,
+                contentDescription = button.description,
             ) { tint ->
                 ToolButtonContent(
                     button = button,
@@ -2630,7 +2738,11 @@ private fun ToolButtonItem(
                         role = Role.Button,
                         onLongClick = button.onLongClick,
                         onClick = button.onClick,
-                    ),
+                    )
+                    .semantics {
+                        contentDescription = button.description
+                        role = Role.Button
+                    },
             ) {
                 ToolButtonContent(
                     button = button,
@@ -2674,14 +2786,14 @@ private fun ToolButtonContent(
         if (button.customIconPath.isNullOrBlank()) {
             Icon(
                 imageVector = button.icon,
-                contentDescription = button.description,
+                contentDescription = null,
                 modifier = Modifier.size(20.dp),
                 tint = tint,
             )
         } else {
             AsyncImage(
                 model = button.customIconPath,
-                contentDescription = button.description,
+                contentDescription = null,
                 contentScale = ContentScale.Crop,
                 modifier = Modifier
                     .size(36.dp)
@@ -3204,6 +3316,11 @@ private fun BrightnessBar(
         onToggleAuto()
     }
 
+    val brightnessValueDescription = stringResource(
+        R.string.a11y_percent_value,
+        sliderValue.roundToInt().coerceIn(0, 100),
+    )
+
     if (vertical) {
         Column(
             modifier = modifier
@@ -3234,6 +3351,8 @@ private fun BrightnessBar(
                 valueRange = 0f..100f,
                 enabled = !brightnessAuto,
                 height = 168.dp,
+                accessibilityLabel = stringResource(R.string.brightness),
+                accessibilityValue = brightnessValueDescription,
             )
             ReadMenuGlassIconButton(
                 onClick = onTogglePosition,
@@ -3273,10 +3392,13 @@ private fun BrightnessBar(
                 onValueChangeFinished = {
                     commitSliderValue(sliderValue)
                 },
+                onValueCommit = ::commitSliderValue,
                 valueRange = 0f..100f,
                 enabled = !brightnessAuto,
                 backdrop = backdrop,
                 glassThumbEnabled = glassThumbEnabled,
+                accessibilityLabel = stringResource(R.string.brightness),
+                accessibilityValue = brightnessValueDescription,
                 modifier = Modifier
                     .weight(1f)
                     .padding(horizontal = 8.dp),

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

@@ -43,11 +43,17 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import io.legado.app.R
 import io.legado.app.ui.book.readRecord.component.ReadingTimeBarChartCard
 import io.legado.app.ui.book.readRecord.component.StatItem
 import io.legado.app.ui.book.readRecord.component.StatsGridCard
@@ -82,7 +88,7 @@ fun ReadRecordOverviewScreen(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
             GlassMediumFlexibleTopAppBar(
-                title = "阅读总览",
+                title = stringResource(R.string.read_record_overview),
                 scrollBehavior = scrollBehavior,
                 navigationIcon = {
                     TopBarNavigationButton(onClick = onBackClick)
@@ -120,14 +126,14 @@ fun ReadRecordOverviewScreen(
 
                 item {
                     val stats = listOf(
-                        StatItem("阅读时间", ReadRecordFormatter.formatDuration(state.totalTime)),
-                        StatItem("阅读天数", "${state.readingDays}天"),
-                        StatItem("累计读过", "${state.totalBooks}本"),
-                        StatItem("读完书籍", "${state.finishedBooks}本"),
-                        StatItem("在读书籍", "${state.readingBooks}本"),
-                        StatItem("阅读字数", ReadRecordFormatter.formatWords(state.totalWords))
+                        StatItem(stringResource(R.string.reading_time), ReadRecordFormatter.formatDuration(state.totalTime)),
+                        StatItem(stringResource(R.string.reading_days), stringResource(R.string.days_format, state.readingDays)),
+                        StatItem(stringResource(R.string.total_read_books), stringResource(R.string.books_format, state.totalBooks)),
+                        StatItem(stringResource(R.string.finished_books), stringResource(R.string.books_format, state.finishedBooks)),
+                        StatItem(stringResource(R.string.reading_books), stringResource(R.string.books_format, state.readingBooks)),
+                        StatItem(stringResource(R.string.reading_words), ReadRecordFormatter.formatWords(state.totalWords))
                     )
-                    StatsGridCard(title = "阅读数据", items = stats)
+                    StatsGridCard(title = stringResource(R.string.reading_data), items = stats)
                 }
 
                 item {
@@ -189,7 +195,8 @@ fun DateNavigator(
     ) {
         MediumPlainButton(
             onClick = onPrevClick,
-            icon = Icons.AutoMirrored.Filled.ArrowLeft
+            icon = Icons.AutoMirrored.Filled.ArrowLeft,
+            contentDescription = stringResource(R.string.previous)
         )
         AnimatedContent(
             targetState = referenceDate,
@@ -226,7 +233,8 @@ fun DateNavigator(
         }
         MediumPlainButton(
             onClick = onNextClick,
-            icon = Icons.AutoMirrored.Filled.ArrowRight
+            icon = Icons.AutoMirrored.Filled.ArrowRight,
+            contentDescription = stringResource(R.string.next)
         )
     }
 }
@@ -247,7 +255,7 @@ fun HeatmapCard(state: ReadRecordOverviewUiState) {
                     modifier = Modifier.size(20.dp)
                 )
                 Spacer(modifier = Modifier.width(8.dp))
-                AppText("阅读热力图", style = LegadoTheme.typography.titleMedium)
+                AppText(stringResource(R.string.reading_heatmap), style = LegadoTheme.typography.titleMedium)
             }
             Spacer(modifier = Modifier.height(8.dp))
             HeatmapCalendarSection(
@@ -285,7 +293,7 @@ fun TopReadingListCard(
                 )
                 Spacer(modifier = Modifier.width(8.dp))
                 AppText(
-                    text = "阅读时长榜",
+                    text = stringResource(R.string.reading_time_ranking),
                     style = LegadoTheme.typography.titleMedium
                 )
             }
@@ -296,11 +304,22 @@ fun TopReadingListCard(
                 LaunchedEffect(book.bookName, book.bookAuthor) {
                     coverPath = viewModel.getBookCover(book.bookName, book.bookAuthor)
                 }
+                val rankingDescription = stringResource(
+                    R.string.a11y_reading_ranking_item,
+                    index + 1,
+                    book.bookName,
+                    book.bookAuthor,
+                    ReadRecordFormatter.formatDuration(book.readTime)
+                )
 
                 Row(
                     modifier = Modifier
                         .fillMaxWidth()
                         .clickable { onBookClick(book.bookName, book.bookAuthor) }
+                        .semantics(mergeDescendants = true) {
+                            contentDescription = rankingDescription
+                            role = Role.Button
+                        }
                         .padding(vertical = 8.dp, horizontal = 8.dp),
                     verticalAlignment = Alignment.CenterVertically
                 ) {
@@ -368,7 +387,7 @@ fun ReadingCalendarCard(
                     modifier = Modifier.size(20.dp)
                 )
                 Spacer(modifier = Modifier.width(8.dp))
-                AppText("读书日历", style = LegadoTheme.typography.titleMedium)
+                AppText(stringResource(R.string.reading_calendar), style = LegadoTheme.typography.titleMedium)
             }
             Spacer(modifier = Modifier.height(16.dp))
             
@@ -426,12 +445,25 @@ fun CalendarDayCell(
             coverPath = viewModel.getBookCover(name, author)
         }
     }
+    val dayDescription = if (topBook != null) {
+        stringResource(
+            R.string.a11y_reading_calendar_day_with_book,
+            date.toString(),
+            topBook.first,
+            topBook.second
+        )
+    } else {
+        stringResource(R.string.a11y_reading_calendar_day_empty, date.toString())
+    }
     
     Box(
         modifier = Modifier
             .fillMaxSize()
             .clip(RoundedCornerShape(4.dp))
-            .background(LegadoTheme.colorScheme.surfaceVariant),
+            .background(LegadoTheme.colorScheme.surfaceVariant)
+            .semantics {
+                contentDescription = dayDescription
+            },
         contentAlignment = Alignment.Center
     ) {
         if (topBook != null) {

+ 131 - 41
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt

@@ -51,6 +51,12 @@ import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontWeight
@@ -59,6 +65,7 @@ import androidx.compose.ui.text.withStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 import cn.hutool.core.date.DateUtil
+import io.legado.app.R
 import io.legado.app.data.entities.readRecord.ReadRecord
 import io.legado.app.data.entities.readRecord.ReadRecordDetail
 import io.legado.app.data.entities.readRecord.ReadRecordSession
@@ -120,6 +127,7 @@ fun ReadRecordScreen(
 ) {
     val snackbarHostState = remember { SnackbarHostState() }
     val scope = rememberCoroutineScope()
+    val context = LocalContext.current
 
     val state by viewModel.uiState.collectAsState()
     val displayMode by viewModel.displayMode.collectAsState()
@@ -182,12 +190,12 @@ fun ReadRecordScreen(
         topBar = {
             Column {
                 GlassMediumFlexibleTopAppBar(
-                    title = "阅读记录",
+                    title = stringResource(R.string.read_record),
                     subtitle = run {
                         val subTitle = when (displayMode) {
-                            DisplayMode.AGGREGATE -> "汇总视图"
-                            DisplayMode.TIMELINE -> "时间线视图"
-                            DisplayMode.LATEST -> "最后阅读"
+                            DisplayMode.AGGREGATE -> stringResource(R.string.read_record_view_aggregate)
+                            DisplayMode.TIMELINE -> stringResource(R.string.read_record_view_timeline)
+                            DisplayMode.LATEST -> stringResource(R.string.read_record_view_latest)
                         }
                         subTitle
                     },
@@ -208,17 +216,21 @@ fun ReadRecordScreen(
                                 DisplayMode.TIMELINE -> Icons.Default.Schedule
                                 DisplayMode.LATEST -> Icons.AutoMirrored.Filled.List
                             }
-                            val description = if (displayMode == DisplayMode.AGGREGATE) "Switch to Timeline" else "Switch to Aggregate"
+                            val description = when (displayMode) {
+                                DisplayMode.AGGREGATE -> stringResource(R.string.a11y_switch_to_timeline_view)
+                                DisplayMode.TIMELINE -> stringResource(R.string.a11y_switch_to_latest_view)
+                                DisplayMode.LATEST -> stringResource(R.string.a11y_switch_to_aggregate_view)
+                            }
                             AppIcon(icon, description)
                         }
                         AppIconButton(onClick = { showCalendar = !showCalendar }) {
                             AppIcon(
                                 Icons.Default.CalendarMonth,
-                                contentDescription = "Toggle Calendar"
+                                contentDescription = stringResource(R.string.a11y_toggle_read_calendar)
                             )
                         }
                         AppIconButton(onClick = { showSearch = !showSearch }) {
-                            AppIcon(Icons.Default.Search, contentDescription = null)
+                            AppIcon(Icons.Default.Search, contentDescription = stringResource(R.string.search))
                         }
                     },
                     scrollBehavior = scrollBehavior
@@ -256,7 +268,7 @@ fun ReadRecordScreen(
                                 top = padding.calculateTopPadding(),
                                 bottom = padding.calculateBottomPadding()
                             ),
-                        message = "加载中",
+                        message = stringResource(R.string.loading),
                         isLoading = true
                     )
                 }
@@ -269,7 +281,7 @@ fun ReadRecordScreen(
                                 top = padding.calculateTopPadding(),
                                 bottom = padding.calculateBottomPadding()
                             ),
-                        message = "没有记录"
+                        message = stringResource(R.string.no_read_record)
                     )
                 }
 
@@ -296,7 +308,9 @@ fun ReadRecordScreen(
                                     scope.launch {
                                         val candidates = viewModel.getMergeCandidates(record)
                                         if (candidates.isEmpty()) {
-                                            snackbarHostState.showSnackbar("没有可合并的同名记录")
+                                            snackbarHostState.showSnackbar(
+                                                context.getString(R.string.no_merge_candidates)
+                                            )
                                         } else {
                                             mergeDialogData = record to candidates
                                         }
@@ -341,25 +355,25 @@ fun ReadRecordScreen(
     AppAlertDialog(
         data = pendingDeleteAction,
         onDismissRequest = { pendingDeleteAction = null },
-        title = "确认删除",
+        title = stringResource(R.string.confirm_delete_read_record),
         content = { _ ->
             Column {
-                AppText("确定要删除这条记录吗?")
+                AppText(stringResource(R.string.delete_read_record_message))
                 Spacer(modifier = Modifier.height(8.dp))
                 CheckboxItem(
-                    title = "不再提示",
+                    title = stringResource(R.string.do_not_remind_again),
                     checked = skipDeleteConfirmTemp,
                     onCheckedChange = { skipDeleteConfirmTemp = it }
                 )
             }
         },
-        confirmText = "删除",
+        confirmText = stringResource(R.string.delete),
         onConfirm = { action ->
             action.invoke()
             pendingDeleteAction = null
             skipDeleteConfirm = skipDeleteConfirmTemp
         },
-        dismissText = "取消",
+        dismissText = stringResource(R.string.cancel),
         onDismiss = {
             pendingDeleteAction = null
         }
@@ -407,14 +421,21 @@ fun ReadRecordScreen(
     AppAlertDialog(
         data = mergeDialogData,
         onDismissRequest = { mergeDialogData = null },
-        title = "合并阅读记录",
+        title = stringResource(R.string.merge_read_records),
         content = { (targetRecord, candidates) ->
+            val unknownAuthor = stringResource(R.string.unknown_author)
             Column {
-                AppText("将以下作者的“${targetRecord.bookName}”合并到 ${targetRecord.bookAuthor.ifBlank { "未知作者" }}")
+                AppText(
+                    stringResource(
+                        R.string.merge_read_records_message,
+                        targetRecord.bookName,
+                        targetRecord.bookAuthor.ifBlank { unknownAuthor }
+                    )
+                )
                 Spacer(modifier = Modifier.height(8.dp))
 
                 candidates.forEach { candidate ->
-                    val author = candidate.bookAuthor.ifBlank { "未知作者" }
+                    val author = candidate.bookAuthor.ifBlank { unknownAuthor }
                     val isChecked = selectedAuthors.contains(candidate.bookAuthor)
 
                     CheckboxItem(
@@ -431,7 +452,7 @@ fun ReadRecordScreen(
                 }
             }
         },
-        confirmText = "合并",
+        confirmText = stringResource(R.string.merge),
         onConfirm = { (targetRecord, candidates) ->
             viewModel.mergeReadRecords(
                 targetRecord,
@@ -439,7 +460,7 @@ fun ReadRecordScreen(
             )
             mergeDialogData = null
         },
-        dismissText = "取消",
+        dismissText = stringResource(R.string.cancel),
         onDismiss = { mergeDialogData = null }
     )
 }
@@ -461,7 +482,11 @@ fun SummarySection(
             val dailyTime = dailyDetails.sumOf { it.readTime }
 
             ReadingSummaryCard(
-                title = selectedDate.format(DateTimeFormatter.ofPattern("M月d日阅读概览")),
+                title = stringResource(
+                    R.string.read_record_date_overview,
+                    selectedDate.monthValue,
+                    selectedDate.dayOfMonth
+                ),
                 bookCount = distinctBooks.size,
                 totalTimeMillis = dailyTime,
                 bookNamesForCover = distinctBooks.take(3),
@@ -475,7 +500,7 @@ fun SummarySection(
 
         if (allBooksCount > 0) {
             ReadingSummaryCard(
-                title = "累计阅读成就",
+                title = stringResource(R.string.read_record_total_achievement),
                 bookCount = allBooksCount,
                 totalTimeMillis = totalTime,
                 bookNamesForCover = state.latestRecords.take(5).map { it.bookName to it.bookAuthor },
@@ -581,6 +606,7 @@ fun LazyListScope.renderListByMode(
                     items = details,
                     key = { "agg_item_${date}|${it.bookName}_${it.bookAuthor}_${it.date}" }
                 ) { detail ->
+                    val deleteActionDescription = stringResource(R.string.del_read_record)
                     SwipeActionContainer(
                         modifier = Modifier.animateItem(),
                         startAction = SwipeAction(
@@ -588,7 +614,8 @@ fun LazyListScope.renderListByMode(
                             background = LegadoTheme.colorScheme.error,
                             onSwipe = {
                                 onConfirmDelete { viewModel.deleteDetail(detail) }
-                            }
+                            },
+                            contentDescription = deleteActionDescription
                         )
                     ) {
                         ReadRecordItem(
@@ -604,6 +631,7 @@ fun LazyListScope.renderListByMode(
             state.timelineRecords.forEach { (date, sessions) ->
                 item(key = "timeline_header_$date") { DateHeader(date) }
                 items(items = sessions, key = { "timeline_item_${date}|${it.id}" }) { session ->
+                    val deleteActionDescription = stringResource(R.string.del_read_record)
                     SwipeActionContainer(
                         modifier = Modifier.animateItem(),
                         startAction = SwipeAction(
@@ -611,7 +639,8 @@ fun LazyListScope.renderListByMode(
                             background = LegadoTheme.colorScheme.error,
                             onSwipe = {
                                 onConfirmDelete { viewModel.deleteSession(session) }
-                            }
+                            },
+                            contentDescription = deleteActionDescription
                         )
                     ) {
                         TimelineSessionItem(
@@ -626,6 +655,8 @@ fun LazyListScope.renderListByMode(
 
         DisplayMode.LATEST -> {
             items(items = state.latestRecords, key = { "${it.bookName}_${it.bookAuthor}" }) { record ->
+                val deleteActionDescription = stringResource(R.string.del_read_record)
+                val mergeActionDescription = stringResource(R.string.a11y_merge_same_name_read_records)
                 SwipeActionContainer(
                     modifier = Modifier.animateItem(),
                     startAction = SwipeAction(
@@ -633,14 +664,16 @@ fun LazyListScope.renderListByMode(
                         background = LegadoTheme.colorScheme.error,
                         onSwipe = {
                             onConfirmDelete { viewModel.deleteReadRecord(record) }
-                        }
+                        },
+                        contentDescription = deleteActionDescription
                     ),
                     endAction = SwipeAction(
                         icon = Icons.Default.Merge,
                         background = LegadoTheme.colorScheme.primary,
                         onSwipe = {
                             onMergeClick(record)
-                        }
+                        },
+                        contentDescription = mergeActionDescription
                     )
                 ) {
                         LatestReadItem(
@@ -666,11 +699,25 @@ fun LatestReadItem(
     LaunchedEffect(record.bookName, record.bookAuthor) {
         coverPath = viewModel.getBookCover(record.bookName, record.bookAuthor)
     }
+    val unknownAuthor = stringResource(R.string.unknown_author)
+    val author = record.bookAuthor.ifBlank { unknownAuthor }
+    val lastReadText = DateUtil.format(Date(record.lastRead), "yyyy-MM-dd HH:mm")
+    val itemDescription = stringResource(
+        R.string.a11y_read_record_latest_item,
+        record.bookName,
+        author,
+        formatDuring(record.readTime),
+        lastReadText
+    )
 
     Row(
         modifier = modifier
             .fillMaxWidth()
             .clickable(onClick = onClick)
+            .semantics(mergeDescendants = true) {
+                contentDescription = itemDescription
+                role = Role.Button
+            }
             .adaptiveHorizontalPadding(vertical = 8.dp),
         verticalAlignment = Alignment.CenterVertically
     ) {
@@ -691,7 +738,7 @@ fun LatestReadItem(
                 overflow = TextOverflow.Ellipsis
             )
             AppText(
-                text = record.bookAuthor.ifBlank { "未知作者" },
+                text = author,
                 style = LegadoTheme.typography.bodySmall,
                 color = LegadoTheme.colorScheme.outline,
                 maxLines = 1,
@@ -711,7 +758,7 @@ fun LatestReadItem(
                         append(" • ")
                     }
                     withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) {
-                        append("${DateUtil.format(Date(record.lastRead), "yyyy-MM-dd HH:mm")}")
+                        append(lastReadText)
                     }
                 },
                 style = LegadoTheme.typography.labelSmall,
@@ -730,15 +777,28 @@ fun TimelineSessionItem(
 ) {
     val session = item.session
     var coverPath by remember { mutableStateOf<String?>(null) }
-    var chapterTitle by remember { mutableStateOf<String?>("加载中...") }
+    val loadingText = stringResource(R.string.loading)
+    val fallbackChapterTitle = stringResource(R.string.chapter_index_format, session.words)
+    var chapterTitle by remember { mutableStateOf<String?>(loadingText) }
 
-    LaunchedEffect(session.bookName, session.bookAuthor) {
+    LaunchedEffect(session.bookName, session.bookAuthor, session.words, fallbackChapterTitle) {
         coverPath = viewModel.getBookCover(session.bookName, session.bookAuthor)
         val title = viewModel.getChapterTitle(session.bookName, session.bookAuthor, session.words)
-        chapterTitle = title ?: "第 ${session.words} 章"
+        chapterTitle = title ?: fallbackChapterTitle
     }
 
     val endTimeText = DateUtil.format(Date(session.endTime), "HH:mm")
+    val unknownAuthor = stringResource(R.string.unknown_author)
+    val author = session.bookAuthor.ifBlank { unknownAuthor }
+    val duration = formatDuring(session.endTime - session.startTime)
+    val itemDescription = stringResource(
+        R.string.a11y_read_record_timeline_item,
+        session.bookName,
+        author,
+        chapterTitle.orEmpty(),
+        duration,
+        endTimeText
+    )
 
     val nodeRadius = 4.dp
     val lineWidth = 2.dp
@@ -752,6 +812,10 @@ fun TimelineSessionItem(
         modifier = Modifier
             .fillMaxWidth()
             .clickable { onBookClick(session.bookName, session.bookAuthor) }
+            .semantics(mergeDescendants = true) {
+                contentDescription = itemDescription
+                role = Role.Button
+            }
             .drawBehind {
                 val x = timelineX.toPx()
                 val h = size.height
@@ -805,7 +869,7 @@ fun TimelineSessionItem(
                         overflow = TextOverflow.Ellipsis
                     )
                     AppText(
-                        text = session.bookAuthor.ifBlank { "未知作者" },
+                        text = author,
                         style = LegadoTheme.typography.bodySmall,
                         color = MaterialTheme.colorScheme.outline,
                         maxLines = 1,
@@ -837,11 +901,23 @@ fun ReadRecordItem(
     LaunchedEffect(detail.bookName, detail.bookAuthor) {
         coverPath = viewModel.getBookCover(detail.bookName, detail.bookAuthor)
     }
+    val unknownAuthor = stringResource(R.string.unknown_author)
+    val author = detail.bookAuthor.ifBlank { unknownAuthor }
+    val itemDescription = stringResource(
+        R.string.a11y_read_record_detail_item,
+        detail.bookName,
+        author,
+        formatDuring(detail.readTime)
+    )
 
     Row(
         modifier = modifier
             .fillMaxWidth()
             .clickable(onClick = onClick)
+            .semantics(mergeDescendants = true) {
+                contentDescription = itemDescription
+                role = Role.Button
+            }
             .adaptiveHorizontalPadding(vertical = 8.dp),
         verticalAlignment = Alignment.CenterVertically
     ) {
@@ -861,13 +937,13 @@ fun ReadRecordItem(
                 maxLines = 1
             )
             AppText(
-                text = detail.bookAuthor.ifBlank { "未知作者" },
+                text = author,
                 style = LegadoTheme.typography.bodySmall,
                 color = MaterialTheme.colorScheme.outline,
             )
             Spacer(modifier = Modifier.height(8.dp))
             AppText(
-                text = "阅读时长: ${formatDuring(detail.readTime)}",
+                text = stringResource(R.string.reading_time_with_value, formatDuring(detail.readTime)),
                 color = MaterialTheme.colorScheme.outline,
                 style = LegadoTheme.typography.labelSmall
             )
@@ -895,7 +971,7 @@ fun DateHeader(
             )
             dailyTotalTime?.let { total ->
                 AppText(
-                    text = "已读 ${formatDuring(total)}",
+                    text = stringResource(R.string.read_duration_done, formatDuring(total)),
                     style = LegadoTheme.typography.bodySmall,
                     color = LegadoTheme.colorScheme.onSurface
                 )
@@ -921,11 +997,21 @@ fun ReadingSummaryCard(
     }
 
     val totalDurationMinutes = totalTimeMillis / 60000
+    val cardDescription = stringResource(
+        R.string.a11y_reading_summary_card,
+        title,
+        bookCount,
+        formatDuring(totalTimeMillis)
+    )
 
     GlassCard(
         onClick = onClick,
         modifier = Modifier
             .fillMaxWidth()
+            .semantics(mergeDescendants = true) {
+                contentDescription = cardDescription
+                role = Role.Button
+            }
             .adaptiveHorizontalPadding(vertical = 8.dp),
         containerColor = LegadoTheme.colorScheme.surfaceContainer
     ) {
@@ -948,8 +1034,8 @@ fun ReadingSummaryCard(
                 Spacer(modifier = Modifier.height(4.dp))
 
                 Row(verticalAlignment = Alignment.Bottom) {
-                    AppText(
-                        text = "已读 ",
+                AppText(
+                    text = stringResource(R.string.read_books_prefix),
                         style = LegadoTheme.typography.titleMedium
                     )
                     AppText(
@@ -958,8 +1044,8 @@ fun ReadingSummaryCard(
                         color = LegadoTheme.colorScheme.primary,
                         fontWeight = FontWeight.Bold,
                     )
-                    AppText(
-                        text = " 本书",
+                AppText(
+                    text = stringResource(R.string.read_books_suffix),
                         style = LegadoTheme.typography.titleMedium
                     )
                 }
@@ -968,10 +1054,14 @@ fun ReadingSummaryCard(
 
                 val hours = totalDurationMinutes / 60
                 val minutes = totalDurationMinutes % 60
-                val timeString = if (hours > 0) "${hours}小时${minutes}分钟" else "${minutes}分钟"
+                val timeString = if (hours > 0) {
+                    stringResource(R.string.hours_minutes_format, hours, minutes)
+                } else {
+                    stringResource(R.string.minutes_format, minutes)
+                }
 
                 AppText(
-                    text = "共阅读 $timeString",
+                    text = stringResource(R.string.total_reading_time_format, timeString),
                     style = LegadoTheme.typography.bodySmall,
                     color = LegadoTheme.colorScheme.onSurfaceVariant
                 )

+ 3 - 1
app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt

@@ -93,7 +93,8 @@ fun ScopeSelectSheet(
             {
                 MediumPlainButton(
                     onClick = it,
-                    icon = AppIcons.Settings
+                    icon = AppIcons.Settings,
+                    contentDescription = stringResource(R.string.book_source_manage),
                 )
             }
         },
@@ -105,6 +106,7 @@ fun ScopeSelectSheet(
                         onDismissRequest()
                     },
                     icon = AppIcons.Check,
+                    contentDescription = stringResource(R.string.ok),
                 )
             }
         },

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt

@@ -56,6 +56,7 @@ class SearchScopeDialog : BaseBottomSheetDialogFragment(R.layout.dialog_search_s
 
     private fun initSearchView() {
         val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView
+        searchView.queryHint = getString(R.string.screen)
         searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
             override fun onQueryTextSubmit(query: String?): Boolean {
                 return false
@@ -255,4 +256,4 @@ class SearchScopeDialog : BaseBottomSheetDialogFragment(R.layout.dialog_search_s
 
     }
 
-}
+}

+ 4 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchSourceSection.kt

@@ -18,9 +18,11 @@ import androidx.compose.material.icons.automirrored.filled.ArrowForward
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.ui.main.bookCoverSharedElementKey
 import io.legado.app.ui.theme.LegadoTheme
@@ -73,7 +75,8 @@ fun SearchSourceSection(
             if (onViewAll != null) {
                 SmallTonalButton(
                     onClick = onViewAll,
-                    icon = Icons.AutoMirrored.Filled.ArrowForward
+                    icon = Icons.AutoMirrored.Filled.ArrowForward,
+                    contentDescription = stringResource(R.string.show_all),
                 )
             }
         }

+ 19 - 1
app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt

@@ -5,6 +5,7 @@ import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
+import android.widget.EditText
 import androidx.activity.viewModels
 import androidx.appcompat.widget.SearchView
 import androidx.lifecycle.lifecycleScope
@@ -73,6 +74,11 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
         searchView.onActionViewExpanded()
         searchView.isSubmitButtonEnabled = true
         searchView.queryHint = getString(R.string.search_book_key)
+        searchView.contentDescription = getString(R.string.search_book_key)
+        searchView.findViewById<EditText>(androidx.appcompat.R.id.search_src_text)?.apply {
+            hint = getString(R.string.search_book_key)
+            contentDescription = getString(R.string.search_book_key)
+        }
         searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
             override fun onQueryTextSubmit(query: String?): Boolean {
                 searchView.clearFocus()
@@ -98,6 +104,7 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
                 binding.textMy.text = it
             }
         }
+        updateHelpContentDescriptions()
         binding.textMy.onClick {
             searchView.setQuery(binding.textMy.text, true)
         }
@@ -131,6 +138,7 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
                 }
                 exploreKinds?.firstOrNull()?.let {
                     binding.textFx.text = "${it.title}::${it.url}"
+                    updateHelpContentDescriptions()
                     if (it.title.startsWith("ERROR:")) {
                         adapter.addItem("获取发现出错\n${it.url}")
                         openOrCloseHelp(false)
@@ -145,6 +153,7 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
                         selector("选择发现", exploreKindTitles) { _, index ->
                             val explore = exploreKinds[index]
                             binding.textFx.text = "${explore.title}::${explore.url}"
+                            updateHelpContentDescriptions()
                             searchView.setQuery(binding.textFx.text, true)
                         }
                     }
@@ -157,6 +166,15 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
         }
     }
 
+    private fun updateHelpContentDescriptions() = binding.run {
+        textMy.contentDescription = getString(R.string.a11y_debug_example, textMy.text)
+        textXt.contentDescription = getString(R.string.a11y_debug_example, textXt.text)
+        textFx.contentDescription = getString(R.string.a11y_debug_example, textFx.text)
+        textInfo.contentDescription = getString(R.string.a11y_debug_example, textInfo.text)
+        textToc.contentDescription = getString(R.string.a11y_debug_example, textToc.text)
+        textContent.contentDescription = getString(R.string.a11y_debug_example, textContent.text)
+    }
+
     private fun prefixAutoComplete(prefix: String) {
         val query = searchView.query
         if (query.isNullOrBlank() || query.length <= 2) {
@@ -214,4 +232,4 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
         return super.onCompatOptionsItemSelected(item)
     }
 
-}
+}

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt

@@ -72,6 +72,7 @@ class BookSourceEditAdapter : RecyclerView.Adapter<BookSourceEditAdapter.MyViewH
             }
             editText.setText(editEntity.value)
             textInputLayout.hint = editEntity.hint
+            editText.hint = editEntity.hint
             val textWatcher = object : TextWatcher {
                 override fun beforeTextChanged(
                     s: CharSequence,
@@ -96,4 +97,4 @@ class BookSourceEditAdapter : RecyclerView.Adapter<BookSourceEditAdapter.MyViewH
     }
 
 
-}
+}

+ 64 - 7
app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt

@@ -72,6 +72,12 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
@@ -614,7 +620,9 @@ fun TocScreen(
             ) {
                 AppLinearProgressIndicator(
                     progress = state.titleReplaceProgress ?: 0f,
-                    modifier = Modifier.fillMaxWidth()
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .clearAndSetSemantics { }
                 )
             }
 
@@ -754,10 +762,53 @@ fun ChapterItem(
             else -> LegadoTheme.colorScheme.onSurfaceVariant
         }, label = "BgColor"
     )
+    val selectedDescription = stringResource(R.string.a11y_selected)
+    val currentReadingDescription = stringResource(R.string.a11y_current_reading)
+    val lockedDescription = stringResource(R.string.a11y_vip_locked)
+    val downloadedDescription = stringResource(R.string.a11y_downloaded)
+    val downloadingDescription = stringResource(R.string.a11y_downloading)
+    val downloadFailedDescription = stringResource(R.string.a11y_download_failed)
+    val notDownloadedDescription = stringResource(R.string.a11y_not_downloaded)
+    val wordCountDescription = item.wordCount?.let {
+        stringResource(R.string.a11y_word_count, it)
+    }
+    val downloadStateDescription = when (item.downloadState) {
+        DownloadState.SUCCESS -> downloadedDescription
+        DownloadState.DOWNLOADING -> downloadingDescription
+        DownloadState.ERROR -> downloadFailedDescription
+        DownloadState.NONE -> notDownloadedDescription
+        DownloadState.LOCAL -> null
+    }
+    val chapterContentDescription = buildList {
+        add(item.title)
+        item.tag?.takeIf { it.isNotBlank() }?.let(::add)
+        if (item.isDur) add(currentReadingDescription)
+        if (item.isSelected) add(selectedDescription)
+        if (item.isVip && !item.isPay) add(lockedDescription)
+        if (showWordCount) wordCountDescription?.let(::add)
+        downloadStateDescription?.let(::add)
+    }.joinToString(", ")
+    val canDownload = item.downloadState == DownloadState.NONE ||
+            item.downloadState == DownloadState.ERROR
+    val downloadActionDescription = stringResource(
+        if (item.downloadState == DownloadState.ERROR) {
+            R.string.a11y_retry_chapter
+        } else {
+            R.string.download_chapter
+        },
+        item.title
+    )
 
     Surface(
         modifier = modifier
             .fillMaxWidth()
+            .semantics {
+                role = Role.Button
+                contentDescription = chapterContentDescription
+                if (item.isSelected) {
+                    stateDescription = selectedDescription
+                }
+            }
             .combinedClickable(
                 onClick = onClick,
                 onLongClick = onLongClick
@@ -817,12 +868,18 @@ fun ChapterItem(
                         .padding(start = 8.dp)
                         .wrapContentSize()
                         .clip(MaterialTheme.shapes.medium)
-                        .combinedClickable(
-                            enabled = item.downloadState != DownloadState.LOCAL,
-                            onClick = {
-                                if (item.downloadState == DownloadState.NONE) {
-                                    onDownloadClick()
-                                }
+                        .then(
+                            if (canDownload) {
+                                Modifier
+                                    .combinedClickable(
+                                        role = Role.Button,
+                                        onClick = onDownloadClick
+                                    )
+                                    .semantics {
+                                        contentDescription = downloadActionDescription
+                                    }
+                            } else {
+                                Modifier.clearAndSetSemantics { }
                             }
                         ),
                     contentAlignment = Alignment.Center

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

@@ -299,7 +299,7 @@ fun TxtRuleScreen(
             onIntent(TxtTocRuleIntent.SetSearchMode(active))
         },
         onSearchQueryChange = { onIntent(TxtTocRuleIntent.UpdateSearchQuery(it)) },
-        searchPlaceholder = stringResource(R.string.replace_purify_search),
+        searchPlaceholder = stringResource(R.string.search_txt_toc_rule),
         onClearSelection = { onIntent(TxtTocRuleIntent.ClearSelection) },
         onSelectAll = { onIntent(TxtTocRuleIntent.SelectAll) },
         onSelectInvert = { onIntent(TxtTocRuleIntent.InvertSelection) },
@@ -351,6 +351,27 @@ fun TxtRuleScreen(
                     } else {
                         selectedIds.contains(item.id)
                     }
+                    val enabledState = stringResource(
+                        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(
+                        item.name,
+                        item.example.takeIf { it.isNotBlank() },
+                        enabledState,
+                        selectedState,
+                        if (!isPickMode && !inSelectionMode) {
+                            stringResource(R.string.a11y_long_press_reorder)
+                        } else {
+                            null
+                        }
+                    ).joinToString()
 
                     ReorderableSelectionItem(
                         state = reorderableState,
@@ -371,11 +392,22 @@ fun TxtRuleScreen(
                         onEnabledChange = { enabled ->
                             onIntent(TxtTocRuleIntent.SetRuleEnabled(item.rule, enabled))
                         },
+                        contentDescription = itemDescription,
+                        stateDescription = selectedState,
+                        enableSwitchContentDescription = stringResource(
+                            R.string.a11y_rule_enabled_switch,
+                            item.name
+                        ),
+                        editContentDescription = stringResource(R.string.a11y_edit_named, item.name),
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         trailingAction = {
                             SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                icon = AppIcons.Delete
+                                icon = AppIcons.Delete,
+                                contentDescription = stringResource(
+                                    R.string.a11y_delete_named,
+                                    item.name
+                                )
                             )
                         }
                     )

+ 6 - 1
app/src/main/java/io/legado/app/ui/config/ai/AiProviderEditScreen.kt

@@ -219,8 +219,13 @@ fun AiProviderEditScreen(
                     keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
                     trailingIcon = {
                         val image = if (apiKeyVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
+                        val description = if (apiKeyVisible) {
+                            stringResource(R.string.hide_password)
+                        } else {
+                            stringResource(R.string.show_password)
+                        }
                         IconButton(onClick = { apiKeyVisible = !apiKeyVisible }) {
-                            Icon(imageVector = image, contentDescription = null)
+                            Icon(imageVector = image, contentDescription = description)
                         }
                     }
                 )

+ 15 - 1
app/src/main/java/io/legado/app/ui/config/readConfig/TextSelectMenuFilterSheet.kt

@@ -25,6 +25,11 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+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 coil.compose.AsyncImage
 import io.legado.app.R
@@ -108,6 +113,9 @@ fun TextSelectMenuFilterSheet(
                 ) {
                     items(apps) { app ->
                         val isEnabled = !disabledSet.contains(app.componentName)
+                        val switchStateDescription = stringResource(
+                            if (isEnabled) R.string.a11y_on else R.string.a11y_off
+                        )
                         NormalCard(
                             onClick = {
                                 val nextSet = if (isEnabled) {
@@ -117,6 +125,10 @@ fun TextSelectMenuFilterSheet(
                                 }
                                 pendingFilterString = nextSet.joinToString(",")
                             },
+                            modifier = Modifier.semantics(mergeDescendants = true) {
+                                role = Role.Switch
+                                stateDescription = switchStateDescription
+                            },
                             cornerRadius = 12.dp,
                             containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
                         ) {
@@ -149,6 +161,7 @@ fun TextSelectMenuFilterSheet(
                                     )
                                 }
                                 AdaptiveSwitch(
+                                    modifier = Modifier.clearAndSetSemantics { },
                                     checked = isEnabled,
                                     onCheckedChange = { checked ->
                                         val nextSet = if (checked) {
@@ -157,7 +170,8 @@ fun TextSelectMenuFilterSheet(
                                             disabledSet + app.componentName
                                         }
                                         pendingFilterString = nextSet.joinToString(",")
-                                    }
+                                    },
+                                    includeStateSemantics = false
                                 )
                             }
                         }

+ 10 - 3
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -66,6 +66,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.constraintlayout.compose.ConstraintLayout
@@ -919,12 +920,19 @@ fun ThemeModeSelector(
         val modifiers = listOf(Modifier.weight(1.2f), Modifier.weight(1f), Modifier.weight(1f))
 
         modes.forEachIndexed { index, (value, label, icon) ->
+            val checked = selectedIndex == index
+            val modeStateDescription = stringResource(
+                if (checked) R.string.a11y_selected else R.string.a11y_not_selected
+            )
 
             ToggleButton(
-                checked = selectedIndex == index,
+                checked = checked,
                 onCheckedChange = { onModeSelected(value) },
                 modifier = modifiers[index]
-                    .semantics { role = Role.RadioButton },
+                    .semantics {
+                        role = Role.RadioButton
+                        stateDescription = modeStateDescription
+                    },
 
                 shapes = when (index) {
                     0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
@@ -1261,4 +1269,3 @@ private fun getThemeColors(
         onSurfaceVariant = colorScheme.onSurfaceVariant
     )
 }
-

+ 32 - 4
app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt

@@ -194,7 +194,7 @@ fun DictRuleScreen(
     )
 
     BatchImportDialog(
-        title = "导入词典规则",
+        title = stringResource(R.string.import_dict_rule),
         importState = importState,
         onDismissRequest = { onIntent(DictRuleIntent.CancelImport) },
         onToggleItem = { onIntent(DictRuleIntent.ToggleImportSelection(it)) },
@@ -270,14 +270,14 @@ fun DictRuleScreen(
     )
 
     RuleListScaffold(
-        title = "字典规则",
+        title = stringResource(R.string.dict_rule),
         state = state,
         onBackClick = { onBackClick() },
         onSearchToggle = { active ->
             onIntent(DictRuleIntent.SetSearchMode(active))
         },
         onSearchQueryChange = { onIntent(DictRuleIntent.UpdateSearchQuery(it)) },
-        searchPlaceholder = stringResource(R.string.replace_purify_search),
+        searchPlaceholder = stringResource(R.string.search_dict_rule),
         onClearSelection = { onIntent(DictRuleIntent.ClearSelection) },
         onSelectAll = { onIntent(DictRuleIntent.SelectAll) },
         onSelectInvert = {
@@ -325,6 +325,23 @@ fun DictRuleScreen(
                 verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {
                 items(rules, key = { it.id }) { item ->
+                    val enabledState = stringResource(
+                        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(
+                        item.id,
+                        item.urlRule.takeIf { it.isNotBlank() },
+                        enabledState,
+                        selectedState,
+                        stringResource(R.string.a11y_long_press_reorder)
+                    ).joinToString()
                     ReorderableSelectionItem(
                         state = reorderableState,
                         key = item.id,
@@ -336,11 +353,22 @@ fun DictRuleScreen(
                         onEnabledChange = { enabled ->
                             onIntent(DictRuleIntent.SetRuleEnabled(item.rule, enabled))
                         },
+                        contentDescription = itemDescription,
+                        stateDescription = selectedState,
+                        enableSwitchContentDescription = stringResource(
+                            R.string.a11y_rule_enabled_switch,
+                            item.id
+                        ),
+                        editContentDescription = stringResource(R.string.a11y_edit_named, item.id),
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         trailingAction = {
                             SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                icon = AppIcons.Delete
+                                icon = AppIcons.Delete,
+                                contentDescription = stringResource(
+                                    R.string.a11y_delete_named,
+                                    item.id
+                                )
                             )
                         }
                     )

+ 3 - 2
app/src/main/java/io/legado/app/ui/highlightTagRule/HighlightTagRuleEditSheet.kt

@@ -82,7 +82,7 @@ fun HighlightTagRuleEditSheet(
             MediumPlainButton(
                 onClick = onDismissRequest,
                 icon = Icons.Default.Close,
-                contentDescription = "Close",
+                contentDescription = stringResource(R.string.close),
             )
         },
         endAction = {
@@ -90,7 +90,7 @@ fun HighlightTagRuleEditSheet(
                 MediumPlainButton(
                     onClick = { showMenu = true },
                     icon = Icons.Default.MoreVert,
-                    contentDescription = "More"
+                    contentDescription = stringResource(R.string.more_menu)
                 )
                 RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
                     RoundDropdownMenuItem(
@@ -169,6 +169,7 @@ fun HighlightTagRuleEditSheet(
                 modifier = Modifier
                     .align(Alignment.BottomEnd)
                     .padding(16.dp),
+                tooltipText = stringResource(R.string.action_save),
                 icon = Icons.Default.Save
             )
         }

+ 38 - 3
app/src/main/java/io/legado/app/ui/highlightTagRule/HighlightTagRuleScreen.kt

@@ -179,7 +179,7 @@ private fun HighlightTagRuleContent(
     FilePickerSheet(
         show = showImportSheet,
         onDismissRequest = { showImportSheet = false },
-        title = stringResource(R.string.import_str),
+        title = stringResource(R.string.import_highlight_tag_rule),
         onSelectSysFile = { types ->
             importDoc.launch(types)
             showImportSheet = false
@@ -246,7 +246,7 @@ private fun HighlightTagRuleContent(
             onIntent(HighlightTagRuleIntent.SetSearchMode(active))
         },
         onSearchQueryChange = { onIntent(HighlightTagRuleIntent.UpdateSearchQuery(it)) },
-        searchPlaceholder = stringResource(R.string.replace_purify_search),
+        searchPlaceholder = stringResource(R.string.search_highlight_tag_rule),
         onClearSelection = { onIntent(HighlightTagRuleIntent.ClearSelection) },
         onSelectAll = { onIntent(HighlightTagRuleIntent.SelectAll) },
         onSelectInvert = {
@@ -294,6 +294,27 @@ private fun HighlightTagRuleContent(
                 verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {
                 items(rules, key = { it.id }) { item ->
+                    val enabledState = stringResource(
+                        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(
+                        item.displayName,
+                        item.pattern.takeIf { it.isNotBlank() },
+                        enabledState,
+                        selectedState,
+                        if (!inSelectionMode) {
+                            stringResource(R.string.a11y_long_press_reorder)
+                        } else {
+                            null
+                        }
+                    ).joinToString()
                     ReorderableSelectionItem(
                         state = reorderableState,
                         key = item.id,
@@ -306,11 +327,25 @@ private fun HighlightTagRuleContent(
                         onEnabledChange = { enabled ->
                             onIntent(HighlightTagRuleIntent.SetRuleEnabled(item.rule, enabled))
                         },
+                        contentDescription = itemDescription,
+                        stateDescription = selectedState,
+                        enableSwitchContentDescription = stringResource(
+                            R.string.a11y_rule_enabled_switch,
+                            item.displayName
+                        ),
+                        editContentDescription = stringResource(
+                            R.string.a11y_edit_named,
+                            item.displayName
+                        ),
                         onClickEdit = { editingRule = item.rule; showEditSheet = true },
                         trailingAction = {
                             SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.rule },
-                                icon = AppIcons.Delete
+                                icon = AppIcons.Delete,
+                                contentDescription = stringResource(
+                                    R.string.a11y_delete_named,
+                                    item.displayName
+                                )
                             )
                         }
                     )

+ 9 - 4
app/src/main/java/io/legado/app/ui/main/MainScreen.kt

@@ -266,7 +266,9 @@ fun MainScreen(
                                     Icons.AutoMirrored.Filled.MenuOpen
                                 else
                                     Icons.Default.Menu,
-                                contentDescription = null
+                                contentDescription = stringResource(
+                                    if (expanded) R.string.collapse else R.string.expand
+                                )
                             )
                         }
 
@@ -286,10 +288,11 @@ fun MainScreen(
                     val selected = pagerState.targetPage == index
                     var showGroupMenu by remember { mutableStateOf(false) }
                     val haptic = LocalHapticFeedback.current
+                    val destinationLabel = stringResource(destination.labelId)
 
                     WideNavigationRailItem(
                         modifier = Modifier.semantics(mergeDescendants = true) {
-                            contentDescription = "nav_${destination.route}"
+                            contentDescription = destinationLabel
                         },
                         railExpanded = navState.targetValue == WideNavigationRailValue.Expanded,
                         selected = selected,
@@ -349,9 +352,10 @@ fun MainScreen(
                         destinations.forEachIndexed { index, destination ->
                             val selected = pagerState.targetPage == index
                             val customIconPath = destination.customIconPath
+                            val destinationLabel = stringResource(destination.labelId)
                             AppNavigationBarItem(
                                 modifier = Modifier.semantics(mergeDescendants = true) {
-                                    contentDescription = "nav_${destination.route}"
+                                    contentDescription = destinationLabel
                                 },
                                 selected = selected,
                                 onClick = {
@@ -532,6 +536,7 @@ fun MainScreen(
                             destinations.forEachIndexed { index, destination ->
                                 val selected = pagerState.targetPage == index
                                 val hasCustomIcon = destination.customIconPath.isNotEmpty()
+                                val destinationLabel = stringResource(destination.labelId)
                                 FloatingBottomBarItem(
                                     onClick = {
                                         handleMainDestinationClick(index, destination)
@@ -539,7 +544,7 @@ fun MainScreen(
                                     modifier = Modifier
                                         .defaultMinSize(minWidth = 76.dp)
                                         .semantics(mergeDescendants = true) {
-                                            contentDescription = "nav_${destination.route}"
+                                            contentDescription = destinationLabel
                                         }
                                 ) {
                                     NavigationIcon(

+ 77 - 4
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -36,6 +36,11 @@ import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.buildAnnotatedString
@@ -84,6 +89,7 @@ fun BookshelfItem(
     coverShadow: Boolean = false,
     titleColor: Color? = null,
     descAnnotated: AnnotatedString? = null,
+    accessibilityLabel: String? = null,
     coverWidth: Int = 84,
     onClick: () -> Unit,
     onLongClick: (() -> Unit)?
@@ -96,6 +102,14 @@ fun BookshelfItem(
     } else {
         LegadoTheme.colorScheme.cardContainer
     }
+    val itemAccessibilityLabel = accessibilityLabel ?: title
+    val accessibilityModifier = Modifier.semantics(mergeDescendants = true) {
+        contentDescription = itemAccessibilityLabel
+        role = Role.Button
+        if (isSelected) {
+            selected = true
+        }
+    }
 
     if (isGrid) {
         Box(
@@ -110,9 +124,11 @@ fun BookshelfItem(
                     }
                 )
                 .combinedClickable(
+                    role = Role.Button,
                     onClick = onClick,
                     onLongClick = onLongClick
                 )
+                .then(accessibilityModifier)
         ) {
             Column(
                 horizontalAlignment = Alignment.CenterHorizontally,
@@ -183,7 +199,8 @@ fun BookshelfItem(
             NormalCard(
                 modifier = modifier
                     .fillMaxWidth()
-                    .padding(vertical = 4.dp),
+                    .padding(vertical = 4.dp)
+                    .then(accessibilityModifier),
                 cornerRadius = 8.dp,
                 containerColor = if (isSelected) {
                     LegadoTheme.colorScheme.secondaryContainer
@@ -436,6 +453,7 @@ fun BookGroupItemGrid(
             )
         },
         title = group.groupName,
+        accessibilityLabel = groupAccessibilityLabel(group.groupName, countText),
         modifier = modifier,
         titleSmallFont = titleSmallFont,
         titleCenter = titleCenter,
@@ -493,6 +511,11 @@ fun BookGroupItemList(
         title = group.groupName,
         subTitle = countText,
         descAnnotated = descAnnotated,
+        accessibilityLabel = groupAccessibilityLabel(
+            group.groupName,
+            countText,
+            firstBookName?.let { "${stringResource(R.string.recently_read)}$it" },
+        ),
         titleSmallFont = titleSmallFont,
         titleCenter = titleCenter,
         titleMaxLines = titleMaxLines,
@@ -521,7 +544,11 @@ fun BookGroupItemHorizontalCovers(
         NormalCard(
             modifier = modifier
                 .fillMaxWidth()
-                .padding(all = 4.dp),
+                .padding(all = 4.dp)
+                .semantics {
+                    contentDescription = groupAccessibilityLabel(group.groupName, countText)
+                    role = Role.Button
+                },
             cornerRadius = 12.dp,
             containerColor = if (bookshelfCardColor != 0) {
                 Color(bookshelfCardColor)
@@ -560,7 +587,7 @@ fun BookGroupItemHorizontalCovers(
                     AppIcon(
                         modifier = Modifier.padding(end = 4.dp),
                         imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
-                        contentDescription = "",
+                        contentDescription = null,
                         tint = LegadoTheme.colorScheme.onSurfaceVariant
                     )
                 }
@@ -578,7 +605,17 @@ fun BookGroupItemHorizontalCovers(
                                 .weight(1f)
                                 .aspectRatio(5f / 7f)
                                 .clip(RoundedCornerShape(4.dp))
-                                .clickable { onBookClick?.invoke(book) }
+                                .clickable(
+                                    role = Role.Button,
+                                    onClick = { onBookClick?.invoke(book) }
+                                )
+                                .semantics(mergeDescendants = true) {
+                                    contentDescription = bookAccessibilityLabel(
+                                        book.name,
+                                        book.author,
+                                    )
+                                    role = Role.Button
+                                }
                         ) {
                             CoilBookCover(
                                 name = book.name,
@@ -767,8 +804,44 @@ fun BookItem(
         titleCenter = titleCenter,
         titleMaxLines = titleMaxLines,
         coverShadow = coverShadow,
+        accessibilityLabel = bookAccessibilityLabel(
+            name = book.name,
+            author = book.author,
+            unreadText?.let { "$it ${stringResource(R.string.is_unread)}" },
+            if (isUpdating) stringResource(R.string.loading) else null,
+            book.durChapterTitle,
+            book.latestChapterTitle,
+            matchedSourceLabel,
+            bookTypeLabel,
+        ),
         coverWidth = if (layoutMode == 0) BookshelfConfig.bookshelfListCoverWidth else BookshelfConfig.bookshelfGridCoverWidth,
         onClick = onClick,
         onLongClick = onLongClick
     )
 }
+
+private fun groupAccessibilityLabel(
+    groupName: String,
+    countText: String?,
+    detail: String? = null,
+): String {
+    return listOfNotNull(
+        groupName.takeIf { it.isNotBlank() },
+        countText?.takeIf { it.isNotBlank() },
+        detail?.takeIf { it.isNotBlank() },
+    ).joinToString(separator = ", ")
+}
+
+private fun bookAccessibilityLabel(
+    name: String,
+    author: String,
+    vararg details: String?,
+): String {
+    return buildList {
+        name.takeIf { it.isNotBlank() }?.let(::add)
+        author.takeIf { it.isNotBlank() }?.let(::add)
+        details.forEach { detail ->
+            detail?.takeIf { it.isNotBlank() }?.let(::add)
+        }
+    }.distinct().joinToString(separator = ", ")
+}

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

@@ -94,7 +94,9 @@ import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Dialog
@@ -938,6 +940,7 @@ fun BookshelfScreen(
                     .align(Alignment.TopCenter)
                     .padding(top = paddingValues.calculateTopPadding() + 6.dp),
             ) { summary ->
+                val closeDescription = stringResource(R.string.close)
                 Box {
                     NormalCard(
                         cornerRadius = 32.dp,
@@ -950,6 +953,10 @@ fun BookshelfScreen(
                         ) {
                             TextCard(
                                 icon = AppIcons.Close,
+                                modifier = Modifier.semantics {
+                                    contentDescription = closeDescription
+                                    role = Role.Button
+                                },
                                 backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
                                 cornerRadius = 16.dp,
                                 verticalPadding = 8.dp,
@@ -982,6 +989,9 @@ fun BookshelfScreen(
                                     cornerRadius = 16.dp,
                                     verticalPadding = 8.dp,
                                     horizontalPadding = 12.dp,
+                                    modifier = Modifier.semantics {
+                                        role = Role.Button
+                                    },
                                     onClick = {
                                         viewModel.showOverlay(BookshelfOverlay.GroupMenu)
                                     }
@@ -1208,8 +1218,12 @@ private fun BookshelfOverlays(
     )
 
     if (uiState.isLoading) {
+        val loadingDescription = uiState.loadingText ?: stringResource(R.string.loading)
         Dialog(onDismissRequest = {}) {
             NormalCard(
+                modifier = Modifier.semantics {
+                    contentDescription = loadingDescription
+                },
                 cornerRadius = 12.dp,
                 containerColor = LegadoTheme.colorScheme.surfaceContainerHigh
             ) {
@@ -1320,6 +1334,7 @@ fun BookshelfPage(
     val bookItemTitleMaxLines by BookshelfConfig.bookshelfTitleMaxLinesState
     val bookItemCoverShadow by BookshelfConfig.bookshelfCoverShadowState
     val showFastScroll by BookshelfConfig.showBookshelfFastScrollerState
+    val listContentDescription = stringResource(R.string.bookshelf)
     val totalHorizontalPadding =
         if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
     val gridContentHorizontalPadding = totalHorizontalPadding / 2
@@ -1352,7 +1367,7 @@ fun BookshelfPage(
             state = gridState,
             modifier = Modifier
                 .fillMaxSize()
-                .semantics(mergeDescendants = true) { contentDescription = "bookshelf_list" }
+                .semantics { contentDescription = listContentDescription }
                 .then(
                     with(sharedTransitionScope) {
                         if (this != null) Modifier.skipToLookaheadSize() else Modifier

+ 2 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt

@@ -121,7 +121,8 @@ fun GroupManageSheet(
                     var showMenu by remember { mutableStateOf(false) }
                     SmallPlainButton(
                         onClick = { showMenu = true },
-                        icon = Icons.Default.Add
+                        icon = Icons.Default.Add,
+                        contentDescription = stringResource(R.string.add),
                     )
                     RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
                         RoundDropdownMenuItem(

+ 22 - 2
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -49,6 +49,11 @@ import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+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.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
@@ -280,6 +285,10 @@ private fun ExploreDiscoveryScreen(
                     cornerRadius = 12.dp,
                     horizontalPadding = 12.dp,
                     verticalPadding = 8.dp,
+                    modifier = Modifier.semantics {
+                        contentDescription = item.bookSourceName
+                        role = Role.Button
+                    },
                     onClick = {
                         scope.launch {
                             val index = listItems.indexOfFirst {
@@ -327,6 +336,9 @@ fun ExploreSourceHeader(
 ) {
     var showMenu by remember { mutableStateOf(false) }
     val rotation by animateFloatAsState(if (isExpanded) 90f else 0f, label = "rotation")
+    val expandActionLabel = stringResource(if (isExpanded) R.string.collapse else R.string.expand)
+    val loadingLabel = stringResource(R.string.loading)
+    val moreMenuLabel = stringResource(R.string.more_menu)
 
     val containerColor by animateColorAsState(
         targetValue = if (isExpanded)
@@ -356,9 +368,19 @@ fun ExploreSourceHeader(
         ListItem(
             modifier = Modifier
                 .combinedClickable(
+                    role = Role.Button,
+                    onClickLabel = expandActionLabel,
+                    onLongClickLabel = moreMenuLabel,
                     onClick = onClick,
                     onLongClick = { showMenu = true }
                 )
+                .semantics(mergeDescendants = true) {
+                    contentDescription = item.bookSourceName
+                    role = Role.Button
+                    if (loadingKinds) {
+                        stateDescription = loadingLabel
+                    }
+                }
                 .fillMaxWidth(),
             colors = ListItemDefaults.colors(
                 containerColor = Color.Transparent
@@ -435,5 +457,3 @@ fun ExploreSourceHeader(
         )
     }
 }
-
-

+ 32 - 4
app/src/main/java/io/legado/app/ui/main/home/HomeScreen.kt

@@ -72,6 +72,12 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.style.TextOverflow
@@ -426,7 +432,7 @@ fun HomeScreen(
                         TopBarActionButton(
                             onClick = { showPageMenu = true },
                             imageVector = Icons.Default.MoreVert,
-                            contentDescription = null,
+                            contentDescription = stringResource(R.string.more_menu),
                         )
                         RoundDropdownMenu(
                             expanded = showPageMenu,
@@ -575,7 +581,10 @@ fun HomeScreen(
                         GlassCard(
                             modifier = Modifier
                                 .padding(horizontal = 12.dp)
-                                .clickable { showSourceMenu = true },
+                                .clickable(
+                                    role = Role.Button,
+                                    onClick = { showSourceMenu = true },
+                                ),
                             cornerRadius = 32.dp
                         ) {
                             Row(
@@ -1015,7 +1024,14 @@ private fun RecentHistoryBookCard(
             .width(60.dp)
             .aspectRatio(5f / 7f)
             .clip(RoundedCornerShape(12.dp))
-            .clickable(onClick = onClick),
+            .clickable(
+                role = Role.Button,
+                onClick = onClick,
+            )
+            .semantics {
+                contentDescription = bookAccessibilityLabel(book.name, book.author)
+                role = Role.Button
+            },
     ) {
         BookshelfCover(
             name = book.name,
@@ -1115,7 +1131,12 @@ private fun SemiCircleProgress(
     val progressColor = LegadoTheme.colorScheme.primary
 
     Box(
-        modifier = modifier,
+        modifier = modifier.semantics {
+            progressBarRangeInfo = ProgressBarRangeInfo(
+                current = animatedProgress.coerceIn(0f, 1f),
+                range = 0f..1f,
+            )
+        },
         contentAlignment = Alignment.BottomCenter,
     ) {
         Canvas(modifier = Modifier.fillMaxSize()) {
@@ -1402,3 +1423,10 @@ private fun HomeDashboardSection.labelRes(): Int = when (this) {
     HomeDashboardSection.DailyGoal -> R.string.home_today_reading_goal
     HomeDashboardSection.WebDavBackup -> R.string.home_webdav_backup
 }
+
+private fun bookAccessibilityLabel(
+    name: String,
+    author: String,
+): String {
+    return if (author.isBlank()) name else "$name, $author"
+}

+ 7 - 2
app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleFeed.kt

@@ -35,6 +35,7 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
@@ -206,7 +207,10 @@ fun HomepageModuleFeed(
                                                     Box(
                                                         modifier = Modifier
                                                             .fillMaxWidth()
-                                                            .clickable {
+                                                            .clickable(
+                                                                role = Role.Button,
+                                                                onClickLabel = stringResource(R.string.retry),
+                                                            ) {
                                                                 actions.onRetryModule(moduleUi.globalId)
                                                             }
                                                             .padding(vertical = 10.dp),
@@ -486,7 +490,8 @@ private fun ModuleHeader(
         if (onNavigate != null) {
             SmallTonalButton(
                 onClick = onNavigate,
-                icon = Icons.AutoMirrored.Filled.ArrowForward
+                icon = Icons.AutoMirrored.Filled.ArrowForward,
+                contentDescription = stringResource(R.string.homepage_show_all),
             )
         }
     }

+ 17 - 1
app/src/main/java/io/legado/app/ui/main/homepage/modules/BannerModule.kt

@@ -19,6 +19,10 @@ import androidx.compose.material.icons.filled.Shuffle
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.domain.model.BookShelfState
@@ -66,9 +70,14 @@ fun BannerModule(
                     modifier = Modifier
                         .width(96.dp)
                         .combinedClickable(
+                            role = Role.Button,
                             onClick = { onClick(book, sharedCoverKey) },
                             onLongClick = onLongClick?.let { cb -> { cb(book, sharedCoverKey) } }
-                        ),
+                        )
+                        .semantics {
+                            contentDescription = bookAccessibilityLabel(book.name, book.author)
+                            role = Role.Button
+                        },
                     sharedTransitionScope = sharedTransitionScope,
                     animatedVisibilityScope = animatedVisibilityScope,
                     sharedCoverKey = sharedCoverKey,
@@ -94,3 +103,10 @@ fun BannerModule(
         }
     }
 }
+
+private fun bookAccessibilityLabel(
+    name: String,
+    author: String,
+): String {
+    return if (author.isBlank()) name else "$name, $author"
+}

+ 2 - 1
app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt

@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
@@ -89,7 +90,7 @@ fun RankingModule(
                 Box(
                     modifier = Modifier
                         .fillMaxWidth()
-                        .clickable {
+                        .clickable(role = Role.Button) {
                             visibleCount =
                                 if (visibleCount == INITIAL_COUNT) MAX_COUNT else INITIAL_COUNT
                         }

+ 1 - 2
app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt

@@ -92,7 +92,7 @@ fun MyScreen(
                             )
                         },
                         imageVector = Icons.AutoMirrored.Filled.HelpOutline,
-                        contentDescription = null
+                        contentDescription = stringResource(R.string.help)
                     )
                 },
                 scrollBehavior = scrollBehavior
@@ -283,4 +283,3 @@ fun WebServiceSettingBlock(
         }
     }
 }
-

+ 29 - 4
app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt

@@ -41,6 +41,10 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
@@ -179,12 +183,19 @@ fun RssScreen(
                 verticalArrangement = Arrangement.spacedBy(12.dp)
             ) {
                 item(span = { GridItemSpan(maxLineSpan) }) {
+                    val ruleSubscriptionLabel = stringResource(R.string.rule_subscription)
+                    val favoriteLabel = stringResource(R.string.favorite)
                     Row(
                         modifier = Modifier.fillMaxWidth(),
                         horizontalArrangement = Arrangement.spacedBy(12.dp)
                     ) {
                         GlassCard(
-                            modifier = Modifier.weight(1f),
+                            modifier = Modifier
+                                .weight(1f)
+                                .semantics(mergeDescendants = true) {
+                                    contentDescription = ruleSubscriptionLabel
+                                    role = Role.Button
+                                },
                             onClick = { viewModel.openRuleSub() }
                         ) {
                             Row(
@@ -200,13 +211,18 @@ fun RssScreen(
                                 )
                                 Spacer(modifier = Modifier.width(12.dp))
                                 AppText(
-                                    text = stringResource(R.string.rule_subscription),
+                                    text = ruleSubscriptionLabel,
                                     style = LegadoTheme.typography.labelMediumEmphasized
                                 )
                             }
                         }
                         GlassCard(
-                            modifier = Modifier.weight(1f),
+                            modifier = Modifier
+                                .weight(1f)
+                                .semantics(mergeDescendants = true) {
+                                    contentDescription = favoriteLabel
+                                    role = Role.Button
+                                },
                             onClick = { viewModel.openFavorites() }
                         ) {
                             Row(
@@ -222,7 +238,7 @@ fun RssScreen(
                                 )
                                 Spacer(modifier = Modifier.width(12.dp))
                                 AppText(
-                                    text = stringResource(R.string.favorite),
+                                    text = favoriteLabel,
                                     style = LegadoTheme.typography.labelMediumEmphasized
                                 )
                             }
@@ -272,14 +288,23 @@ fun RssSourceGridItem(
     onLogin: () -> Unit
 ) {
     var showMenu by remember { mutableStateOf(false) }
+    val openLabel = stringResource(R.string.open)
+    val moreMenuLabel = stringResource(R.string.more_menu)
 
     Column(
         modifier = modifier
             .clip(RoundedCornerShape(16.dp))
             .combinedClickable(
+                role = Role.Button,
+                onClickLabel = openLabel,
+                onLongClickLabel = moreMenuLabel,
                 onClick = onClick,
                 onLongClick = { showMenu = true }
             )
+            .semantics(mergeDescendants = true) {
+                contentDescription = source.sourceName
+                role = Role.Button
+            }
             .padding(8.dp),
         horizontalAlignment = Alignment.CenterHorizontally
     ) {

+ 33 - 0
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt

@@ -409,6 +409,28 @@ fun ReplaceRuleScreen(
                 verticalArrangement = Arrangement.spacedBy(8.dp)
             ) {
                 items(rules, key = { it.id }) { ui ->
+                    val enabledState = stringResource(
+                        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) {
+                        stringResource(R.string.a11y_long_press_reorder)
+                    } else {
+                        null
+                    }
+                    val itemDescription = listOfNotNull(
+                        ui.name,
+                        ui.pattern.takeIf { it.isNotBlank() },
+                        enabledState,
+                        selectedState,
+                        reorderHint
+                    ).joinToString()
                     ReorderableSelectionItem(
                         state = reorderableState,
                         key = ui.id,
@@ -423,6 +445,17 @@ fun ReplaceRuleScreen(
                         onEnabledChange = { enabled ->
                             onIntent(ReplaceRuleIntent.SetRuleEnabled(ui.id, enabled))
                         },
+                        contentDescription = itemDescription,
+                        stateDescription = selectedState,
+                        enableSwitchContentDescription = stringResource(
+                            R.string.a11y_rule_enabled_switch,
+                            ui.name
+                        ),
+                        editContentDescription = stringResource(R.string.a11y_edit_named, ui.name),
+                        moreContentDescription = stringResource(
+                            R.string.a11y_more_actions_for,
+                            ui.name
+                        ),
                         onClickEdit = {
                             onNavigateToEdit(
                                 ReplaceEditRoute(

+ 2 - 1
app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt

@@ -377,7 +377,8 @@ fun GroupSelector(
         }
         MediumPlainButton(
             onClick = onManageClick,
-            icon = Icons.Default.Settings
+            icon = Icons.Default.Settings,
+            contentDescription = stringResource(R.string.group_management)
         )
     }
 }

+ 14 - 1
app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt

@@ -45,6 +45,10 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -332,12 +336,21 @@ private fun RssArticleItem(
         RssArticleLayout.GridCard -> 2
         RssArticleLayout.Waterfall -> 3
     }
+    val articleAccessibilityLabel = remember(article.title, article.pubDate) {
+        listOfNotNull(article.title, article.pubDate?.takeIf { it.isNotBlank() })
+            .joinToString()
+    }
 
     GlassCard(
         onClick = { onClick(article) },
         cornerRadius = 12.dp,
         containerColor = containerColor,
-        modifier = Modifier.fillMaxWidth()
+        modifier = Modifier
+            .fillMaxWidth()
+            .semantics(mergeDescendants = true) {
+                contentDescription = articleAccessibilityLabel
+                role = Role.Button
+            }
     ) {
         when (layout) {
             RssArticleLayout.List -> {

+ 1 - 1
app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt

@@ -171,7 +171,7 @@ fun RssSortScreen(
                     TopBarActionButton(
                         onClick = { showMainMenu = true },
                         imageVector = AppIcons.MoreVert,
-                        contentDescription = "Menu"
+                        contentDescription = stringResource(R.string.more_menu)
                     )
                     RoundDropdownMenu(
                         expanded = showMainMenu,

+ 1 - 2
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt

@@ -232,7 +232,7 @@ fun RssFavoritesScreen(
                             SmallPlainButton(
                                 onClick = openAction,
                                 icon = Icons.AutoMirrored.Filled.OpenInNew,
-                                contentDescription = "Open"
+                                contentDescription = stringResource(R.string.open)
                             )
                         },
                         dropdownContent = { dismiss ->
@@ -257,4 +257,3 @@ fun RssFavoritesScreen(
         }
     }
 }
-

+ 1 - 1
app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt

@@ -230,7 +230,7 @@ fun RssReadRouteScreen(
                         Box {
                         TopBarActionButton(
                             imageVector = Icons.Default.MoreVert,
-                            contentDescription = "Menu",
+                            contentDescription = stringResource(R.string.more_menu),
                             onClick = { showMenu = true }
                         )
                             RoundDropdownMenu(

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

@@ -362,7 +362,8 @@ fun RssSourceScreen(
                         trailingAction = {
                             SmallPlainButton(
                                 onClick = { showDeleteRuleDialog = item.source },
-                                icon = Icons.Default.Delete
+                                icon = Icons.Default.Delete,
+                                contentDescription = stringResource(R.string.delete)
                             )
                         }
                     )
@@ -384,4 +385,3 @@ fun RssSourceScreen(
         }
     }
 }
-

+ 1 - 1
app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt

@@ -169,7 +169,7 @@ fun RuleSubScreen(
                             SmallPlainButton(
                                 onClick = { showEditDialog = ruleSub },
                                 icon = Icons.Default.Edit,
-                                contentDescription = "Edit"
+                                contentDescription = stringResource(R.string.edit)
                             )
                         },
                         dropdownContent = { dismiss ->

+ 12 - 6
app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt

@@ -4,9 +4,11 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.util.AttributeSet
 import android.view.LayoutInflater
+import android.view.View
 import android.widget.FrameLayout
 import android.widget.SeekBar
 import androidx.appcompat.widget.TooltipCompat
+import androidx.core.view.ViewCompat
 import com.google.android.material.slider.Slider
 import io.legado.app.R
 import io.legado.app.databinding.ViewDetailSeekBarBinding
@@ -58,8 +60,11 @@ class DetailSeekBar @JvmOverloads constructor(
 
         binding.tvSeekTitle.apply {
             text = title
+            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
             TooltipCompat.setTooltipText(this, title)
         }
+        binding.tvSeekValue.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+        binding.slider.contentDescription = title
 
         binding.slider.valueFrom = typedArray.getInteger(R.styleable.DetailSeekBar_min, 0).toFloat()
         binding.slider.valueTo = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0).toFloat()
@@ -84,14 +89,13 @@ class DetailSeekBar @JvmOverloads constructor(
             }
         })
 
+        upValue()
     }
 
     private fun upValue(progress: Int = binding.slider.value.toInt()) {
-        valueFormat?.let {
-            binding.tvSeekValue.text = it.invoke(progress)
-        } ?: let {
-            binding.tvSeekValue.text = progress.toString()
-        }
+        val displayValue = valueFormat?.invoke(progress) ?: progress.toString()
+        binding.tvSeekValue.text = displayValue
+        ViewCompat.setStateDescription(binding.slider, displayValue)
     }
 
     override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@@ -100,6 +104,8 @@ class DetailSeekBar @JvmOverloads constructor(
 
     fun setTitle(title: CharSequence?) {
         binding.tvSeekTitle.text = title
+        binding.slider.contentDescription = title
         TooltipCompat.setTooltipText(binding.tvSeekTitle, title)
+        upValue()
     }
-}
+}

+ 14 - 1
app/src/main/java/io/legado/app/ui/widget/SimpleSliderView.kt

@@ -5,11 +5,13 @@ import android.content.Context
 import android.os.Handler
 import android.os.Looper
 import android.util.AttributeSet
+import android.view.View
 import android.view.LayoutInflater
 import android.widget.FrameLayout
 import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.appcompat.widget.TooltipCompat
+import androidx.core.view.ViewCompat
 import io.legado.app.R
 import io.legado.app.databinding.ViewSimpleSliderBinding
 import io.legado.app.lib.dialogs.alert
@@ -35,6 +37,7 @@ class SimpleSliderView @JvmOverloads constructor(
     private val repeatInterval = 320L
 
     private val tvTit: TextView
+    private var titleText: CharSequence? = null
 
     var progress: Int
         get() = _progress
@@ -63,6 +66,7 @@ class SimpleSliderView @JvmOverloads constructor(
         val typedArray = context.obtainStyledAttributes(attrs, R.styleable.DetailSeekBar)
         isBottomBackground = typedArray.getBoolean(R.styleable.DetailSeekBar_isBottomBackground, false)
         val title = typedArray.getText(R.styleable.DetailSeekBar_title)
+        titleText = title
         _max = typedArray.getInt(R.styleable.DetailSeekBar_max, 100)
         _min = typedArray.getInt(R.styleable.DetailSeekBar_min, 0)
         _progress = _progress.coerceIn(_min, _max)
@@ -88,6 +92,8 @@ class SimpleSliderView @JvmOverloads constructor(
         typedArray.recycle()
 
         tvTit.text = title
+        tvTit.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+        slider.contentDescription = title
         TooltipCompat.setTooltipText(tvTit, title)
 
         binding.tvSeekValue.setOnClickListener {
@@ -128,7 +134,14 @@ class SimpleSliderView @JvmOverloads constructor(
     }
 
     private fun updateValue() {
-        binding.tvSeekValue.text = valueFormat?.invoke(_progress) ?: _progress.toString()
+        val displayValue = valueFormat?.invoke(_progress) ?: _progress.toString()
+        binding.tvSeekValue.text = displayValue
+        binding.tvSeekValue.contentDescription = context.getString(
+            R.string.a11y_slider_value,
+            titleText ?: tvTit.text,
+            displayValue,
+        )
+        ViewCompat.setStateDescription(binding.slider, displayValue)
         if (binding.slider.value.toInt() != _progress) {
             binding.slider.value = _progress.toFloat()
         }

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

@@ -3,6 +3,9 @@ package io.legado.app.ui.widget.components
 import androidx.compose.material3.Slider
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
@@ -16,12 +19,18 @@ fun AppSlider(
     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
     steps: Int = 0,
     onValueChangeFinished: (() -> Unit)? = null,
+    accessibilityLabel: String? = null,
+    accessibilityValue: String? = null,
 ) {
+    val sliderModifier = modifier.sliderAccessibility(
+        label = accessibilityLabel,
+        value = accessibilityValue,
+    )
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         MiuixSlider(
             value = value,
             onValueChange = onValueChange,
-            modifier = modifier,
+            modifier = sliderModifier,
             enabled = enabled,
             valueRange = valueRange,
             steps = steps,
@@ -31,7 +40,7 @@ fun AppSlider(
         Slider(
             value = value,
             onValueChange = onValueChange,
-            modifier = modifier,
+            modifier = sliderModifier,
             enabled = enabled,
             valueRange = valueRange,
             steps = steps,
@@ -39,3 +48,14 @@ fun AppSlider(
         )
     }
 }
+
+internal fun Modifier.sliderAccessibility(
+    label: String?,
+    value: String?,
+): Modifier {
+    if (label == null && value == null) return this
+    return semantics {
+        label?.let { contentDescription = it }
+        value?.let { stateDescription = it }
+    }
+}

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

@@ -31,11 +31,17 @@ fun AppVerticalSlider(
     steps: Int = 0,
     onValueChangeFinished: (() -> Unit)? = null,
     height: Dp = 160.dp,
+    accessibilityLabel: String? = null,
+    accessibilityValue: String? = null,
 ) {
     val sliderModifier = Modifier
         .requiredWidth(height)
         .requiredHeight(48.dp)
         .graphicsLayer { rotationZ = -90f }
+        .sliderAccessibility(
+            label = accessibilityLabel,
+            value = accessibilityValue,
+        )
 
     Box(
         modifier = modifier

+ 49 - 10
app/src/main/java/io/legado/app/ui/widget/components/IconSwitch.kt

@@ -11,6 +11,10 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.semantics
+import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import top.yukonga.miuix.kmp.basic.Switch as MiuixSwitch
@@ -23,15 +27,25 @@ fun AdaptiveSwitch(
     enabled: Boolean = true,
     checkedIcon: ImageVector = Icons.Filled.Check,
     uncheckedIcon: ImageVector? = null,
-    showIcon: Boolean = true
+    showIcon: Boolean = true,
+    includeStateSemantics: Boolean = true
 ) {
     val composeEngine = LegadoTheme.composeEngine
+    val switchStateDescription = stringResource(
+        if (checked) R.string.a11y_on else R.string.a11y_off
+    )
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         MiuixSwitch(
             checked = checked,
             onCheckedChange = onCheckedChange,
-            modifier = modifier,
+            modifier = if (includeStateSemantics) {
+                modifier.semantics {
+                    stateDescription = switchStateDescription
+                }
+            } else {
+                modifier
+            },
             enabled = enabled
         )
     } else {
@@ -42,7 +56,8 @@ fun AdaptiveSwitch(
             enabled = enabled,
             checkedIcon = checkedIcon,
             uncheckedIcon = uncheckedIcon,
-            showIcon = showIcon
+            showIcon = showIcon,
+            includeStateSemantics = includeStateSemantics
         )
     }
 }
@@ -55,26 +70,39 @@ fun TinySwitch(
     enabled: Boolean = true,
     checkedIcon: ImageVector = Icons.Filled.Check,
     uncheckedIcon: ImageVector? = null,
-    showIcon: Boolean = true
+    showIcon: Boolean = true,
+    includeStateSemantics: Boolean = true
 ) {
     val composeEngine = LegadoTheme.composeEngine
+    val switchStateDescription = stringResource(
+        if (checked) R.string.a11y_on else R.string.a11y_off
+    )
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         MiuixSwitch(
             checked = checked,
             onCheckedChange = onCheckedChange,
-            modifier = Modifier.scale(0.9f),
+            modifier = if (includeStateSemantics) {
+                modifier
+                    .scale(0.9f)
+                    .semantics {
+                        stateDescription = switchStateDescription
+                    }
+            } else {
+                modifier.scale(0.9f)
+            },
             enabled = enabled
         )
     } else {
         IconSwitch(
-            modifier = Modifier.scale(0.8f),
+            modifier = modifier.scale(0.8f),
             checked = checked,
             onCheckedChange = onCheckedChange,
             enabled = enabled,
             checkedIcon = checkedIcon,
             uncheckedIcon = uncheckedIcon,
-            showIcon = showIcon
+            showIcon = showIcon,
+            includeStateSemantics = includeStateSemantics
         )
     }
 }
@@ -88,10 +116,21 @@ fun IconSwitch(
     checkedIcon: ImageVector = Icons.Filled.Check,
     uncheckedIcon: ImageVector? = null,
     showIcon: Boolean = true,
-    colors: SwitchColors = SwitchDefaults.colors()
+    colors: SwitchColors = SwitchDefaults.colors(),
+    includeStateSemantics: Boolean = true
 ) {
+    val switchStateDescription = stringResource(
+        if (checked) R.string.a11y_on else R.string.a11y_off
+    )
+
     Switch(
-        modifier = modifier,
+        modifier = if (includeStateSemantics) {
+            modifier.semantics {
+                stateDescription = switchStateDescription
+            }
+        } else {
+            modifier
+        },
         checked = checked,
         onCheckedChange = onCheckedChange,
         enabled = enabled,
@@ -110,4 +149,4 @@ fun IconSwitch(
             }
         }
     )
-}
+}

+ 16 - 8
app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt

@@ -29,7 +29,9 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.icon.AppIcon
@@ -66,6 +68,9 @@ fun SelectionBottomBar(
 ) {
     var showMenu by remember { mutableStateOf(false) }
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+    val selectAllDescription = stringResource(R.string.select_all)
+    val invertSelectionDescription = stringResource(R.string.invert_selection)
+    val moreActionsDescription = stringResource(R.string.more_menu)
 
     if (isMiuix) {
         MiuixFloatingToolbar(
@@ -79,13 +84,13 @@ fun SelectionBottomBar(
                 MiuixIconButton(onClick = onSelectAll) {
                     MiuixIcon(
                         imageVector = Icons.Default.SelectAll,
-                        contentDescription = "Select All"
+                        contentDescription = selectAllDescription
                     )
                 }
                 MiuixIconButton(onClick = onSelectInvert) {
                     MiuixIcon(
                         imageVector = Icons.Default.Refresh,
-                        contentDescription = "Invert Selection"
+                        contentDescription = invertSelectionDescription
                     )
                 }
 
@@ -96,7 +101,7 @@ fun SelectionBottomBar(
                 ) {
                     MiuixIcon(
                         imageVector = primaryAction.icon,
-                        contentDescription = null,
+                        contentDescription = primaryAction.text,
                         tint = MiuixTheme.colorScheme.onSecondaryContainer
                     )
                 }
@@ -106,7 +111,7 @@ fun SelectionBottomBar(
                         MiuixIconButton(onClick = { showMenu = true }) {
                             MiuixIcon(
                                 imageVector = Icons.Default.MoreVert,
-                                contentDescription = "More actions"
+                                contentDescription = moreActionsDescription
                             )
                         }
                         RoundDropdownMenu(
@@ -135,13 +140,13 @@ fun SelectionBottomBar(
                 IconButton(onClick = onSelectAll) {
                     AppIcon(
                         imageVector = Icons.Default.SelectAll,
-                        contentDescription = "Select All"
+                        contentDescription = selectAllDescription
                     )
                 }
                 IconButton(onClick = onSelectInvert) {
                     AppIcon(
                         imageVector = Icons.Default.Refresh,
-                        contentDescription = "Invert Selection"
+                        contentDescription = invertSelectionDescription
                     )
                 }
             },
@@ -151,7 +156,7 @@ fun SelectionBottomBar(
                         IconButton(onClick = { showMenu = true }) {
                             AppIcon(
                                 imageVector = Icons.Default.MoreVert,
-                                contentDescription = "More actions"
+                                contentDescription = moreActionsDescription
                             )
                         }
                         RoundDropdownMenu(
@@ -193,7 +198,10 @@ fun SelectionBottomBar(
                             contentColor = LegadoTheme.colorScheme.onSecondaryContainer,
                         ),
                     ) {
-                        AppIcon(imageVector = primaryAction.icon, contentDescription = null)
+                        AppIcon(
+                            imageVector = primaryAction.icon,
+                            contentDescription = primaryAction.text
+                        )
                     }
                 }
             }

+ 10 - 8
app/src/main/java/io/legado/app/ui/widget/components/bookmark/BookmarkEditSheet.kt

@@ -12,7 +12,9 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.ui.widget.components.AppTextFieldSurface
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -41,7 +43,7 @@ fun BookmarkEditSheet(
             SmallTonalButton(
                 onClick = { showDeleteConfirmDialog = true },
                 icon = AppIcons.Delete,
-                contentDescription = "删除"
+                contentDescription = stringResource(R.string.delete)
             )
         },
         endAction = {
@@ -54,7 +56,7 @@ fun BookmarkEditSheet(
                     onSave(newBookmark)
                 },
                 icon = AppIcons.Check,
-                contentDescription = "保存"
+                contentDescription = stringResource(R.string.action_save)
             )
         }
     ) {
@@ -70,14 +72,14 @@ fun BookmarkEditSheet(
     AppAlertDialog(
         show = showDeleteConfirmDialog,
         onDismissRequest = { showDeleteConfirmDialog = false },
-        title = "确认删除",
-        text = "你确定要删除这条书签吗?",
-        confirmText = "删除",
+        title = stringResource(R.string.confirm_delete_bookmark),
+        text = stringResource(R.string.delete_bookmark_message),
+        confirmText = stringResource(R.string.delete),
         onConfirm = {
             showDeleteConfirmDialog = false
             onDelete(bookmark)
         },
-        dismissText = "取消",
+        dismissText = stringResource(R.string.cancel),
         onDismiss = { showDeleteConfirmDialog = false }
     )
 }
@@ -98,7 +100,7 @@ fun BookmarkEditContent(
         AppTextFieldSurface(
             value = bookText,
             onValueChange = onBookTextChange,
-            label = "原文",
+            label = stringResource(R.string.bookmark_original_text),
             modifier = Modifier.fillMaxWidth(),
             maxLines = 10
         )
@@ -108,7 +110,7 @@ fun BookmarkEditContent(
         AppTextFieldSurface(
             value = content,
             onValueChange = onContentChange,
-            label = "摘要/笔记",
+            label = stringResource(R.string.bookmark_note),
             modifier = Modifier.fillMaxWidth(),
             maxLines = 5
         )

+ 24 - 2
app/src/main/java/io/legado/app/ui/widget/components/bookmark/BookmarkItem.kt

@@ -13,8 +13,14 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
@@ -34,14 +40,30 @@ fun BookmarkItem(
         else Color.Transparent,
         label = "BgColor"
     )
+    val itemDescription = listOfNotNull(
+        bookmark.chapterName,
+        bookmark.bookText.takeIf { it.isNotBlank() }?.let {
+            stringResource(R.string.bookmark_original_text_description, it)
+        },
+        bookmark.content.takeIf { it.isNotBlank() }?.let {
+            stringResource(R.string.bookmark_note_description, it)
+        }
+    ).joinToString()
+    val editLabel = stringResource(R.string.edit)
 
     Surface(
         modifier = modifier
             .fillMaxWidth()
             .combinedClickable(
+                onClickLabel = editLabel,
+                onLongClickLabel = editLabel,
                 onClick = onClick,
                 onLongClick = onLongClick
-            ),
+            )
+            .semantics(mergeDescendants = true) {
+                contentDescription = itemDescription
+                role = Role.Button
+            },
         color = backgroundColor
     ) {
         Column(
@@ -76,4 +98,4 @@ fun BookmarkItem(
             }
         }
     }
-}
+}

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

@@ -29,10 +29,18 @@ import androidx.compose.ui.draw.scale
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
+import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
@@ -63,7 +71,12 @@ fun SelectionItemCard(
     trailingAction: @Composable (RowScope.() -> Unit)? = null,
     dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null,
     containerColor: Color? = null,
-    selectedContainerColor: Color? = null
+    selectedContainerColor: Color? = null,
+    contentDescription: String? = null,
+    stateDescription: String? = null,
+    enableSwitchContentDescription: String? = null,
+    editContentDescription: String? = null,
+    moreContentDescription: String? = null
 ) {
     val composeEngine = ThemeResolver.isMiuixEngine(composeEngine)
     val animatedContainerColor by animateColorAsState(
@@ -80,7 +93,21 @@ fun SelectionItemCard(
     GlassCard(
         onClick = onToggleSelection,
         modifier = modifier
-            .fillMaxWidth(),
+            .fillMaxWidth()
+            .then(
+                if (contentDescription != null || stateDescription != null || isSelected) {
+                    Modifier.semantics {
+                        contentDescription?.let { this.contentDescription = it }
+                        stateDescription?.let { this.stateDescription = it }
+                        if (isSelected) {
+                            this.selected = true
+                        }
+                        role = Role.Button
+                    }
+                } else {
+                    Modifier
+                }
+            ),
         cornerRadius = 12.dp,
         containerColor = animatedContainerColor,
         elevation = elevation
@@ -96,7 +123,10 @@ fun SelectionItemCard(
             onEnabledChange = onEnabledChange,
             onClickEdit = onClickEdit,
             trailingAction = trailingAction,
-            dropdownContent = dropdownContent
+            dropdownContent = dropdownContent,
+            enableSwitchContentDescription = enableSwitchContentDescription,
+            editContentDescription = editContentDescription,
+            moreContentDescription = moreContentDescription
         )
     }
 }
@@ -113,9 +143,14 @@ fun SelectionItemCardContent(
     onEnabledChange: ((Boolean) -> Unit)? = null,
     onClickEdit: (() -> Unit)? = null,
     trailingAction: @Composable (RowScope.() -> Unit)? = null,
-    dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null
+    dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null,
+    enableSwitchContentDescription: String? = null,
+    editContentDescription: String? = null,
+    moreContentDescription: String? = null
 ) {
     var showMenu by remember { mutableStateOf(false) }
+    val defaultEditDescription = stringResource(R.string.edit)
+    val defaultMoreDescription = stringResource(R.string.more_menu)
 
     Row(
         modifier = Modifier
@@ -205,7 +240,15 @@ fun SelectionItemCardContent(
         ) {
             onEnabledChange?.let {
                 AdaptiveSwitch(
-                    modifier = Modifier.scale(0.8f),
+                    modifier = Modifier
+                        .scale(0.8f)
+                        .then(
+                            enableSwitchContentDescription?.let { description ->
+                                Modifier.semantics {
+                                    contentDescription = description
+                                }
+                            } ?: Modifier
+                        ),
                     checked = isEnabled,
                     onCheckedChange = it
                 )
@@ -215,7 +258,7 @@ fun SelectionItemCardContent(
                 SmallPlainButton(
                     onClick = onClickEdit,
                     icon = AppIcons.Edit,
-                    contentDescription = "Edit"
+                    contentDescription = editContentDescription ?: defaultEditDescription
                 )
             }
 
@@ -228,7 +271,7 @@ fun SelectionItemCardContent(
                     SmallPlainButton(
                         onClick = { showMenu = true },
                         icon = AppIcons.MoreVert,
-                        contentDescription = "More"
+                        contentDescription = moreContentDescription ?: defaultMoreDescription
                     )
                     RoundDropdownMenu(
                         expanded = showMenu,
@@ -262,7 +305,12 @@ fun LazyItemScope.ReorderableSelectionItem(
     trailingAction: @Composable (RowScope.() -> Unit)? = null,
     dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null,
     containerColor: Color? = null,
-    selectedContainerColor: Color? = null
+    selectedContainerColor: Color? = null,
+    contentDescription: String? = null,
+    stateDescription: String? = null,
+    enableSwitchContentDescription: String? = null,
+    editContentDescription: String? = null,
+    moreContentDescription: String? = null
 ) {
     val hapticFeedback = LocalHapticFeedback.current
 
@@ -288,6 +336,11 @@ fun LazyItemScope.ReorderableSelectionItem(
             dropdownContent = dropdownContent,
             containerColor = containerColor,
             selectedContainerColor = selectedContainerColor,
+            contentDescription = contentDescription,
+            stateDescription = stateDescription,
+            enableSwitchContentDescription = enableSwitchContentDescription,
+            editContentDescription = editContentDescription,
+            moreContentDescription = moreContentDescription,
             modifier = modifier
                 .zIndex(if (isDragging) 1f else 0f)
                 .then(
@@ -306,4 +359,3 @@ fun LazyItemScope.ReorderableSelectionItem(
         )
     }
 }
-

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

@@ -4,7 +4,11 @@ import androidx.compose.material3.Checkbox
 import androidx.compose.material3.TriStateCheckbox
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.state.ToggleableState
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.semantics
+import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import top.yukonga.miuix.kmp.basic.Checkbox as MiuixCheckbox
@@ -14,22 +18,33 @@ fun AppCheckbox(
     checked: Boolean,
     onCheckedChange: ((Boolean) -> Unit)?,
     modifier: Modifier = Modifier,
-    enabled: Boolean = true
+    enabled: Boolean = true,
+    includeStateSemantics: Boolean = true
 ) {
     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) {
+        modifier.semantics {
+            stateDescription = checkboxStateDescription
+        }
+    } else {
+        modifier
+    }
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         MiuixCheckbox(
             state = state,
             onClick = onCheckedChange?.let { { it(!checked) } },
-            modifier = modifier,
+            modifier = semanticModifier,
             enabled = enabled
         )
     } else {
         Checkbox(
             checked = checked,
             onCheckedChange = onCheckedChange,
-            modifier = modifier,
+            modifier = semanticModifier,
             enabled = enabled
         )
     }
@@ -57,4 +72,4 @@ fun AppTriStateCheckbox(
             enabled = enabled
         )
     }
-}
+}

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

@@ -12,7 +12,15 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+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 io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.text.AppText
@@ -26,6 +34,9 @@ fun CheckboxItem(
     onCheckedChange: (Boolean) -> Unit
 ) {
     val alpha = if (enabled) 1f else 0.5f
+    val checkboxStateDescription = stringResource(
+        if (checked) R.string.a11y_selected else R.string.a11y_not_selected
+    )
 
     GlassCard(
         cornerRadius = 12.dp,
@@ -34,6 +45,11 @@ fun CheckboxItem(
         Row(
             modifier = Modifier
                 .fillMaxWidth()
+                .semantics(mergeDescendants = true) {
+                    role = Role.Checkbox
+                    stateDescription = checkboxStateDescription
+                    if (!enabled) disabled()
+                }
                 .clickable(enabled = enabled) { onCheckedChange(!checked) }
                 .padding(vertical = 12.dp, horizontal = 12.dp),
             verticalAlignment = Alignment.CenterVertically
@@ -42,7 +58,10 @@ fun CheckboxItem(
                 checked = checked,
                 onCheckedChange = null,
                 enabled = enabled,
-                modifier = Modifier.alpha(alpha)
+                includeStateSemantics = false,
+                modifier = Modifier
+                    .alpha(alpha)
+                    .clearAndSetSemantics { }
             )
             AppText(
                 text = title,
@@ -55,4 +74,4 @@ fun CheckboxItem(
         }
     }
 
-}
+}

+ 18 - 2
app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt

@@ -9,6 +9,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
@@ -47,6 +52,17 @@ fun ExploreKindItem(
         } else {
             LegadoTheme.colorScheme.primary
         }
+        val itemModifier = if (isClickable) {
+            modifier.semantics(mergeDescendants = true) {
+                contentDescription = displayText
+                role = Role.Button
+                if (isSelected) {
+                    selected = true
+                }
+            }
+        } else {
+            modifier
+        }
 
         if (isClickable) {
             GlassCard(
@@ -54,7 +70,7 @@ fun ExploreKindItem(
                 cornerRadius = cornerRadius,
                 containerColor = containerColor,
                 contentColor = contentColor,
-                modifier = modifier,
+                modifier = itemModifier,
             ) {
                 KindText(
                     text = displayText,
@@ -68,7 +84,7 @@ fun ExploreKindItem(
                 cornerRadius = cornerRadius,
                 containerColor = containerColor,
                 contentColor = contentColor,
-                modifier = modifier,
+                modifier = itemModifier,
             ) {
                 KindText(
                     text = displayText,

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindSelectSheet.kt

@@ -84,7 +84,7 @@ fun ExploreKindSelectSheet(
                         onDismissRequest()
                     },
                     imageVector = Icons.Default.Check,
-                    contentDescription = "Confirm"
+                    contentDescription = stringResource(R.string.confirm)
                 )
             }
         }

+ 6 - 1
app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindTextField.kt

@@ -15,6 +15,8 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
@@ -41,7 +43,10 @@ fun ExploreKindCompactTextField(
         modifier = modifier
             .height(34.dp)
             .clip(shape)
-            .background(backgroundColor),
+            .background(backgroundColor)
+            .semantics {
+                contentDescription = placeholder
+            },
         decorationBox = { innerTextField ->
             Box(
                 modifier = Modifier

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

@@ -31,6 +31,10 @@ import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.PathEffect
 import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -41,6 +45,7 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.series.MediumOutlinedButton
 import io.legado.app.ui.widget.components.button.series.MediumToggleButton
 import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.utils.formatReadDuration
 import java.time.LocalDate
 
 /**
@@ -196,10 +201,29 @@ fun HeatmapCalendarCell(
 ) {
     val level = rememberHeatmapLevel(day, mode, dailyReadCounts, dailyReadTimes)
     val cellColor = heatmapColorForLevel(level)
+    val readCount = dailyReadCounts[day] ?: 0
+    val readDuration = formatReadDuration(dailyReadTimes[day] ?: 0L)
+    val cellDescription = if (mode == HeatmapMode.COUNT) {
+        stringResource(R.string.a11y_heatmap_day_count, day.toString(), readCount)
+    } else {
+        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(
         modifier = modifier
             .size(config.cellSize)
+            .semantics {
+                contentDescription = cellDescription
+                stateDescription = selectedStateDescription
+                if (isSelected) {
+                    selected = true
+                }
+            }
             .clip(RoundedCornerShape(config.cornerRadius))
             .background(cellColor)
             .border(

+ 3 - 2
app/src/main/java/io/legado/app/ui/widget/components/rules/RuleEditSheet.kt

@@ -77,7 +77,7 @@ fun <T> RuleEditSheet(
             MediumPlainButton(
                 onClick = onDismissRequest,
                 icon = Icons.Default.Close,
-                contentDescription = "Close",
+                contentDescription = stringResource(R.string.close),
             )
         },
         endAction = {
@@ -85,7 +85,7 @@ fun <T> RuleEditSheet(
                 MediumPlainButton(
                     onClick = { showMenu = true },
                     icon = Icons.Default.MoreVert,
-                    contentDescription = "More"
+                    contentDescription = stringResource(R.string.more_menu)
                 )
                 RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
                     RoundDropdownMenuItem(
@@ -155,6 +155,7 @@ fun <T> RuleEditSheet(
                 modifier = Modifier
                     .align(Alignment.BottomEnd)
                     .padding(16.dp),
+                tooltipText = stringResource(R.string.action_save),
                 icon = Icons.Default.Save
             )
         }

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt

@@ -62,7 +62,7 @@ fun <T> RuleListScaffold(
                     visible = state.selectedIds.isEmpty(),
                     alignment = Alignment.BottomEnd,
                 ),
-                tooltipText = "添加",
+                tooltipText = stringResource(R.string.add),
                 icon = Icons.Default.Add
             )
         }

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

@@ -21,17 +21,26 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+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.draw.scale
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
 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.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.ValueStepper
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.sliderAccessibility
 import top.yukonga.miuix.kmp.basic.BasicComponent
 import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
 import top.yukonga.miuix.kmp.preference.ArrowPreference
@@ -134,6 +143,7 @@ fun CompactSliderSettingItem(
 
     var sliderValue by remember(value) { mutableFloatStateOf(value) }
     var displayValue by remember(value) { mutableFloatStateOf(value) }
+    val sliderAccessibilityValue = description ?: displayValue.toString()
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         Column(modifier = Modifier.fillMaxWidth()) {
@@ -168,7 +178,12 @@ fun CompactSliderSettingItem(
                         },
                         valueRange = valueRange,
                         steps = steps,
-                        modifier = Modifier.fillMaxWidth()
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .sliderAccessibility(
+                                label = title,
+                                value = sliderAccessibilityValue,
+                            )
                     )
                 }
             }
@@ -202,7 +217,12 @@ fun CompactSliderSettingItem(
                     },
                     valueRange = valueRange,
                     steps = steps,
-                    modifier = Modifier.fillMaxWidth()
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .sliderAccessibility(
+                            label = title,
+                            value = sliderAccessibilityValue,
+                        )
                 )
             }
         )
@@ -227,12 +247,21 @@ fun CompactSwitchSettingItem(
     enabled: Boolean = true,
     onCheckedChange: (Boolean) -> Unit
 ) {
+    val switchStateDescription = stringResource(
+        if (checked) R.string.a11y_on else R.string.a11y_off
+    )
+
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
         SwitchPreference(
             title = title,
             summary = description,
             checked = checked,
             onCheckedChange = onCheckedChange,
+            modifier = Modifier.semantics(mergeDescendants = true) {
+                role = Role.Switch
+                stateDescription = switchStateDescription
+                if (!enabled) disabled()
+            },
             enabled = enabled,
         )
     } else {
@@ -242,10 +271,15 @@ fun CompactSwitchSettingItem(
             imageVector = imageVector,
             color = color,
             shape = shape,
+            enabled = enabled,
+            semanticRole = Role.Switch,
+            semanticStateDescription = switchStateDescription,
             onClick = { if (enabled) onCheckedChange(!checked) },
             trailingContent = {
                 Switch(
-                    modifier = Modifier.scale(0.8f),
+                    modifier = Modifier
+                        .scale(0.8f)
+                        .clearAndSetSemantics { },
                     checked = checked,
                     onCheckedChange = onCheckedChange,
                     enabled = enabled

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

@@ -31,6 +31,11 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.semantics.Role
+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 io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.card.SettingCard
@@ -52,6 +57,9 @@ fun SettingItem(
     dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null,
     onClick: (() -> Unit)? = null,
     onLongClick: (() -> Unit)? = null,
+    enabled: Boolean = true,
+    semanticRole: Role? = null,
+    semanticStateDescription: String? = null,
     expanded: Boolean = false,
     onExpandChange: ((Boolean) -> Unit)? = null,
     expandContent: (@Composable ColumnScope.() -> Unit)? = null
@@ -71,7 +79,14 @@ fun SettingItem(
         Column {
             ListItem(
                 modifier = Modifier
+                    .semantics(mergeDescendants = true) {
+                        semanticRole?.let { role = it }
+                        semanticStateDescription?.let { stateDescription = it }
+                        if (!enabled) disabled()
+                    }
                     .combinedClickable(
+                        enabled = enabled,
+                        role = semanticRole,
                         onClick = {
                             when {
                                 dropdownMenu != null -> showMenu = true

+ 15 - 3
app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt

@@ -38,6 +38,7 @@ import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
 import io.legado.app.ui.widget.components.SplicedColumnDivider
+import io.legado.app.ui.widget.components.sliderAccessibility
 import io.legado.app.ui.widget.components.text.AppText
 import top.yukonga.miuix.kmp.basic.BasicComponent
 import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
@@ -64,6 +65,8 @@ fun SliderSettingItem(
         sliderValue = value
     }
 
+    val sliderAccessibilityValue = description ?: sliderValue.toString()
+
     LaunchedEffect(isInputMode) {
         if (isInputMode) {
             textFieldState.edit {
@@ -138,7 +141,12 @@ fun SliderSettingItem(
                                 },
                                 valueRange = valueRange,
                                 steps = steps,
-                                modifier = Modifier.fillMaxWidth()
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .sliderAccessibility(
+                                        label = title,
+                                        value = sliderAccessibilityValue,
+                                    )
                             )
                         }
                     }
@@ -222,7 +230,12 @@ fun SliderSettingItem(
                                 },
                                 valueRange = valueRange,
                                 steps = steps,
-                                modifier = Modifier.fillMaxWidth()
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .sliderAccessibility(
+                                        label = title,
+                                        value = sliderAccessibilityValue,
+                                    )
                             )
                         }
                     }
@@ -252,4 +265,3 @@ fun SliderSettingItem(
         )
     }
 }
-

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

@@ -1,8 +1,17 @@
 package io.legado.app.ui.widget.components.settingItem
 
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+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.graphics.Color
 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.ThemeResolver
 import io.legado.app.ui.widget.components.AdaptiveSwitch
@@ -21,6 +30,9 @@ fun SwitchSettingItem(
     onCheckedChange: (Boolean) -> Unit
 ) {
     val composeEngine = LegadoTheme.composeEngine
+    val switchStateDescription = stringResource(
+        if (checked) R.string.a11y_on else R.string.a11y_off
+    )
     SplicedColumnDivider()
 
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
@@ -29,6 +41,11 @@ fun SwitchSettingItem(
             summary = description,
             checked = checked,
             onCheckedChange = onCheckedChange,
+            modifier = Modifier.semantics(mergeDescendants = true) {
+                role = Role.Switch
+                stateDescription = switchStateDescription
+                if (!enabled) disabled()
+            },
             enabled = enabled,
         )
     } else {
@@ -37,12 +54,17 @@ fun SwitchSettingItem(
             description = description,
             imageVector = imageVector,
             color = color,
+            enabled = enabled,
+            semanticRole = Role.Switch,
+            semanticStateDescription = switchStateDescription,
             onClick = { if (enabled) onCheckedChange(!checked) },
             trailingContent = {
                 AdaptiveSwitch(
+                    modifier = Modifier.clearAndSetSemantics { },
                     checked = checked,
                     onCheckedChange = onCheckedChange,
-                    enabled = enabled
+                    enabled = enabled,
+                    includeStateSemantics = false
                 )
             }
         )

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

@@ -46,6 +46,12 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+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.text.style.TextOverflow
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
@@ -74,6 +80,8 @@ fun TinySettingItem(
     onExpandChange: ((Boolean) -> Unit)? = null,
     expandContent: (@Composable ColumnScope.() -> Unit)? = null,
     enabled: Boolean = true,
+    semanticRole: Role? = null,
+    semanticStateDescription: String? = null,
     onClick: (() -> Unit)? = null,
     onLongClick: (() -> Unit)? = null,
 ) {
@@ -93,7 +101,12 @@ fun TinySettingItem(
         modifier = modifier
             .padding(bottom = 4.dp)
             .heightIn(min = 56.dp)
-            .fillMaxWidth(),
+            .fillMaxWidth()
+            .semantics(mergeDescendants = true) {
+                semanticRole?.let { role = it }
+                semanticStateDescription?.let { stateDescription = it }
+                if (!enabled) disabled()
+            },
         cornerRadius = 12.dp,
         containerColor = color?.copy(alpha = alpha),
         contentColor = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
@@ -281,6 +294,8 @@ fun TinySliderSettingItem(
                 steps = steps,
                 enabled = enabled,
                 modifier = Modifier.fillMaxWidth(),
+                accessibilityLabel = title,
+                accessibilityValue = description ?: displayValue.toString(),
             )
         },
     )
@@ -304,6 +319,10 @@ fun TinySwitchSettingItem(
     enabled: Boolean = true,
     onCheckedChange: (Boolean) -> Unit,
 ) {
+    val switchStateDescription = stringResource(
+        if (checked) R.string.a11y_on else R.string.a11y_off
+    )
+
     TinySettingItem(
         title = title,
         description = description,
@@ -311,11 +330,15 @@ fun TinySwitchSettingItem(
         modifier = modifier,
         color = color,
         enabled = enabled,
+        semanticRole = Role.Switch,
+        semanticStateDescription = switchStateDescription,
         trailingContent = {
             TinySwitch(
+                modifier = Modifier.clearAndSetSemantics { },
                 checked = checked,
                 onCheckedChange = onCheckedChange,
                 enabled = enabled,
+                includeStateSemantics = false,
             )
         },
         onClick = { onCheckedChange(!checked) },

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

@@ -7,5 +7,6 @@ data class SwipeAction(
     val icon: ImageVector,
     val background: Color,
     val onSwipe: () -> Unit,
-    val hapticFeedback: Boolean = true
+    val hapticFeedback: Boolean = true,
+    val contentDescription: String? = null
 )

+ 35 - 2
app/src/main/java/io/legado/app/ui/widget/components/swipe/SwipeActionContainer.kt

@@ -26,6 +26,9 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 
@@ -70,7 +73,9 @@ fun SwipeActionContainer(
 
     SwipeToDismissBox(
         state = dismissState,
-        modifier = modifier,
+        modifier = modifier.then(
+            swipeActionsSemantics(startAction, endAction)
+        ),
         enableDismissFromStartToEnd = startAction != null,
         enableDismissFromEndToStart = endAction != null,
         backgroundContent = {
@@ -114,6 +119,34 @@ fun SwipeActionContainer(
     )
 }
 
+private fun swipeActionsSemantics(
+    startAction: SwipeAction?,
+    endAction: SwipeAction?
+): Modifier {
+    val accessibilityActions = buildList {
+        startAction?.contentDescription?.let { label ->
+            add(
+                CustomAccessibilityAction(label = label) {
+                    startAction.onSwipe()
+                    true
+                }
+            )
+        }
+        endAction?.contentDescription?.let { label ->
+            add(
+                CustomAccessibilityAction(label = label) {
+                    endAction.onSwipe()
+                    true
+                }
+            )
+        }
+    }
+    if (accessibilityActions.isEmpty()) return Modifier
+    return Modifier.semantics {
+        customActions = accessibilityActions
+    }
+}
+
 @Composable
 private fun SwipeBackground(
     action: SwipeAction,
@@ -164,4 +197,4 @@ private fun SwipeBackground(
                 LegadoTheme.colorScheme.onSurfaceVariant
         )
     }
-}
+}

+ 13 - 6
app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt

@@ -20,6 +20,8 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import io.legado.app.R
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.SearchBar
 import io.legado.app.ui.widget.components.icon.AppIcons
@@ -57,8 +59,12 @@ fun <T> DynamicTopAppBar(
         modifier = Modifier
             .fillMaxWidth(),
         title = when {
-            state.isLoading -> "请稍后..."
-            isSelecting -> "已选择 ${state.selectedIds.size}/${state.items.size}"
+            state.isLoading -> stringResource(R.string.list_loading_title)
+            isSelecting -> stringResource(
+                R.string.list_selected_count,
+                state.selectedIds.size,
+                state.items.size
+            )
             else -> title
         },
         useCharMode = isSelecting || state.isLoading,
@@ -68,7 +74,9 @@ fun <T> DynamicTopAppBar(
                 TopBarNavigationButton(
                     onClick = { if (isSelecting) onClearSelection() else onBackClick?.invoke() },
                     imageVector = if (isSelecting) AppIcons.Close else backNavigationIcon,
-                    contentDescription = if (isSelecting) "取消选择" else "返回"
+                    contentDescription = stringResource(
+                        if (isSelecting) R.string.cancel_select else R.string.back
+                    )
                 )
             }
         },
@@ -78,7 +86,7 @@ fun <T> DynamicTopAppBar(
                     TopBarActionButton(
                         onClick = { onSearchToggle(!state.isSearch) },
                         imageVector = AppIcons.Search,
-                        contentDescription = "搜索"
+                        contentDescription = stringResource(R.string.search)
                     )
                 }
 
@@ -89,7 +97,7 @@ fun <T> DynamicTopAppBar(
                         TopBarActionButton(
                             onClick = { showMenu = true },
                             imageVector = Icons.Default.MoreVert,
-                            contentDescription = "更多"
+                            contentDescription = stringResource(R.string.more_menu)
                         )
                         RoundDropdownMenu(
                             expanded = showMenu,
@@ -124,4 +132,3 @@ fun <T> DynamicTopAppBar(
         }
     )
 }
-

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

@@ -117,7 +117,8 @@ fun TopBarActionButton(
             MediumPlainButton(
                 onClick = onClick,
                 modifier = modifier,
-                icon = imageVector
+                icon = imageVector,
+                contentDescription = contentDescription,
             )
         }
     }

+ 5 - 1
app/src/main/res/layout/activity_book_source_edit.xml

@@ -27,9 +27,11 @@
             tools:ignore="VisualLintBounds">
 
             <TextView
+                android:id="@+id/tv_book_type"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent"
                 android:gravity="center"
+                android:labelFor="@id/sp_type"
                 android:text="@string/book_type"
                 tools:ignore="RtlHardcoded" />
 
@@ -37,7 +39,9 @@
                 android:id="@+id/sp_type"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:contentDescription="@string/book_type"
                 android:entries="@array/book_type"
+                android:prompt="@string/book_type"
                 tools:ignore="TouchTargetSizeCheck" />
 
             <io.legado.app.lib.theme.view.ThemeCheckBox
@@ -120,4 +124,4 @@
         android:clipToPadding="false"
         tools:listitem="@layout/item_source_edit" />
 
-</LinearLayout>
+</LinearLayout>

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

@@ -30,6 +30,7 @@
         android:layout_width="36dp"
         android:layout_height="36dp"
         android:layout_margin="6dp"
+        android:contentDescription="@string/loading"
         android:visibility="gone"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
@@ -192,4 +193,4 @@
 
     </io.legado.app.ui.widget.NoChildScrollNestedScrollView>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -20,6 +20,7 @@
             android:layout_height="wrap_content"
             android:paddingVertical="16dp"
             android:minHeight="0dp"
+            android:importantForAccessibility="no"
             android:layout_gravity="center_horizontal" />
 
         <TextView
@@ -54,7 +55,7 @@
             android:layout_height="wrap_content"
             app:max="100"
             app:min="1"
-            app:title="翻页速度" />
+            app:title="@string/auto_next_page_speed" />
 
 
     </LinearLayout>

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

@@ -25,6 +25,7 @@
                 android:layout_height="wrap_content"
                 android:paddingVertical="16dp"
                 android:minHeight="0dp"
+                android:importantForAccessibility="no"
                 android:layout_gravity="center_horizontal" />
 
             <TextView

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

@@ -29,6 +29,7 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginEnd="16dp"
+        android:contentDescription="@string/delete"
         app:backgroundTint="@android:color/transparent"
         app:iconTint="?attr/colorOnSurface"
         app:icon="@drawable/ic_baseline_close" />

+ 6 - 2
app/src/main/res/layout/item_theme_card.xml

@@ -19,7 +19,8 @@
         android:checkable="true"
         app:strokeWidth="4dp"
         android:clickable="true"
-        android:focusable="true">
+        android:focusable="true"
+        android:screenReaderFocusable="true">
 
         <androidx.constraintlayout.widget.ConstraintLayout
             android:layout_width="match_parent"
@@ -34,6 +35,7 @@
                 app:strokeWidth="0dp"
                 app:cardCornerRadius="8dp"
                 app:cardBackgroundColor="#FF0000"
+                android:importantForAccessibility="no"
                 app:layout_constraintTop_toTopOf="parent"
                 app:layout_constraintStart_toStartOf="parent" />
 
@@ -46,6 +48,7 @@
                 app:strokeWidth="0dp"
                 app:cardCornerRadius="8dp"
                 app:cardBackgroundColor="#FFFF00"
+                android:importantForAccessibility="no"
                 app:layout_constraintTop_toBottomOf="@id/cv_title"
                 app:layout_constraintStart_toStartOf="@id/cv_title" />
 
@@ -58,6 +61,7 @@
                 app:strokeWidth="0dp"
                 app:cardCornerRadius="4dp"
                 app:cardBackgroundColor="#0000FF"
+                android:importantForAccessibility="no"
                 app:layout_constraintTop_toTopOf="@id/cv_book"
                 app:layout_constraintEnd_toEndOf="@id/cv_book" />
 
@@ -70,6 +74,7 @@
                 app:strokeWidth="0dp"
                 app:cardCornerRadius="0dp"
                 app:cardBackgroundColor="#FFFF00"
+                android:importantForAccessibility="no"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent" >
@@ -117,4 +122,3 @@
         android:textSize="14sp" />
 
 </LinearLayout>
-

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

@@ -6,4 +6,5 @@
     android:layout_height="wrap_content"
     android:focusable="false"
     android:clickable="false"
-    android:background="@null" />
+    android:importantForAccessibility="no"
+    android:background="@null" />

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

@@ -9,5 +9,6 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:id="@+id/preview"
+        android:importantForAccessibility="no"
         android:scaleType="centerCrop"/>
-</com.google.android.material.card.MaterialCardView>
+</com.google.android.material.card.MaterialCardView>

+ 7 - 2
app/src/main/res/layout/view_manga_menu.xml

@@ -11,7 +11,7 @@
         android:id="@+id/vw_menu_bg"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:contentDescription="@string/content"
+        android:importantForAccessibility="no"
         tools:layout_editor_absoluteX="0dp"
         tools:layout_editor_absoluteY="0dp" />
 
@@ -91,6 +91,8 @@
                 android:layout_height="wrap_content"
                 style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
                 app:backgroundTint="?attr/colorSurfaceContainer"
+                android:contentDescription="@string/previous_chapter"
+                android:tooltipText="@string/previous_chapter"
                 app:icon="@drawable/ic_previous"
                 tools:ignore="TouchTargetSizeCheck" />
 
@@ -106,6 +108,7 @@
                     android:id="@+id/seek_read_page"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    android:contentDescription="@string/progress"
                     android:layout_gravity="center_vertical" />
             </com.google.android.material.card.MaterialCardView>
 
@@ -115,6 +118,8 @@
                 android:layout_height="wrap_content"
                 style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
                 app:backgroundTint="?attr/colorSurfaceContainer"
+                android:contentDescription="@string/next_chapter"
+                android:tooltipText="@string/next_chapter"
                 app:icon="@drawable/ic_next"
                 tools:ignore="TouchTargetSizeCheck" />
         </LinearLayout>
@@ -198,4 +203,4 @@
 
     </LinearLayout>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 4 - 2
app/src/main/res/layout/view_preference.xml

@@ -11,7 +11,8 @@
     android:clickable="true"
     android:orientation="horizontal"
     android:gravity="center_vertical"
-    android:focusable="true">
+    android:focusable="true"
+    android:screenReaderFocusable="true">
 
     <ImageView
         android:id="@+id/preference_icon"
@@ -20,6 +21,7 @@
         android:visibility="visible"
         android:layout_width="28dp"
         android:layout_height="28dp"
+        android:importantForAccessibility="no"
         app:tint="?attr/colorPrimary"
         tools:ignore="ContentDescription,RtlHardcoded" />
 
@@ -58,4 +60,4 @@
         android:layout_height="wrap_content"
         tools:ignore="RtlHardcoded" />
 
-</LinearLayout>
+</LinearLayout>

+ 1 - 1
app/src/main/res/layout/view_preference_tip.xml

@@ -19,6 +19,7 @@
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:layout_marginBottom="8dp"
+            android:importantForAccessibility="no"
             android:src="@drawable/ic_cfg_about" />
 
         <TextView
@@ -30,4 +31,3 @@
             android:textSize="12sp" />
     </LinearLayout>
 </com.google.android.material.card.MaterialCardView>
-

+ 119 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -2244,4 +2244,123 @@
     <string name="cover_album_imported_name">%1$s(导入)</string>
     <string name="cover_album_day_night_count">日间 %1$d · 夜间 %2$d</string>
     <string name="cover_album_selected">当前使用</string>
+
+    <!-- 无障碍与 nav_my 子页面 -->
+    <string name="list_loading_title">请稍后…</string>
+    <string name="list_selected_count">已选择 %1$d/%2$d</string>
+    <string name="a11y_selected">已选中</string>
+    <string name="a11y_not_selected">未选中</string>
+    <string name="a11y_on">开启</string>
+    <string name="a11y_off">关闭</string>
+    <string name="a11y_expanded">已展开</string>
+    <string name="a11y_collapsed">已折叠</string>
+    <string name="a11y_long_press_reorder">长按并拖动可排序</string>
+    <string name="a11y_rule_enabled_switch">%1$s 的启用状态</string>
+    <string name="a11y_edit_named">编辑 %1$s</string>
+    <string name="a11y_delete_named">删除 %1$s</string>
+    <string name="a11y_more_actions_for">%1$s 的更多操作</string>
+    <string name="a11y_slider_value">%1$s,当前 %2$s</string>
+    <string name="a11y_percent_value">%1$d%%</string>
+    <string name="a11y_book_cover_actions">《%1$s》的封面,点击更换封面,长按预览</string>
+    <string name="a11y_related_books_more">查看更多相关书籍:%1$s</string>
+    <string name="a11y_current_reading">当前阅读位置</string>
+    <string name="a11y_vip_locked">VIP 或锁定</string>
+    <string name="a11y_downloaded">已下载</string>
+    <string name="a11y_downloading">下载中</string>
+    <string name="a11y_download_failed">下载失败</string>
+    <string name="a11y_not_downloaded">未下载</string>
+    <string name="a11y_word_count">字数 %1$s</string>
+    <string name="a11y_retry_chapter">重试下载章节:%1$s</string>
+    <string name="a11y_read_progress_page">第 %1$d 页,共 %2$d 页</string>
+    <string name="a11y_read_progress_chapter">第 %1$d 章,共 %2$d 章</string>
+    <string name="a11y_debug_example">调试示例:%1$s</string>
+    <string name="a11y_book_source_action">书源:%1$s,长按打开书源操作</string>
+    <string name="search_txt_toc_rule">搜索 TXT 目录规则</string>
+    <string name="search_dict_rule">搜索字典规则</string>
+    <string name="search_highlight_tag_rule">搜索标签规则</string>
+    <string name="export_started">开始导出…</string>
+    <string name="previous">上一个</string>
+    <string name="next">下一个</string>
+    <string name="unknown_author">未知作者</string>
+    <string name="merge">合并</string>
+    <string name="a11y_expand_all_bookmark_groups">展开全部书签分组</string>
+    <string name="a11y_collapse_all_bookmark_groups">折叠全部书签分组</string>
+    <string name="bookmark_original_text">原文</string>
+    <string name="bookmark_note">摘要/笔记</string>
+    <string name="bookmark_original_text_description">原文:%1$s</string>
+    <string name="bookmark_note_description">摘要或笔记:%1$s</string>
+    <string name="confirm_delete_bookmark">删除书签?</string>
+    <string name="delete_bookmark_message">确定要删除这条书签吗?</string>
+    <string name="read_record_view_aggregate">汇总视图</string>
+    <string name="read_record_view_timeline">时间线视图</string>
+    <string name="read_record_view_latest">最后阅读视图</string>
+    <string name="a11y_switch_to_timeline_view">切换到时间线视图</string>
+    <string name="a11y_switch_to_latest_view">切换到最后阅读视图</string>
+    <string name="a11y_switch_to_aggregate_view">切换到汇总视图</string>
+    <string name="a11y_toggle_read_calendar">显示或隐藏阅读日历</string>
+    <string name="no_read_record">没有阅读记录</string>
+    <string name="confirm_delete_read_record">删除阅读记录?</string>
+    <string name="delete_read_record_message">确定要删除这条记录吗?</string>
+    <string name="do_not_remind_again">不再提示</string>
+    <string name="merge_read_records">合并阅读记录</string>
+    <string name="merge_read_records_message">将以下作者的“%1$s”合并到 %2$s</string>
+    <string name="no_merge_candidates">没有可合并的同名记录</string>
+    <string name="a11y_merge_same_name_read_records">合并同名阅读记录</string>
+    <string name="read_record_date_overview">%1$d月%2$d日阅读概览</string>
+    <string name="read_record_total_achievement">累计阅读成就</string>
+    <string name="chapter_index_format">第 %1$d 章</string>
+    <string name="a11y_read_record_latest_item">%1$s,%2$s,阅读时长 %3$s,最后阅读 %4$s</string>
+    <string name="a11y_read_record_timeline_item">%1$s,%2$s,%3$s,阅读时长 %4$s,结束于 %5$s</string>
+    <string name="a11y_read_record_detail_item">%1$s,%2$s,阅读时长 %3$s</string>
+    <string name="reading_time_with_value">阅读时长:%1$s</string>
+    <string name="a11y_reading_summary_card">%1$s,%2$d 本书,总时长 %3$s,打开总览</string>
+    <string name="read_books_prefix">已读 </string>
+    <string name="read_books_suffix"> 本书</string>
+    <string name="hours_minutes_format">%1$d小时%2$d分钟</string>
+    <string name="minutes_format">%1$d分钟</string>
+    <string name="total_reading_time_format">共阅读 %1$s</string>
+    <string name="read_duration_done">已读 %1$s</string>
+    <string name="read_record_overview">阅读总览</string>
+    <string name="reading_time">阅读时间</string>
+    <string name="reading_days">阅读天数</string>
+    <string name="total_read_books">累计读过</string>
+    <string name="finished_books">读完书籍</string>
+    <string name="reading_books">在读书籍</string>
+    <string name="reading_words">阅读字数</string>
+    <string name="reading_data">阅读数据</string>
+    <string name="reading_heatmap">阅读热力图</string>
+    <string name="reading_time_ranking">阅读时长榜</string>
+    <string name="reading_calendar">读书日历</string>
+    <string name="days_format">%1$d天</string>
+    <string name="books_format">%1$d本</string>
+    <string name="a11y_reading_ranking_item">第 %1$d 名,%2$s,%3$s,阅读时长 %4$s</string>
+    <string name="a11y_reading_calendar_day_with_book">%1$s,阅读最多:%2$s,作者 %3$s</string>
+    <string name="a11y_reading_calendar_day_empty">%1$s,没有阅读记录</string>
+    <string name="a11y_heatmap_day_count">%1$s,阅读 %2$d 次</string>
+    <string name="a11y_heatmap_day_duration">%1$s,阅读时长 %2$s</string>
+    <string name="start_download">开始下载</string>
+    <string name="stop_download">停止下载</string>
+    <string name="cache_section_bookshelf">书架书籍</string>
+    <string name="cache_empty_bookshelf">没有书架内书籍缓存或下载任务</string>
+    <string name="cache_section_not_in_bookshelf">未在书架</string>
+    <string name="cache_empty_not_in_bookshelf">没有未在书架的书籍下载状态</string>
+    <string name="cache_download_status_summary">下载中 %1$d · 等待 %2$d · 暂停 %3$d · 失败 %4$d</string>
+    <string name="cache_progress_description">已缓存 %1$d/%2$d 章,进度 %3$d%%</string>
+    <string name="a11y_cache_book_item">%1$s,%2$s,%3$s,%4$s</string>
+    <string name="pause_book_download">暂停《%1$s》的下载</string>
+    <string name="resume_book_download">继续《%1$s》的下载</string>
+    <string name="start_book_download">开始下载《%1$s》</string>
+    <string name="delete_book_cache">删除《%1$s》的缓存</string>
+    <string name="a11y_cache_chapter_item">%1$s,%2$s</string>
+    <string name="cache_chapter_progress_description">章节下载进度 %1$d%%</string>
+    <string name="pause_chapter_download">暂停章节下载:%1$s</string>
+    <string name="resume_chapter_download">继续章节下载:%1$s</string>
+    <string name="download_chapter">下载章节:%1$s</string>
+    <string name="delete_chapter_cache">删除章节缓存:%1$s</string>
+    <string name="download_paused">已暂停</string>
+    <string name="not_cached">未缓存</string>
+    <string name="delete_book_cache_message">删除《%1$s》的全部缓存,并从下载队列移除?</string>
+    <string name="delete_chapter_cache_message">删除章节缓存:%1$s?</string>
+    <string name="about_open_project_homepage">打开项目主页</string>
+    <string name="about_open_github">打开 GitHub</string>
 </resources>

+ 103 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -2236,4 +2236,107 @@
     <string name="cover_album_imported_name">%1$s(匯入)</string>
     <string name="cover_album_day_night_count">日間 %1$d · 夜間 %2$d</string>
     <string name="cover_album_selected">目前使用</string>
+
+    <!-- 無障礙與 nav_my 子頁面 -->
+    <string name="list_loading_title">請稍候…</string>
+    <string name="list_selected_count">已選擇 %1$d/%2$d</string>
+    <string name="a11y_selected">已選取</string>
+    <string name="a11y_not_selected">未選取</string>
+    <string name="a11y_on">開啟</string>
+    <string name="a11y_off">關閉</string>
+    <string name="a11y_expanded">已展開</string>
+    <string name="a11y_collapsed">已收合</string>
+    <string name="a11y_long_press_reorder">長按並拖曳可排序</string>
+    <string name="a11y_rule_enabled_switch">%1$s 的啟用狀態</string>
+    <string name="a11y_edit_named">編輯 %1$s</string>
+    <string name="a11y_delete_named">刪除 %1$s</string>
+    <string name="a11y_more_actions_for">%1$s 的更多操作</string>
+    <string name="search_txt_toc_rule">搜尋 TXT 目錄規則</string>
+    <string name="search_dict_rule">搜尋字典規則</string>
+    <string name="search_highlight_tag_rule">搜尋標籤規則</string>
+    <string name="export_started">開始匯出…</string>
+    <string name="previous">上一個</string>
+    <string name="next">下一個</string>
+    <string name="unknown_author">未知作者</string>
+    <string name="merge">合併</string>
+    <string name="a11y_expand_all_bookmark_groups">展開全部書籤分組</string>
+    <string name="a11y_collapse_all_bookmark_groups">收合全部書籤分組</string>
+    <string name="bookmark_original_text">原文</string>
+    <string name="bookmark_note">摘要/筆記</string>
+    <string name="bookmark_original_text_description">原文:%1$s</string>
+    <string name="bookmark_note_description">摘要或筆記:%1$s</string>
+    <string name="confirm_delete_bookmark">刪除書籤?</string>
+    <string name="delete_bookmark_message">確定要刪除這條書籤嗎?</string>
+    <string name="read_record_view_aggregate">彙總檢視</string>
+    <string name="read_record_view_timeline">時間軸檢視</string>
+    <string name="read_record_view_latest">最後閱讀檢視</string>
+    <string name="a11y_switch_to_timeline_view">切換到時間軸檢視</string>
+    <string name="a11y_switch_to_latest_view">切換到最後閱讀檢視</string>
+    <string name="a11y_switch_to_aggregate_view">切換到彙總檢視</string>
+    <string name="a11y_toggle_read_calendar">顯示或隱藏閱讀日曆</string>
+    <string name="no_read_record">沒有閱讀記錄</string>
+    <string name="confirm_delete_read_record">刪除閱讀記錄?</string>
+    <string name="delete_read_record_message">確定要刪除這條記錄嗎?</string>
+    <string name="do_not_remind_again">不再提示</string>
+    <string name="merge_read_records">合併閱讀記錄</string>
+    <string name="merge_read_records_message">將以下作者的「%1$s」合併到 %2$s</string>
+    <string name="no_merge_candidates">沒有可合併的同名記錄</string>
+    <string name="a11y_merge_same_name_read_records">合併同名閱讀記錄</string>
+    <string name="read_record_date_overview">%1$d月%2$d日閱讀概覽</string>
+    <string name="read_record_total_achievement">累計閱讀成就</string>
+    <string name="chapter_index_format">第 %1$d 章</string>
+    <string name="a11y_read_record_latest_item">%1$s,%2$s,閱讀時長 %3$s,最後閱讀 %4$s</string>
+    <string name="a11y_read_record_timeline_item">%1$s,%2$s,%3$s,閱讀時長 %4$s,結束於 %5$s</string>
+    <string name="a11y_read_record_detail_item">%1$s,%2$s,閱讀時長 %3$s</string>
+    <string name="reading_time_with_value">閱讀時長:%1$s</string>
+    <string name="a11y_reading_summary_card">%1$s,%2$d 本書,總時長 %3$s,開啟總覽</string>
+    <string name="read_books_prefix">已讀 </string>
+    <string name="read_books_suffix"> 本書</string>
+    <string name="hours_minutes_format">%1$d小時%2$d分鐘</string>
+    <string name="minutes_format">%1$d分鐘</string>
+    <string name="total_reading_time_format">共閱讀 %1$s</string>
+    <string name="read_duration_done">已讀 %1$s</string>
+    <string name="read_record_overview">閱讀總覽</string>
+    <string name="reading_time">閱讀時間</string>
+    <string name="reading_days">閱讀天數</string>
+    <string name="total_read_books">累計讀過</string>
+    <string name="finished_books">讀完書籍</string>
+    <string name="reading_books">在讀書籍</string>
+    <string name="reading_words">閱讀字數</string>
+    <string name="reading_data">閱讀資料</string>
+    <string name="reading_heatmap">閱讀熱力圖</string>
+    <string name="reading_time_ranking">閱讀時長榜</string>
+    <string name="reading_calendar">讀書日曆</string>
+    <string name="days_format">%1$d天</string>
+    <string name="books_format">%1$d本</string>
+    <string name="a11y_reading_ranking_item">第 %1$d 名,%2$s,%3$s,閱讀時長 %4$s</string>
+    <string name="a11y_reading_calendar_day_with_book">%1$s,閱讀最多:%2$s,作者 %3$s</string>
+    <string name="a11y_reading_calendar_day_empty">%1$s,沒有閱讀記錄</string>
+    <string name="a11y_heatmap_day_count">%1$s,閱讀 %2$d 次</string>
+    <string name="a11y_heatmap_day_duration">%1$s,閱讀時長 %2$s</string>
+    <string name="start_download">開始下載</string>
+    <string name="stop_download">停止下載</string>
+    <string name="cache_section_bookshelf">書架書籍</string>
+    <string name="cache_empty_bookshelf">沒有書架內書籍緩存或下載任務</string>
+    <string name="cache_section_not_in_bookshelf">未在書架</string>
+    <string name="cache_empty_not_in_bookshelf">沒有未在書架的書籍下載狀態</string>
+    <string name="cache_download_status_summary">下載中 %1$d · 等待 %2$d · 暫停 %3$d · 失敗 %4$d</string>
+    <string name="cache_progress_description">已緩存 %1$d/%2$d 章,進度 %3$d%%</string>
+    <string name="a11y_cache_book_item">%1$s,%2$s,%3$s,%4$s</string>
+    <string name="pause_book_download">暫停《%1$s》的下載</string>
+    <string name="resume_book_download">繼續《%1$s》的下載</string>
+    <string name="start_book_download">開始下載《%1$s》</string>
+    <string name="delete_book_cache">刪除《%1$s》的緩存</string>
+    <string name="a11y_cache_chapter_item">%1$s,%2$s</string>
+    <string name="cache_chapter_progress_description">章節下載進度 %1$d%%</string>
+    <string name="pause_chapter_download">暫停章節下載:%1$s</string>
+    <string name="resume_chapter_download">繼續章節下載:%1$s</string>
+    <string name="download_chapter">下載章節:%1$s</string>
+    <string name="delete_chapter_cache">刪除章節緩存:%1$s</string>
+    <string name="download_paused">已暫停</string>
+    <string name="not_cached">未緩存</string>
+    <string name="delete_book_cache_message">刪除《%1$s》的全部緩存,並從下載隊列移除?</string>
+    <string name="delete_chapter_cache_message">刪除章節緩存:%1$s?</string>
+    <string name="about_open_project_homepage">開啟項目首頁</string>
+    <string name="about_open_github">開啟 GitHub</string>
 </resources>

+ 103 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -2241,4 +2241,107 @@
     <string name="cover_album_imported_name">%1$s(匯入)</string>
     <string name="cover_album_day_night_count">日間 %1$d · 夜間 %2$d</string>
     <string name="cover_album_selected">目前使用</string>
+
+    <!-- 無障礙與 nav_my 子頁面 -->
+    <string name="list_loading_title">請稍候…</string>
+    <string name="list_selected_count">已選擇 %1$d/%2$d</string>
+    <string name="a11y_selected">已選取</string>
+    <string name="a11y_not_selected">未選取</string>
+    <string name="a11y_on">開啟</string>
+    <string name="a11y_off">關閉</string>
+    <string name="a11y_expanded">已展開</string>
+    <string name="a11y_collapsed">已收合</string>
+    <string name="a11y_long_press_reorder">長按並拖曳可排序</string>
+    <string name="a11y_rule_enabled_switch">%1$s 的啟用狀態</string>
+    <string name="a11y_edit_named">編輯 %1$s</string>
+    <string name="a11y_delete_named">刪除 %1$s</string>
+    <string name="a11y_more_actions_for">%1$s 的更多操作</string>
+    <string name="search_txt_toc_rule">搜尋 TXT 目錄規則</string>
+    <string name="search_dict_rule">搜尋字典規則</string>
+    <string name="search_highlight_tag_rule">搜尋標籤規則</string>
+    <string name="export_started">開始匯出…</string>
+    <string name="previous">上一個</string>
+    <string name="next">下一個</string>
+    <string name="unknown_author">未知作者</string>
+    <string name="merge">合併</string>
+    <string name="a11y_expand_all_bookmark_groups">展開全部書籤分組</string>
+    <string name="a11y_collapse_all_bookmark_groups">收合全部書籤分組</string>
+    <string name="bookmark_original_text">原文</string>
+    <string name="bookmark_note">摘要/筆記</string>
+    <string name="bookmark_original_text_description">原文:%1$s</string>
+    <string name="bookmark_note_description">摘要或筆記:%1$s</string>
+    <string name="confirm_delete_bookmark">刪除書籤?</string>
+    <string name="delete_bookmark_message">確定要刪除這條書籤嗎?</string>
+    <string name="read_record_view_aggregate">彙總檢視</string>
+    <string name="read_record_view_timeline">時間軸檢視</string>
+    <string name="read_record_view_latest">最後閱讀檢視</string>
+    <string name="a11y_switch_to_timeline_view">切換到時間軸檢視</string>
+    <string name="a11y_switch_to_latest_view">切換到最後閱讀檢視</string>
+    <string name="a11y_switch_to_aggregate_view">切換到彙總檢視</string>
+    <string name="a11y_toggle_read_calendar">顯示或隱藏閱讀日曆</string>
+    <string name="no_read_record">沒有閱讀記錄</string>
+    <string name="confirm_delete_read_record">刪除閱讀記錄?</string>
+    <string name="delete_read_record_message">確定要刪除這條記錄嗎?</string>
+    <string name="do_not_remind_again">不再提示</string>
+    <string name="merge_read_records">合併閱讀記錄</string>
+    <string name="merge_read_records_message">將以下作者的「%1$s」合併到 %2$s</string>
+    <string name="no_merge_candidates">沒有可合併的同名記錄</string>
+    <string name="a11y_merge_same_name_read_records">合併同名閱讀記錄</string>
+    <string name="read_record_date_overview">%1$d月%2$d日閱讀概覽</string>
+    <string name="read_record_total_achievement">累計閱讀成就</string>
+    <string name="chapter_index_format">第 %1$d 章</string>
+    <string name="a11y_read_record_latest_item">%1$s,%2$s,閱讀時長 %3$s,最後閱讀 %4$s</string>
+    <string name="a11y_read_record_timeline_item">%1$s,%2$s,%3$s,閱讀時長 %4$s,結束於 %5$s</string>
+    <string name="a11y_read_record_detail_item">%1$s,%2$s,閱讀時長 %3$s</string>
+    <string name="reading_time_with_value">閱讀時長:%1$s</string>
+    <string name="a11y_reading_summary_card">%1$s,%2$d 本書,總時長 %3$s,開啟總覽</string>
+    <string name="read_books_prefix">已讀 </string>
+    <string name="read_books_suffix"> 本書</string>
+    <string name="hours_minutes_format">%1$d小時%2$d分鐘</string>
+    <string name="minutes_format">%1$d分鐘</string>
+    <string name="total_reading_time_format">共閱讀 %1$s</string>
+    <string name="read_duration_done">已讀 %1$s</string>
+    <string name="read_record_overview">閱讀總覽</string>
+    <string name="reading_time">閱讀時間</string>
+    <string name="reading_days">閱讀天數</string>
+    <string name="total_read_books">累計讀過</string>
+    <string name="finished_books">讀完書籍</string>
+    <string name="reading_books">在讀書籍</string>
+    <string name="reading_words">閱讀字數</string>
+    <string name="reading_data">閱讀資料</string>
+    <string name="reading_heatmap">閱讀熱力圖</string>
+    <string name="reading_time_ranking">閱讀時長榜</string>
+    <string name="reading_calendar">讀書日曆</string>
+    <string name="days_format">%1$d天</string>
+    <string name="books_format">%1$d本</string>
+    <string name="a11y_reading_ranking_item">第 %1$d 名,%2$s,%3$s,閱讀時長 %4$s</string>
+    <string name="a11y_reading_calendar_day_with_book">%1$s,閱讀最多:%2$s,作者 %3$s</string>
+    <string name="a11y_reading_calendar_day_empty">%1$s,沒有閱讀記錄</string>
+    <string name="a11y_heatmap_day_count">%1$s,閱讀 %2$d 次</string>
+    <string name="a11y_heatmap_day_duration">%1$s,閱讀時長 %2$s</string>
+    <string name="start_download">開始下載</string>
+    <string name="stop_download">停止下載</string>
+    <string name="cache_section_bookshelf">書架書籍</string>
+    <string name="cache_empty_bookshelf">沒有書架內書籍快取或下載任務</string>
+    <string name="cache_section_not_in_bookshelf">未在書架</string>
+    <string name="cache_empty_not_in_bookshelf">沒有未在書架的書籍下載狀態</string>
+    <string name="cache_download_status_summary">下載中 %1$d · 等待 %2$d · 暫停 %3$d · 失敗 %4$d</string>
+    <string name="cache_progress_description">已快取 %1$d/%2$d 章,進度 %3$d%%</string>
+    <string name="a11y_cache_book_item">%1$s,%2$s,%3$s,%4$s</string>
+    <string name="pause_book_download">暫停《%1$s》的下載</string>
+    <string name="resume_book_download">繼續《%1$s》的下載</string>
+    <string name="start_book_download">開始下載《%1$s》</string>
+    <string name="delete_book_cache">刪除《%1$s》的快取</string>
+    <string name="a11y_cache_chapter_item">%1$s,%2$s</string>
+    <string name="cache_chapter_progress_description">章節下載進度 %1$d%%</string>
+    <string name="pause_chapter_download">暫停章節下載:%1$s</string>
+    <string name="resume_chapter_download">繼續章節下載:%1$s</string>
+    <string name="download_chapter">下載章節:%1$s</string>
+    <string name="delete_chapter_cache">刪除章節快取:%1$s</string>
+    <string name="download_paused">已暫停</string>
+    <string name="not_cached">未快取</string>
+    <string name="delete_book_cache_message">刪除《%1$s》的全部快取,並從下載佇列移除?</string>
+    <string name="delete_chapter_cache_message">刪除章節快取:%1$s?</string>
+    <string name="about_open_project_homepage">開啟專案首頁</string>
+    <string name="about_open_github">開啟 GitHub</string>
 </resources>

+ 119 - 0
app/src/main/res/values/strings.xml

@@ -2249,4 +2249,123 @@
     <string name="cover_album_imported_name">%1$s (Imported)</string>
     <string name="cover_album_day_night_count">Light %1$d · Dark %2$d</string>
     <string name="cover_album_selected">Currently selected</string>
+
+    <!-- Accessibility and nav_my subpages -->
+    <string name="list_loading_title">Please wait…</string>
+    <string name="list_selected_count">Selected %1$d/%2$d</string>
+    <string name="a11y_selected">Selected</string>
+    <string name="a11y_not_selected">Not selected</string>
+    <string name="a11y_on">On</string>
+    <string name="a11y_off">Off</string>
+    <string name="a11y_expanded">Expanded</string>
+    <string name="a11y_collapsed">Collapsed</string>
+    <string name="a11y_long_press_reorder">Long press and drag to reorder</string>
+    <string name="a11y_rule_enabled_switch">%1$s enabled state</string>
+    <string name="a11y_edit_named">Edit %1$s</string>
+    <string name="a11y_delete_named">Delete %1$s</string>
+    <string name="a11y_more_actions_for">More actions for %1$s</string>
+    <string name="a11y_slider_value">%1$s, %2$s</string>
+    <string name="a11y_percent_value">%1$d percent</string>
+    <string name="a11y_book_cover_actions">Cover for %1$s. Opens cover picker. Long press to preview.</string>
+    <string name="a11y_related_books_more">More related books: %1$s</string>
+    <string name="a11y_current_reading">Current reading position</string>
+    <string name="a11y_vip_locked">VIP or locked</string>
+    <string name="a11y_downloaded">Downloaded</string>
+    <string name="a11y_downloading">Downloading</string>
+    <string name="a11y_download_failed">Download failed</string>
+    <string name="a11y_not_downloaded">Not downloaded</string>
+    <string name="a11y_word_count">Word count %1$s</string>
+    <string name="a11y_retry_chapter">Retry chapter download: %1$s</string>
+    <string name="a11y_read_progress_page">Page %1$d of %2$d</string>
+    <string name="a11y_read_progress_chapter">Chapter %1$d of %2$d</string>
+    <string name="a11y_debug_example">Debug example: %1$s</string>
+    <string name="a11y_book_source_action">Book source: %1$s. Long press for source actions.</string>
+    <string name="search_txt_toc_rule">Search TXT directory rules</string>
+    <string name="search_dict_rule">Search dictionary rules</string>
+    <string name="search_highlight_tag_rule">Search tag rules</string>
+    <string name="export_started">Export started…</string>
+    <string name="previous">Previous</string>
+    <string name="next">Next</string>
+    <string name="unknown_author">Unknown author</string>
+    <string name="merge">Merge</string>
+    <string name="a11y_expand_all_bookmark_groups">Expand all bookmark groups</string>
+    <string name="a11y_collapse_all_bookmark_groups">Collapse all bookmark groups</string>
+    <string name="bookmark_original_text">Original text</string>
+    <string name="bookmark_note">Summary / note</string>
+    <string name="bookmark_original_text_description">Original text: %1$s</string>
+    <string name="bookmark_note_description">Summary or note: %1$s</string>
+    <string name="confirm_delete_bookmark">Delete bookmark?</string>
+    <string name="delete_bookmark_message">Delete this bookmark?</string>
+    <string name="read_record_view_aggregate">Summary view</string>
+    <string name="read_record_view_timeline">Timeline view</string>
+    <string name="read_record_view_latest">Last read view</string>
+    <string name="a11y_switch_to_timeline_view">Switch to timeline view</string>
+    <string name="a11y_switch_to_latest_view">Switch to last read view</string>
+    <string name="a11y_switch_to_aggregate_view">Switch to summary view</string>
+    <string name="a11y_toggle_read_calendar">Show or hide reading calendar</string>
+    <string name="no_read_record">No reading records</string>
+    <string name="confirm_delete_read_record">Delete reading record?</string>
+    <string name="delete_read_record_message">Delete this reading record?</string>
+    <string name="do_not_remind_again">Do not remind again</string>
+    <string name="merge_read_records">Merge reading records</string>
+    <string name="merge_read_records_message">Merge the following authors of “%1$s” into %2$s</string>
+    <string name="no_merge_candidates">No same-title records can be merged</string>
+    <string name="a11y_merge_same_name_read_records">Merge same-title reading records</string>
+    <string name="read_record_date_overview">%1$d/%2$d reading overview</string>
+    <string name="read_record_total_achievement">Total reading achievement</string>
+    <string name="chapter_index_format">Chapter %1$d</string>
+    <string name="a11y_read_record_latest_item">%1$s, %2$s, read for %3$s, last read %4$s</string>
+    <string name="a11y_read_record_timeline_item">%1$s, %2$s, %3$s, read for %4$s, ended at %5$s</string>
+    <string name="a11y_read_record_detail_item">%1$s, %2$s, read for %3$s</string>
+    <string name="reading_time_with_value">Reading time: %1$s</string>
+    <string name="a11y_reading_summary_card">%1$s, %2$d books, %3$s total. Open overview</string>
+    <string name="read_books_prefix">Read </string>
+    <string name="read_books_suffix"> books</string>
+    <string name="hours_minutes_format">%1$d h %2$d min</string>
+    <string name="minutes_format">%1$d min</string>
+    <string name="total_reading_time_format">Total reading %1$s</string>
+    <string name="read_duration_done">Read %1$s</string>
+    <string name="read_record_overview">Reading overview</string>
+    <string name="reading_time">Reading time</string>
+    <string name="reading_days">Reading days</string>
+    <string name="total_read_books">Books read</string>
+    <string name="finished_books">Finished books</string>
+    <string name="reading_books">Reading books</string>
+    <string name="reading_words">Words read</string>
+    <string name="reading_data">Reading data</string>
+    <string name="reading_heatmap">Reading heatmap</string>
+    <string name="reading_time_ranking">Reading time ranking</string>
+    <string name="reading_calendar">Reading calendar</string>
+    <string name="days_format">%1$d days</string>
+    <string name="books_format">%1$d books</string>
+    <string name="a11y_reading_ranking_item">No. %1$d, %2$s, %3$s, read for %4$s</string>
+    <string name="a11y_reading_calendar_day_with_book">%1$s, top book %2$s by %3$s</string>
+    <string name="a11y_reading_calendar_day_empty">%1$s, no reading record</string>
+    <string name="a11y_heatmap_day_count">%1$s, %2$d readings</string>
+    <string name="a11y_heatmap_day_duration">%1$s, read for %2$s</string>
+    <string name="start_download">Start download</string>
+    <string name="stop_download">Stop download</string>
+    <string name="cache_section_bookshelf">Bookshelf books</string>
+    <string name="cache_empty_bookshelf">No cache or download tasks for bookshelf books</string>
+    <string name="cache_section_not_in_bookshelf">Not in bookshelf</string>
+    <string name="cache_empty_not_in_bookshelf">No download state for books outside the bookshelf</string>
+    <string name="cache_download_status_summary">Downloading %1$d · Waiting %2$d · Paused %3$d · Failed %4$d</string>
+    <string name="cache_progress_description">%1$d of %2$d chapters cached, %3$d percent</string>
+    <string name="a11y_cache_book_item">%1$s, %2$s, %3$s, %4$s</string>
+    <string name="pause_book_download">Pause downloads for %1$s</string>
+    <string name="resume_book_download">Resume downloads for %1$s</string>
+    <string name="start_book_download">Start downloads for %1$s</string>
+    <string name="delete_book_cache">Delete cache for %1$s</string>
+    <string name="a11y_cache_chapter_item">%1$s, %2$s</string>
+    <string name="cache_chapter_progress_description">Chapter download %1$d percent</string>
+    <string name="pause_chapter_download">Pause chapter download: %1$s</string>
+    <string name="resume_chapter_download">Resume chapter download: %1$s</string>
+    <string name="download_chapter">Download chapter: %1$s</string>
+    <string name="delete_chapter_cache">Delete chapter cache: %1$s</string>
+    <string name="download_paused">Paused</string>
+    <string name="not_cached">Not cached</string>
+    <string name="delete_book_cache_message">Delete all cache for “%1$s” and remove it from the download queue?</string>
+    <string name="delete_chapter_cache_message">Delete chapter cache: %1$s?</string>
+    <string name="about_open_project_homepage">Open project homepage</string>
+    <string name="about_open_github">Open GitHub</string>
 </resources>