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

Merge pull request #1929 from maker255/fix/popup-font-scale-density-v2

fix: 修复弹窗字体缩放不跟随设置且窗口尺寸变化后失效
Kudomaga 3 месяцев назад
Родитель
Сommit
8a62e03ead
23 измененных файлов с 725 добавлено и 572 удалено
  1. 3 8
      app/src/main/java/io/legado/app/base/AppContextWrapper.kt
  2. 2 0
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  3. 8 0
      app/src/main/java/io/legado/app/base/BaseComposeActivity.kt
  4. 99 94
      app/src/main/java/io/legado/app/ui/book/read/TextActionSelectionMenu.kt
  5. 48 39
      app/src/main/java/io/legado/app/ui/book/read/sheet/CharsetConfigSheet.kt
  6. 37 30
      app/src/main/java/io/legado/app/ui/book/read/sheet/DownloadSheet.kt
  7. 11 8
      app/src/main/java/io/legado/app/ui/book/read/sheet/GlobalThemePage.kt
  8. 15 12
      app/src/main/java/io/legado/app/ui/book/read/sheet/PageAnimConfigSheet.kt
  9. 78 71
      app/src/main/java/io/legado/app/ui/book/read/sheet/PageKeyConfigSheet.kt
  10. 52 49
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudScreen.kt
  11. 56 49
      app/src/main/java/io/legado/app/ui/book/read/sheet/SimulatedReadingSheet.kt
  12. 20 17
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  13. 2 1
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
  14. 45 0
      app/src/main/java/io/legado/app/ui/theme/AppDensity.kt
  15. 1 4
      app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
  16. 7 4
      app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt
  17. 22 19
      app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt
  18. 12 9
      app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt
  19. 2 1
      app/src/main/java/io/legado/app/ui/widget/components/SelectionBottomBar.kt
  20. 64 55
      app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt
  21. 45 36
      app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt
  22. 75 66
      app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt
  23. 21 0
      app/src/test/java/io/legado/app/ui/theme/AppDensityTest.kt

+ 3 - 8
app/src/main/java/io/legado/app/base/AppContextWrapper.kt

@@ -4,7 +4,7 @@ import android.app.Activity
 import android.content.Context
 import android.content.res.Configuration
 import io.legado.app.ui.config.themeConfig.ThemeConfig
-import io.legado.app.utils.sysConfiguration
+import io.legado.app.ui.theme.resolveAppFontScale
 
 
 @Suppress("unused")
@@ -21,12 +21,7 @@ object AppContextWrapper {
         activity.resources.updateConfiguration(newConfig, activity.resources.displayMetrics)
     }
 
-    fun getFontScale(context: Context): Float {
-        var fontScale = ThemeConfig.fontScale / 10f
-        if (fontScale !in 0.8f..1.6f) {
-            fontScale = sysConfiguration.fontScale
-        }
-        return fontScale
-    }
+    fun getFontScale(context: Context): Float =
+        resolveAppFontScale(ThemeConfig.fontScale)
 
 }

+ 2 - 0
app/src/main/java/io/legado/app/base/BaseActivity.kt

