为MIUIX主题添加了专属的关于界面
This commit is contained in:
@@ -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 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ dependencies {
|
||||
implementation(libs.markwon.image.glide)
|
||||
implementation(libs.markwon.ext.tables)
|
||||
implementation(libs.markwon.html)
|
||||
implementation(libs.markdown.renderer.m3)
|
||||
implementation(libs.quick.chinese.transfer.core)
|
||||
implementation(libs.hutool.crypto)
|
||||
//noinspection GradleDependency
|
||||
@@ -276,9 +277,8 @@ dependencies {
|
||||
implementation(libs.miuix.ui.android)
|
||||
implementation(libs.miuix.preference.android)
|
||||
implementation(libs.miuix.icons.android)
|
||||
//implementation(libs.miuix.blur.android)
|
||||
implementation(libs.miuix.blur.android)
|
||||
implementation(libs.miuix.core)
|
||||
implementation(libs.miuix.shapes.android)
|
||||
implementation(libs.capsule)
|
||||
implementation(libs.backdrop)
|
||||
implementation(libs.lyricViewx)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="top.yukonga.miuix.kmp.blur" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
@@ -212,11 +214,6 @@
|
||||
android:name=".ui.book.search.SearchActivity"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:launchMode="standard" />
|
||||
<!-- 关于界面 -->
|
||||
<activity
|
||||
android:name=".ui.about.AboutActivity"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.about.CrashReportActivity"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
|
||||
@@ -58,6 +58,7 @@ import io.legado.app.help.coil.CoverFetcher
|
||||
import io.legado.app.help.coil.CoverInterceptor
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.okHttpClientManga
|
||||
import io.legado.app.ui.about.AboutViewModel
|
||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||
import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
||||
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
||||
@@ -189,6 +190,7 @@ val appModule = module {
|
||||
viewModelOf(::MyViewModel)
|
||||
viewModelOf(::BookshelfViewModel)
|
||||
viewModelOf(::MainViewModel)
|
||||
viewModelOf(::AboutViewModel)
|
||||
viewModelOf(::GroupViewModel)
|
||||
viewModelOf(::ReplaceRuleViewModel)
|
||||
viewModelOf(::AllBookmarkViewModel)
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.update.AppUpdate
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.compress.ZipUtils
|
||||
import io.legado.app.utils.createFileIfNotExist
|
||||
import io.legado.app.utils.createFolderIfNotExist
|
||||
import io.legado.app.utils.delete
|
||||
import io.legado.app.utils.externalCache
|
||||
import io.legado.app.utils.find
|
||||
import io.legado.app.utils.list
|
||||
import io.legado.app.utils.openInputStream
|
||||
import io.legado.app.utils.openOutputStream
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.share
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.delay
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
|
||||
class AboutActivity : BaseComposeActivity() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
AboutScreen(
|
||||
onBack = { finish() },
|
||||
onCheckUpdate = { checkUpdate() },
|
||||
onOpenUrl = { openUrl(it) },
|
||||
onShowMdFile = { title, file -> showMdFile(title, file) },
|
||||
onSaveLog = { saveLog() },
|
||||
onCreateHeapDump = { createHeapDump() },
|
||||
onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() },
|
||||
)
|
||||
}
|
||||
|
||||
private val waitDialog by lazy {
|
||||
WaitDialog(this).setText(R.string.checking_update)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.about, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_share_it -> {
|
||||
share(
|
||||
getString(R.string.app_share_description),
|
||||
getString(R.string.app_name)
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMdFile(title: String, fileName: String) {
|
||||
val mdText = String(this.assets.open(fileName).readBytes())
|
||||
showDialogFragment(TextDialog(title, mdText, TextDialog.Mode.MD))
|
||||
}
|
||||
|
||||
private fun checkUpdate() {
|
||||
waitDialog.show()
|
||||
AppUpdate.gitHubUpdate?.run {
|
||||
check(lifecycleScope)
|
||||
.onSuccess {
|
||||
showDialogFragment(UpdateDialog(it, UpdateDialog.Mode.UPDATE))
|
||||
}.onError {
|
||||
appCtx.toastOnUi("${getString(R.string.check_update)}\n${it.localizedMessage}")
|
||||
}.onFinally {
|
||||
waitDialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun openUrl(@StringRes addressID: Int) {
|
||||
this.openUrl(getString(addressID))
|
||||
}
|
||||
|
||||
private fun saveLog() {
|
||||
Coroutine.async {
|
||||
val backupPath = AppConfig.backupPath ?: let {
|
||||
toastOnUi("未设置备份目录")
|
||||
return@async
|
||||
}
|
||||
if (!AppConfig.recordLog) {
|
||||
toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
|
||||
delay(3000)
|
||||
}
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
copyLogs(doc)
|
||||
copyHeapDump(doc)
|
||||
toastOnUi("已保存至备份目录")
|
||||
}.onError {
|
||||
AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHeapDump() {
|
||||
Coroutine.async {
|
||||
val backupPath = AppConfig.backupPath ?: let {
|
||||
toastOnUi("未设置备份目录")
|
||||
return@async
|
||||
}
|
||||
if (!AppConfig.recordHeapDump) {
|
||||
toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
|
||||
delay(3000)
|
||||
}
|
||||
toastOnUi("开始创建堆转储")
|
||||
System.gc()
|
||||
CrashHandler.doHeapDump(true)
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
if (!copyHeapDump(doc)) {
|
||||
toastOnUi("未找到堆转储文件")
|
||||
} else {
|
||||
toastOnUi("已保存至备份目录")
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyLogs(doc: FileDoc) {
|
||||
val cacheDir = externalCache
|
||||
val logFiles = File(cacheDir, "logs")
|
||||
val crashFiles = File(cacheDir, "crash")
|
||||
val logcatFile = File(cacheDir, "logcat.txt")
|
||||
|
||||
dumpLogcat(logcatFile)
|
||||
|
||||
val zipFile = File(cacheDir, "logs.zip")
|
||||
ZipUtils.zipFiles(arrayListOf(logFiles, crashFiles, logcatFile), zipFile)
|
||||
|
||||
doc.find("logs.zip")?.delete()
|
||||
|
||||
zipFile.inputStream().use { input ->
|
||||
doc.createFileIfNotExist("logs.zip").openOutputStream().getOrNull()
|
||||
?.use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
zipFile.delete()
|
||||
}
|
||||
|
||||
private fun copyHeapDump(doc: FileDoc): Boolean {
|
||||
val heapFile = FileDoc.fromFile(File(externalCache, "heapDump")).list()
|
||||
?.firstOrNull() ?: return false
|
||||
doc.find("heapDump")?.delete()
|
||||
val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
|
||||
heapFile.openInputStream().getOrNull()?.use { input ->
|
||||
heapDumpDoc.createFileIfNotExist(heapFile.name).openOutputStream().getOrNull()
|
||||
?.use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun dumpLogcat(file: File) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("logcat -d")
|
||||
file.outputStream().use {
|
||||
process.inputStream.copyTo(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("保存Logcat失败\n$e", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.legado.app.help.update.AppUpdate
|
||||
import io.legado.app.utils.FileDoc
|
||||
|
||||
@Stable
|
||||
data class AboutUiState(
|
||||
val sheet: AboutSheet = AboutSheet.None,
|
||||
val dialog: AboutDialog? = null,
|
||||
val crashLogFiles: List<FileDoc> = emptyList(),
|
||||
)
|
||||
|
||||
sealed interface AboutSheet {
|
||||
data object None : AboutSheet
|
||||
data class Markdown(val title: String, val content: String) : AboutSheet
|
||||
data object CrashLogs : AboutSheet
|
||||
data class Update(
|
||||
val updateInfo: AppUpdate.UpdateInfo,
|
||||
val mode: UpdateMode = UpdateMode.UPDATE,
|
||||
) : AboutSheet
|
||||
}
|
||||
|
||||
enum class UpdateMode { UPDATE, VIEW_LOG }
|
||||
|
||||
sealed interface AboutDialog {
|
||||
data object CheckingUpdate : AboutDialog
|
||||
}
|
||||
|
||||
sealed interface AboutIntent {
|
||||
data object DismissSheet : AboutIntent
|
||||
data object DismissDialog : AboutIntent
|
||||
data object CheckUpdate : AboutIntent
|
||||
data class ShowMdFile(val title: String, val fileName: String) : AboutIntent
|
||||
data object ShowCrashLogs : AboutIntent
|
||||
data object SaveLog : AboutIntent
|
||||
data object CreateHeapDump : AboutIntent
|
||||
data class OpenUrl(val url: String) : AboutIntent
|
||||
data object ClearCrashLogs : AboutIntent
|
||||
data class ReadCrashFile(val fileDoc: FileDoc) : AboutIntent
|
||||
data object StartDownload : AboutIntent
|
||||
}
|
||||
|
||||
sealed interface AboutEffect {
|
||||
data class OpenUrl(val url: String) : AboutEffect
|
||||
data class ShowToast(val message: String) : AboutEffect
|
||||
data class StartDownload(val url: String, val fileName: String) : AboutEffect
|
||||
}
|
||||
@@ -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>(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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.update.AppUpdate
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.compress.ZipUtils
|
||||
import io.legado.app.utils.createFileIfNotExist
|
||||
import io.legado.app.utils.createFolderIfNotExist
|
||||
import io.legado.app.utils.delete
|
||||
import io.legado.app.utils.externalCache
|
||||
import io.legado.app.utils.find
|
||||
import io.legado.app.utils.getFile
|
||||
import io.legado.app.utils.list
|
||||
import io.legado.app.utils.openInputStream
|
||||
import io.legado.app.utils.openOutputStream
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
|
||||
class AboutViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
private val _uiState = MutableStateFlow(AboutUiState())
|
||||
val uiState: StateFlow<AboutUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _effects = MutableSharedFlow<AboutEffect>(extraBufferCapacity = 8)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
fun onIntent(intent: AboutIntent) {
|
||||
when (intent) {
|
||||
is AboutIntent.DismissSheet -> _uiState.update { it.copy(sheet = AboutSheet.None) }
|
||||
is AboutIntent.DismissDialog -> _uiState.update { it.copy(dialog = null) }
|
||||
is AboutIntent.CheckUpdate -> checkUpdate()
|
||||
is AboutIntent.ShowMdFile -> showMdFile(intent.title, intent.fileName)
|
||||
is AboutIntent.ShowCrashLogs -> showCrashLogs()
|
||||
is AboutIntent.SaveLog -> saveLog()
|
||||
is AboutIntent.CreateHeapDump -> createHeapDump()
|
||||
is AboutIntent.OpenUrl -> _effects.tryEmit(AboutEffect.OpenUrl(intent.url))
|
||||
is AboutIntent.ClearCrashLogs -> clearCrashLogs()
|
||||
is AboutIntent.ReadCrashFile -> readCrashFile(intent.fileDoc)
|
||||
is AboutIntent.StartDownload -> startDownload()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUpdate() {
|
||||
_uiState.update { it.copy(dialog = AboutDialog.CheckingUpdate) }
|
||||
AppUpdate.gitHubUpdate?.run {
|
||||
check(viewModelScope)
|
||||
.onSuccess { updateInfo ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
sheet = AboutSheet.Update(updateInfo)
|
||||
)
|
||||
}
|
||||
}.onError { e ->
|
||||
_uiState.update { it.copy(dialog = null) }
|
||||
_effects.tryEmit(
|
||||
AboutEffect.ShowToast("${context.getString(R.string.check_update)}\n${e.localizedMessage}")
|
||||
)
|
||||
}.onFinally {
|
||||
_uiState.update { it.copy(dialog = null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMdFile(title: String, fileName: String) {
|
||||
execute {
|
||||
String(context.assets.open(fileName).readBytes())
|
||||
}.onSuccess { content ->
|
||||
_uiState.update { it.copy(sheet = AboutSheet.Markdown(title, content)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCrashLogs() {
|
||||
execute {
|
||||
loadCrashLogFiles()
|
||||
}.onSuccess { files ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
crashLogFiles = files,
|
||||
sheet = AboutSheet.CrashLogs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readCrashFile(fileDoc: FileDoc) {
|
||||
execute {
|
||||
String(fileDoc.readBytes())
|
||||
}.onSuccess { content ->
|
||||
_uiState.update { it.copy(sheet = AboutSheet.Markdown(fileDoc.name, content)) }
|
||||
}.onError {
|
||||
_effects.tryEmit(AboutEffect.ShowToast(it.localizedMessage ?: ""))
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCrashLogs() {
|
||||
execute {
|
||||
context.externalCacheDir
|
||||
?.getFile("crash")
|
||||
?.let { FileUtils.delete(it, false) }
|
||||
val backupPath = AppConfig.backupPath
|
||||
if (!backupPath.isNullOrEmpty()) {
|
||||
val uri = Uri.parse(backupPath)
|
||||
FileDoc.fromUri(uri, true)
|
||||
.find("crash")
|
||||
?.delete()
|
||||
}
|
||||
}.onError {
|
||||
_effects.tryEmit(AboutEffect.ShowToast(it.localizedMessage ?: ""))
|
||||
}.onFinally {
|
||||
execute {
|
||||
loadCrashLogFiles()
|
||||
}.onSuccess { files ->
|
||||
_uiState.update { it.copy(crashLogFiles = files) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCrashLogFiles(): List<FileDoc> {
|
||||
val list = arrayListOf<FileDoc>()
|
||||
context.externalCacheDir
|
||||
?.getFile("crash")
|
||||
?.listFiles(FileFilter { it.isFile })
|
||||
?.forEach { list.add(FileDoc.fromFile(it)) }
|
||||
val backupPath = AppConfig.backupPath
|
||||
if (!backupPath.isNullOrEmpty()) {
|
||||
val uri = Uri.parse(backupPath)
|
||||
FileDoc.fromUri(uri, true)
|
||||
.find("crash")
|
||||
?.list { !it.isDir }
|
||||
?.let { list.addAll(it) }
|
||||
}
|
||||
return list.sortedByDescending { it.name }.distinctBy { it.name }
|
||||
}
|
||||
|
||||
private fun saveLog() {
|
||||
execute {
|
||||
val backupPath = AppConfig.backupPath ?: run {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("未设置备份目录"))
|
||||
return@execute
|
||||
}
|
||||
if (!AppConfig.recordLog) {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("未开启日志记录,请去其他设置里打开记录日志"))
|
||||
delay(3000)
|
||||
}
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
copyLogs(doc)
|
||||
copyHeapDump(doc)
|
||||
_effects.tryEmit(AboutEffect.ShowToast("已保存至备份目录"))
|
||||
}.onError {
|
||||
AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHeapDump() {
|
||||
execute {
|
||||
val backupPath = AppConfig.backupPath ?: run {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("未设置备份目录"))
|
||||
return@execute
|
||||
}
|
||||
if (!AppConfig.recordHeapDump) {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("未开启堆转储记录,请去其他设置里打开记录堆转储"))
|
||||
delay(3000)
|
||||
}
|
||||
_effects.tryEmit(AboutEffect.ShowToast("开始创建堆转储"))
|
||||
System.gc()
|
||||
CrashHandler.doHeapDump(true)
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
if (!copyHeapDump(doc)) {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("未找到堆转储文件"))
|
||||
} else {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("已保存至备份目录"))
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDownload() {
|
||||
val sheet = _uiState.value.sheet
|
||||
if (sheet is AboutSheet.Update) {
|
||||
val info = sheet.updateInfo
|
||||
if (info.downloadUrl.isBlank() || info.fileName.isBlank()) {
|
||||
_effects.tryEmit(AboutEffect.ShowToast("下载信息不完整"))
|
||||
} else {
|
||||
_effects.tryEmit(AboutEffect.StartDownload(info.downloadUrl, info.fileName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyLogs(doc: FileDoc) {
|
||||
val cacheDir = context.externalCache
|
||||
val logFiles = File(cacheDir, "logs")
|
||||
val crashFiles = File(cacheDir, "crash")
|
||||
val logcatFile = File(cacheDir, "logcat.txt")
|
||||
dumpLogcat(logcatFile)
|
||||
val zipFile = File(cacheDir, "logs.zip")
|
||||
ZipUtils.zipFiles(arrayListOf(logFiles, crashFiles, logcatFile), zipFile)
|
||||
doc.find("logs.zip")?.delete()
|
||||
zipFile.inputStream().use { input ->
|
||||
doc.createFileIfNotExist("logs.zip").openOutputStream().getOrNull()
|
||||
?.use { input.copyTo(it) }
|
||||
}
|
||||
zipFile.delete()
|
||||
}
|
||||
|
||||
private fun copyHeapDump(doc: FileDoc): Boolean {
|
||||
val heapFile = FileDoc.fromFile(File(context.externalCache, "heapDump")).list()
|
||||
?.firstOrNull() ?: return false
|
||||
doc.find("heapDump")?.delete()
|
||||
val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
|
||||
heapFile.openInputStream().getOrNull()?.use { input ->
|
||||
heapDumpDoc.createFileIfNotExist(heapFile.name).openOutputStream().getOrNull()
|
||||
?.use { input.copyTo(it) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun dumpLogcat(file: File) {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("logcat -d")
|
||||
file.outputStream().use { process.inputStream.copyTo(it) }
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("保存Logcat失败\n$e", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,11 +7,17 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import io.legado.app.model.Download
|
||||
import io.legado.app.ui.about.AboutEffect
|
||||
import io.legado.app.ui.about.AboutScreen
|
||||
import io.legado.app.ui.about.AboutViewModel
|
||||
import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen
|
||||
import io.legado.app.ui.book.explore.ExploreShowScreen
|
||||
import io.legado.app.ui.book.import.local.ImportBookScreen
|
||||
@@ -40,9 +46,12 @@ import io.legado.app.ui.rss.favorites.RssFavoritesScreen
|
||||
import io.legado.app.ui.rss.read.MainRouteRssRead
|
||||
import io.legado.app.ui.rss.read.RssReadRouteScreen
|
||||
import io.legado.app.ui.rss.subscription.RuleSubScreen
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@@ -127,6 +136,9 @@ fun MainActivity.mainEntryProvider(
|
||||
onNavigateToReadRecord = {
|
||||
onNavigateToRoute(MainRouteReadRecord)
|
||||
},
|
||||
onNavigateToAbout = {
|
||||
onNavigateToRoute(MainRouteAbout)
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
)
|
||||
@@ -420,4 +432,27 @@ fun MainActivity.mainEntryProvider(
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteAbout> {
|
||||
val viewModel = koinViewModel<AboutViewModel>()
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.effects.collectLatest { effect ->
|
||||
when (effect) {
|
||||
is AboutEffect.OpenUrl -> context.openUrl(effect.url)
|
||||
is AboutEffect.ShowToast -> context.toastOnUi(effect.message)
|
||||
is AboutEffect.StartDownload -> Download.start(
|
||||
context,
|
||||
effect.url,
|
||||
effect.fileName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AboutScreen(
|
||||
state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||
onIntent = viewModel::onIntent,
|
||||
onBack = { onNavigateBack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package io.legado.app.ui.util
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.blur.BlendColorEntry
|
||||
import top.yukonga.miuix.kmp.blur.BlurColors
|
||||
import top.yukonga.miuix.kmp.blur.LayerBackdrop
|
||||
import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
|
||||
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
|
||||
import top.yukonga.miuix.kmp.blur.textureBlur
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.utils.overScrollVertical
|
||||
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
|
||||
|
||||
@Stable
|
||||
data class AppState(
|
||||
val enableBlur: Boolean = true,
|
||||
val enableScrollEndHaptic: Boolean = true,
|
||||
val showTopAppBar: Boolean = true,
|
||||
)
|
||||
|
||||
val LocalAppState = compositionLocalOf { AppState() }
|
||||
val LocalIsWideScreen = staticCompositionLocalOf { false }
|
||||
|
||||
interface MiuixNavigator {
|
||||
fun pop()
|
||||
fun push(route: Any)
|
||||
}
|
||||
|
||||
val LocalNavigator = staticCompositionLocalOf<MiuixNavigator> { error("No navigator found!") }
|
||||
|
||||
@Composable
|
||||
fun shouldShowSplitPane(): Boolean {
|
||||
val windowInfo = LocalWindowInfo.current
|
||||
val density = LocalDensity.current
|
||||
return with(density) {
|
||||
val widthDp = windowInfo.containerSize.width.toDp()
|
||||
val heightDp = windowInfo.containerSize.height.toDp()
|
||||
val ratio = heightDp / widthDp
|
||||
widthDp >= 840.dp || (widthDp >= 600.dp && ratio < 1.2f)
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.pageScrollModifiers(
|
||||
enableScrollEndHaptic: Boolean,
|
||||
showTopAppBar: Boolean,
|
||||
topAppBarScrollBehavior: ScrollBehavior,
|
||||
): Modifier = this
|
||||
.then(if (enableScrollEndHaptic) Modifier.scrollEndHaptic() else Modifier)
|
||||
.overScrollVertical()
|
||||
.then(if (showTopAppBar) Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) else Modifier)
|
||||
.fillMaxHeight()
|
||||
|
||||
@Composable
|
||||
fun pageContentPadding(
|
||||
innerPadding: PaddingValues,
|
||||
outerPadding: PaddingValues,
|
||||
isWideScreen: Boolean,
|
||||
extraTop: Dp = 0.dp,
|
||||
extraStart: Dp = 0.dp,
|
||||
extraEnd: Dp = 0.dp,
|
||||
): PaddingValues {
|
||||
val topPadding = innerPadding.calculateTopPadding() + extraTop
|
||||
val bottomPadding = if (isWideScreen) {
|
||||
WindowInsets.navigationBars.asPaddingValues()
|
||||
.calculateBottomPadding() + outerPadding.calculateBottomPadding()
|
||||
} else {
|
||||
outerPadding.calculateBottomPadding()
|
||||
}
|
||||
return remember(topPadding, bottomPadding, extraStart, extraEnd) {
|
||||
PaddingValues(
|
||||
top = topPadding,
|
||||
start = extraStart,
|
||||
end = extraEnd,
|
||||
bottom = bottomPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdaptiveTopAppBar(
|
||||
title: String,
|
||||
showTopAppBar: Boolean,
|
||||
isWideScreen: Boolean,
|
||||
scrollBehavior: ScrollBehavior,
|
||||
subtitle: String = "",
|
||||
color: Color = MiuixTheme.colorScheme.surface,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
bottomContent: @Composable () -> Unit = {},
|
||||
) {
|
||||
if (showTopAppBar) {
|
||||
if (isWideScreen) {
|
||||
SmallTopAppBar(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
color = color,
|
||||
scrollBehavior = scrollBehavior,
|
||||
defaultWindowInsetsPadding = false,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
bottomContent = bottomContent,
|
||||
)
|
||||
} else {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
color = color,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
bottomContent = bottomContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBlurBackdrop(): LayerBackdrop? {
|
||||
val appState = LocalAppState.current
|
||||
if (!appState.enableBlur || !isRenderEffectSupported()) return null
|
||||
val surfaceColor = MiuixTheme.colorScheme.surface
|
||||
return rememberLayerBackdrop {
|
||||
drawRect(surfaceColor)
|
||||
drawContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BlurredBar(
|
||||
backdrop: LayerBackdrop?,
|
||||
blurEnabled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = if (blurEnabled && backdrop != null) {
|
||||
Modifier.textureBlur(
|
||||
backdrop = backdrop,
|
||||
shape = RectangleShape,
|
||||
blurRadius = 25f,
|
||||
colors = BlurColors(
|
||||
blendColors = listOf(
|
||||
BlendColorEntry(color = MiuixTheme.colorScheme.surface.copy(0.8f)),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package io.legado.app.ui.widget.components.effect
|
||||
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.node.DrawModifierNode
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.node.invalidateDraw
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal fun Modifier.bgEffectDraw(
|
||||
painter: BgEffectPainter,
|
||||
preset: BgEffectConfig.Config,
|
||||
deviceType: DeviceType,
|
||||
isDarkTheme: Boolean,
|
||||
surface: Color,
|
||||
effectBackground: Boolean,
|
||||
isFullSize: Boolean,
|
||||
playing: Boolean,
|
||||
colorStage: () -> Float,
|
||||
alpha: () -> Float,
|
||||
): Modifier = this then BgEffectElement(
|
||||
painter = painter,
|
||||
preset = preset,
|
||||
deviceType = deviceType,
|
||||
isDarkTheme = isDarkTheme,
|
||||
surface = surface,
|
||||
effectBackground = effectBackground,
|
||||
isFullSize = isFullSize,
|
||||
playing = playing,
|
||||
colorStage = colorStage,
|
||||
alpha = alpha,
|
||||
)
|
||||
|
||||
private data class BgEffectElement(
|
||||
val painter: BgEffectPainter,
|
||||
val preset: BgEffectConfig.Config,
|
||||
val deviceType: DeviceType,
|
||||
val isDarkTheme: Boolean,
|
||||
val surface: Color,
|
||||
val effectBackground: Boolean,
|
||||
val isFullSize: Boolean,
|
||||
val playing: Boolean,
|
||||
val colorStage: () -> Float,
|
||||
val alpha: () -> Float,
|
||||
) : ModifierNodeElement<BgEffectNode>() {
|
||||
|
||||
override fun create(): BgEffectNode = BgEffectNode(
|
||||
painter = painter,
|
||||
preset = preset,
|
||||
deviceType = deviceType,
|
||||
isDarkTheme = isDarkTheme,
|
||||
surface = surface,
|
||||
effectBackground = effectBackground,
|
||||
isFullSize = isFullSize,
|
||||
playing = playing,
|
||||
colorStage = colorStage,
|
||||
alpha = alpha,
|
||||
)
|
||||
|
||||
override fun update(node: BgEffectNode) {
|
||||
node.update(
|
||||
painter = painter,
|
||||
preset = preset,
|
||||
deviceType = deviceType,
|
||||
isDarkTheme = isDarkTheme,
|
||||
surface = surface,
|
||||
effectBackground = effectBackground,
|
||||
isFullSize = isFullSize,
|
||||
playing = playing,
|
||||
colorStage = colorStage,
|
||||
alpha = alpha,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class BgEffectNode(
|
||||
private var painter: BgEffectPainter,
|
||||
private var preset: BgEffectConfig.Config,
|
||||
private var deviceType: DeviceType,
|
||||
private var isDarkTheme: Boolean,
|
||||
private var surface: Color,
|
||||
private var effectBackground: Boolean,
|
||||
private var isFullSize: Boolean,
|
||||
private var playing: Boolean,
|
||||
private var colorStage: () -> Float,
|
||||
private var alpha: () -> Float,
|
||||
) : Modifier.Node(),
|
||||
DrawModifierNode {
|
||||
|
||||
private var animationJob: Job? = null
|
||||
private var animTime: Float = 0f
|
||||
private var startOffset: Float = 0f
|
||||
|
||||
override fun onAttach() {
|
||||
if (playing) startAnimation()
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
animationJob?.cancel()
|
||||
animationJob = null
|
||||
}
|
||||
|
||||
fun update(
|
||||
painter: BgEffectPainter,
|
||||
preset: BgEffectConfig.Config,
|
||||
deviceType: DeviceType,
|
||||
isDarkTheme: Boolean,
|
||||
surface: Color,
|
||||
effectBackground: Boolean,
|
||||
isFullSize: Boolean,
|
||||
playing: Boolean,
|
||||
colorStage: () -> Float,
|
||||
alpha: () -> Float,
|
||||
) {
|
||||
this.painter = painter
|
||||
this.preset = preset
|
||||
this.deviceType = deviceType
|
||||
this.isDarkTheme = isDarkTheme
|
||||
this.surface = surface
|
||||
this.effectBackground = effectBackground
|
||||
this.isFullSize = isFullSize
|
||||
this.colorStage = colorStage
|
||||
this.alpha = alpha
|
||||
|
||||
if (this.playing != playing) {
|
||||
this.playing = playing
|
||||
if (playing) {
|
||||
startAnimation()
|
||||
} else {
|
||||
animationJob?.cancel()
|
||||
animationJob = null
|
||||
}
|
||||
}
|
||||
invalidateDraw()
|
||||
}
|
||||
|
||||
private fun startAnimation() {
|
||||
animationJob?.cancel()
|
||||
startOffset = animTime
|
||||
animationJob = coroutineScope.launch {
|
||||
val minDeltaNanos = 1_000_000_000L / 60L
|
||||
val origin = withFrameNanos { it }
|
||||
var lastEmit = origin
|
||||
while (isActive) {
|
||||
val now = withFrameNanos { it }
|
||||
if (now - lastEmit < minDeltaNanos) continue
|
||||
lastEmit = now
|
||||
animTime = startOffset + (now - origin) / 1_000_000_000f
|
||||
invalidateDraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun ContentDrawScope.draw() {
|
||||
drawRect(surface)
|
||||
if (effectBackground) {
|
||||
val alphaValue = alpha()
|
||||
if (alphaValue > 0f) {
|
||||
val drawHeight = if (isFullSize) size.height * 0.8f else size.height * 0.5f
|
||||
|
||||
painter.updateResolution(size.width, size.height)
|
||||
painter.updateBoundIfNeeded(drawHeight, size.height, size.width)
|
||||
painter.updatePresetIfNeeded(deviceType, isDarkTheme)
|
||||
painter.updateColors(preset, colorStage())
|
||||
painter.updateAnimTime(animTime)
|
||||
painter.updatePointsAnim(animTime, preset)
|
||||
|
||||
drawRect(painter.brush, alpha = alphaValue)
|
||||
}
|
||||
}
|
||||
drawContent()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.ui.widget.components.effect
|
||||
|
||||
enum class DeviceType {
|
||||
PHONE,
|
||||
PAD,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
"""
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.legado.app.ui.main.MainDestination
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import okhttp3.internal.http2.Settings
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.basic.Search
|
||||
import top.yukonga.miuix.kmp.icon.extended.Album
|
||||
@@ -102,25 +101,25 @@ object AppIcons {
|
||||
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
|
||||
return when (destination) {
|
||||
MainDestination.Bookshelf -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.ContactsBook else MiuixIcons.Regular.ContactsBook
|
||||
if (selected) MiuixIcons.Regular.ContactsBook else MiuixIcons.Regular.ContactsBook
|
||||
} else {
|
||||
if (selected) Icons.AutoMirrored.Filled.LibraryBooks else Icons.AutoMirrored.Outlined.LibraryBooks
|
||||
}
|
||||
|
||||
MainDestination.Explore -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.Album else MiuixIcons.Regular.Album
|
||||
if (selected) MiuixIcons.Regular.Album else MiuixIcons.Regular.Album
|
||||
} else {
|
||||
if (selected) Icons.Default.Explore else Icons.Outlined.Explore
|
||||
}
|
||||
|
||||
MainDestination.Rss -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.Favorites else MiuixIcons.Regular.Favorites
|
||||
if (selected) MiuixIcons.Regular.Favorites else MiuixIcons.Regular.Favorites
|
||||
} else {
|
||||
if (selected) Icons.Default.RssFeed else Icons.Outlined.RssFeed
|
||||
}
|
||||
|
||||
MainDestination.My -> if (isMiuix) {
|
||||
if (selected) MiuixIcons.Heavy.Settings else MiuixIcons.Regular.Settings
|
||||
if (selected) MiuixIcons.Regular.Settings else MiuixIcons.Regular.Settings
|
||||
} else {
|
||||
if (selected) Icons.Default.Person else Icons.Outlined.Person
|
||||
}
|
||||
|
||||
+29
-57
@@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package io.legado.app.ui.widget.components.log
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.FileDoc
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CrashLogSheet(
|
||||
show: Boolean,
|
||||
logFiles: List<FileDoc>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onReadFile: (FileDoc) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
) {
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
endAction = {
|
||||
IconButton(onClick = onClear) {
|
||||
Icon(Icons.Default.DeleteSweep, contentDescription = "Clear")
|
||||
}
|
||||
},
|
||||
title = stringResource(R.string.crash_log),
|
||||
) {
|
||||
if (logFiles.isEmpty()) {
|
||||
EmptyMessage(message = "暂无崩溃日志")
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
items(logFiles) { fileDoc ->
|
||||
AppText(
|
||||
text = fileDoc.name,
|
||||
style = LegadoTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onReadFile(fileDoc) }
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="io.legado.app.ui.about.AboutActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
|
||||
app:collapsedTitleTextAppearance="?attr/textAppearanceTitleLarge"
|
||||
app:expandedTitleTextAppearance="?attr/textAppearanceHeadlineLarge"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||
app:titleCollapseMode="scale">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:theme="@style/ThemeOverlay.Material3.Toolbar.CircleNavigationButton"
|
||||
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_arrow_back"
|
||||
app:title="@string/about" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cd_about"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="120dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:src="@drawable/ic_launcher_foreground"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/app_name"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cd_about" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cd_ver"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="8dp"
|
||||
app:cardBackgroundColor="?attr/colorTertiaryContainer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_name"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:text="@string/text"
|
||||
android:textColor="?attr/colorOnTertiaryContainer"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="SmallSp" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/about_description"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cd_ver" />
|
||||
|
||||
<com.google.android.material.button.MaterialButtonGroup
|
||||
android:id="@+id/ll_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_detail">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_web"
|
||||
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:icon="@drawable/ic_web_outline" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_github"
|
||||
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:icon="@drawable/ic_github" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_update"
|
||||
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_import" />
|
||||
</com.google.android.material.button.MaterialButtonGroup>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_contributors"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_btn">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/contributors"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/contributors_summary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_update_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_contributors">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/update_log"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_privacy_policy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_update_log">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/privacy_policy"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_license"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_privacy_policy">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/license"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_disclaimer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_license">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/disclaimer"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_crash_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_disclaimer">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/crash_log"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_save_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_crash_log">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/save_log"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_create_heap_dump"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_save_log">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:text="@string/create_heap_dump"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_share_it"
|
||||
android:title="@string/share"
|
||||
android:icon="@drawable/ic_share"
|
||||
app:showAsAction="always"
|
||||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
</menu>
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user