Explorar el Código

为MIUIX主题添加了专属的关于界面

HapeLee hace 5 meses
padre
commit
f0bbd4697f
Se han modificado 35 ficheros con 2437 adiciones y 660 borrados
  1. 2 1
      .claude/settings.local.json
  2. 2 2
      app/build.gradle.kts
  3. 2 5
      app/src/main/AndroidManifest.xml
  4. 2 0
      app/src/main/java/io/legado/app/di/appModule.kt
  5. 0 197
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  6. 48 0
      app/src/main/java/io/legado/app/ui/about/AboutContract.kt
  7. 124 36
      app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
  8. 193 0
      app/src/main/java/io/legado/app/ui/about/AboutSheets.kt
  9. 243 0
      app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt
  10. 515 0
      app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt
  11. 1 1
      app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
  12. 1 1
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt
  13. 35 0
      app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
  14. 4 0
      app/src/main/java/io/legado/app/ui/main/MainNavKey.kt
  15. 12 0
      app/src/main/java/io/legado/app/ui/main/MainNavigator.kt
  16. 3 1
      app/src/main/java/io/legado/app/ui/main/MainScreen.kt
  17. 3 0
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  18. 1 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  19. 4 5
      app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
  20. 1 0
      app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt
  21. 174 0
      app/src/main/java/io/legado/app/ui/util/MiuixUtils.kt
  22. 87 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectBackground.kt
  23. 181 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectConfig.kt
  24. 177 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectModifier.kt
  25. 175 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectPainter.kt
  26. 209 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/ColorBlendToken.kt
  27. 6 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/DeviceType.kt
  28. 88 0
      app/src/main/java/io/legado/app/ui/widget/components/effect/OS3BgFrag.kt
  29. 4 5
      app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt
  30. 29 57
      app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt
  31. 64 0
      app/src/main/java/io/legado/app/ui/widget/components/log/CrashLogSheet.kt
  32. 44 0
      app/src/main/java/io/legado/app/ui/widget/components/log/LogDetailSheet.kt
  33. 0 330
      app/src/main/res/layout/activity_about.xml
  34. 0 13
      app/src/main/res/menu/about.xml
  35. 3 5
      gradle/libs.versions.toml

+ 2 - 1
.claude/settings.local.json

@@ -89,7 +89,8 @@
       "WebFetch(domain:s01.oss.sonatype.org)",
       "WebFetch(domain:api.github.com)",
       "Bash(javap -public /tmp/classes.jar!top/yukonga/miuix/kmp/basic/NavigationBarKt.class)",
-      "Bash(javap -public -cp /tmp/classes.jar top.yukonga.miuix.kmp.basic.NavigationBarKt)"
+      "Bash(javap -public -cp /tmp/classes.jar top.yukonga.miuix.kmp.basic.NavigationBarKt)",
+      "PowerShell(Join-Path *)"
     ]
   }
 }

+ 2 - 2
app/build.gradle.kts

@@ -229,6 +229,7 @@ dependencies {
     implementation(libs.markwon.image.glide)
     implementation(libs.markwon.ext.tables)
     implementation(libs.markwon.html)
+    implementation(libs.markdown.renderer.m3)
     implementation(libs.quick.chinese.transfer.core)
     implementation(libs.hutool.crypto)
     //noinspection GradleDependency
@@ -276,9 +277,8 @@ dependencies {
     implementation(libs.miuix.ui.android)
     implementation(libs.miuix.preference.android)
     implementation(libs.miuix.icons.android)
-    //implementation(libs.miuix.blur.android)
+    implementation(libs.miuix.blur.android)
     implementation(libs.miuix.core)
-    implementation(libs.miuix.shapes.android)
     implementation(libs.capsule)
     implementation(libs.backdrop)
     implementation(libs.lyricViewx)

+ 2 - 5
app/src/main/AndroidManifest.xml

@@ -2,6 +2,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
+    <uses-sdk tools:overrideLibrary="top.yukonga.miuix.kmp.blur" />
+
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
@@ -212,11 +214,6 @@
             android:name=".ui.book.search.SearchActivity"
             android:enableOnBackInvokedCallback="true"
             android:launchMode="standard" />
-        <!-- 关于界面 -->
-        <activity
-            android:name=".ui.about.AboutActivity"
-            android:enableOnBackInvokedCallback="true"
-            android:launchMode="singleTask" />
         <activity
             android:name=".ui.about.CrashReportActivity"
             android:enableOnBackInvokedCallback="true"

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

@@ -58,6 +58,7 @@ import io.legado.app.help.coil.CoverFetcher
 import io.legado.app.help.coil.CoverInterceptor
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.http.okHttpClientManga
+import io.legado.app.ui.about.AboutViewModel
 import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
 import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
 import io.legado.app.ui.book.changecover.ChangeCoverViewModel
@@ -189,6 +190,7 @@ val appModule = module {
     viewModelOf(::MyViewModel)
     viewModelOf(::BookshelfViewModel)
     viewModelOf(::MainViewModel)
+    viewModelOf(::AboutViewModel)
     viewModelOf(::GroupViewModel)
     viewModelOf(::ReplaceRuleViewModel)
     viewModelOf(::AllBookmarkViewModel)

+ 0 - 197
app/src/main/java/io/legado/app/ui/about/AboutActivity.kt

@@ -1,197 +0,0 @@
-package io.legado.app.ui.about
-
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import androidx.activity.compose.setContent
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.core.net.toUri
-import androidx.lifecycle.lifecycleScope
-import io.legado.app.R
-import io.legado.app.base.BaseComposeActivity
-import io.legado.app.constant.AppLog
-import io.legado.app.help.CrashHandler
-import io.legado.app.help.config.AppConfig
-import io.legado.app.help.coroutine.Coroutine
-import io.legado.app.help.update.AppUpdate
-import io.legado.app.ui.widget.dialog.TextDialog
-import io.legado.app.ui.widget.dialog.WaitDialog
-import io.legado.app.utils.FileDoc
-import io.legado.app.utils.compress.ZipUtils
-import io.legado.app.utils.createFileIfNotExist
-import io.legado.app.utils.createFolderIfNotExist
-import io.legado.app.utils.delete
-import io.legado.app.utils.externalCache
-import io.legado.app.utils.find
-import io.legado.app.utils.list
-import io.legado.app.utils.openInputStream
-import io.legado.app.utils.openOutputStream
-import io.legado.app.utils.openUrl
-import io.legado.app.utils.share
-import io.legado.app.utils.showDialogFragment
-import io.legado.app.utils.toastOnUi
-import kotlinx.coroutines.delay
-import splitties.init.appCtx
-import java.io.File
-
-
-class AboutActivity : BaseComposeActivity() {
-
-    @Composable
-    override fun Content() {
-        AboutScreen(
-            onBack = { finish() },
-            onCheckUpdate = { checkUpdate() },
-            onOpenUrl = { openUrl(it) },
-            onShowMdFile = { title, file -> showMdFile(title, file) },
-            onSaveLog = { saveLog() },
-            onCreateHeapDump = { createHeapDump() },
-            onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() },
-        )
-    }
-
-    private val waitDialog by lazy {
-        WaitDialog(this).setText(R.string.checking_update)
-    }
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
-        menuInflater.inflate(R.menu.about, menu)
-        return true
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        return when (item.itemId) {
-            R.id.menu_share_it -> {
-                share(
-                    getString(R.string.app_share_description),
-                    getString(R.string.app_name)
-                )
-                true
-            }
-            else -> super.onOptionsItemSelected(item)
-        }
-    }
-
-    private fun showMdFile(title: String, fileName: String) {
-        val mdText = String(this.assets.open(fileName).readBytes())
-        showDialogFragment(TextDialog(title, mdText, TextDialog.Mode.MD))
-    }
-
-    private fun checkUpdate() {
-        waitDialog.show()
-        AppUpdate.gitHubUpdate?.run {
-            check(lifecycleScope)
-                .onSuccess {
-                    showDialogFragment(UpdateDialog(it, UpdateDialog.Mode.UPDATE))
-                }.onError {
-                    appCtx.toastOnUi("${getString(R.string.check_update)}\n${it.localizedMessage}")
-                }.onFinally {
-                    waitDialog.dismiss()
-                }
-        }
-    }
-
-    @Suppress("SameParameterValue")
-    private fun openUrl(@StringRes addressID: Int) {
-        this.openUrl(getString(addressID))
-    }
-
-    private fun saveLog() {
-        Coroutine.async {
-            val backupPath = AppConfig.backupPath ?: let {
-                toastOnUi("未设置备份目录")
-                return@async
-            }
-            if (!AppConfig.recordLog) {
-                toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
-                delay(3000)
-            }
-            val doc = FileDoc.fromUri(backupPath.toUri(), true)
-            copyLogs(doc)
-            copyHeapDump(doc)
-            toastOnUi("已保存至备份目录")
-        }.onError {
-            AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
-        }
-    }
-
-    private fun createHeapDump() {
-        Coroutine.async {
-            val backupPath = AppConfig.backupPath ?: let {
-                toastOnUi("未设置备份目录")
-                return@async
-            }
-            if (!AppConfig.recordHeapDump) {
-                toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
-                delay(3000)
-            }
-            toastOnUi("开始创建堆转储")
-            System.gc()
-            CrashHandler.doHeapDump(true)
-            val doc = FileDoc.fromUri(backupPath.toUri(), true)
-            if (!copyHeapDump(doc)) {
-                toastOnUi("未找到堆转储文件")
-            } else {
-                toastOnUi("已保存至备份目录")
-            }
-        }.onError {
-            AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
-        }
-    }
-
-    private fun copyLogs(doc: FileDoc) {
-        val cacheDir = externalCache
-        val logFiles = File(cacheDir, "logs")
-        val crashFiles = File(cacheDir, "crash")
-        val logcatFile = File(cacheDir, "logcat.txt")
-
-        dumpLogcat(logcatFile)
-
-        val zipFile = File(cacheDir, "logs.zip")
-        ZipUtils.zipFiles(arrayListOf(logFiles, crashFiles, logcatFile), zipFile)
-
-        doc.find("logs.zip")?.delete()
-
-        zipFile.inputStream().use { input ->
-            doc.createFileIfNotExist("logs.zip").openOutputStream().getOrNull()
-                ?.use {
-                    input.copyTo(it)
-                }
-        }
-        zipFile.delete()
-    }
-
-    private fun copyHeapDump(doc: FileDoc): Boolean {
-        val heapFile = FileDoc.fromFile(File(externalCache, "heapDump")).list()
-            ?.firstOrNull() ?: return false
-        doc.find("heapDump")?.delete()
-        val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
-        heapFile.openInputStream().getOrNull()?.use { input ->
-            heapDumpDoc.createFileIfNotExist(heapFile.name).openOutputStream().getOrNull()
-                ?.use {
-                    input.copyTo(it)
-                }
-        }
-        return true
-    }
-
-    private fun dumpLogcat(file: File) {
-        try {
-            val process = Runtime.getRuntime().exec("logcat -d")
-            file.outputStream().use {
-                process.inputStream.copyTo(it)
-            }
-        } catch (e: Exception) {
-            AppLog.put("保存Logcat失败\n$e", e)
-        }
-    }
-
-}

+ 48 - 0
app/src/main/java/io/legado/app/ui/about/AboutContract.kt

@@ -0,0 +1,48 @@
+package io.legado.app.ui.about
+
+import androidx.compose.runtime.Stable
+import io.legado.app.help.update.AppUpdate
+import io.legado.app.utils.FileDoc
+
+@Stable
+data class AboutUiState(
+    val sheet: AboutSheet = AboutSheet.None,
+    val dialog: AboutDialog? = null,
+    val crashLogFiles: List<FileDoc> = emptyList(),
+)
+
+sealed interface AboutSheet {
+    data object None : AboutSheet
+    data class Markdown(val title: String, val content: String) : AboutSheet
+    data object CrashLogs : AboutSheet
+    data class Update(
+        val updateInfo: AppUpdate.UpdateInfo,
+        val mode: UpdateMode = UpdateMode.UPDATE,
+    ) : AboutSheet
+}
+
+enum class UpdateMode { UPDATE, VIEW_LOG }
+
+sealed interface AboutDialog {
+    data object CheckingUpdate : AboutDialog
+}
+
+sealed interface AboutIntent {
+    data object DismissSheet : AboutIntent
+    data object DismissDialog : AboutIntent
+    data object CheckUpdate : AboutIntent
+    data class ShowMdFile(val title: String, val fileName: String) : AboutIntent
+    data object ShowCrashLogs : AboutIntent
+    data object SaveLog : AboutIntent
+    data object CreateHeapDump : AboutIntent
+    data class OpenUrl(val url: String) : AboutIntent
+    data object ClearCrashLogs : AboutIntent
+    data class ReadCrashFile(val fileDoc: FileDoc) : AboutIntent
+    data object StartDownload : AboutIntent
+}
+
+sealed interface AboutEffect {
+    data class OpenUrl(val url: String) : AboutEffect
+    data class ShowToast(val message: String) : AboutEffect
+    data class StartDownload(val url: String, val fileName: String) : AboutEffect
+}

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

@@ -2,6 +2,7 @@ package io.legado.app.ui.about
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -16,11 +17,15 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.FilledTonalIconButton
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
@@ -29,30 +34,56 @@ import androidx.compose.ui.unit.sp
 import io.legado.app.R
 import io.legado.app.constant.AppConst.appInfo
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.AppScaffold
+import io.legado.app.ui.widget.components.SettingItemWithDivider
 import io.legado.app.ui.widget.components.SplicedColumnGroup
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.log.CrashLogSheet
+import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.settingItem.SettingItem
-import io.legado.app.ui.widget.components.SettingItemWithDivider
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import kotlinx.coroutines.delay
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 fun AboutScreen(
-    versionName: String = appInfo.versionName,
+    state: AboutUiState,
+    onIntent: (AboutIntent) -> Unit,
     onBack: () -> Unit = {},
-    onCheckUpdate: () -> Unit = {},
-    onOpenUrl: (String) -> Unit = {},
-    onShowMdFile: (String, String) -> Unit = { _, _ -> },
-    onSaveLog: () -> Unit = {},
-    onCreateHeapDump: () -> Unit = {},
-    onShowCrashLogs: () -> Unit = {}
+    versionName: String = appInfo.versionName,
 ) {
-    LocalContext.current
+    if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
+        MiuixAboutScreen(
+            state = state,
+            onIntent = onIntent,
+            onBack = onBack,
+            versionName = versionName,
+        )
+    } else {
+        MaterialAboutScreen(
+            state = state,
+            onIntent = onIntent,
+            onBack = onBack,
+            versionName = versionName,
+        )
+    }
 
+    AboutOverlays(state = state, onIntent = onIntent)
+}
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun MaterialAboutScreen(
+    state: AboutUiState,
+    onIntent: (AboutIntent) -> Unit,
+    onBack: () -> Unit,
+    versionName: String,
+) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
 
     AppScaffold(
@@ -88,14 +119,16 @@ fun AboutScreen(
                 fontWeight = FontWeight.Bold,
                 modifier = Modifier
                     .fillMaxWidth()
-                    .wrapContentWidth(Alignment.CenterHorizontally))
+                    .wrapContentWidth(Alignment.CenterHorizontally)
+            )
             TextCard(
                 text = versionName,
                 cornerRadius = 8.dp,
                 modifier = Modifier
                     .fillMaxWidth()
                     .wrapContentWidth(Alignment.CenterHorizontally)
-                    .padding(vertical = 4.dp))
+                    .padding(vertical = 4.dp)
+            )
             AppText(
                 text = stringResource(R.string.about_description),
                 style = LegadoTheme.typography.bodyLarge,
@@ -103,25 +136,25 @@ fun AboutScreen(
                 modifier = Modifier
                     .fillMaxWidth()
                     .wrapContentWidth(Alignment.CenterHorizontally)
-                    .padding(bottom = 4.dp))
-            Row (modifier = Modifier
-                .fillMaxWidth(),
-                horizontalArrangement = Arrangement.Center){
-                FilledTonalIconButton ( onClick = { onOpenUrl("https://example.com") } ) {
+                    .padding(bottom = 4.dp)
+            )
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.Center
+            ) {
+                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)
                     )
                 }
