From f0bbd4697ff87ab366ad8d71d80d636106e07a07 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sun, 17 May 2026 01:20:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BAMIUIX=E4=B8=BB=E9=A2=98=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E4=B8=93=E5=B1=9E=E7=9A=84=E5=85=B3=E4=BA=8E?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 7 +- .../main/java/io/legado/app/di/appModule.kt | 2 + .../io/legado/app/ui/about/AboutActivity.kt | 197 ------- .../io/legado/app/ui/about/AboutContract.kt | 48 ++ .../io/legado/app/ui/about/AboutScreen.kt | 164 ++++-- .../io/legado/app/ui/about/AboutSheets.kt | 193 +++++++ .../io/legado/app/ui/about/AboutViewModel.kt | 243 +++++++++ .../legado/app/ui/about/MiuixAboutScreen.kt | 515 ++++++++++++++++++ .../legado/app/ui/book/info/BookInfoScreen.kt | 2 +- .../ui/book/manage/BookshelfManageScreen.kt | 2 +- .../io/legado/app/ui/main/MainNavGraph.kt | 35 ++ .../java/io/legado/app/ui/main/MainNavKey.kt | 4 + .../io/legado/app/ui/main/MainNavigator.kt | 12 + .../java/io/legado/app/ui/main/MainScreen.kt | 4 +- .../io/legado/app/ui/main/MainViewModel.kt | 3 + .../app/ui/main/bookshelf/BookshelfScreen.kt | 2 +- .../java/io/legado/app/ui/main/my/MyScreen.kt | 9 +- .../io/legado/app/ui/main/my/MyViewModel.kt | 1 + .../java/io/legado/app/ui/util/MiuixUtils.kt | 174 ++++++ .../components/effect/BgEffectBackground.kt | 87 +++ .../components/effect/BgEffectConfig.kt | 181 ++++++ .../components/effect/BgEffectModifier.kt | 177 ++++++ .../components/effect/BgEffectPainter.kt | 175 ++++++ .../components/effect/ColorBlendToken.kt | 209 +++++++ .../ui/widget/components/effect/DeviceType.kt | 6 + .../ui/widget/components/effect/OS3BgFrag.kt | 88 +++ .../app/ui/widget/components/icon/AppIcons.kt | 9 +- .../components/log}/AppLogSheet.kt | 86 +-- .../ui/widget/components/log/CrashLogSheet.kt | 64 +++ .../widget/components/log/LogDetailSheet.kt | 44 ++ app/src/main/res/layout/activity_about.xml | 330 ----------- app/src/main/res/menu/about.xml | 13 - gradle/libs.versions.toml | 8 +- 35 files changed, 2439 insertions(+), 662 deletions(-) delete mode 100644 app/src/main/java/io/legado/app/ui/about/AboutActivity.kt create mode 100644 app/src/main/java/io/legado/app/ui/about/AboutContract.kt create mode 100644 app/src/main/java/io/legado/app/ui/about/AboutSheets.kt create mode 100644 app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt create mode 100644 app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt create mode 100644 app/src/main/java/io/legado/app/ui/util/MiuixUtils.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectBackground.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectConfig.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectModifier.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectPainter.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/ColorBlendToken.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/DeviceType.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/effect/OS3BgFrag.kt rename app/src/main/java/io/legado/app/ui/{about => widget/components/log}/AppLogSheet.kt (51%) create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/log/CrashLogSheet.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/log/LogDetailSheet.kt delete mode 100644 app/src/main/res/layout/activity_about.xml delete mode 100644 app/src/main/res/menu/about.xml diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f77832291..35230e14a 100644 --- a/.claude/settings.local.json +++ b/.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 *)" ] } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 22e174e5a..1343b12df 100644 --- a/app/build.gradle.kts +++ b/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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6fe50aa6e..9c6502674 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + @@ -212,11 +214,6 @@ android:name=".ui.book.search.SearchActivity" android:enableOnBackInvokedCallback="true" android:launchMode="standard" /> - - showMdFile(title, file) }, - onSaveLog = { saveLog() }, - onCreateHeapDump = { createHeapDump() }, - onShowCrashLogs = { showDialogFragment() }, - ) - } - - 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) - } - } - -} diff --git a/app/src/main/java/io/legado/app/ui/about/AboutContract.kt b/app/src/main/java/io/legado/app/ui/about/AboutContract.kt new file mode 100644 index 000000000..f7fde8c40 --- /dev/null +++ b/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 = 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 +} diff --git a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt index 50bda48ea..47524eae9 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt +++ b/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.SplicedColumnGroup -import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton -import io.legado.app.ui.widget.components.card.TextCard -import io.legado.app.ui.widget.components.settingItem.SettingItem import io.legado.app.ui.widget.components.SettingItemWithDivider +import io.legado.app.ui.widget.components.SplicedColumnGroup +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.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.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() + } + } + ) +} diff --git a/app/src/main/java/io/legado/app/ui/about/AboutSheets.kt b/app/src/main/java/io/legado/app/ui/about/AboutSheets.kt new file mode 100644 index 000000000..ce7e7a421 --- /dev/null +++ b/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)) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt b/app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt new file mode 100644 index 000000000..999d08c6c --- /dev/null +++ b/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 = _uiState.asStateFlow() + + private val _effects = MutableSharedFlow(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 { + val list = arrayListOf() + 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) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt b/app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt new file mode 100644 index 000000000..ff9095f94 --- /dev/null +++ b/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, + ) +} diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt index b0cb1a9ec..1f2fd67e2 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt index 66d5d7d4b..ecabe0531 100644 --- a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreen.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt index 43b35bd92..9af1db32a 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt +++ b/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 { + val viewModel = koinViewModel() + 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() }, + ) + } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt b/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt index eb170274b..ee8a3c2db 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt +++ b/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" } diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt b/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt index 1a6eaed15..ca7786091 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt +++ b/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 } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index cc5d6ab53..c43070981 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/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)) } ) } diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 889e6a026..ac5e3297e 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index 5a4daee1b..5442750a6 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt b/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt index 51eaa1dc4..c57a0c336 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt +++ b/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( diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt b/app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt index 17d1717a4..d528f9d1f 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt +++ b/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() } diff --git a/app/src/main/java/io/legado/app/ui/util/MiuixUtils.kt b/app/src/main/java/io/legado/app/ui/util/MiuixUtils.kt new file mode 100644 index 000000000..9d6c7c31c --- /dev/null +++ b/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 { 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() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectBackground.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectBackground.kt new file mode 100644 index 000000000..5fb925445 --- /dev/null +++ b/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() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectConfig.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectConfig.kt new file mode 100644 index 000000000..638026a01 --- /dev/null +++ b/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 +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectModifier.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectModifier.kt new file mode 100644 index 000000000..f1de97228 --- /dev/null +++ b/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() { + + 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() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectPainter.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectPainter.kt new file mode 100644 index 000000000..310da3185 --- /dev/null +++ b/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 + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/ColorBlendToken.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/ColorBlendToken.kt new file mode 100644 index 000000000..f97a72eaf --- /dev/null +++ b/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), + ) +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/DeviceType.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/DeviceType.kt new file mode 100644 index 000000000..64f534edc --- /dev/null +++ b/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, +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/effect/OS3BgFrag.kt b/app/src/main/java/io/legado/app/ui/widget/components/effect/OS3BgFrag.kt new file mode 100644 index 000000000..e8f310070 --- /dev/null +++ b/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); + } +""" diff --git a/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt b/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt index 1c19d5953..5afd32b94 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt +++ b/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 } diff --git a/app/src/main/java/io/legado/app/ui/about/AppLogSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt similarity index 51% rename from app/src/main/java/io/legado/app/ui/about/AppLogSheet.kt rename to app/src/main/java/io/legado/app/ui/widget/components/log/AppLogSheet.kt index 5ba55187c..c32920750 100644 --- a/app/src/main/java/io/legado/app/ui/about/AppLogSheet.kt +++ b/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 } ) } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/log/CrashLogSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/log/CrashLogSheet.kt new file mode 100644 index 000000000..e8118a146 --- /dev/null +++ b/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, + 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) + ) + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/log/LogDetailSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/log/LogDetailSheet.kt new file mode 100644 index 000000000..405ec3be8 --- /dev/null +++ b/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 + ) +} diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index a2ba54873..000000000 --- a/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/about.xml b/app/src/main/res/menu/about.xml deleted file mode 100644 index 133940e61..000000000 --- a/app/src/main/res/menu/about.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4ce7f6689..68760ee17 100644 --- a/gradle/libs.versions.toml +++ b/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" }