@@ -164,6 +164,8 @@ abstract class BaseActivity<VB : ViewBinding>(
         appUiConfigurationGateway.synchronizeSystemDarkTheme(newConfig.isNightMode)
         super.onConfigurationChanged(newConfig)
         lastPlatformConfiguration = Configuration(newConfig)
+        // 窗口尺寸变化会重置 resources 配置里的字体缩放,需要重新应用。
+        AppContextWrapper.applyFont(this)
         appLocaleGateway.synchronizeFromPlatform()
 
         val platformDiff = AppUiConfigurationDiff(

+ 8 - 0
app/src/main/java/io/legado/app/base/BaseComposeActivity.kt

@@ -119,6 +119,9 @@ abstract class BaseComposeActivity(
         // 可直接响应语言与深浅色变化,无需销毁 Activity。AppCompat 手动回调本方法时
         // 不会走 View 树分发,需要自己同步给 Compose(LocalConfiguration),并刷新
         // 系统栏与背景图。
+        // 系统在窗口尺寸变化(小窗/分屏/全屏切换)时会重建 Activity 的 resources 配置,
+        // onCreate 里写入的字体缩放被重置,需要重新应用。
+        AppContextWrapper.applyFont(this)
         window.decorView.dispatchConfigurationChanged(newConfig)
         appLocaleGateway.synchronizeFromPlatform()
         setupSystemBar()
@@ -201,6 +204,11 @@ abstract class BaseComposeActivity(
                     lastUiConfiguration = configuration
                     if (previous == null) return@collect
                     val diff = configuration.diffFrom(previous)
+                    if (diff.fontScaleChanged) {
+                        // Compose 树通过 AppTheme/ProvideAppDensity 自行响应;这里同步 resources
+                        // 配置,供仍读取平台字体缩放的组件(三方弹窗内部、legacy View)使用。
+                        AppContextWrapper.applyFont(this@BaseComposeActivity)
+                    }
                     if (diff.windowChanged) {
                         setupSystemBar()
                         if (imageBg) upBackgroundImage()

+ 99 - 94
app/src/main/java/io/legado/app/ui/book/read/TextActionSelectionMenu.kt

@@ -63,6 +63,7 @@ import androidx.compose.ui.window.PopupProperties
 import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.text.AppText
 import kotlin.math.roundToInt
@@ -131,75 +132,77 @@ fun TextActionSelectionMenu(
             dismissOnClickOutside = !showMoreMenu,
         )
     ) {
+        ProvideAppDensity {
 
-        AnimatedVisibility(
-            visibleState = visibilityState,
-            enter = fadeIn(animationSpec = tween(360)) + scaleIn(
-                initialScale = 0.94f,
-                animationSpec = tween(400),
-            ),
-            exit = fadeOut(animationSpec = tween(320)) + scaleOut(
-                targetScale = 0.96f,
-                animationSpec = tween(280),
-            ),
-        ) {
-            if (expandTextMenu) {
-                Box(modifier = Modifier.padding(menuShadowPadding)) {
-                    NormalCard(
-                        modifier = Modifier.widthIn(max = menuCardMaxWidth),
-                        containerColor = LegadoTheme.colorScheme.surfaceBright,
-                        elevation = 12.dp,
-                        cornerRadius = 12.dp,
-                    ) {
-                        MultiLineMenuView(
-                            items = activeMultiItems,
-                            onItemClick = onItemClick,
-                            onManageClick = {
-                                onDismiss()
-                                onOpenManage()
-                            }
-                        )
+            AnimatedVisibility(
+                visibleState = visibilityState,
+                enter = fadeIn(animationSpec = tween(360)) + scaleIn(
+                    initialScale = 0.94f,
+                    animationSpec = tween(400),
+                ),
+                exit = fadeOut(animationSpec = tween(320)) + scaleOut(
+                    targetScale = 0.96f,
+                    animationSpec = tween(280),
+                ),
+            ) {
+                if (expandTextMenu) {
+                    Box(modifier = Modifier.padding(menuShadowPadding)) {
+                        NormalCard(
+                            modifier = Modifier.widthIn(max = menuCardMaxWidth),
+                            containerColor = LegadoTheme.colorScheme.surfaceBright,
+                            elevation = 12.dp,
+                            cornerRadius = 12.dp,
+                        ) {
+                            MultiLineMenuView(
+                                items = activeMultiItems,
+                                onItemClick = onItemClick,
+                                onManageClick = {
+                                    onDismiss()
+                                    onOpenManage()
+                                }
+                            )
+                        }
                     }
-                }
-            } else {
-                val quickMenuScale by animateFloatAsState(
-                    targetValue = if (showMoreMenu) 0.96f else 1f,
-                    animationSpec = tween(durationMillis = 400),
-                    label = "quickMenuScale"
-                )
-                val quickMenuAlpha by animateFloatAsState(
-                    targetValue = if (showMoreMenu) 0.82f else 1f,
-                    animationSpec = tween(durationMillis = 360),
-                    label = "quickMenuAlpha"
-                )
+                } else {
+                    val quickMenuScale by animateFloatAsState(
+                        targetValue = if (showMoreMenu) 0.96f else 1f,
+                        animationSpec = tween(durationMillis = 400),
+                        label = "quickMenuScale"
+                    )
+                    val quickMenuAlpha by animateFloatAsState(
+                        targetValue = if (showMoreMenu) 0.82f else 1f,
+                        animationSpec = tween(durationMillis = 360),
+                        label = "quickMenuAlpha"
+                    )
 
-                Box(
-                    modifier = Modifier
-                        .graphicsLayer {
-                            scaleX = quickMenuScale
-                            scaleY = quickMenuScale
-                            alpha = quickMenuAlpha
-                            transformOrigin = TransformOrigin(1f, 0.5f)
-                        }
-                        .padding(menuShadowPadding),
-                ) {
-                    NormalCard(
-                        modifier = Modifier.widthIn(max = menuCardMaxWidth),
-                        containerColor = LegadoTheme.colorScheme.surfaceBright,
-                        elevation = 6.dp,
-                        cornerRadius = 12.dp,
-                    ) {
-                        QuickMenuView(
-                            items = primaryItems,
-                            hasMore = collapsedItems.isNotEmpty(),
-                            onItemClick = onItemClick,
-                            onMoreClick = { showMoreMenu = true },
-                            onMoreAnchorChanged = { moreMenuAnchor = it },
-                            onSettingsClick = {
-                                onDismiss()
-                                onOpenManage()
+                    Box(
+                        modifier = Modifier
+                            .graphicsLayer {
+                                scaleX = quickMenuScale
+                                scaleY = quickMenuScale
+                                alpha = quickMenuAlpha
+                                transformOrigin = TransformOrigin(1f, 0.5f)
                             }
-                        )
+                            .padding(menuShadowPadding),
+                    ) {
+                        NormalCard(
+                            modifier = Modifier.widthIn(max = menuCardMaxWidth),
+                            containerColor = LegadoTheme.colorScheme.surfaceBright,
+                            elevation = 6.dp,
+                            cornerRadius = 12.dp,
+                        ) {
+                            QuickMenuView(
+                                items = primaryItems,
+                                hasMore = collapsedItems.isNotEmpty(),
+                                onItemClick = onItemClick,
+                                onMoreClick = { showMoreMenu = true },
+                                onMoreAnchorChanged = { moreMenuAnchor = it },
+                                onSettingsClick = {
+                                    onDismiss()
+                                    onOpenManage()
+                                }
+                            )
+                        }
                     }
                 }
             }
@@ -231,37 +234,39 @@ fun TextActionSelectionMenu(
                 dismissOnClickOutside = true,
             )
         ) {
-            AnimatedVisibility(
-                visibleState = moreVisibilityState,
-                enter = fadeIn(animationSpec = tween(360)) + scaleIn(
-                    initialScale = 0.94f,
-                    transformOrigin = TransformOrigin(1f, 0.5f),
-                    animationSpec = tween(400),
-                ),
-                exit = fadeOut(animationSpec = tween(240)) + scaleOut(
-                    targetScale = 0.96f,
-                    transformOrigin = TransformOrigin(1f, 0.5f),
-                    animationSpec = tween(280),
-                ),
-            ) {
-                Box(
-                    modifier = Modifier.padding(menuShadowPadding),
+            ProvideAppDensity {
+                AnimatedVisibility(
+                    visibleState = moreVisibilityState,
+                    enter = fadeIn(animationSpec = tween(360)) + scaleIn(
+                        initialScale = 0.94f,
+                        transformOrigin = TransformOrigin(1f, 0.5f),
+                        animationSpec = tween(400),
+                    ),
+                    exit = fadeOut(animationSpec = tween(240)) + scaleOut(
+                        targetScale = 0.96f,
+                        transformOrigin = TransformOrigin(1f, 0.5f),
+                        animationSpec = tween(280),
+                    ),
                 ) {
-                    NormalCard(
-                        modifier = Modifier.widthIn(max = menuCardMaxWidth),
-                        containerColor = LegadoTheme.colorScheme.surfaceBright,
-                        elevation = 6.dp,
-                        cornerRadius = 12.dp,
+                    Box(
+                        modifier = Modifier.padding(menuShadowPadding),
                     ) {
-                        MoreMenuView(
-                            items = collapsedItems,
-                            onItemClick = onItemClick,
-                            onBack = { showMoreMenu = false },
-                            onManageClick = {
-                                onDismiss()
-                                onOpenManage()
-                            }
-                        )
+                        NormalCard(
+                            modifier = Modifier.widthIn(max = menuCardMaxWidth),
+                            containerColor = LegadoTheme.colorScheme.surfaceBright,
+                            elevation = 6.dp,
+                            cornerRadius = 12.dp,
+                        ) {
+                            MoreMenuView(
+                                items = collapsedItems,
+                                onItemClick = onItemClick,
+                                onBack = { showMoreMenu = false },
+                                onManageClick = {
+                                    onDismiss()
+                                    onOpenManage()
+                                }
+                            )
+                        }
                     }
                 }
             }

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

@@ -22,6 +22,7 @@ import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -35,57 +36,65 @@ fun CharsetConfigSheet(
     AlertDialog(
         onDismissRequest = onDismissRequest,
         containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { Text(stringResource(R.string.set_charset)) },
+        title = { ProvideAppDensity { Text(stringResource(R.string.set_charset)) } },
         text = {
-            Column {
-                ExposedDropdownMenuBox(
-                    expanded = expanded,
-                    onExpandedChange = { expanded = it },
-                ) {
-                    OutlinedTextField(
-                        value = charset,
-                        onValueChange = { charset = it },
-                        modifier = Modifier
-                            .menuAnchor(MenuAnchorType.PrimaryEditable)
-                            .fillMaxWidth(),
-                        readOnly = false,
-                        label = { Text(stringResource(R.string.set_charset)) },
-                        singleLine = true,
-                        trailingIcon = {
-                            ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
-                        },
-                        colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
-                    )
-                    ExposedDropdownMenu(
+            ProvideAppDensity {
+                Column {
+                    ExposedDropdownMenuBox(
                         expanded = expanded,
-                        onDismissRequest = { expanded = false },
+                        onExpandedChange = { expanded = it },
                     ) {
-                        charsetEntries.forEach { entry ->
-                            DropdownMenuItem(
-                                text = { Text(entry) },
-                                onClick = {
-                                    charset = entry
-                                    expanded = false
-                                },
-                            )
+                        OutlinedTextField(
+                            value = charset,
+                            onValueChange = { charset = it },
+                            modifier = Modifier
+                                .menuAnchor(MenuAnchorType.PrimaryEditable)
+                                .fillMaxWidth(),
+                            readOnly = false,
+                            label = { Text(stringResource(R.string.set_charset)) },
+                            singleLine = true,
+                            trailingIcon = {
+                                ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
+                            },
+                            colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
+                        )
+                        ExposedDropdownMenu(
+                            expanded = expanded,
+                            onDismissRequest = { expanded = false },
+                        ) {
+                            ProvideAppDensity {
+                                charsetEntries.forEach { entry ->
+                                    DropdownMenuItem(
+                                        text = { Text(entry) },
+                                        onClick = {
+                                            charset = entry
+                                            expanded = false
+                                        },
+                                    )
+                                }
+                            }
                         }
                     }
                 }
             }
         },
         confirmButton = {
-            TextButton(
-                onClick = {
-                    ReadBook.setCharset(charset)
-                    onDismissRequest()
-                },
-            ) {
-                Text(stringResource(R.string.ok))
+            ProvideAppDensity {
+                TextButton(
+                    onClick = {
+                        ReadBook.setCharset(charset)
+                        onDismissRequest()
+                    },
+                ) {
+                    Text(stringResource(R.string.ok))
+                }
             }
         },
         dismissButton = {
-            TextButton(onClick = onDismissRequest) {
-                Text(stringResource(R.string.cancel))
+            ProvideAppDensity {
+                TextButton(onClick = onDismissRequest) {
+                    Text(stringResource(R.string.cancel))
+                }
             }
         },
     )

+ 37 - 30
app/src/main/java/io/legado/app/ui/book/read/sheet/DownloadSheet.kt

@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 
 @Composable
 fun DownloadSheet(
@@ -32,43 +33,49 @@ fun DownloadSheet(
     AlertDialog(
         onDismissRequest = onDismissRequest,
         containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { Text(stringResource(R.string.offline_cache)) },
+        title = { ProvideAppDensity { Text(stringResource(R.string.offline_cache)) } },
         text = {
-            androidx.compose.foundation.layout.Column {
-                OutlinedTextField(
-                    value = startChapter,
-                    onValueChange = { startChapter = it },
-                    label = { Text(stringResource(R.string.start_chapter)) },
-                    singleLine = true,
-                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
-                )
-                Spacer(modifier = Modifier.height(12.dp))
-                OutlinedTextField(
-                    value = endChapter,
-                    onValueChange = { endChapter = it },
-                    label = { Text(stringResource(R.string.end_chapter)) },
-                    singleLine = true,
-                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
-                )
+            ProvideAppDensity {
+                androidx.compose.foundation.layout.Column {
+                    OutlinedTextField(
+                        value = startChapter,
+                        onValueChange = { startChapter = it },
+                        label = { Text(stringResource(R.string.start_chapter)) },
+                        singleLine = true,
+                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                    )
+                    Spacer(modifier = Modifier.height(12.dp))
+                    OutlinedTextField(
+                        value = endChapter,
+                        onValueChange = { endChapter = it },
+                        label = { Text(stringResource(R.string.end_chapter)) },
+                        singleLine = true,
+                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                    )
+                }
             }
         },
         confirmButton = {
-            TextButton(
-                onClick = {
-                    val start = startChapter.toIntOrNull() ?: return@TextButton
-                    val end = endChapter.toIntOrNull() ?: return@TextButton
-                    if (start <= end) {
-                        onDownload(start, end)
-                        onDismissRequest()
-                    }
-                },
-            ) {
-                Text(stringResource(R.string.ok))
+            ProvideAppDensity {
+                TextButton(
+                    onClick = {
+                        val start = startChapter.toIntOrNull() ?: return@TextButton
+                        val end = endChapter.toIntOrNull() ?: return@TextButton
+                        if (start <= end) {
+                            onDownload(start, end)
+                            onDismissRequest()
+                        }
+                    },
+                ) {
+                    Text(stringResource(R.string.ok))
+                }
             }
         },
         dismissButton = {
-            TextButton(onClick = onDismissRequest) {
-                Text(stringResource(R.string.cancel))
+            ProvideAppDensity {
+                TextButton(onClick = onDismissRequest) {
+                    Text(stringResource(R.string.cancel))
+                }
             }
         },
     )

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

@@ -59,6 +59,7 @@ import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookSheet
 import io.legado.app.ui.book.read.ReadBookStyleConfig
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.fadingEdge
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.card.NormalCard
@@ -195,14 +196,16 @@ fun GlobalThemePage(
                         TooltipAnchorPosition.Above
                     ),
                     tooltip = {
-                        PlainTooltip(
-                            containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                            contentColor = LegadoTheme.colorScheme.onSurface,
-                        ) {
-                            AppText(
-                                text = stringResource(R.string.share_layout),
-                                style = LegadoTheme.typography.bodyMedium,
-                            )
+                        ProvideAppDensity {
+                            PlainTooltip(
+                                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                                contentColor = LegadoTheme.colorScheme.onSurface,
+                            ) {
+                                AppText(
+                                    text = stringResource(R.string.share_layout),
+                                    style = LegadoTheme.typography.bodyMedium,
+                                )
+                            }
                         }
                     },
                     state = rememberTooltipState(),

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

@@ -7,6 +7,7 @@ import androidx.compose.ui.res.stringResource
 import io.legado.app.R
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
 
 @Composable
@@ -26,19 +27,21 @@ fun PageAnimConfigSheet(
     AlertDialog(
         onDismissRequest = onDismissRequest,
         containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { Text(stringResource(R.string.page_anim)) },
+        title = { ProvideAppDensity { Text(stringResource(R.string.page_anim)) } },
         text = {
-            TinyDropdownSettingItem(
-                title = stringResource(R.string.page_anim),
-                selectedValue = ReadBook.book?.getPageAnim()?.toString() ?: "-1",
-                displayEntries = items.map { stringResource(it) }.toTypedArray(),
-                entryValues = items.indices.map { (it - 1).toString() }.toTypedArray(),
-                onValueChange = {
-                    ReadBook.book?.setPageAnim(it.toInt())
-                    onAnimChanged()
-                    onDismissRequest()
-                },
-            )
+            ProvideAppDensity {
+                TinyDropdownSettingItem(
+                    title = stringResource(R.string.page_anim),
+                    selectedValue = ReadBook.book?.getPageAnim()?.toString() ?: "-1",
+                    displayEntries = items.map { stringResource(it) }.toTypedArray(),
+                    entryValues = items.indices.map { (it - 1).toString() }.toTypedArray(),
+                    onValueChange = {
+                        ReadBook.book?.setPageAnim(it.toInt())
+                        onAnimChanged()
+                        onDismissRequest()
+                    },
+                )
+            }
         },
         confirmButton = {},
     )

+ 78 - 71
app/src/main/java/io/legado/app/ui/book/read/sheet/PageKeyConfigSheet.kt

@@ -29,6 +29,7 @@ import io.legado.app.R
 import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import kotlinx.coroutines.launch
 import org.koin.compose.koinInject
 
@@ -52,88 +53,94 @@ fun PageKeyConfigSheet(
     AlertDialog(
         onDismissRequest = onDismissRequest,
         containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { Text(stringResource(R.string.custom_page_key)) },
+        title = { ProvideAppDensity { Text(stringResource(R.string.custom_page_key)) } },
         text = {
-            Column(
-                modifier = Modifier.verticalScroll(rememberScrollState()),
-            ) {
-                OutlinedTextField(
-                    value = prevKeys,
-                    onValueChange = { prevKeys = it },
-                    label = { Text(stringResource(R.string.prev_page_key)) },
-                    singleLine = true,
-                    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .onKeyEvent { event ->
-                            val keyCode = event.nativeKeyEvent.keyCode
-                            if (keyCode != android.view.KeyEvent.KEYCODE_BACK &&
-                                keyCode != android.view.KeyEvent.KEYCODE_DEL
-                            ) {
-                                prevKeys = if (prevKeys.isEmpty() || prevKeys.endsWith(",")) {
-                                    "$prevKeys$keyCode"
+            ProvideAppDensity {
+                Column(
+                    modifier = Modifier.verticalScroll(rememberScrollState()),
+                ) {
+                    OutlinedTextField(
+                        value = prevKeys,
+                        onValueChange = { prevKeys = it },
+                        label = { Text(stringResource(R.string.prev_page_key)) },
+                        singleLine = true,
+                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onKeyEvent { event ->
+                                val keyCode = event.nativeKeyEvent.keyCode
+                                if (keyCode != android.view.KeyEvent.KEYCODE_BACK &&
+                                    keyCode != android.view.KeyEvent.KEYCODE_DEL
+                                ) {
+                                    prevKeys = if (prevKeys.isEmpty() || prevKeys.endsWith(",")) {
+                                        "$prevKeys$keyCode"
+                                    } else {
+                                        "$prevKeys,$keyCode"
+                                    }
+                                    true
                                 } else {
-                                    "$prevKeys,$keyCode"
+                                    false
                                 }
-                                true
-                            } else {
-                                false
-                            }
-                        },
-                )
-                Spacer(modifier = Modifier.height(12.dp))
-                OutlinedTextField(
-                    value = nextKeys,
-                    onValueChange = { nextKeys = it },
-                    label = { Text(stringResource(R.string.next_page_key)) },
-                    singleLine = true,
-                    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .onKeyEvent { event ->
-                            val keyCode = event.nativeKeyEvent.keyCode
-                            if (keyCode != android.view.KeyEvent.KEYCODE_BACK &&
-                                keyCode != android.view.KeyEvent.KEYCODE_DEL
-                            ) {
-                                nextKeys = if (nextKeys.isEmpty() || nextKeys.endsWith(",")) {
-                                    "$nextKeys$keyCode"
+                            },
+                    )
+                    Spacer(modifier = Modifier.height(12.dp))
+                    OutlinedTextField(
+                        value = nextKeys,
+                        onValueChange = { nextKeys = it },
+                        label = { Text(stringResource(R.string.next_page_key)) },
+                        singleLine = true,
+                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onKeyEvent { event ->
+                                val keyCode = event.nativeKeyEvent.keyCode
+                                if (keyCode != android.view.KeyEvent.KEYCODE_BACK &&
+                                    keyCode != android.view.KeyEvent.KEYCODE_DEL
+                                ) {
+                                    nextKeys = if (nextKeys.isEmpty() || nextKeys.endsWith(",")) {
+                                        "$nextKeys$keyCode"
+                                    } else {
+                                        "$nextKeys,$keyCode"
+                                    }
+                                    true
                                 } else {
-                                    "$nextKeys,$keyCode"
+                                    false
                                 }
-                                true
-                            } else {
-                                false
-                            }
-                        },
-                )
-                Spacer(modifier = Modifier.height(8.dp))
-                Text(
-                    text = stringResource(R.string.page_key_set_help),
-                    style = MaterialTheme.typography.bodySmall,
-                    color = MaterialTheme.colorScheme.onSurfaceVariant,
-                )
+                            },
+                    )
+                    Spacer(modifier = Modifier.height(8.dp))
+                    Text(
+                        text = stringResource(R.string.page_key_set_help),
+                        style = MaterialTheme.typography.bodySmall,
+                        color = MaterialTheme.colorScheme.onSurfaceVariant,
+                    )
+                }
             }
         },
         dismissButton = {
-            TextButton(
-                onClick = {
-                    prevKeys = ""
-                    nextKeys = ""
-                },
-            ) {
-                Text(stringResource(R.string.reset))
+            ProvideAppDensity {
+                TextButton(
+                    onClick = {
+                        prevKeys = ""
+                        nextKeys = ""
+                    },
+                ) {
+                    Text(stringResource(R.string.reset))
+                }
             }
         },
         confirmButton = {
-            TextButton(
-                onClick = {
-                    scope.launch {
-                        readSettingsRepository.setPageKeys(prevKeys, nextKeys)
-                        onDismissRequest()
-                    }
-                },
-            ) {
-                Text(stringResource(R.string.ok))
+            ProvideAppDensity {
+                TextButton(
+                    onClick = {
+                        scope.launch {
+                            readSettingsRepository.setPageKeys(prevKeys, nextKeys)
+                            onDismissRequest()
+                        }
+                    },
+                ) {
+                    Text(stringResource(R.string.ok))
+                }
             }
         },
     )

+ 52 - 49
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudScreen.kt

@@ -61,6 +61,7 @@ import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerIntent
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerScreenContent
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerUiState
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.ProvideThemeOverride
 import io.legado.app.ui.theme.ThemeOverrideState
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
@@ -131,60 +132,62 @@ fun ReadAloudScreen(
             dragHandle = null,
             properties = ModalBottomSheetProperties(shouldDismissOnBackPress = false),
         ) {
-            BackHandler(enabled = page == ReadAloudPage.Config, onBack = returnFromConfig)
-            PredictiveBackHandler(enabled = page != ReadAloudPage.Config) { progress ->
-                try {
-                    progress.collect { event ->
-                        backProgress = event.progress
+            ProvideAppDensity {
+                BackHandler(enabled = page == ReadAloudPage.Config, onBack = returnFromConfig)
+                PredictiveBackHandler(enabled = page != ReadAloudPage.Config) { progress ->
+                    try {
+                        progress.collect { event ->
+                            backProgress = event.progress
+                        }
+                        sheetState.hide()
+                        onDismissRequest()
+                    } finally {
+                        backProgress = 0f
                     }
-                    sheetState.hide()
-                    onDismissRequest()
-                } finally {
-                    backProgress = 0f
                 }
-            }
-            AnimatedContent(
-                targetState = page,
-                label = "ReadAloudPage",
-            ) { targetPage ->
-                when (targetPage) {
-                    ReadAloudPage.Config -> Column {
-                        Row(
-                            modifier = Modifier
-                                .fillMaxWidth()
-                                .windowInsetsPadding(WindowInsets.statusBars)
-                                .padding(horizontal = 16.dp),
-                            verticalAlignment = Alignment.CenterVertically,
-                        ) {
-                            MediumTonalButton(
-                                onClick = {
-                                    returnFromConfig()
-                                },
-                                icon = Icons.AutoMirrored.Filled.ArrowBack,
-                                contentDescription = stringResource(R.string.back),
-                            )
-                            Spacer(Modifier.width(12.dp))
-                            Text(
-                                text = stringResource(R.string.aloud_config),
-                                style = LegadoTheme.typography.titleLarge,
+                AnimatedContent(
+                    targetState = page,
+                    label = "ReadAloudPage",
+                ) { targetPage ->
+                    when (targetPage) {
+                        ReadAloudPage.Config -> Column {
+                            Row(
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .windowInsetsPadding(WindowInsets.statusBars)
+                                    .padding(horizontal = 16.dp),
+                                verticalAlignment = Alignment.CenterVertically,
+                            ) {
+                                MediumTonalButton(
+                                    onClick = {
+                                        returnFromConfig()
+                                    },
+                                    icon = Icons.AutoMirrored.Filled.ArrowBack,
+                                    contentDescription = stringResource(R.string.back),
+                                )
+                                Spacer(Modifier.width(12.dp))
+                                Text(
+                                    text = stringResource(R.string.aloud_config),
+                                    style = LegadoTheme.typography.titleLarge,
+                                )
+                            }
+                            Spacer(modifier = Modifier.padding(vertical = 8.dp))
+                            ReadAloudConfigContent(
+                                state = state,
+                                playerState = playerState,
+                                onIntent = onIntent,
+                                onPlayerIntent = onPlayerIntent,
+                                modifier = Modifier.padding(horizontal = 16.dp)
                             )
                         }
-                        Spacer(modifier = Modifier.padding(vertical = 8.dp))
-                        ReadAloudConfigContent(
-                            state = state,
-                            playerState = playerState,
-                            onIntent = onIntent,
-                            onPlayerIntent = onPlayerIntent,
-                            modifier = Modifier.padding(horizontal = 16.dp)
-                        )
-                    }
 
-                    ReadAloudPage.Player -> ProvideThemeOverride(playerTheme) {
-                        ReadAloudPlayerScreenContent(
-                            state = playerState,
-                            onIntent = onPlayerIntent,
-                            onBack = onDismissRequest,
-                        )
+                        ReadAloudPage.Player -> ProvideThemeOverride(playerTheme) {
+                            ReadAloudPlayerScreenContent(
+                                state = playerState,
+                                onIntent = onPlayerIntent,
+                                onBack = onDismissRequest,
+                            )
+                        }
                     }
                 }
             }

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

@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
 import java.time.Instant
 import java.time.LocalDate
@@ -51,67 +52,73 @@ fun SimulatedReadingSheet(
     AlertDialog(
         onDismissRequest = onDismissRequest,
         containerColor = LegadoTheme.colorScheme.surfaceContainer,
-        title = { Text(stringResource(R.string.simulated_reading)) },
+        title = { ProvideAppDensity { Text(stringResource(R.string.simulated_reading)) } },
         text = {
-            Column {
-                OutlinedTextField(
-                    value = startDate.format(dateFormatter),
-                    onValueChange = {},
-                    label = { Text(stringResource(R.string.start_from)) },
-                    readOnly = true,
-                    singleLine = true,
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .clickable { showDatePicker = true },
-                    enabled = false,
-                )
-                Spacer(modifier = Modifier.height(8.dp))
-                TinySwitchSettingItem(
-                    title = stringResource(R.string.simulated_reading),
-                    checked = enabled,
-                    onCheckedChange = { enabled = it },
-                )
-                Spacer(modifier = Modifier.height(12.dp))
-                // Start chapter + daily chapters
-                Row(modifier = Modifier.fillMaxWidth()) {
+            ProvideAppDensity {
+                Column {
                     OutlinedTextField(
-                        value = startChapter,
-                        onValueChange = { startChapter = it },
-                        label = { Text(stringResource(R.string.start_chapter)) },
+                        value = startDate.format(dateFormatter),
+                        onValueChange = {},
+                        label = { Text(stringResource(R.string.start_from)) },
+                        readOnly = true,
                         singleLine = true,
-                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
-                        modifier = Modifier.weight(1f),
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .clickable { showDatePicker = true },
+                        enabled = false,
                     )
-                    Spacer(modifier = Modifier.width(8.dp))
-                    OutlinedTextField(
-                        value = dailyChapters,
-                        onValueChange = { dailyChapters = it },
-                        label = { Text(stringResource(R.string.daily_chapters)) },
-                        singleLine = true,
-                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
-                        modifier = Modifier.weight(1f),
+                    Spacer(modifier = Modifier.height(8.dp))
+                    TinySwitchSettingItem(
+                        title = stringResource(R.string.simulated_reading),
+                        checked = enabled,
+                        onCheckedChange = { enabled = it },
                     )
+                    Spacer(modifier = Modifier.height(12.dp))
+                    // Start chapter + daily chapters
+                    Row(modifier = Modifier.fillMaxWidth()) {
+                        OutlinedTextField(
+                            value = startChapter,
+                            onValueChange = { startChapter = it },
+                            label = { Text(stringResource(R.string.start_chapter)) },
+                            singleLine = true,
+                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                            modifier = Modifier.weight(1f),
+                        )
+                        Spacer(modifier = Modifier.width(8.dp))
+                        OutlinedTextField(
+                            value = dailyChapters,
+                            onValueChange = { dailyChapters = it },
+                            label = { Text(stringResource(R.string.daily_chapters)) },
+                            singleLine = true,
+                            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+                            modifier = Modifier.weight(1f),
+                        )
+                    }
                 }
             }
         },
         confirmButton = {
-            TextButton(
-                onClick = {
-                    book.setStartDate(startDate)
-                    book.setDailyChapters(dailyChapters.toIntOrNull() ?: 0)
-                    book.setStartChapter(startChapter.toIntOrNull() ?: 0)
-                    book.setReadSimulating(enabled)
-                    book.save()
-                    onApply()
-                    onDismissRequest()
-                },
-            ) {
-                Text(stringResource(R.string.ok))
+            ProvideAppDensity {
+                TextButton(
+                    onClick = {
+                        book.setStartDate(startDate)
+                        book.setDailyChapters(dailyChapters.toIntOrNull() ?: 0)
+                        book.setStartChapter(startChapter.toIntOrNull() ?: 0)
+                        book.setReadSimulating(enabled)
+                        book.save()
+                        onApply()
+                        onDismissRequest()
+                    },
+                ) {
+                    Text(stringResource(R.string.ok))
+                }
             }
         },
         dismissButton = {
-            TextButton(onClick = onDismissRequest) {
-                Text(stringResource(R.string.cancel))
+            ProvideAppDensity {
+                TextButton(onClick = onDismissRequest) {
+                    Text(stringResource(R.string.cancel))
+                }
             }
         },
     )

+ 20 - 17
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -105,6 +105,7 @@ import io.legado.app.ui.book.group.GroupEditSheet
 import io.legado.app.ui.book.info.GroupSelectSheet
 import io.legado.app.ui.main.bookCoverSharedElementKey
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.adaptiveContentPaddingBookshelf
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
@@ -1285,24 +1286,26 @@ 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
-            ) {
-                Column(
-                    modifier = Modifier.padding(24.dp),
-                    horizontalAlignment = Alignment.CenterHorizontally
+            ProvideAppDensity {
+                NormalCard(
+                    modifier = Modifier.semantics {
+                        contentDescription = loadingDescription
+                    },
+                    cornerRadius = 12.dp,
+                    containerColor = LegadoTheme.colorScheme.surfaceContainerHigh
                 ) {
-                    AppCircularProgressIndicator()
-                    uiState.loadingText?.let {
-                        AppText(
-                            text = it,
-                            modifier = Modifier.padding(top = 16.dp),
-                            style = LegadoTheme.typography.bodyMedium
-                        )
+                    Column(
+                        modifier = Modifier.padding(24.dp),
+                        horizontalAlignment = Alignment.CenterHorizontally
+                    ) {
+                        AppCircularProgressIndicator()
+                        uiState.loadingText?.let {
+                            AppText(
+                                text = it,
+                                modifier = Modifier.padding(top = 16.dp),
+                                style = LegadoTheme.typography.bodyMedium
+                            )
+                        }
                     }
                 }
             }

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

@@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.ActionItem
@@ -219,7 +220,7 @@ fun ReplaceRuleScreen(
     )
 
     if (importState is BaseImportUiState.Loading) {
-        Dialog(onDismissRequest = { onIntent(ReplaceRuleIntent.CancelImport) }) { LoadingIndicator() }
+        Dialog(onDismissRequest = { onIntent(ReplaceRuleIntent.CancelImport) }) { ProvideAppDensity { LoadingIndicator() } }
     }
 
     LaunchedEffect(importState) {

+ 45 - 0
app/src/main/java/io/legado/app/ui/theme/AppDensity.kt

@@ -0,0 +1,45 @@
+package io.legado.app.ui.theme
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import io.legado.app.utils.sysConfiguration
+
+/**
+ * 应用设置里的字体缩放(10 = 1.0 倍),超出 0.8~1.6 时回落到 [systemFontScale]。
+ */
+fun resolveAppFontScale(fontScaleSetting: Int, systemFontScale: Float): Float =
+    (fontScaleSetting / 10f).takeIf { it in 0.8f..1.6f } ?: systemFontScale
+
+/**
+ * 同上,回落到当前系统字体缩放。
+ */
+fun resolveAppFontScale(fontScaleSetting: Int): Float =
+    resolveAppFontScale(fontScaleSetting, sysConfiguration.fontScale)
+
+/**
+ * 以平台像素密度 + 应用字体缩放构造 [Density]。
+ */
+@Composable
+fun rememberAppDensity(): Density {
+    val platformDensity = LocalDensity.current
+    val fontScale = resolveAppFontScale(LocalAppUiConfiguration.current.appShell.fontScale)
+    return remember(platformDensity.density, fontScale) {
+        Density(platformDensity.density, fontScale)
+    }
+}
+
+/**
+ * 在弹窗内部重新提供 [LocalDensity]。
+ *
+ * Popup/Dialog/ModalBottomSheet 各自持有独立窗口与 AndroidComposeView,子组合会用
+ * `Density(context)` 覆盖 [AppTheme] 提供的 [LocalDensity],从而丢弃应用内的字体缩放设置:
+ * 窗口尺寸变化(小窗/全屏切换)后 Activity 的 resources 配置被系统重置,弹窗字号回到 1.0;
+ * 修改设置时也要等进程重启才生效。弹窗内容外层套一层本组件即可跟随设置实时生效。
+ */
+@Composable
+fun ProvideAppDensity(content: @Composable () -> Unit) {
+    CompositionLocalProvider(LocalDensity provides rememberAppDensity(), content = content)
+}

+ 1 - 4
app/src/main/java/io/legado/app/ui/theme/AppTheme.kt

@@ -14,7 +14,6 @@ import androidx.compose.ui.unit.Density
 import com.materialkolor.PaletteStyle
 import io.legado.app.domain.model.settings.AppUiConfiguration
 import io.legado.app.domain.model.settings.customColors
-import io.legado.app.utils.sysConfiguration
 import top.yukonga.miuix.kmp.theme.ColorSchemeMode
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -83,9 +82,7 @@ private fun AppThemeActual(
     val customNightPrimary = themeSettings.customNightPrimary
     val appFontPath = themeSettings.appFontPath
     val currentDensity = LocalDensity.current
-    val fontScale = (appShellSettings.fontScale / 10f)
-        .takeIf { it in 0.8f..1.6f }
-        ?: sysConfiguration.fontScale
+    val fontScale = resolveAppFontScale(appShellSettings.fontScale)
     val appDensity = remember(currentDensity.density, fontScale) {
         Density(currentDensity.density, fontScale)
     }

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

@@ -46,6 +46,7 @@ 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.ProvideAppDensity
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.text.AppText
 import top.yukonga.miuix.kmp.basic.Button
@@ -101,10 +102,12 @@ fun AppFloatingActionButton(
                         TooltipAnchorPosition.Above
                     ),
                     tooltip = {
-                        PlainTooltip(
-                            containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                            contentColor = LegadoTheme.colorScheme.onSurface,
-                        ) { AppText(tooltipText) }
+                        ProvideAppDensity {
+                            PlainTooltip(
+                                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                                contentColor = LegadoTheme.colorScheme.onSurface,
+                            ) { AppText(tooltipText) }
+                        }
                     },
                     state = rememberTooltipState(),
                 ) {

+ 22 - 19
app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt

@@ -51,6 +51,7 @@ import io.legado.app.R
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.help.loadFontFiles
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.cnCompare
 import kotlinx.coroutines.CancellationException
@@ -166,25 +167,27 @@ fun FontSelectGrid(
                         expanded = expanded,
                         onDismissRequest = { expanded = false }
                     ) {
-                        FontSort.entries.forEach { sort ->
-                            DropdownMenuItem(
-                                text = { Text(stringResource(sort.labelRes)) },
-                                onClick = {
-                                    scope.launch {
-                                        otherSettings.update { it.copy(fontSort = sort.ordinal) }
-                                    }
-                                    expanded = false
-                                },
-                                trailingIcon = if (fontSort == sort.ordinal) {
-                                    {
-                                        Icon(
-                                            imageVector = Icons.Default.Check,
-                                            contentDescription = null,
-                                            tint = LegadoTheme.colorScheme.primary
-                                        )
-                                    }
-                                } else null
-                            )
+                        ProvideAppDensity {
+                            FontSort.entries.forEach { sort ->
+                                DropdownMenuItem(
+                                    text = { Text(stringResource(sort.labelRes)) },
+                                    onClick = {
+                                        scope.launch {
+                                            otherSettings.update { it.copy(fontSort = sort.ordinal) }
+                                        }
+                                        expanded = false
+                                    },
+                                    trailingIcon = if (fontSort == sort.ordinal) {
+                                        {
+                                            Icon(
+                                                imageVector = Icons.Default.Check,
+                                                contentDescription = null,
+                                                tint = LegadoTheme.colorScheme.primary
+                                            )
+                                        }
+                                    } else null
+                                )
+                            }
                         }
                     }
                 }

+ 12 - 9
app/src/main/java/io/legado/app/ui/widget/components/FontSelectSheet.kt

@@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.core.net.toUri
 import androidx.documentfile.provider.DocumentFile
 import io.legado.app.R
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.utils.FileDoc
@@ -65,15 +66,17 @@ fun FontSelectSheet(
                     expanded = showTypefaceMenu,
                     onDismissRequest = { showTypefaceMenu = false },
                 ) {
-                    systemTypefaces.forEachIndexed { index, name ->
-                        DropdownMenuItem(
-                            text = { Text(name) },
-                            onClick = {
-                                onSelectSystemTypeface(index)
-                                showTypefaceMenu = false
-                                onDismissRequest()
-                            },
-                        )
+                    ProvideAppDensity {
+                        systemTypefaces.forEachIndexed { index, name ->
+                            DropdownMenuItem(
+                                text = { Text(name) },
+                                onClick = {
+                                    onSelectSystemTypeface(index)
+                                    showTypefaceMenu = false
+                                    onDismissRequest()
+                                },
+                            )
+                        }
                     }
                 }
                 MediumTonalButton(

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

@@ -32,6 +32,7 @@ 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.ProvideAppDensity
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcons
@@ -187,7 +188,7 @@ fun SelectionBottomBar(
                     positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
                         TooltipAnchorPosition.Above
                     ),
-                    tooltip = { PlainTooltip { AppText(primaryAction.text) } },
+                    tooltip = { ProvideAppDensity { PlainTooltip { AppText(primaryAction.text) } } },
                     state = rememberTooltipState(),
                 ) {
                     FilledIconButton(

+ 64 - 55
app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt

@@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.button.PrimaryButton
 import io.legado.app.ui.widget.components.button.SecondaryButton
@@ -49,42 +50,44 @@ fun AppAlertDialog(
             summary = text,
             onDismissRequest = onDismissRequest,
             content = {
-                if (content != null) {
-                    Column(
+                ProvideAppDensity {
+                    if (content != null) {
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .padding(top = 12.dp)
+                                .verticalScroll(rememberScrollState())
+                        ) {
+                            content()
+                        }
+                    }
+
+                    Row(
                         modifier = Modifier
                             .fillMaxWidth()
-                            .padding(top = 12.dp)
-                            .verticalScroll(rememberScrollState())
+                            .padding(top = 24.dp),
+                        horizontalArrangement = Arrangement.spacedBy(12.dp),
+                        verticalAlignment = Alignment.CenterVertically
                     ) {
-                        content()
-                    }
-                }
-
-                Row(
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .padding(top = 24.dp),
-                    horizontalArrangement = Arrangement.spacedBy(12.dp),
-                    verticalAlignment = Alignment.CenterVertically
-                ) {
-                    if (onDismiss != null) {
-                        SecondaryButton(
-                            text = dismissText,
-                            modifier = Modifier.weight(1f),
-                            onClick = {
-                                onDismiss()
-                            }
-                        )
-                    }
+                        if (onDismiss != null) {
+                            SecondaryButton(
+                                text = dismissText,
+                                modifier = Modifier.weight(1f),
+                                onClick = {
+                                    onDismiss()
+                                }
+                            )
+                        }
 
-                    if (onConfirm != null) {
-                        PrimaryButton(
-                            text = confirmText,
-                            modifier = Modifier.weight(1f),
-                            onClick = {
-                                onConfirm()
-                            }
-                        )
+                        if (onConfirm != null) {
+                            PrimaryButton(
+                                text = confirmText,
+                                modifier = Modifier.weight(1f),
+                                onClick = {
+                                    onConfirm()
+                                }
+                            )
+                        }
                     }
                 }
             }
@@ -99,40 +102,46 @@ fun AppAlertDialog(
                 titleContentColor = LegadoTheme.colorScheme.onSurface,
                 textContentColor = LegadoTheme.colorScheme.onSurfaceVariant,
                 tonalElevation = AlertDialogDefaults.TonalElevation,
-                title = title?.let { { Text(text = it) } },
+                title = title?.let { { ProvideAppDensity { Text(text = it) } } },
                 text = {
-                    Column(
-                        modifier = Modifier.verticalScroll(rememberScrollState())
-                    ) {
-                        if (text != null) {
-                            SelectionContainer {
-                                Text(
-                                    text = text,
-                                    modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
-                                )
+                    ProvideAppDensity {
+                        Column(
+                            modifier = Modifier.verticalScroll(rememberScrollState())
+                        ) {
+                            if (text != null) {
+                                SelectionContainer {
+                                    Text(
+                                        text = text,
+                                        modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
+                                    )
+                                }
+                            }
+                            if (content != null) {
+                                content()
                             }
-                        }
-                        if (content != null) {
-                            content()
                         }
                     }
                 },
                 confirmButton = {
                     if (onConfirm != null) {
-                        PrimaryButton(
-                            onClick = onConfirm,
-                            text = confirmText
-                        )
+                        ProvideAppDensity {
+                            PrimaryButton(
+                                onClick = onConfirm,
+                                text = confirmText
+                            )
+                        }
                     }
                 },
                 dismissButton = {
                     if (onDismiss != null) {
-                        SecondaryButton(
-                            onClick = {
-                                onDismiss()
-                            },
-                            text = dismissText
-                        )
+                        ProvideAppDensity {
+                            SecondaryButton(
+                                onClick = {
+                                    onDismiss()
+                                },
+                                text = dismissText
+                            )
+                        }
                     }
                 }
             )

+ 45 - 36
app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt

@@ -24,6 +24,7 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ProvideAppContentColor
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.rememberOpaqueColorScheme
 import top.yukonga.miuix.kmp.basic.ListPopupColumn
@@ -53,12 +54,14 @@ fun RoundDropdownMenu(
             onDismissRequest = onDismissRequest,
             popupModifier = modifier
         ) {
-            ProvideAppContentColor(popupContentColor) {
-                ListPopupColumn {
-                    Column(modifier = Modifier.background(popupContainerColor)) {
-                        Spacer(Modifier.height(12.dp))
-                        content(onDismissRequest)
-                        Spacer(Modifier.height(12.dp))
+            ProvideAppDensity {
+                ProvideAppContentColor(popupContentColor) {
+                    ListPopupColumn {
+                        Column(modifier = Modifier.background(popupContainerColor)) {
+                            Spacer(Modifier.height(12.dp))
+                            content(onDismissRequest)
+                            Spacer(Modifier.height(12.dp))
+                        }
                     }
                 }
             }
@@ -75,16 +78,18 @@ fun RoundDropdownMenu(
             shadowElevation = shadowElevation,
             containerColor = popupContainerColor
         ) {
-            MaterialExpressiveTheme(
-                colorScheme = colorScheme,
-                typography = Typography(),
-                motionScheme = MotionScheme.expressive(),
-                shapes = Shapes()
-            ) {
-                Column(
-                    verticalArrangement = Arrangement.spacedBy(verticalSpacing)
+            ProvideAppDensity {
+                MaterialExpressiveTheme(
+                    colorScheme = colorScheme,
+                    typography = Typography(),
+                    motionScheme = MotionScheme.expressive(),
+                    shapes = Shapes()
                 ) {
-                    content(onDismissRequest)
+                    Column(
+                        verticalArrangement = Arrangement.spacedBy(verticalSpacing)
+                    ) {
+                        content(onDismissRequest)
+                    }
                 }
             }
         }
@@ -113,17 +118,19 @@ fun RoundDropdownMenuLazy(
             onDismissRequest = onDismissRequest,
             popupModifier = modifier
         ) {
-            ProvideAppContentColor(popupContentColor) {
-                ListPopupColumn {
-                    LazyColumn(
-                        modifier = Modifier
-                            .background(popupContainerColor)
-                            .heightIn(max = maxHeight),
-                        verticalArrangement = Arrangement.spacedBy(verticalSpacing)
-                    ) {
-                        item { Spacer(Modifier.height(12.dp)) }
-                        content(onDismissRequest)
-                        item { Spacer(Modifier.height(12.dp)) }
+            ProvideAppDensity {
+                ProvideAppContentColor(popupContentColor) {
+                    ListPopupColumn {
+                        LazyColumn(
+                            modifier = Modifier
+                                .background(popupContainerColor)
+                                .heightIn(max = maxHeight),
+                            verticalArrangement = Arrangement.spacedBy(verticalSpacing)
+                        ) {
+                            item { Spacer(Modifier.height(12.dp)) }
+                            content(onDismissRequest)
+                            item { Spacer(Modifier.height(12.dp)) }
+                        }
                     }
                 }
             }
@@ -140,17 +147,19 @@ fun RoundDropdownMenuLazy(
             shadowElevation = shadowElevation,
             containerColor = popupContainerColor
         ) {
-            MaterialExpressiveTheme(
-                colorScheme = colorScheme,
-                typography = Typography(),
-                motionScheme = MotionScheme.expressive(),
-                shapes = Shapes()
-            ) {
-                LazyColumn(
-                    modifier = Modifier.heightIn(max = maxHeight),
-                    verticalArrangement = Arrangement.spacedBy(verticalSpacing)
+            ProvideAppDensity {
+                MaterialExpressiveTheme(
+                    colorScheme = colorScheme,
+                    typography = Typography(),
+                    motionScheme = MotionScheme.expressive(),
+                    shapes = Shapes()
                 ) {
-                    content(onDismissRequest)
+                    LazyColumn(
+                        modifier = Modifier.heightIn(max = maxHeight),
+                        verticalArrangement = Arrangement.spacedBy(verticalSpacing)
+                    ) {
+                        content(onDismissRequest)
+                    }
                 }
             }
         }

+ 75 - 66
app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt

@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LocalLegadoThemeColors
 import io.legado.app.ui.theme.ProvideAppContentColor
+import io.legado.app.ui.theme.ProvideAppDensity
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.menuItem.LocalUseMiuixWindowPopup
 import top.yukonga.miuix.kmp.window.WindowBottomSheet
@@ -68,18 +69,22 @@ fun AppModalBottomSheet(
             title = title,
             startAction = startAction?.let { action ->
                 {
-                    ProvideAppContentColor(sheetContentColor) {
-                        CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
-                            action()
+                    ProvideAppDensity {
+                        ProvideAppContentColor(sheetContentColor) {
+                            CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
+                                action()
+                            }
                         }
                     }
                 }
             },
             endAction = endAction?.let { action ->
                 {
-                    ProvideAppContentColor(sheetContentColor) {
-                        CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
-                            action()
+                    ProvideAppDensity {
+                        ProvideAppContentColor(sheetContentColor) {
+                            CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
+                                action()
+                            }
                         }
                     }
                 }
@@ -91,23 +96,25 @@ fun AppModalBottomSheet(
             enableWindowDim = true,
             allowDismiss = true
         ) {
-            ProvideAppContentColor(sheetContentColor) {
-                CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
-                    Column(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .let {
-                                if (contentPaddingEnabled) {
-                                    it.padding(bottom = 24.dp)
-                                } else {
-                                    it.navigationBarsPadding()
+            ProvideAppDensity {
+                ProvideAppContentColor(sheetContentColor) {
+                    CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .let {
+                                    if (contentPaddingEnabled) {
+                                        it.padding(bottom = 24.dp)
+                                    } else {
+                                        it.navigationBarsPadding()
+                                    }
                                 }
-                            }
-                            .let { contentModifier ->
-                                if (animateContentSize) contentModifier.animateContentSize() else contentModifier
-                            },
-                        content = content
-                    )
+                                .let { contentModifier ->
+                                    if (animateContentSize) contentModifier.animateContentSize() else contentModifier
+                                },
+                            content = content
+                        )
+                    }
                 }
             }
         }
@@ -136,59 +143,61 @@ fun AppModalBottomSheet(
                     dragHandle = { BottomSheetDefaults.DragHandle(color = sheetDragHandleColor) },
                     contentWindowInsets = contentWindowInsets
                 ) {
-                    Column(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .let {
-                                if (contentPaddingEnabled) {
-                                    it.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
-                                } else {
-                                    it
+                    ProvideAppDensity {
+                        Column(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .let {
+                                    if (contentPaddingEnabled) {
+                                        it.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+                                    } else {
+                                        it
+                                    }
                                 }
-                            }
-                            .heightIn(max = maxHeight)
-                            .let { contentModifier ->
-                                if (animateContentSize) contentModifier.animateContentSize() else contentModifier
-                            }
-                            .then(modifier)
-                    ) {
-                        val hasHeader =
-                            !title.isNullOrEmpty() || startAction != null || endAction != null
+                                .heightIn(max = maxHeight)
+                                .let { contentModifier ->
+                                    if (animateContentSize) contentModifier.animateContentSize() else contentModifier
+                                }
+                                .then(modifier)
+                        ) {
+                            val hasHeader =
+                                !title.isNullOrEmpty() || startAction != null || endAction != null
 
-                        if (hasHeader) {
-                            Box(
-                                modifier = Modifier
-                                    .fillMaxWidth()
-                                    .padding(bottom = 16.dp),
-                                contentAlignment = Alignment.Center
-                            ) {
-                                if (startAction != null) {
-                                    Box(modifier = Modifier.align(Alignment.CenterStart)) {
-                                        startAction()
+                            if (hasHeader) {
+                                Box(
+                                    modifier = Modifier
+                                        .fillMaxWidth()
+                                        .padding(bottom = 16.dp),
+                                    contentAlignment = Alignment.Center
+                                ) {
+                                    if (startAction != null) {
+                                        Box(modifier = Modifier.align(Alignment.CenterStart)) {
+                                            startAction()
+                                        }
                                     }
-                                }
 
-                                if (!title.isNullOrEmpty()) {
-                                    Text(
-                                        text = title,
-                                        style = LegadoTheme.typography.titleMediumEmphasized,
-                                        color = sheetContentColor,
-                                        textAlign = TextAlign.Center,
-                                        maxLines = 1,
-                                        overflow = TextOverflow.Ellipsis,
-                                        modifier = Modifier.padding(horizontal = 56.dp)
-                                    )
-                                }
+                                    if (!title.isNullOrEmpty()) {
+                                        Text(
+                                            text = title,
+                                            style = LegadoTheme.typography.titleMediumEmphasized,
+                                            color = sheetContentColor,
+                                            textAlign = TextAlign.Center,
+                                            maxLines = 1,
+                                            overflow = TextOverflow.Ellipsis,
+                                            modifier = Modifier.padding(horizontal = 56.dp)
+                                        )
+                                    }
 
-                                if (endAction != null) {
-                                    Box(modifier = Modifier.align(Alignment.CenterEnd)) {
-                                        endAction()
+                                    if (endAction != null) {
+                                        Box(modifier = Modifier.align(Alignment.CenterEnd)) {
+                                            endAction()
+                                        }
                                     }
                                 }
                             }
-                        }
 
-                        content()
+                            content()
+                        }
                     }
                 }
             }

+ 21 - 0
app/src/test/java/io/legado/app/ui/theme/AppDensityTest.kt

@@ -0,0 +1,21 @@
+package io.legado.app.ui.theme
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class AppDensityTest {
+
+    @Test
+    fun resolve_usesSettingWithinSupportedRange() {
+        assertEquals(0.8f, resolveAppFontScale(8, systemFontScale = 1f), 0f)
+        assertEquals(1f, resolveAppFontScale(10, systemFontScale = 1.3f), 0f)
+        assertEquals(1.6f, resolveAppFontScale(16, systemFontScale = 1f), 0f)
+    }
+
+    @Test
+    fun resolve_fallsBackToSystemScaleOutsideSupportedRange() {
+        assertEquals(1.3f, resolveAppFontScale(7, systemFontScale = 1.3f), 0f)
+        assertEquals(1.3f, resolveAppFontScale(17, systemFontScale = 1.3f), 0f)
+        assertEquals(1.3f, resolveAppFontScale(0, systemFontScale = 1.3f), 0f)
+    }
+}