-
-                FilledTonalIconButton (onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") }) {
+                FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
                     Icon(
                         painter = painterResource(R.drawable.ic_github),
                         contentDescription = stringResource(R.string.back)
                     )
                 }
-
-                FilledTonalIconButton(onClick = onCheckUpdate) {
+                FilledTonalIconButton(onClick = { onIntent(AboutIntent.CheckUpdate) }) {
                     Icon(
                         painter = painterResource(R.drawable.ic_import),
                         contentDescription = stringResource(R.string.back)
@@ -132,58 +165,113 @@ fun AboutScreen(
             SplicedColumnGroup(
                 modifier = Modifier.padding(horizontal = 16.dp),
                 title = ""
-            ){
+            ) {
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.contributors),
-                        onClick = {
-                            onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
-                        }
+                        onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }
                     )
                 }
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.privacy_policy),
                         onClick = {
-                            onShowMdFile("隐私政策", "privacyPolicy.md")
+                            onIntent(
+                                AboutIntent.ShowMdFile(
+                                    "隐私政策",
+                                    "privacyPolicy.md"
+                                )
+                            )
                         }
                     )
                 }
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.license),
-                        onClick = {
-                            onShowMdFile("许可证", "LICENSE.md")
-                        }
+                        onClick = { onIntent(AboutIntent.ShowMdFile("许可证", "LICENSE.md")) }
                     )
                 }
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.disclaimer),
-                        onClick = {
-                            onShowMdFile("免责声明", "disclaimer.md")
-                        }
+                        onClick = { onIntent(AboutIntent.ShowMdFile("免责声明", "disclaimer.md")) }
                     )
                 }
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.crash_log),
-                        onClick = onShowCrashLogs
+                        onClick = { onIntent(AboutIntent.ShowCrashLogs) }
                     )
                 }
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.save_log),
-                        onClick = onSaveLog
+                        onClick = { onIntent(AboutIntent.SaveLog) }
                     )
                 }
                 SettingItemWithDivider {
                     SettingItem(
                         title = stringResource(R.string.create_heap_dump),
-                        onClick = onCreateHeapDump
+                        onClick = { onIntent(AboutIntent.CreateHeapDump) }
                     )
                 }
             }
         }
     }
 }
+
+@Composable
+private fun AboutOverlays(
+    state: AboutUiState,
+    onIntent: (AboutIntent) -> Unit,
+) {
+    val currentSheet = state.sheet
+    var renderedSheet by remember { mutableStateOf<AboutSheet>(AboutSheet.None) }
+    LaunchedEffect(currentSheet) {
+        if (currentSheet is AboutSheet.None) {
+            delay(300)
+            renderedSheet = AboutSheet.None
+        } else {
+            renderedSheet = currentSheet
+        }
+    }
+
+    when (val sheet = renderedSheet) {
+        is AboutSheet.None -> Unit
+        is AboutSheet.Markdown -> MarkdownSheet(
+            show = currentSheet is AboutSheet.Markdown,
+            title = sheet.title,
+            content = sheet.content,
+            onDismissRequest = { onIntent(AboutIntent.DismissSheet) },
+        )
+
+        is AboutSheet.CrashLogs -> CrashLogSheet(
+            show = currentSheet is AboutSheet.CrashLogs,
+            logFiles = state.crashLogFiles,
+            onDismissRequest = { onIntent(AboutIntent.DismissSheet) },
+            onReadFile = { onIntent(AboutIntent.ReadCrashFile(it)) },
+            onClear = { onIntent(AboutIntent.ClearCrashLogs) },
+        )
+
+        is AboutSheet.Update -> UpdateSheet(
+            show = currentSheet is AboutSheet.Update,
+            updateInfo = sheet.updateInfo,
+            mode = sheet.mode,
+            onDismissRequest = { onIntent(AboutIntent.DismissSheet) },
+            onStartDownload = { onIntent(AboutIntent.StartDownload) },
+        )
+    }
+
+    AppAlertDialog(
+        show = state.dialog is AboutDialog.CheckingUpdate,
+        onDismissRequest = {},
+        content = {
+            Box(
+                modifier = Modifier.fillMaxWidth(),
+                contentAlignment = Alignment.Center
+            ) {
+                AppCircularProgressIndicator()
+            }
+        }
+    )
+}

+ 193 - 0
app/src/main/java/io/legado/app/ui/about/AboutSheets.kt

@@ -0,0 +1,193 @@
+package io.legado.app.ui.about
+
+import android.os.Build
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+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.unit.dp
+import com.mikepenz.markdown.m3.Markdown
+import com.mikepenz.markdown.m3.markdownColor
+import com.mikepenz.markdown.m3.markdownTypography
+import io.legado.app.BuildConfig
+import io.legado.app.R
+import io.legado.app.constant.AppConst.appInfo
+import io.legado.app.help.config.AppConfig
+import io.legado.app.help.update.AppUpdate
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.PrimaryButton
+import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.text.AppText
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MarkdownSheet(
+    show: Boolean,
+    title: String,
+    content: String,
+    onDismissRequest: () -> Unit,
+) {
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = title,
+    ) {
+        SelectionContainer {
+            Column(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .verticalScroll(rememberScrollState())
+            ) {
+                Markdown(
+                    content = content,
+                    colors = markdownColor(
+                        text = LegadoTheme.colorScheme.onBackground,
+                        codeBackground = LegadoTheme.colorScheme.onBackground.copy(alpha = 0.1f),
+                        inlineCodeBackground = LegadoTheme.colorScheme.onBackground.copy(alpha = 0.1f),
+                        dividerColor = LegadoTheme.colorScheme.outlineVariant,
+                        tableBackground = LegadoTheme.colorScheme.onBackground.copy(alpha = 0.02f),
+                    ),
+                    typography = markdownTypography(),
+                    modifier = Modifier.fillMaxWidth(),
+                )
+                Spacer(modifier = Modifier.heightIn(min = 16.dp))
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun UpdateSheet(
+    show: Boolean,
+    updateInfo: AppUpdate.UpdateInfo,
+    mode: UpdateMode,
+    onDismissRequest: () -> Unit,
+    onStartDownload: () -> Unit,
+) {
+    val title = when (mode) {
+        UpdateMode.UPDATE -> stringResource(R.string.check_update)
+        UpdateMode.VIEW_LOG -> "已经更新至"
+    }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = title,
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .verticalScroll(rememberScrollState())
+        ) {
+            if (mode == UpdateMode.UPDATE) {
+                Row(
+                    modifier = Modifier.padding(bottom = 4.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    AppText(
+                        text = "当前版本",
+                        style = LegadoTheme.typography.bodyMedium,
+                    )
+                    Spacer(modifier = Modifier.width(8.dp))
+                    AppText(
+                        text = BuildConfig.VERSION_NAME,
+                        style = LegadoTheme.typography.bodyMedium,
+                        color = MaterialTheme.colorScheme.outline,
+                    )
+                }
+                Row(
+                    modifier = Modifier.padding(bottom = 4.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    AppText(
+                        text = "新版本",
+                        style = LegadoTheme.typography.bodyMedium,
+                    )
+                    Spacer(modifier = Modifier.width(8.dp))
+                    AppText(
+                        text = updateInfo.tagName,
+                        style = LegadoTheme.typography.bodyMedium,
+                        color = MaterialTheme.colorScheme.primary,
+                    )
+                }
+                Row(
+                    modifier = Modifier.padding(bottom = 4.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    AppText(
+                        text = "ABI",
+                        style = LegadoTheme.typography.bodyMedium,
+                    )
+                    Spacer(modifier = Modifier.width(8.dp))
+                    AppText(
+                        text = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown",
+                        style = LegadoTheme.typography.bodyMedium,
+                        color = MaterialTheme.colorScheme.outline,
+                    )
+                }
+                Row(
+                    modifier = Modifier.padding(bottom = 8.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    AppText(
+                        text = "渠道",
+                        style = LegadoTheme.typography.bodyMedium,
+                    )
+                    Spacer(modifier = Modifier.width(8.dp))
+                    AppText(
+                        text = AppConfig.updateToVariant ?: appInfo.appVariant.toString(),
+                        style = LegadoTheme.typography.bodyMedium,
+                        color = MaterialTheme.colorScheme.outline,
+                    )
+                }
+            } else {
+                AppText(
+                    text = BuildConfig.VERSION_NAME,
+                    style = LegadoTheme.typography.headlineMedium,
+                    modifier = Modifier.padding(bottom = 16.dp),
+                )
+            }
+
+            val updateLog = updateInfo.updateLog
+            if (updateLog.isNotBlank()) {
+                Markdown(
+                    content = updateLog,
+                    colors = markdownColor(
+                        text = LegadoTheme.colorScheme.onBackground,
+                        codeBackground = LegadoTheme.colorScheme.onBackground.copy(alpha = 0.1f),
+                        inlineCodeBackground = LegadoTheme.colorScheme.onBackground.copy(alpha = 0.1f),
+                        dividerColor = LegadoTheme.colorScheme.outlineVariant,
+                        tableBackground = LegadoTheme.colorScheme.onBackground.copy(alpha = 0.02f),
+                    ),
+                    typography = markdownTypography(),
+                    modifier = Modifier.fillMaxWidth(),
+                )
+            }
+
+            if (mode == UpdateMode.UPDATE) {
+                Spacer(modifier = Modifier.height(16.dp))
+                PrimaryButton(
+                    onClick = onStartDownload,
+                    text = "更新",
+                    modifier = Modifier.fillMaxWidth()
+                )
+            }
+
+            Spacer(modifier = Modifier.height(16.dp))
+        }
+    }
+}

+ 243 - 0
app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt

@@ -0,0 +1,243 @@
+package io.legado.app.ui.about
+
+import android.app.Application
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.lifecycle.viewModelScope
+import io.legado.app.R
+import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.AppLog
+import io.legado.app.help.CrashHandler
+import io.legado.app.help.config.AppConfig
+import io.legado.app.help.update.AppUpdate
+import io.legado.app.utils.FileDoc
+import io.legado.app.utils.FileUtils
+import io.legado.app.utils.compress.ZipUtils
+import io.legado.app.utils.createFileIfNotExist
+import io.legado.app.utils.createFolderIfNotExist
+import io.legado.app.utils.delete
+import io.legado.app.utils.externalCache
+import io.legado.app.utils.find
+import io.legado.app.utils.getFile
+import io.legado.app.utils.list
+import io.legado.app.utils.openInputStream
+import io.legado.app.utils.openOutputStream
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import java.io.File
+import java.io.FileFilter
+
+class AboutViewModel(application: Application) : BaseViewModel(application) {
+
+    private val _uiState = MutableStateFlow(AboutUiState())
+    val uiState: StateFlow<AboutUiState> = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<AboutEffect>(extraBufferCapacity = 8)
+    val effects = _effects.asSharedFlow()
+
+    fun onIntent(intent: AboutIntent) {
+        when (intent) {
+            is AboutIntent.DismissSheet -> _uiState.update { it.copy(sheet = AboutSheet.None) }
+            is AboutIntent.DismissDialog -> _uiState.update { it.copy(dialog = null) }
+            is AboutIntent.CheckUpdate -> checkUpdate()
+            is AboutIntent.ShowMdFile -> showMdFile(intent.title, intent.fileName)
+            is AboutIntent.ShowCrashLogs -> showCrashLogs()
+            is AboutIntent.SaveLog -> saveLog()
+            is AboutIntent.CreateHeapDump -> createHeapDump()
+            is AboutIntent.OpenUrl -> _effects.tryEmit(AboutEffect.OpenUrl(intent.url))
+            is AboutIntent.ClearCrashLogs -> clearCrashLogs()
+            is AboutIntent.ReadCrashFile -> readCrashFile(intent.fileDoc)
+            is AboutIntent.StartDownload -> startDownload()
+        }
+    }
+
+    private fun checkUpdate() {
+        _uiState.update { it.copy(dialog = AboutDialog.CheckingUpdate) }
+        AppUpdate.gitHubUpdate?.run {
+            check(viewModelScope)
+                .onSuccess { updateInfo ->
+                    _uiState.update {
+                        it.copy(
+                            dialog = null,
+                            sheet = AboutSheet.Update(updateInfo)
+                        )
+                    }
+                }.onError { e ->
+                    _uiState.update { it.copy(dialog = null) }
+                    _effects.tryEmit(
+                        AboutEffect.ShowToast("${context.getString(R.string.check_update)}\n${e.localizedMessage}")
+                    )
+                }.onFinally {
+                    _uiState.update { it.copy(dialog = null) }
+                }
+        }
+    }
+
+    private fun showMdFile(title: String, fileName: String) {
+        execute {
+            String(context.assets.open(fileName).readBytes())
+        }.onSuccess { content ->
+            _uiState.update { it.copy(sheet = AboutSheet.Markdown(title, content)) }
+        }
+    }
+
+    private fun showCrashLogs() {
+        execute {
+            loadCrashLogFiles()
+        }.onSuccess { files ->
+            _uiState.update {
+                it.copy(
+                    crashLogFiles = files,
+                    sheet = AboutSheet.CrashLogs
+                )
+            }
+        }
+    }
+
+    private fun readCrashFile(fileDoc: FileDoc) {
+        execute {
+            String(fileDoc.readBytes())
+        }.onSuccess { content ->
+            _uiState.update { it.copy(sheet = AboutSheet.Markdown(fileDoc.name, content)) }
+        }.onError {
+            _effects.tryEmit(AboutEffect.ShowToast(it.localizedMessage ?: ""))
+        }
+    }
+
+    private fun clearCrashLogs() {
+        execute {
+            context.externalCacheDir
+                ?.getFile("crash")
+                ?.let { FileUtils.delete(it, false) }
+            val backupPath = AppConfig.backupPath
+            if (!backupPath.isNullOrEmpty()) {
+                val uri = Uri.parse(backupPath)
+                FileDoc.fromUri(uri, true)
+                    .find("crash")
+                    ?.delete()
+            }
+        }.onError {
+            _effects.tryEmit(AboutEffect.ShowToast(it.localizedMessage ?: ""))
+        }.onFinally {
+            execute {
+                loadCrashLogFiles()
+            }.onSuccess { files ->
+                _uiState.update { it.copy(crashLogFiles = files) }
+            }
+        }
+    }
+
+    private fun loadCrashLogFiles(): List<FileDoc> {
+        val list = arrayListOf<FileDoc>()
+        context.externalCacheDir
+            ?.getFile("crash")
+            ?.listFiles(FileFilter { it.isFile })
+            ?.forEach { list.add(FileDoc.fromFile(it)) }
+        val backupPath = AppConfig.backupPath
+        if (!backupPath.isNullOrEmpty()) {
+            val uri = Uri.parse(backupPath)
+            FileDoc.fromUri(uri, true)
+                .find("crash")
+                ?.list { !it.isDir }
+                ?.let { list.addAll(it) }
+        }
+        return list.sortedByDescending { it.name }.distinctBy { it.name }
+    }
+
+    private fun saveLog() {
+        execute {
+            val backupPath = AppConfig.backupPath ?: run {
+                _effects.tryEmit(AboutEffect.ShowToast("未设置备份目录"))
+                return@execute
+            }
+            if (!AppConfig.recordLog) {
+                _effects.tryEmit(AboutEffect.ShowToast("未开启日志记录,请去其他设置里打开记录日志"))
+                delay(3000)
+            }
+            val doc = FileDoc.fromUri(backupPath.toUri(), true)
+            copyLogs(doc)
+            copyHeapDump(doc)
+            _effects.tryEmit(AboutEffect.ShowToast("已保存至备份目录"))
+        }.onError {
+            AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
+        }
+    }
+
+    private fun createHeapDump() {
+        execute {
+            val backupPath = AppConfig.backupPath ?: run {
+                _effects.tryEmit(AboutEffect.ShowToast("未设置备份目录"))
+                return@execute
+            }
+            if (!AppConfig.recordHeapDump) {
+                _effects.tryEmit(AboutEffect.ShowToast("未开启堆转储记录,请去其他设置里打开记录堆转储"))
+                delay(3000)
+            }
+            _effects.tryEmit(AboutEffect.ShowToast("开始创建堆转储"))
+            System.gc()
+            CrashHandler.doHeapDump(true)
+            val doc = FileDoc.fromUri(backupPath.toUri(), true)
+            if (!copyHeapDump(doc)) {
+                _effects.tryEmit(AboutEffect.ShowToast("未找到堆转储文件"))
+            } else {
+                _effects.tryEmit(AboutEffect.ShowToast("已保存至备份目录"))
+            }
+        }.onError {
+            AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
+        }
+    }
+
+    private fun startDownload() {
+        val sheet = _uiState.value.sheet
+        if (sheet is AboutSheet.Update) {
+            val info = sheet.updateInfo
+            if (info.downloadUrl.isBlank() || info.fileName.isBlank()) {
+                _effects.tryEmit(AboutEffect.ShowToast("下载信息不完整"))
+            } else {
+                _effects.tryEmit(AboutEffect.StartDownload(info.downloadUrl, info.fileName))
+            }
+        }
+    }
+
+    private fun copyLogs(doc: FileDoc) {
+        val cacheDir = context.externalCache
+        val logFiles = File(cacheDir, "logs")
+        val crashFiles = File(cacheDir, "crash")
+        val logcatFile = File(cacheDir, "logcat.txt")
+        dumpLogcat(logcatFile)
+        val zipFile = File(cacheDir, "logs.zip")
+        ZipUtils.zipFiles(arrayListOf(logFiles, crashFiles, logcatFile), zipFile)
+        doc.find("logs.zip")?.delete()
+        zipFile.inputStream().use { input ->
+            doc.createFileIfNotExist("logs.zip").openOutputStream().getOrNull()
+                ?.use { input.copyTo(it) }
+        }
+        zipFile.delete()
+    }
+
+    private fun copyHeapDump(doc: FileDoc): Boolean {
+        val heapFile = FileDoc.fromFile(File(context.externalCache, "heapDump")).list()
+            ?.firstOrNull() ?: return false
+        doc.find("heapDump")?.delete()
+        val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
+        heapFile.openInputStream().getOrNull()?.use { input ->
+            heapDumpDoc.createFileIfNotExist(heapFile.name).openOutputStream().getOrNull()
+                ?.use { input.copyTo(it) }
+        }
+        return true
+    }
+
+    private fun dumpLogcat(file: File) {
+        try {
+            val process = Runtime.getRuntime().exec("logcat -d")
+            file.outputStream().use { process.inputStream.copyTo(it) }
+        } catch (e: Exception) {
+            AppLog.put("保存Logcat失败\n$e", e)
+        }
+    }
+}

+ 515 - 0
app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt

@@ -0,0 +1,515 @@
+@file:OptIn(ExperimentalScrollBarApi::class)
+
+package io.legado.app.ui.about
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.legado.app.R
+import io.legado.app.constant.AppConst.appInfo
+import io.legado.app.ui.util.BlurredBar
+import io.legado.app.ui.util.LocalAppState
+import io.legado.app.ui.util.LocalIsWideScreen
+import io.legado.app.ui.util.LocalNavigator
+import io.legado.app.ui.util.MiuixNavigator
+import io.legado.app.ui.util.pageContentPadding
+import io.legado.app.ui.util.pageScrollModifiers
+import io.legado.app.ui.util.rememberBlurBackdrop
+import io.legado.app.ui.util.shouldShowSplitPane
+import io.legado.app.ui.widget.components.effect.BgEffectBackground
+import io.legado.app.ui.widget.components.effect.ColorBlendToken
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
+import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
+import top.yukonga.miuix.kmp.basic.Scaffold
+import top.yukonga.miuix.kmp.basic.ScrollBehavior
+import top.yukonga.miuix.kmp.basic.SmallTopAppBar
+import top.yukonga.miuix.kmp.basic.Text
+import top.yukonga.miuix.kmp.basic.VerticalScrollBar
+import top.yukonga.miuix.kmp.basic.rememberScrollBarAdapter
+import top.yukonga.miuix.kmp.blur.BlendColorEntry
+import top.yukonga.miuix.kmp.blur.BlurBlendMode
+import top.yukonga.miuix.kmp.blur.BlurColors
+import top.yukonga.miuix.kmp.blur.BlurDefaults
+import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
+import top.yukonga.miuix.kmp.blur.layerBackdrop
+import top.yukonga.miuix.kmp.blur.textureBlur
+import top.yukonga.miuix.kmp.interfaces.ExperimentalScrollBarApi
+import top.yukonga.miuix.kmp.preference.ArrowPreference
+import top.yukonga.miuix.kmp.theme.MiuixTheme
+import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode
+
+@Composable
+fun MiuixAboutScreen(
+    state: AboutUiState,
+    onIntent: (AboutIntent) -> Unit,
+    onBack: () -> Unit = {},
+    versionName: String = appInfo.versionName,
+) {
+    val topAppBarScrollBehavior = MiuixScrollBehavior()
+    val lazyListState = rememberLazyListState()
+    val isWideScreen = shouldShowSplitPane()
+
+    val scrollProgress by remember {
+        derivedStateOf {
+            when {
+                lazyListState.firstVisibleItemIndex > 0 -> 1f
+
+                else -> {
+                    val spacer =
+                        lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == "logoSpacer" }
+                    if (spacer != null && spacer.size > 0) {
+                        (lazyListState.firstVisibleItemScrollOffset.toFloat() / spacer.size).coerceIn(
+                            0f,
+                            1f
+                        )
+                    } else {
+                        0f
+                    }
+                }
+            }
+        }
+    }
+
+    val backdrop = rememberBlurBackdrop()
+    val blurActive = backdrop != null && scrollProgress == 1f
+    val barColor = if (blurActive) {
+        Color.Transparent
+    } else {
+        if (scrollProgress == 1f) MiuixTheme.colorScheme.surface else Color.Transparent
+    }
+
+    val navigator = remember {
+        object : MiuixNavigator {
+            override fun pop() {
+                onBack()
+            }
+
+            override fun push(route: Any) {
+                if (route == "License") {
+                    onIntent(AboutIntent.ShowMdFile("许可证", "LICENSE.md"))
+                }
+            }
+        }
+    }
+
+    CompositionLocalProvider(
+        LocalNavigator provides navigator,
+        LocalIsWideScreen provides isWideScreen
+    ) {
+        Scaffold(
+            topBar = {
+                BlurredBar(backdrop, blurActive) {
+                    SmallTopAppBar(
+                        title = stringResource(R.string.about),
+                        scrollBehavior = topAppBarScrollBehavior,
+                        color = barColor,
+                        titleColor = MiuixTheme.colorScheme.onSurface.copy(
+                            alpha = ((scrollProgress - 0.35f) / 0.65f).coerceIn(0f, 1f),
+                        ),
+                        defaultWindowInsetsPadding = false,
+                        navigationIcon = {
+                            TopBarNavigationButton(onClick = onBack)
+                        },
+                    )
+                }
+            },
+        ) { innerPadding ->
+            Box(modifier = if (backdrop != null) Modifier.layerBackdrop(backdrop) else Modifier) {
+                AboutContent(
+                    padding = innerPadding,
+                    topAppBarScrollBehavior = topAppBarScrollBehavior,
+                    lazyListState = lazyListState,
+                    scrollProgress = scrollProgress,
+                    versionName = versionName,
+                    onIntent = onIntent,
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun AboutContent(
+    padding: PaddingValues,
+    topAppBarScrollBehavior: ScrollBehavior,
+    lazyListState: LazyListState,
+    scrollProgress: Float,
+    versionName: String,
+    onIntent: (AboutIntent) -> Unit,
+) {
+    val appState = LocalAppState.current
+    val isWideScreen = LocalIsWideScreen.current
+
+    val backdrop = rememberBlurBackdrop()
+    var blurRadius by remember { mutableFloatStateOf(60f) }
+    var noiseCoefficient by remember { mutableFloatStateOf(BlurDefaults.NoiseCoefficient) }
+    var brightness by remember { mutableFloatStateOf(0f) }
+    var contrast by remember { mutableFloatStateOf(1f) }
+    var saturation by remember { mutableFloatStateOf(1f) }
+
+    val scrollPadding = pageContentPadding(
+        padding,
+        PaddingValues(0.dp), // outerPadding placeholder
+        isWideScreen,
+        extraStart = WindowInsets.displayCutout.asPaddingValues()
+            .calculateLeftPadding(LayoutDirection.Ltr) + 16.dp,
+        extraEnd = WindowInsets.displayCutout.asPaddingValues()
+            .calculateRightPadding(LayoutDirection.Ltr) + 16.dp,
+    )
+    val logoPadding = pageContentPadding(
+        padding,
+        PaddingValues(0.dp),
+        isWideScreen,
+        extraTop = 40.dp,
+        extraStart = WindowInsets.displayCutout.asPaddingValues()
+            .calculateLeftPadding(LayoutDirection.Ltr) + 16.dp,
+        extraEnd = WindowInsets.displayCutout.asPaddingValues()
+            .calculateRightPadding(LayoutDirection.Ltr) + 16.dp,
+    )
+
+    val isInDark = isSystemInDarkTheme()
+    val dynamicBackground = isRuntimeShaderSupported()
+
+    val cardBlend =
+        if (isInDark) ColorBlendToken.Overlay_Thin_Light else ColorBlendToken.Pured_Regular_Light
+    val logoBlend = remember(isInDark) {
+        if (isInDark) {
+            listOf(
+                BlendColorEntry(Color(0xe6a1a1a1), BlurBlendMode.ColorDodge),
+                BlendColorEntry(Color(0x4de6e6e6), BlurBlendMode.LinearLight),
+                BlendColorEntry(Color(0xff1af500), BlurBlendMode.Lab),
+            )
+        } else {
+            listOf(
+                BlendColorEntry(Color(0xcc4a4a4a), BlurBlendMode.ColorBurn),
+                BlendColorEntry(Color(0xff4f4f4f), BlurBlendMode.LinearLight),
+                BlendColorEntry(Color(0xff1af200), BlurBlendMode.Lab),
+            )
+        }
+    }
+
+    val density = LocalDensity.current
+    var logoHeightDp by remember { mutableStateOf(300.dp) }
+
+    val versionCodeProgress = ((scrollProgress - 0.05f) / 0.15f).coerceIn(0f, 1f)
+    val projectNameProgress = ((scrollProgress - 0.20f) / 0.15f).coerceIn(0f, 1f)
+    val iconProgress = ((scrollProgress - 0.35f) / 0.15f).coerceIn(0f, 1f)
+
+    BgEffectBackground(
+        dynamicBackground = dynamicBackground,
+        isOs3Effect = true,
+        isFullSize = true,
+        modifier = Modifier.fillMaxSize(),
+        bgModifier = if (backdrop != null) Modifier.layerBackdrop(backdrop) else Modifier,
+        alpha = { 1f - scrollProgress },
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(
+                    top = logoPadding.calculateTopPadding() + 52.dp,
+                    start = logoPadding.calculateLeftPadding(LayoutDirection.Ltr),
+                    end = logoPadding.calculateRightPadding(LayoutDirection.Ltr),
+                )
+                .onSizeChanged { size ->
+                    with(density) { logoHeightDp = size.height.toDp() }
+                },
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Box(
+                contentAlignment = Alignment.Center,
+                modifier = Modifier
+                    .size(88.dp)
+                    .graphicsLayer {
+                        clip = true
+                        shape = RoundedCornerShape(24.dp)
+                        alpha = 1 - iconProgress
+                        scaleX = 1 - (iconProgress * 0.05f)
+                        scaleY = 1 - (iconProgress * 0.05f)
+                    }
+                    .background(Color.White),
+            ) {
+                Image(
+                    painter = painterResource(R.drawable.ic_launcher_foreground),
+                    contentDescription = null,
+                    modifier = Modifier.scale(1.1f)
+                )
+            }
+            Text(
+                modifier = Modifier
+                    .padding(top = 12.dp, bottom = 5.dp)
+                    .graphicsLayer {
+                        alpha = 1 - projectNameProgress
+                        scaleX = 1 - (projectNameProgress * 0.05f)
+                        scaleY = 1 - (projectNameProgress * 0.05f)
+                    }
+                    .then(
+                        if (backdrop != null) {
+                            Modifier
+                                .textureBlur(
+                                    backdrop = backdrop,
+                                    shape = RoundedCornerShape(16.dp),
+                                    blurRadius = 150f,
+                                    noiseCoefficient = noiseCoefficient,
+                                    colors = BlurColors(
+                                        blendColors = logoBlend,
+                                    ),
+                                    contentBlendMode = ComposeBlendMode.DstIn,
+                                )
+                        } else {
+                            Modifier
+                        },
+                    ),
+                text = stringResource(R.string.app_name),
+                color = MiuixTheme.colorScheme.onBackground,
+                fontWeight = FontWeight.Bold,
+                fontSize = 35.sp,
+            )
+            Text(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .graphicsLayer {
+                        alpha = 1 - versionCodeProgress
+                        scaleX = 1 - (versionCodeProgress * 0.05f)
+                        scaleY = 1 - (versionCodeProgress * 0.05f)
+                    },
+                color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
+                text = versionName,
+                fontSize = 14.sp,
+                textAlign = TextAlign.Center,
+            )
+        }
+
+        // Scrollable content
+        LazyColumn(
+            state = lazyListState,
+            modifier = Modifier
+                .fillMaxSize()
+                .pageScrollModifiers(
+                    appState.enableScrollEndHaptic,
+                    appState.showTopAppBar,
+                    topAppBarScrollBehavior,
+                ),
+            contentPadding = PaddingValues(
+                top = scrollPadding.calculateTopPadding(),
+                start = scrollPadding.calculateLeftPadding(LayoutDirection.Ltr),
+                end = scrollPadding.calculateRightPadding(LayoutDirection.Ltr),
+            ),
+        ) {
+            // Transparent spacer matching logo height
+            item(key = "logoSpacer") {
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .height(
+                            logoHeightDp + 52.dp + logoPadding.calculateTopPadding() - scrollPadding.calculateTopPadding() + 126.dp,
+                        ),
+                    contentAlignment = Alignment.TopCenter,
+                    content = { },
+                )
+            }
+
+            item(key = "about") {
+                Column(
+                    modifier = Modifier
+                        .fillParentMaxHeight()
+                        .padding(bottom = scrollPadding.calculateBottomPadding()),
+                ) {
+                    Card(
+                        modifier = Modifier
+                            .then(
+                                if (backdrop != null) {
+                                    Modifier
+                                        .textureBlur(
+                                            backdrop = backdrop,
+                                            shape = RoundedCornerShape(16.dp),
+                                            blurRadius = blurRadius,
+                                            noiseCoefficient = noiseCoefficient,
+                                            colors = BlurColors(
+                                                blendColors = cardBlend,
+                                                brightness = brightness,
+                                                contrast = contrast,
+                                                saturation = saturation,
+                                            ),
+                                        )
+                                } else {
+                                    Modifier
+                                },
+                            ),
+                        colors = CardDefaults.defaultColors(
+                            if (backdrop != null) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer,
+                            Color.Transparent,
+                        ),
+                    ) {
+                        ArrowPreference(
+                            title = stringResource(R.string.check_update),
+                            onClick = { onIntent(AboutIntent.CheckUpdate) },
+                        )
+                        ArrowPreference(
+                            title = stringResource(R.string.contributors),
+                            endActions = {
+                                ValueText("GitHub")
+                            },
+                            onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) },
+                        )
+                    }
+
+                    Card(
+                        modifier = Modifier
+                            .padding(top = 12.dp)
+                            .then(
+                                if (backdrop != null) {
+                                    Modifier
+                                        .textureBlur(
+                                            backdrop = backdrop,
+                                            shape = RoundedCornerShape(16.dp),
+                                            blurRadius = blurRadius,
+                                            noiseCoefficient = noiseCoefficient,
+                                            colors = BlurColors(
+                                                blendColors = cardBlend,
+                                                brightness = brightness,
+                                                contrast = contrast,
+                                                saturation = saturation,
+                                            ),
+                                        )
+                                } else {
+                                    Modifier
+                                },
+                            ),
+                        colors = CardDefaults.defaultColors(
+                            if (backdrop != null) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer,
+                            Color.Transparent,
+                        ),
+                    ) {
+                        ArrowPreference(
+                            title = stringResource(R.string.privacy_policy),
+                            onClick = {
+                                onIntent(
+                                    AboutIntent.ShowMdFile(
+                                        "隐私政策",
+                                        "privacyPolicy.md"
+                                    )
+                                )
+                            },
+                        )
+                        ArrowPreference(
+                            title = stringResource(R.string.license),
+                            onClick = { onIntent(AboutIntent.ShowMdFile("许可证", "LICENSE.md")) },
+                        )
+                        ArrowPreference(
+                            title = stringResource(R.string.disclaimer),
+                            onClick = {
+                                onIntent(
+                                    AboutIntent.ShowMdFile(
+                                        "免责声明",
+                                        "disclaimer.md"
+                                    )
+                                )
+                            },
+                        )
+                    }
+
+                    Card(
+                        modifier = Modifier
+                            .padding(top = 12.dp)
+                            .then(
+                                if (backdrop != null) {
+                                    Modifier
+                                        .textureBlur(
+                                            backdrop = backdrop,
+                                            shape = RoundedCornerShape(16.dp),
+                                            blurRadius = blurRadius,
+                                            noiseCoefficient = noiseCoefficient,
+                                            colors = BlurColors(
+                                                blendColors = cardBlend,
+                                                brightness = brightness,
+                                                contrast = contrast,
+                                                saturation = saturation,
+                                            ),
+                                        )
+                                } else {
+                                    Modifier
+                                },
+                            ),
+                        colors = CardDefaults.defaultColors(
+                            if (backdrop != null) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer,
+                            Color.Transparent,
+                        ),
+                    ) {
+                        ArrowPreference(
+                            title = stringResource(R.string.crash_log),
+                            onClick = { onIntent(AboutIntent.ShowCrashLogs) },
+                        )
+                        ArrowPreference(
+                            title = stringResource(R.string.save_log),
+                            onClick = { onIntent(AboutIntent.SaveLog) },
+                        )
+                        ArrowPreference(
+                            title = stringResource(R.string.create_heap_dump),
+                            onClick = { onIntent(AboutIntent.CreateHeapDump) },
+                        )
+                    }
+                    Spacer(modifier = Modifier.height(12.dp))
+                }
+            }
+        }
+        VerticalScrollBar(
+            adapter = rememberScrollBarAdapter(lazyListState),
+            modifier = Modifier
+                .align(Alignment.CenterEnd)
+                .fillMaxHeight(),
+            trackPadding = scrollPadding,
+        )
+    }
+}
+
+@Suppress("FunctionName")
+@Composable
+private fun ValueText(text: String) {
+    Text(
+        text = text,
+        fontSize = MiuixTheme.textStyles.body2.fontSize,
+        color = MiuixTheme.colorScheme.onSurfaceVariantActions,
+    )
+}

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

@@ -75,7 +75,6 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.BookCover
-import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LocalHazeState
@@ -95,6 +94,7 @@ import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
 import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.ui.widget.components.log.AppLogSheet
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator

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

@@ -77,7 +77,6 @@ import io.legado.app.help.book.getExportFileName
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.tryParesExportFileName
 import io.legado.app.service.ExportBookService
-import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
 import io.legado.app.ui.book.info.ChangeSourceSheet
 import io.legado.app.ui.book.info.GroupSelectSheet
@@ -99,6 +98,7 @@ import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.list.ListUiState
+import io.legado.app.ui.widget.components.log.AppLogSheet
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet

+ 35 - 0
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -7,11 +7,17 @@ import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.togetherWith
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.navigation3.runtime.NavKey
 import androidx.navigation3.runtime.entryProvider
 import androidx.navigation3.ui.LocalNavAnimatedContentScope
 import androidx.navigation3.ui.NavDisplay
+import io.legado.app.model.Download
+import io.legado.app.ui.about.AboutEffect
+import io.legado.app.ui.about.AboutScreen
+import io.legado.app.ui.about.AboutViewModel
 import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen
 import io.legado.app.ui.book.explore.ExploreShowScreen
 import io.legado.app.ui.book.import.local.ImportBookScreen
@@ -40,9 +46,12 @@ import io.legado.app.ui.rss.favorites.RssFavoritesScreen
 import io.legado.app.ui.rss.read.MainRouteRssRead
 import io.legado.app.ui.rss.read.RssReadRouteScreen
 import io.legado.app.ui.rss.subscription.RuleSubScreen
+import io.legado.app.utils.openUrl
 import io.legado.app.utils.startActivity
 import io.legado.app.utils.startActivityForBook
+import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.koin.androidx.compose.koinViewModel
@@ -127,6 +136,9 @@ fun MainActivity.mainEntryProvider(
             onNavigateToReadRecord = {
                 onNavigateToRoute(MainRouteReadRecord)
             },
+            onNavigateToAbout = {
+                onNavigateToRoute(MainRouteAbout)
+            },
             sharedTransitionScope = sharedTransitionScope,
             animatedVisibilityScope = LocalNavAnimatedContentScope.current,
         )
@@ -420,4 +432,27 @@ fun MainActivity.mainEntryProvider(
             animatedVisibilityScope = LocalNavAnimatedContentScope.current,
         )
     }
+
+    entry<MainRouteAbout> {
+        val viewModel = koinViewModel<AboutViewModel>()
+        val context = LocalContext.current
+        LaunchedEffect(viewModel) {
+            viewModel.effects.collectLatest { effect ->
+                when (effect) {
+                    is AboutEffect.OpenUrl -> context.openUrl(effect.url)
+                    is AboutEffect.ShowToast -> context.toastOnUi(effect.message)
+                    is AboutEffect.StartDownload -> Download.start(
+                        context,
+                        effect.url,
+                        effect.fileName
+                    )
+                }
+            }
+        }
+        AboutScreen(
+            state = viewModel.uiState.collectAsStateWithLifecycle().value,
+            onIntent = viewModel::onIntent,
+            onBack = { onNavigateBack() },
+        )
+    }
 }

+ 4 - 0
app/src/main/java/io/legado/app/ui/main/MainNavKey.kt

@@ -80,6 +80,9 @@ data class MainRouteExploreShow(
     val exploreUrl: String?,
 ) : MainRoute
 
+@Serializable
+data object MainRouteAbout : MainRoute
+
 object MainRouteConst {
     const val ROUTE_MAIN = "main"
     const val ROUTE_SETTINGS = "settings"
@@ -103,4 +106,5 @@ object MainRouteConst {
     const val ROUTE_RULE_SUB = "rss/rule_sub"
     const val ROUTE_READ_RECORD = "read_record"
     const val ROUTE_READ_RECORD_OVERVIEW = "read_record_overview"
+    const val ROUTE_ABOUT = "about"
 }

+ 12 - 0
app/src/main/java/io/legado/app/ui/main/MainNavigator.kt

@@ -149,6 +149,16 @@ object MainNavigator {
                 }
             }
 
+            MainRouteAbout -> {
+                if (currentRoute == MainRouteHome) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
             MainRouteReadRecordOverview -> {
                 if (currentRoute == MainRouteHome || currentRoute == MainRouteReadRecord) {
                     backStack.add(route)
@@ -258,6 +268,8 @@ object MainNavigator {
                     )
                 } ?: MainRouteHome
 
+            MainRouteConst.ROUTE_ABOUT -> MainRouteAbout
+
             else -> MainRouteHome
         }
     }

+ 3 - 1
app/src/main/java/io/legado/app/ui/main/MainScreen.kt

@@ -116,6 +116,7 @@ fun MainScreen(
     onNavigateToRssFavorites: () -> Unit,
     onNavigateToRuleSub: () -> Unit,
     onNavigateToReadRecord: () -> Unit,
+    onNavigateToAbout: () -> Unit,
     sharedTransitionScope: SharedTransitionScope? = null,
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
@@ -154,6 +155,7 @@ fun MainScreen(
 
                 MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
                 MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
+                MainEffect.NavigateToAbout -> onNavigateToAbout()
             }
         }
     }
@@ -231,7 +233,7 @@ fun MainScreen(
                             modifier = Modifier.padding(start = 20.dp),
                             onClick = { onNavigateToSearch(null) },
                             expanded = expanded,
-                            icon = { Icon(Icons.Default.Search, contentDescription = null) },
+                            icon = { AppIcon(Icons.Default.Search, contentDescription = null) },
                             text = { AppText(stringResource(R.string.search)) }
                         )
                     }

+ 3 - 0
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -115,6 +115,8 @@ class MainViewModel(
 
             PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
 
+            PrefClickEvent.OpenAbout -> _effects.tryEmit(MainEffect.NavigateToAbout)
+
             else -> Unit
         }
     }
@@ -132,6 +134,7 @@ sealed interface MainEffect {
 
     data object ExitApp : MainEffect
     data object NavigateToReadRecord : MainEffect
+    data object NavigateToAbout : MainEffect
 }
 
 @Stable

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

@@ -99,7 +99,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.data.entities.BookGroup
-import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.book.group.GroupEditSheet
 import io.legado.app.ui.book.info.GroupSelectSheet
 import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
@@ -125,6 +124,7 @@ import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
+import io.legado.app.ui.widget.components.log.AppLogSheet
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator

+ 4 - 5
app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt

@@ -3,20 +3,20 @@ package io.legado.app.ui.main.my
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.expandVertically
 import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.only
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.systemBars
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ExitToApp
 import androidx.compose.material.icons.automirrored.filled.HelpOutline
@@ -45,7 +45,6 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
-import io.legado.app.ui.about.AboutActivity
 import io.legado.app.ui.book.bookmark.AllBookmarkActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
@@ -206,7 +205,7 @@ fun MyScreen(
                     title = stringResource(R.string.about),
                     imageVector = Icons.Default.Info,
                     onClick = {
-                        onNavigate(PrefClickEvent.StartActivity(AboutActivity::class.java))
+                        onNavigate(PrefClickEvent.OpenAbout)
                     }
                 )
                 ClickableSettingItem(

+ 1 - 0
app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt

@@ -26,6 +26,7 @@ sealed class PrefClickEvent {
     data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
     object OpenReadRecord : PrefClickEvent()
     object OpenBookCacheManage : PrefClickEvent()
+    object OpenAbout : PrefClickEvent()
     object ToggleWebService : PrefClickEvent()
     object ExitApp : PrefClickEvent()
 }

+ 174 - 0
app/src/main/java/io/legado/app/ui/util/MiuixUtils.kt

@@ -0,0 +1,174 @@
+package io.legado.app.ui.util
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalWindowInfo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import top.yukonga.miuix.kmp.basic.ScrollBehavior
+import top.yukonga.miuix.kmp.basic.SmallTopAppBar
+import top.yukonga.miuix.kmp.basic.TopAppBar
+import top.yukonga.miuix.kmp.blur.BlendColorEntry
+import top.yukonga.miuix.kmp.blur.BlurColors
+import top.yukonga.miuix.kmp.blur.LayerBackdrop
+import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
+import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
+import top.yukonga.miuix.kmp.blur.textureBlur
+import top.yukonga.miuix.kmp.theme.MiuixTheme
+import top.yukonga.miuix.kmp.utils.overScrollVertical
+import top.yukonga.miuix.kmp.utils.scrollEndHaptic
+
+@Stable
+data class AppState(
+    val enableBlur: Boolean = true,
+    val enableScrollEndHaptic: Boolean = true,
+    val showTopAppBar: Boolean = true,
+)
+
+val LocalAppState = compositionLocalOf { AppState() }
+val LocalIsWideScreen = staticCompositionLocalOf { false }
+
+interface MiuixNavigator {
+    fun pop()
+    fun push(route: Any)
+}
+
+val LocalNavigator = staticCompositionLocalOf<MiuixNavigator> { error("No navigator found!") }
+
+@Composable
+fun shouldShowSplitPane(): Boolean {
+    val windowInfo = LocalWindowInfo.current
+    val density = LocalDensity.current
+    return with(density) {
+        val widthDp = windowInfo.containerSize.width.toDp()
+        val heightDp = windowInfo.containerSize.height.toDp()
+        val ratio = heightDp / widthDp
+        widthDp >= 840.dp || (widthDp >= 600.dp && ratio < 1.2f)
+    }
+}
+
+fun Modifier.pageScrollModifiers(
+    enableScrollEndHaptic: Boolean,
+    showTopAppBar: Boolean,
+    topAppBarScrollBehavior: ScrollBehavior,
+): Modifier = this
+    .then(if (enableScrollEndHaptic) Modifier.scrollEndHaptic() else Modifier)
+    .overScrollVertical()
+    .then(if (showTopAppBar) Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) else Modifier)
+    .fillMaxHeight()
+
+@Composable
+fun pageContentPadding(
+    innerPadding: PaddingValues,
+    outerPadding: PaddingValues,
+    isWideScreen: Boolean,
+    extraTop: Dp = 0.dp,
+    extraStart: Dp = 0.dp,
+    extraEnd: Dp = 0.dp,
+): PaddingValues {
+    val topPadding = innerPadding.calculateTopPadding() + extraTop
+    val bottomPadding = if (isWideScreen) {
+        WindowInsets.navigationBars.asPaddingValues()
+            .calculateBottomPadding() + outerPadding.calculateBottomPadding()
+    } else {
+        outerPadding.calculateBottomPadding()
+    }
+    return remember(topPadding, bottomPadding, extraStart, extraEnd) {
+        PaddingValues(
+            top = topPadding,
+            start = extraStart,
+            end = extraEnd,
+            bottom = bottomPadding,
+        )
+    }
+}
+
+@Composable
+fun AdaptiveTopAppBar(
+    title: String,
+    showTopAppBar: Boolean,
+    isWideScreen: Boolean,
+    scrollBehavior: ScrollBehavior,
+    subtitle: String = "",
+    color: Color = MiuixTheme.colorScheme.surface,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+    bottomContent: @Composable () -> Unit = {},
+) {
+    if (showTopAppBar) {
+        if (isWideScreen) {
+            SmallTopAppBar(
+                title = title,
+                subtitle = subtitle,
+                color = color,
+                scrollBehavior = scrollBehavior,
+                defaultWindowInsetsPadding = false,
+                navigationIcon = navigationIcon,
+                actions = actions,
+                bottomContent = bottomContent,
+            )
+        } else {
+            TopAppBar(
+                title = title,
+                subtitle = subtitle,
+                color = color,
+                scrollBehavior = scrollBehavior,
+                navigationIcon = navigationIcon,
+                actions = actions,
+                bottomContent = bottomContent,
+            )
+        }
+    }
+}
+
+@Composable
+fun rememberBlurBackdrop(): LayerBackdrop? {
+    val appState = LocalAppState.current
+    if (!appState.enableBlur || !isRenderEffectSupported()) return null
+    val surfaceColor = MiuixTheme.colorScheme.surface
+    return rememberLayerBackdrop {
+        drawRect(surfaceColor)
+        drawContent()
+    }
+}
+
+@Composable
+fun BlurredBar(
+    backdrop: LayerBackdrop?,
+    blurEnabled: Boolean,
+    content: @Composable () -> Unit,
+) {
+    Box(
+        modifier = if (blurEnabled && backdrop != null) {
+            Modifier.textureBlur(
+                backdrop = backdrop,
+                shape = RectangleShape,
+                blurRadius = 25f,
+                colors = BlurColors(
+                    blendColors = listOf(
+                        BlendColorEntry(color = MiuixTheme.colorScheme.surface.copy(0.8f)),
+                    ),
+                ),
+            )
+        } else {
+            Modifier
+        },
+    ) {
+        content()
+    }
+}

+ 87 - 0
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectBackground.kt

@@ -0,0 +1,87 @@
+package io.legado.app.ui.widget.components.effect
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import io.legado.app.ui.util.shouldShowSplitPane
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
+import top.yukonga.miuix.kmp.theme.MiuixTheme
+import kotlin.math.floor
+
+@Composable
+fun BgEffectBackground(
+    dynamicBackground: Boolean,
+    modifier: Modifier = Modifier,
+    bgModifier: Modifier = Modifier,
+    isFullSize: Boolean = false,
+    effectBackground: Boolean = true,
+    isOs3Effect: Boolean = true,
+    alpha: () -> Float = { 1f },
+    content: @Composable (BoxScope.() -> Unit),
+) {
+    val shaderSupported = remember { isRuntimeShaderSupported() }
+    if (!shaderSupported) {
+        Box(modifier = modifier, content = content)
+        return
+    }
+    Box(
+        modifier = modifier,
+    ) {
+        val surface = MiuixTheme.colorScheme.surface
+        val deviceType = if (shouldShowSplitPane()) DeviceType.PAD else DeviceType.PHONE
+        val isDarkTheme = isSystemInDarkTheme()
+        val painter = remember(isOs3Effect) { BgEffectPainter(isOs3Effect) }
+
+        val preset = remember(deviceType, isDarkTheme, isOs3Effect) {
+            BgEffectConfig.get(deviceType, isDarkTheme, isOs3Effect)
+        }
+
+        val colorStage = remember { Animatable(0f) }
+
+        LaunchedEffect(dynamicBackground, preset) {
+            if (!dynamicBackground) return@LaunchedEffect
+            val animatesColors =
+                preset.colors1 !== preset.colors2 || preset.colors2 !== preset.colors3
+            if (!animatesColors) return@LaunchedEffect
+
+            var targetStage = floor(colorStage.value) + 1f
+            while (isActive) {
+                delay((preset.colorInterpPeriod * 500).toLong())
+                colorStage.animateTo(
+                    targetValue = targetStage,
+                    animationSpec = spring(dampingRatio = 0.9f, stiffness = 35f),
+                )
+                targetStage += 1f
+            }
+        }
+
+        Spacer(
+            modifier = Modifier
+                .fillMaxSize()
+                .then(bgModifier)
+                .bgEffectDraw(
+                    painter = painter,
+                    preset = preset,
+                    deviceType = deviceType,
+                    isDarkTheme = isDarkTheme,
+                    surface = surface,
+                    effectBackground = effectBackground,
+                    isFullSize = isFullSize,
+                    playing = dynamicBackground,
+                    colorStage = { colorStage.value },
+                    alpha = alpha,
+                ),
+        )
+        content()
+    }
+}

+ 181 - 0
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectConfig.kt

@@ -0,0 +1,181 @@
+package io.legado.app.ui.widget.components.effect
+
+internal object BgEffectConfig {
+
+    internal class Config(
+        val points: FloatArray,
+        val colors1: FloatArray,
+        val colors2: FloatArray,
+        val colors3: FloatArray,
+        val colorInterpPeriod: Float,
+        val lightOffset: Float,
+        val saturateOffset: Float,
+        val pointOffset: Float,
+    )
+
+    // --- 优化配色 (基于用户提供的颜色调优) ---
+    private val C1_L = floatArrayOf(0.68f, 0.65f, 0.74f, 1.0f) // 亮色优化 1
+    private val C2_L = floatArrayOf(0.92f, 0.62f, 0.73f, 1.0f) // 亮色优化 2
+    private val C3_L = floatArrayOf(0.98f, 0.98f, 0.98f, 1.0f) // 亮色优化 3
+    private val CM_L = floatArrayOf(0.85f, 0.80f, 0.90f, 1.0f) // 亮色过渡
+
+    private val C1_D = floatArrayOf(0.16f, 0.14f, 0.20f, 1.0f) // 暗色优化 1
+    private val C2_D = floatArrayOf(0.49f, 0.17f, 0.29f, 1.0f) // 暗色优化 2
+    private val C3_D = floatArrayOf(0.15f, 0.15f, 0.15f, 1.0f) // 暗色优化 3
+    private val CM_D = floatArrayOf(0.25f, 0.15f, 0.28f, 1.0f) // 暗色过渡
+
+    // OS3 配色组合 (A/B/C 三阶段循环实现流光感)
+    private val COLORS_L_A = floatArrayOf(
+        C1_L[0],
+        C1_L[1],
+        C1_L[2],
+        C1_L[3],
+        C2_L[0],
+        C2_L[1],
+        C2_L[2],
+        C2_L[3],
+        C3_L[0],
+        C3_L[1],
+        C3_L[2],
+        C3_L[3],
+        CM_L[0],
+        CM_L[1],
+        CM_L[2],
+        CM_L[3]
+    )
+    private val COLORS_L_B = floatArrayOf(
+        C2_L[0],
+        C2_L[1],
+        C2_L[2],
+        C2_L[3],
+        C3_L[0],
+        C3_L[1],
+        C3_L[2],
+        C3_L[3],
+        CM_L[0],
+        CM_L[1],
+        CM_L[2],
+        CM_L[3],
+        C1_L[0],
+        C1_L[1],
+        C1_L[2],
+        C1_L[3]
+    )
+    private val COLORS_L_C = floatArrayOf(
+        C3_L[0],
+        C3_L[1],
+        C3_L[2],
+        C3_L[3],
+        CM_L[0],
+        CM_L[1],
+        CM_L[2],
+        CM_L[3],
+        C1_L[0],
+        C1_L[1],
+        C1_L[2],
+        C1_L[3],
+        C2_L[0],
+        C2_L[1],
+        C2_L[2],
+        C2_L[3]
+    )
+
+    private val COLORS_D_A = floatArrayOf(
+        C1_D[0],
+        C1_D[1],
+        C1_D[2],
+        C1_D[3],
+        C2_D[0],
+        C2_D[1],
+        C2_D[2],
+        C2_D[3],
+        C3_D[0],
+        C3_D[1],
+        C3_D[2],
+        C3_D[3],
+        CM_D[0],
+        CM_D[1],
+        CM_D[2],
+        CM_D[3]
+    )
+    private val COLORS_D_B = floatArrayOf(
+        C2_D[0],
+        C2_D[1],
+        C2_D[2],
+        C2_D[3],
+        C3_D[0],
+        C3_D[1],
+        C3_D[2],
+        C3_D[3],
+        CM_D[0],
+        CM_D[1],
+        CM_D[2],
+        CM_D[3],
+        C1_D[0],
+        C1_D[1],
+        C1_D[2],
+        C1_D[3]
+    )
+    private val COLORS_D_C = floatArrayOf(
+        C3_D[0],
+        C3_D[1],
+        C3_D[2],
+        C3_D[3],
+        CM_D[0],
+        CM_D[1],
+        CM_D[2],
+        CM_D[3],
+        C1_D[0],
+        C1_D[1],
+        C1_D[2],
+        C1_D[3],
+        C2_D[0],
+        C2_D[1],
+        C2_D[2],
+        C2_D[3]
+    )
+
+    // OS3 Configs (参数完全对齐原版 OS3)
+    private val OS3_PHONE_LIGHT = Config(
+        points = floatArrayOf(
+            0.8f,
+            0.2f,
+            1.0f,
+            0.8f,
+            0.9f,
+            1.0f,
+            0.2f,
+            0.9f,
+            1.0f,
+            0.2f,
+            0.2f,
+            1.0f
+        ),
+        colors1 = COLORS_L_A, colors2 = COLORS_L_B, colors3 = COLORS_L_C,
+        colorInterpPeriod = 5.0f, lightOffset = 0.1f, saturateOffset = 0.2f, pointOffset = 0.2f
+    )
+    private val OS3_PHONE_DARK = Config(
+        points = floatArrayOf(
+            0.8f,
+            0.2f,
+            1.0f,
+            0.8f,
+            0.9f,
+            1.0f,
+            0.2f,
+            0.9f,
+            1.0f,
+            0.2f,
+            0.2f,
+            1.0f
+        ),
+        colors1 = COLORS_D_A, colors2 = COLORS_D_B, colors3 = COLORS_D_C,
+        colorInterpPeriod = 8.0f, lightOffset = 0.0f, saturateOffset = 0.17f, pointOffset = 0.4f
+    )
+
+    internal fun get(
+        deviceType: DeviceType,
+        isDark: Boolean,
+        isOs3: Boolean = true, // 默认且仅使用 OS3 逻辑
+    ): Config = if (!isDark) OS3_PHONE_LIGHT else OS3_PHONE_DARK
+}

+ 177 - 0
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectModifier.kt

@@ -0,0 +1,177 @@
+package io.legado.app.ui.widget.components.effect
+
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateDraw
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+internal fun Modifier.bgEffectDraw(
+    painter: BgEffectPainter,
+    preset: BgEffectConfig.Config,
+    deviceType: DeviceType,
+    isDarkTheme: Boolean,
+    surface: Color,
+    effectBackground: Boolean,
+    isFullSize: Boolean,
+    playing: Boolean,
+    colorStage: () -> Float,
+    alpha: () -> Float,
+): Modifier = this then BgEffectElement(
+    painter = painter,
+    preset = preset,
+    deviceType = deviceType,
+    isDarkTheme = isDarkTheme,
+    surface = surface,
+    effectBackground = effectBackground,
+    isFullSize = isFullSize,
+    playing = playing,
+    colorStage = colorStage,
+    alpha = alpha,
+)
+
+private data class BgEffectElement(
+    val painter: BgEffectPainter,
+    val preset: BgEffectConfig.Config,
+    val deviceType: DeviceType,
+    val isDarkTheme: Boolean,
+    val surface: Color,
+    val effectBackground: Boolean,
+    val isFullSize: Boolean,
+    val playing: Boolean,
+    val colorStage: () -> Float,
+    val alpha: () -> Float,
+) : ModifierNodeElement<BgEffectNode>() {
+
+    override fun create(): BgEffectNode = BgEffectNode(
+        painter = painter,
+        preset = preset,
+        deviceType = deviceType,
+        isDarkTheme = isDarkTheme,
+        surface = surface,
+        effectBackground = effectBackground,
+        isFullSize = isFullSize,
+        playing = playing,
+        colorStage = colorStage,
+        alpha = alpha,
+    )
+
+    override fun update(node: BgEffectNode) {
+        node.update(
+            painter = painter,
+            preset = preset,
+            deviceType = deviceType,
+            isDarkTheme = isDarkTheme,
+            surface = surface,
+            effectBackground = effectBackground,
+            isFullSize = isFullSize,
+            playing = playing,
+            colorStage = colorStage,
+            alpha = alpha,
+        )
+    }
+}
+
+private class BgEffectNode(
+    private var painter: BgEffectPainter,
+    private var preset: BgEffectConfig.Config,
+    private var deviceType: DeviceType,
+    private var isDarkTheme: Boolean,
+    private var surface: Color,
+    private var effectBackground: Boolean,
+    private var isFullSize: Boolean,
+    private var playing: Boolean,
+    private var colorStage: () -> Float,
+    private var alpha: () -> Float,
+) : Modifier.Node(),
+    DrawModifierNode {
+
+    private var animationJob: Job? = null
+    private var animTime: Float = 0f
+    private var startOffset: Float = 0f
+
+    override fun onAttach() {
+        if (playing) startAnimation()
+    }
+
+    override fun onDetach() {
+        animationJob?.cancel()
+        animationJob = null
+    }
+
+    fun update(
+        painter: BgEffectPainter,
+        preset: BgEffectConfig.Config,
+        deviceType: DeviceType,
+        isDarkTheme: Boolean,
+        surface: Color,
+        effectBackground: Boolean,
+        isFullSize: Boolean,
+        playing: Boolean,
+        colorStage: () -> Float,
+        alpha: () -> Float,
+    ) {
+        this.painter = painter
+        this.preset = preset
+        this.deviceType = deviceType
+        this.isDarkTheme = isDarkTheme
+        this.surface = surface
+        this.effectBackground = effectBackground
+        this.isFullSize = isFullSize
+        this.colorStage = colorStage
+        this.alpha = alpha
+
+        if (this.playing != playing) {
+            this.playing = playing
+            if (playing) {
+                startAnimation()
+            } else {
+                animationJob?.cancel()
+                animationJob = null
+            }
+        }
+        invalidateDraw()
+    }
+
+    private fun startAnimation() {
+        animationJob?.cancel()
+        startOffset = animTime
+        animationJob = coroutineScope.launch {
+            val minDeltaNanos = 1_000_000_000L / 60L
+            val origin = withFrameNanos { it }
+            var lastEmit = origin
+            while (isActive) {
+                val now = withFrameNanos { it }
+                if (now - lastEmit < minDeltaNanos) continue
+                lastEmit = now
+                animTime = startOffset + (now - origin) / 1_000_000_000f
+                invalidateDraw()
+            }
+        }
+    }
+
+    override fun ContentDrawScope.draw() {
+        drawRect(surface)
+        if (effectBackground) {
+            val alphaValue = alpha()
+            if (alphaValue > 0f) {
+                val drawHeight = if (isFullSize) size.height * 0.8f else size.height * 0.5f
+
+                painter.updateResolution(size.width, size.height)
+                painter.updateBoundIfNeeded(drawHeight, size.height, size.width)
+                painter.updatePresetIfNeeded(deviceType, isDarkTheme)
+                painter.updateColors(preset, colorStage())
+                painter.updateAnimTime(animTime)
+                painter.updatePointsAnim(animTime, preset)
+
+                drawRect(painter.brush, alpha = alphaValue)
+            }
+        }
+        drawContent()
+    }
+}

+ 175 - 0
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectPainter.kt

@@ -0,0 +1,175 @@
+package io.legado.app.ui.widget.components.effect
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Brush
+import top.yukonga.miuix.kmp.blur.RuntimeShader
+import top.yukonga.miuix.kmp.blur.asBrush
+import kotlin.math.cos
+import kotlin.math.sin
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+internal class BgEffectPainter(
+    private val isOs3: Boolean = true,
+) {
+
+    val runtimeShader by lazy {
+        val shaderCode = OS3_BG_FRAG
+        RuntimeShader(shaderCode).also {
+            initStaticUniforms(it)
+        }
+    }
+
+    val brush: Brush get() = runtimeShader.asBrush()
+
+    private val resolution = FloatArray(2)
+    private val bound = FloatArray(4)
+    private val colorsBuffer = FloatArray(16)
+    private val pointsAnimBuffer = FloatArray(8)
+
+    private var animTime = Float.NaN
+    private var isDarkCached: Boolean? = null
+    private var deviceTypeCached: DeviceType? = null
+
+    private var presetApplied = false
+
+    private var cachedLogoHeight = Float.NaN
+    private var cachedTotalHeight = Float.NaN
+    private var cachedTotalWidth = Float.NaN
+
+    private var cachedColorStage = Float.NaN
+    private var cachedColorsPreset: BgEffectConfig.Config? = null
+
+    private var cachedPointsAnimTime = Float.NaN
+    private var cachedPointsAnimPreset: BgEffectConfig.Config? = null
+
+    companion object {
+
+        private const val U_TRANSLATE_Y = 0f
+        private const val U_ALPHA_MULTI = 1f
+        private const val U_NOISE_SCALE = 1.5f
+        private const val U_POINT_RADIUS_MULTI = 1f
+    }
+
+    private fun initStaticUniforms(shader: RuntimeShader) {
+        shader.setFloatUniform("uTranslateY", U_TRANSLATE_Y)
+        shader.setFloatUniform("uNoiseScale", U_NOISE_SCALE)
+        shader.setFloatUniform("uPointRadiusMulti", U_POINT_RADIUS_MULTI)
+        shader.setFloatUniform("uAlphaMulti", U_ALPHA_MULTI)
+    }
+
+    fun updateResolution(width: Float, height: Float) {
+        if (resolution[0] == width && resolution[1] == height) return
+        resolution[0] = width
+        resolution[1] = height
+        runtimeShader.setFloatUniform("uResolution", resolution)
+    }
+
+    fun updateAnimTime(time: Float) {
+        if (animTime == time) return
+        animTime = time
+        runtimeShader.setFloatUniform("uAnimTime", animTime)
+    }
+
+    fun updatePointsAnim(time: Float, preset: BgEffectConfig.Config) {
+        if (cachedPointsAnimTime == time && cachedPointsAnimPreset === preset) return
+
+        val offset = preset.pointOffset
+        var i = 0
+        while (i < 4) {
+            val srcX = preset.points[i * 3]
+            val srcY = preset.points[i * 3 + 1]
+            val animX = srcX + sin(time + srcY) * offset
+            val animY = srcY + cos(time + animX) * offset
+            pointsAnimBuffer[i * 2] = animX
+            pointsAnimBuffer[i * 2 + 1] = animY
+            i++
+        }
+        runtimeShader.setFloatUniform("uPointsAnim", pointsAnimBuffer)
+
+        cachedPointsAnimTime = time
+        cachedPointsAnimPreset = preset
+    }
+
+    fun updateColors(preset: BgEffectConfig.Config, stage: Float) {
+        if (cachedColorsPreset === preset && cachedColorStage == stage) return
+
+        val base = stage.toInt()
+        val fraction = stage - base
+        val start = colorsForCycleIndex(preset, base)
+        val end = colorsForCycleIndex(preset, base + 1)
+        for (i in 0 until 16) {
+            colorsBuffer[i] = start[i] + (end[i] - start[i]) * fraction
+        }
+        runtimeShader.setFloatUniform("uColors", colorsBuffer)
+
+        cachedColorsPreset = preset
+        cachedColorStage = stage
+    }
+
+    private fun colorsForCycleIndex(preset: BgEffectConfig.Config, index: Int): FloatArray =
+        when (index.mod(4)) {
+            1 -> preset.colors1
+            3 -> preset.colors3
+            else -> preset.colors2
+        }
+
+    fun updateBoundIfNeeded(
+        logoHeight: Float,
+        totalHeight: Float,
+        totalWidth: Float,
+    ) {
+        if (cachedLogoHeight == logoHeight &&
+            cachedTotalHeight == totalHeight &&
+            cachedTotalWidth == totalWidth
+        ) {
+            return
+        }
+
+        updateBound(logoHeight, totalHeight, totalWidth)
+        runtimeShader.setFloatUniform("uBound", bound)
+
+        cachedLogoHeight = logoHeight
+        cachedTotalHeight = totalHeight
+        cachedTotalWidth = totalWidth
+    }
+
+    fun updatePresetIfNeeded(deviceType: DeviceType, isDark: Boolean) {
+        if (presetApplied && isDarkCached == isDark && deviceTypeCached == deviceType) return
+
+        applyPreset(deviceType, isDark)
+
+        isDarkCached = isDark
+        deviceTypeCached = deviceType
+        presetApplied = true
+    }
+
+    private fun applyPreset(deviceType: DeviceType, isDark: Boolean) {
+        val preset = BgEffectConfig.get(deviceType, isDark, isOs3)
+
+        runtimeShader.setFloatUniform("uPoints", preset.points)
+        runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
+        runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
+    }
+
+    private fun updateBound(
+        logoHeight: Float,
+        totalHeight: Float,
+        totalWidth: Float,
+    ) {
+        val heightRatio = logoHeight / totalHeight
+        if (totalWidth <= totalHeight) {
+            bound[0] = 0f
+            bound[1] = 1f - heightRatio
+            bound[2] = 1f
+            bound[3] = heightRatio
+        } else {
+            val aspectRatio = totalWidth / totalHeight
+            val contentCenterY = 1f - heightRatio / 2f
+            bound[0] = 0f
+            bound[1] = contentCenterY - aspectRatio / 2f
+            bound[2] = 1f
+            bound[3] = aspectRatio
+        }
+    }
+}

+ 209 - 0
app/src/main/java/io/legado/app/ui/widget/components/effect/ColorBlendToken.kt

@@ -0,0 +1,209 @@
+package io.legado.app.ui.widget.components.effect
+
+import androidx.compose.ui.graphics.Color
+import top.yukonga.miuix.kmp.blur.BlendColorEntry
+import top.yukonga.miuix.kmp.blur.BlurBlendMode
+
+object ColorBlendToken {
+    // Info Series
+    val Info_Extra_Thin_Light = listOf(
+        BlendColorEntry(Color(0x3BB0B0B1), BlurBlendMode.PlusDarker),
+    )
+
+    val Info_Extra_Thin_Dark = listOf(
+        BlendColorEntry(Color(0x3BB0B0B1), BlurBlendMode.PlusLighter),
+    )
+
+    val Info_Thin_Light = listOf(
+        BlendColorEntry(Color(0x801E1E1E), BlurBlendMode.PlusLighter),
+    )
+
+    val Info_Thin_Dark = listOf(
+        BlendColorEntry(Color(0x801E1E1E), BlurBlendMode.PlusDarker),
+    )
+
+    val Info_Regular_Light = listOf(
+        BlendColorEntry(Color(0xB3141414), BlurBlendMode.PlusLighter),
+    )
+
+    val Info_Regular_Dark = listOf(
+        BlendColorEntry(Color(0xB3141414), BlurBlendMode.PlusDarker),
+    )
+
+    val Info_Thick_Light = listOf(
+        BlendColorEntry(Color(0xFF9A9A9A), BlurBlendMode.PlusLighter),
+    )
+
+    val Info_Thick_Dark = listOf(
+        BlendColorEntry(Color(0xFF9A9A9A), BlurBlendMode.PlusDarker),
+    )
+
+    // Info Colored
+    val Info_Colored_Regular = listOf(
+        BlendColorEntry(Color(0xFF9C27B0), BlurBlendMode.ColorDodge),
+        BlendColorEntry(Color(0x0FFFFFFF), BlurBlendMode.PlusLighter),
+    )
+
+    // Colored Series
+    val Colored_Extra_Thin_Light = listOf(
+        BlendColorEntry(Color(0x7F040404), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x26F1F1F1), BlurBlendMode.ColorDodge),
+        BlendColorEntry(Color(0x1AC8C8C8), BlurBlendMode.SrcOver),
+    )
+
+    val Colored_Extra_Thin_Dark = listOf(
+        BlendColorEntry(Color(0x6A4A4A4A), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x2E525252), BlurBlendMode.SrcOver),
+    )
+
+    val Colored_Thin_Light = listOf(
+        BlendColorEntry(Color(0x991C1C1C), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x802B2B2B), BlurBlendMode.SoftLight),
+    )
+
+    val Colored_Thin_Dark = listOf(
+        BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x337A7A7A), BlurBlendMode.PlusDarker),
+    )
+
+    val Colored_Regular_Light = listOf(
+        BlendColorEntry(Color(0x803F3F3F), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x1CE6E6E6), BlurBlendMode.PlusLighter),
+    )
+
+    val Colored_Regular_Dark = listOf(
+        BlendColorEntry(Color(0x70000000), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x14000000), BlurBlendMode.SrcOver),
+    )
+
+    val Colored_Thick_Light = listOf(
+        BlendColorEntry(Color(0xE6BDBDBD), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x992B2B2B), BlurBlendMode.ColorDodge),
+        BlendColorEntry(Color(0x339C9C9C), BlurBlendMode.SrcOver),
+    )
+
+    val Colored_Thick_Dark = listOf(
+        BlendColorEntry(Color(0x667A7A7A), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x33747474), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x322B2B2B), BlurBlendMode.SrcOver),
+    )
+
+    val Colored_Extra_Thick_Light = listOf(
+        BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.PlusLighter),
+        BlendColorEntry(Color(0x6BC0C0C0), BlurBlendMode.ColorDodge),
+    )
+
+    val Colored_Extra_Thick_Dark = listOf(
+        BlendColorEntry(Color(0x667A7A7A), BlurBlendMode.PlusDarker),
+        BlendColorEntry(Color(0x619C9C9C), BlurBlendMode.ColorBurn),
+    )
+
+    // Pured Series
+    val Pured_Extra_Thin_Light = listOf(
+        BlendColorEntry(Color(0x7F040404), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x5EFFFFFF), BlurBlendMode.PlusLighter),
+        BlendColorEntry(Color(0x24FF2424), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Extra_Thin_Dark = listOf(
+        BlendColorEntry(Color(0xE6E6E6E6), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x999C9C9C), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Thin_Light = listOf(
+        BlendColorEntry(Color(0x307A7A7A), BlurBlendMode.PlusLighter),
+        BlendColorEntry(Color(0x5EFFFFFF), BlurBlendMode.PlusLighter),
+        BlendColorEntry(Color(0x66FF6666), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Thin_Dark = listOf(
+        BlendColorEntry(Color(0x969C9C9C), BlurBlendMode.PlusDarker),
+        BlendColorEntry(Color(0x66000000), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Regular_Light = listOf(
+        BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight),
+    )
+
+    val Pured_Regular_Dark = listOf(
+        BlendColorEntry(Color(0x75000000), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x52000000), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Thick_Light = listOf(
+        BlendColorEntry(Color(0x4D000000), BlurBlendMode.Overlay),
+        BlendColorEntry(Color(0x80000000), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Thick_Dark = listOf(
+        BlendColorEntry(Color(0x4C000000), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x80030303), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Extra_Thick_Light = listOf(
+        BlendColorEntry(Color(0x66FF6666), BlurBlendMode.PlusLighter),
+        BlendColorEntry(Color(0x999C9C9C), BlurBlendMode.SrcOver),
+    )
+
+    val Pured_Extra_Thick_Dark = listOf(
+        BlendColorEntry(Color(0x999C9C9C), BlurBlendMode.Luminosity),
+        BlendColorEntry(Color(0x54525252), BlurBlendMode.PlusLighter),
+    )
+
+    // Overlay Series
+    val Overlay_Extra_Thin_Light = listOf(
+        BlendColorEntry(Color(0x0F7A7A7A), BlurBlendMode.Luminosity),
+    )
+
+    val Overlay_Extra_Thin_Dark = listOf(
+        BlendColorEntry(Color(0x757A7A7A), BlurBlendMode.Luminosity),
+    )
+
+    val Overlay_Thin_Light = listOf(
+        BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
+        BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker),
+    )
+
+    val Overlay_Regular_Light = listOf(
+        BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
+        BlendColorEntry(Color(0x1A2B2B2B), BlurBlendMode.PlusDarker),
+    )
+
+    val Overlay_Thick_Light = listOf(
+        BlendColorEntry(Color(0xA8A8A8A8), BlurBlendMode.Luminosity),
+        BlendColorEntry(Color(0xFF9A9A9A), BlurBlendMode.Overlay),
+    )
+
+    val Overlay_Thick_Dark = listOf(
+        BlendColorEntry(Color(0x66A8A8A8), BlurBlendMode.Luminosity),
+        BlendColorEntry(Color(0x999C9C9C), BlurBlendMode.PlusDarker),
+    )
+
+    val Overlay_Extra_Thick_Light = listOf(
+        BlendColorEntry(Color(0x99A8A8A8), BlurBlendMode.Luminosity),
+        BlendColorEntry(Color(0x4C000000), BlurBlendMode.ColorBurn),
+    )
+
+    // Deprecated
+    val ExtraHeavy_Light = listOf(
+        BlendColorEntry(Color(0x8F040404), BlurBlendMode.ColorDodge),
+        BlendColorEntry(Color(0xA3A3A3A3), BlurBlendMode.SrcOver),
+    )
+
+    val ExtraHeavy_Dark = listOf(
+        BlendColorEntry(Color(0x757A7A7A), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0x88888888), BlurBlendMode.SrcOver),
+        BlendColorEntry(Color(0x0B000000), BlurBlendMode.SrcOver),
+    )
+
+    val Heavy_Light = listOf(
+        BlendColorEntry(Color(0x949C9C9C), BlurBlendMode.ColorDodge),
+        BlendColorEntry(Color(0x999C9C9C), BlurBlendMode.SrcOver),
+    )
+
+    val Heavy_Dark = listOf(
+        BlendColorEntry(Color(0x7F040404), BlurBlendMode.ColorBurn),
+        BlendColorEntry(Color(0xB3B3B3B3), BlurBlendMode.SrcOver),
+    )
+}

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

@@ -0,0 +1,6 @@
+package io.legado.app.ui.widget.components.effect
+
+enum class DeviceType {
+    PHONE,
+    PAD,
+}

+ 88 - 0
app/src/main/java/io/legado/app/ui/widget/components/effect/OS3BgFrag.kt

@@ -0,0 +1,88 @@
+package io.legado.app.ui.widget.components.effect
+
+const val OS3_BG_FRAG = """
+    uniform vec2 uResolution;
+    uniform float uAnimTime;
+    uniform vec4 uBound;
+    uniform float uTranslateY;
+    uniform vec3 uPoints[4];
+    uniform vec2 uPointsAnim[4];
+    uniform vec4 uColors[4];
+    uniform float uAlphaMulti;
+    uniform float uNoiseScale;
+    uniform float uPointRadiusMulti;
+    uniform float uSaturateOffset;
+    uniform float uLightOffset;
+
+    vec3 rgb2hsv(vec3 c) {
+        vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+        vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+        vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+        float d = q.x - min(q.w, q.y);
+        float e = 1.0e-10;
+        return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+    }
+
+    vec3 hsv2rgb(vec3 c) {
+        vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+        vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+        return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+    }
+
+    float hash(vec2 p) {
+        vec3 p3 = fract(vec3(p.xyx) * 0.13);
+        p3 += dot(p3, p3.yzx + 3.333);
+        return fract((p3.x + p3.y) * p3.z);
+    }
+
+    float perlin(vec2 x) {
+        vec2 i = floor(x); vec2 f = fract(x);
+
+        float a = hash(i); float b = hash(i + vec2(1.0, 0.0));
+        float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0));
+
+        vec2 u = f * f * (3.0 - 2.0 * f);
+        return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
+    }
+
+    float gradientNoise(in vec2 uv) {
+        return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
+    }
+
+    vec4 main(vec2 fragCoord){
+        vec2 vUv = fragCoord/uResolution;
+        vUv.y = 1.0-vUv.y;
+        vec2 uv = vUv;
+        uv -= vec2(0., uTranslateY);
+        uv.xy -= uBound.xy;
+        uv.xy /= uBound.zw;
+
+        vec4 color = vec4(0.0);
+        float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
+
+        for (int i = 0; i < 4; i++){
+            vec4 pointColor = uColors[i];
+            pointColor.rgb *= pointColor.a;
+            vec2 point = uPointsAnim[i];
+            float rad = uPoints[i].z * uPointRadiusMulti;
+
+            float d = distance(uv, point);
+            float pct = smoothstep(rad, 0., d);
+            color.rgb = mix(color.rgb, pointColor.rgb, pct);
+            color.a = mix(color.a, pointColor.a, pct);
+        }
+
+        float oppositeNoise = smoothstep(0., 1., noiseValue);
+        color.rgb /= color.a;
+        vec3 hsv = rgb2hsv(color.rgb);
+        hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
+        color.rgb = hsv2rgb(hsv);
+        color.rgb += oppositeNoise * uLightOffset;
+
+        color.a = clamp(color.a, 0., 1.);
+        color.a *= uAlphaMulti;
+
+        color += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0);
+        return vec4(color.rgb * color.a, color.a);
+    }
+"""

+ 4 - 5
app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt

@@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import io.legado.app.ui.main.MainDestination
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
-import okhttp3.internal.http2.Settings
 import top.yukonga.miuix.kmp.icon.MiuixIcons
 import top.yukonga.miuix.kmp.icon.basic.Search
 import top.yukonga.miuix.kmp.icon.extended.Album
@@ -102,25 +101,25 @@ object AppIcons {
     fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
         return when (destination) {
             MainDestination.Bookshelf -> if (isMiuix) {
-                if (selected) MiuixIcons.Heavy.ContactsBook else MiuixIcons.Regular.ContactsBook
+                if (selected) MiuixIcons.Regular.ContactsBook else MiuixIcons.Regular.ContactsBook
             } else {
                 if (selected) Icons.AutoMirrored.Filled.LibraryBooks else Icons.AutoMirrored.Outlined.LibraryBooks
             }
 
             MainDestination.Explore -> if (isMiuix) {
-                if (selected) MiuixIcons.Heavy.Album else MiuixIcons.Regular.Album
+                if (selected) MiuixIcons.Regular.Album else MiuixIcons.Regular.Album
             } else {
                 if (selected) Icons.Default.Explore else Icons.Outlined.Explore
             }
 
             MainDestination.Rss -> if (isMiuix) {
-                if (selected) MiuixIcons.Heavy.Favorites else MiuixIcons.Regular.Favorites
+                if (selected) MiuixIcons.Regular.Favorites else MiuixIcons.Regular.Favorites
             } else {
                 if (selected) Icons.Default.RssFeed else Icons.Outlined.RssFeed
             }
 
             MainDestination.My -> if (isMiuix) {
-                if (selected) MiuixIcons.Heavy.Settings else MiuixIcons.Regular.Settings
+                if (selected) MiuixIcons.Regular.Settings else MiuixIcons.Regular.Settings
             } else {
                 if (selected) Icons.Default.Person else Icons.Outlined.Person
             }

+ 29 - 57
app/src/main/java/io/legado/app/ui/about/AppLogSheet.kt → app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt

@@ -1,36 +1,29 @@
-package io.legado.app.ui.about
+package io.legado.app.ui.widget.components.log
 
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.selection.SelectionContainer
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.DeleteSweep
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 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.constant.AppLog
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.EmptyMessage
+import io.legado.app.ui.widget.components.button.MediumIconButton
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.LogUtils
@@ -47,62 +40,41 @@ fun AppLogSheet(
 
     AppModalBottomSheet(
         show = show,
-        onDismissRequest = onDismissRequest
-    ) {
-        Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp, vertical = 8.dp),
-            verticalAlignment = Alignment.CenterVertically
-        ) {
-            AppText(
-                text = stringResource(R.string.log),
-                style = LegadoTheme.typography.titleLarge,
-                modifier = Modifier.weight(1f)
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.log),
+        endAction = {
+            MediumIconButton(
+                onClick = {
+                    AppLog.clear()
+                    logs = emptyList()
+                },
+                imageVector = Icons.Default.DeleteSweep
             )
-            IconButton(onClick = {
-                AppLog.clear()
-                logs = emptyList()
-            }) {
-                Icon(Icons.Default.DeleteSweep, contentDescription = "Clear")
-            }
         }
-
-        LazyColumn(
-            modifier = Modifier.fillMaxWidth(),
-            contentPadding = PaddingValues(bottom = 16.dp)
-        ) {
-            items(logs) { item ->
-                LogItem(item) {
-                    item.third?.let {
-                        showStackTrace = it.stackTraceToString()
+    ) {
+        if (logs.isEmpty()) {
+            EmptyMessage(message = "暂无日志")
+        } else {
+            LazyColumn(
+                modifier = Modifier.fillMaxWidth(),
+                contentPadding = PaddingValues(bottom = 16.dp)
+            ) {
+                items(logs) { item ->
+                    LogItem(item) {
+                        item.third?.let {
+                            showStackTrace = it.stackTraceToString()
+                        }
                     }
                 }
             }
         }
     }
 
-
-    AppAlertDialog(
-        data = showStackTrace,
-        onDismissRequest = { showStackTrace = null },
+    LogDetailSheet(
+        show = showStackTrace != null,
         title = "Log",
-        content = { stackTrace ->
-            SelectionContainer {
-                Column(
-                    modifier = Modifier
-                        .heightIn(max = 400.dp)
-                        .verticalScroll(rememberScrollState())
-                ) {
-                    AppText(
-                        text = stackTrace,
-                        style = LegadoTheme.typography.bodySmall
-                    )
-                }
-            }
-        },
-        confirmText = stringResource(android.R.string.ok),
-        onConfirm = { showStackTrace = null }
+        content = showStackTrace.orEmpty(),
+        onDismissRequest = { showStackTrace = null }
     )
 }
 

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

@@ -0,0 +1,64 @@
+package io.legado.app.ui.widget.components.log
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DeleteSweep
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+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.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.EmptyMessage
+import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.utils.FileDoc
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CrashLogSheet(
+    show: Boolean,
+    logFiles: List<FileDoc>,
+    onDismissRequest: () -> Unit,
+    onReadFile: (FileDoc) -> Unit,
+    onClear: () -> Unit,
+) {
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        endAction = {
+            IconButton(onClick = onClear) {
+                Icon(Icons.Default.DeleteSweep, contentDescription = "Clear")
+            }
+        },
+        title = stringResource(R.string.crash_log),
+    ) {
+        if (logFiles.isEmpty()) {
+            EmptyMessage(message = "暂无崩溃日志")
+        } else {
+            LazyColumn(
+                modifier = Modifier.fillMaxWidth(),
+                contentPadding = PaddingValues(bottom = 16.dp)
+            ) {
+                items(logFiles) { fileDoc ->
+                    AppText(
+                        text = fileDoc.name,
+                        style = LegadoTheme.typography.bodyMedium,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .clickable { onReadFile(fileDoc) }
+                            .padding(16.dp)
+                    )
+                }
+            }
+        }
+    }
+}

+ 44 - 0
app/src/main/java/io/legado/app/ui/widget/components/log/LogDetailSheet.kt

@@ -0,0 +1,44 @@
+package io.legado.app.ui.widget.components.log
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.text.AppText
+
+@Composable
+fun LogDetailSheet(
+    show: Boolean,
+    title: String = "Log",
+    content: String,
+    onDismissRequest: () -> Unit,
+) {
+    AppAlertDialog(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = title,
+        content = {
+            SelectionContainer {
+                Column(
+                    modifier = Modifier
+                        .heightIn(max = 400.dp)
+                        .verticalScroll(rememberScrollState())
+                ) {
+                    AppText(
+                        text = content,
+                        style = LegadoTheme.typography.bodySmall
+                    )
+                }
+            }
+        },
+        confirmText = stringResource(android.R.string.ok),
+        onConfirm = onDismissRequest
+    )
+}

+ 0 - 330
app/src/main/res/layout/activity_about.xml

@@ -1,330 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    tools:context="io.legado.app.ui.about.AboutActivity">
-
-    <com.google.android.material.appbar.AppBarLayout
-        android:id="@+id/app_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="@android:color/transparent"
-        android:fitsSystemWindows="true">
-
-        <com.google.android.material.appbar.CollapsingToolbarLayout
-            style="?attr/collapsingToolbarLayoutMediumStyle"
-            android:layout_width="match_parent"
-            android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
-            app:collapsedTitleTextAppearance="?attr/textAppearanceTitleLarge"
-            app:expandedTitleTextAppearance="?attr/textAppearanceHeadlineLarge"
-            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
-            app:titleCollapseMode="scale">
-
-            <com.google.android.material.appbar.MaterialToolbar
-                android:theme="@style/ThemeOverlay.Material3.Toolbar.CircleNavigationButton"
-
-                android:id="@+id/top_bar"
-                android:layout_width="match_parent"
-                android:layout_height="?attr/actionBarSize"
-                app:layout_collapseMode="pin"
-                app:navigationIcon="@drawable/ic_arrow_back"
-                app:title="@string/about" />
-
-        </com.google.android.material.appbar.CollapsingToolbarLayout>
-    </com.google.android.material.appbar.AppBarLayout>
-
-    <androidx.core.widget.NestedScrollView
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:layout_behavior="@string/appbar_scrolling_view_behavior">
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="8dp"
-            android:layout_marginBottom="60dp"
-            android:orientation="vertical"
-            app:layout_behavior="@string/appbar_scrolling_view_behavior">
-
-            <ImageView
-                android:id="@+id/cd_about"
-                android:layout_width="160dp"
-                android:layout_height="120dp"
-                android:scaleType="centerCrop"
-                app:layout_constraintStart_toStartOf="parent"
-                android:src="@drawable/ic_launcher_foreground"
-                app:layout_constraintTop_toTopOf="parent"
-                app:layout_constraintEnd_toEndOf="parent" />
-
-            <TextView
-                android:id="@+id/tv_name"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginStart="8dp"
-                android:text="@string/app_name"
-                android:layout_marginEnd="16dp"
-                android:textSize="24sp"
-                android:textStyle="bold"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/cd_about" />
-
-            <com.google.android.material.card.MaterialCardView
-                android:id="@+id/cd_ver"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:layout_marginTop="8dp"
-                app:cardBackgroundColor="?attr/colorTertiaryContainer"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/tv_name"
-                app:strokeWidth="0dp">
-
-                <TextView
-                    android:id="@+id/tv_version"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:paddingHorizontal="8dp"
-                    android:paddingVertical="4dp"
-                    android:text="@string/text"
-                    android:textColor="?attr/colorOnTertiaryContainer"
-                    android:textSize="10sp"
-                    android:textStyle="bold"
-                    tools:ignore="SmallSp" />
-            </com.google.android.material.card.MaterialCardView>
-
-            <TextView
-                android:id="@+id/tv_detail"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                android:text="@string/about_description"
-                android:textSize="14sp"
-                android:textStyle="bold"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/cd_ver" />
-
-            <com.google.android.material.button.MaterialButtonGroup
-                android:id="@+id/ll_btn"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/tv_detail">
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/btn_web"
-                    style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    app:icon="@drawable/ic_web_outline" />
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/btn_github"
-                    style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="match_parent"
-                    app:icon="@drawable/ic_github" />
-
-                <com.google.android.material.button.MaterialButton
-                    android:id="@+id/btn_update"
-                    style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    app:icon="@drawable/ic_import" />
-            </com.google.android.material.button.MaterialButtonGroup>
-
-            <LinearLayout
-                android:id="@+id/ll_contributors"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_btn">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/contributors"
-                    android:textSize="16sp" />
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="2dp"
-                    android:text="@string/contributors_summary"
-                    android:textSize="13sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_update_log"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_contributors">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/update_log"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_privacy_policy"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_update_log">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/privacy_policy"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_license"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_privacy_policy">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/license"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_disclaimer"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_license">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/disclaimer"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_crash_log"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_disclaimer">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/crash_log"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_save_log"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_crash_log">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/save_log"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/ll_create_heap_dump"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?attr/selectableItemBackground"
-                android:clickable="true"
-                android:focusable="true"
-                android:minHeight="60dp"
-                android:orientation="vertical"
-                android:paddingHorizontal="16dp"
-                android:paddingVertical="16dp"
-                app:layout_constraintTop_toBottomOf="@id/ll_save_log">
-
-                <TextView
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:singleLine="true"
-                    android:text="@string/create_heap_dump"
-                    android:textSize="16sp" />
-
-            </LinearLayout>
-
-        </androidx.constraintlayout.widget.ConstraintLayout>
-
-    </androidx.core.widget.NestedScrollView>
-
-</androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 0 - 13
app/src/main/res/menu/about.xml

@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:tools="http://schemas.android.com/tools"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item
-        android:id="@+id/menu_share_it"
-        android:title="@string/share"
-        android:icon="@drawable/ic_share"
-        app:showAsAction="always"
-        tools:ignore="AlwaysShowAction" />
-
-</menu>

+ 3 - 5
gradle/libs.versions.toml

@@ -45,7 +45,7 @@ coroutines = "1.10.2"
 lifecycleViewmodelCompose = "2.10.0"
 liveeventbus = "1.8.14"
 navigation3 = "1.1.1"
-markdownCompose = "0.5.0"
+markdownRenderer = "0.40.2"
 markwon = "4.6.2"
 material = "1.14.0-rc01"
 material3 = "1.5.0-alpha19"
@@ -85,7 +85,7 @@ biometricKtx = "1.1.0"
 reorderable = "3.1.0"
 haze = "1.7.2"
 timber = "5.0.1"
-miuix = "0.9.0"
+miuix = "0.9.1"
 backdrop = "1.0.6"
 capsule = "2.1.3"
 lyricViewx = "1.3.2"
@@ -134,8 +134,6 @@ androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayo
 
 haze-core = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
 haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
-kmp-miuix-preference = { module = "top.yukonga.miuix.kmp:miuix-preference" }
-kmp-miuix-ui = { module = "top.yukonga.miuix.kmp:miuix-ui" }
 koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
 koin-core = { module = "io.insert-koin:koin-core" }
 koin-android = { group = "io.insert-koin", name = "koin-android" }
@@ -170,7 +168,7 @@ libarchive = { module = "me.zhanghai.android.libarchive:library", version.ref =
 lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
 lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" }
 
-markdown-compose = { module = "com.mikepenz:markdown-compose", version.ref = "markdownCompose" }
+markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
 material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
 media-media = { module = "androidx.media:media", version.ref = "media" }
 media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }