为MIUIX主题添加了专属的关于界面
This commit is contained in:
@@ -89,7 +89,8 @@
|
|||||||
"WebFetch(domain:s01.oss.sonatype.org)",
|
"WebFetch(domain:s01.oss.sonatype.org)",
|
||||||
"WebFetch(domain:api.github.com)",
|
"WebFetch(domain:api.github.com)",
|
||||||
"Bash(javap -public /tmp/classes.jar!top/yukonga/miuix/kmp/basic/NavigationBarKt.class)",
|
"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.image.glide)
|
||||||
implementation(libs.markwon.ext.tables)
|
implementation(libs.markwon.ext.tables)
|
||||||
implementation(libs.markwon.html)
|
implementation(libs.markwon.html)
|
||||||
|
implementation(libs.markdown.renderer.m3)
|
||||||
implementation(libs.quick.chinese.transfer.core)
|
implementation(libs.quick.chinese.transfer.core)
|
||||||
implementation(libs.hutool.crypto)
|
implementation(libs.hutool.crypto)
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
@@ -276,9 +277,8 @@ dependencies {
|
|||||||
implementation(libs.miuix.ui.android)
|
implementation(libs.miuix.ui.android)
|
||||||
implementation(libs.miuix.preference.android)
|
implementation(libs.miuix.preference.android)
|
||||||
implementation(libs.miuix.icons.android)
|
implementation(libs.miuix.icons.android)
|
||||||
//implementation(libs.miuix.blur.android)
|
implementation(libs.miuix.blur.android)
|
||||||
implementation(libs.miuix.core)
|
implementation(libs.miuix.core)
|
||||||
implementation(libs.miuix.shapes.android)
|
|
||||||
implementation(libs.capsule)
|
implementation(libs.capsule)
|
||||||
implementation(libs.backdrop)
|
implementation(libs.backdrop)
|
||||||
implementation(libs.lyricViewx)
|
implementation(libs.lyricViewx)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
@@ -212,11 +214,6 @@
|
|||||||
android:name=".ui.book.search.SearchActivity"
|
android:name=".ui.book.search.SearchActivity"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:launchMode="standard" />
|
android:launchMode="standard" />
|
||||||
<!-- 关于界面 -->
|
|
||||||
<activity
|
|
||||||
android:name=".ui.about.AboutActivity"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:launchMode="singleTask" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.about.CrashReportActivity"
|
android:name=".ui.about.CrashReportActivity"
|
||||||
android:enableOnBackInvokedCallback="true"
|
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.coil.CoverInterceptor
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.help.http.okHttpClientManga
|
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.bookmark.AllBookmarkViewModel
|
||||||
import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
||||||
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
||||||
@@ -189,6 +190,7 @@ val appModule = module {
|
|||||||
viewModelOf(::MyViewModel)
|
viewModelOf(::MyViewModel)
|
||||||
viewModelOf(::BookshelfViewModel)
|
viewModelOf(::BookshelfViewModel)
|
||||||
viewModelOf(::MainViewModel)
|
viewModelOf(::MainViewModel)
|
||||||
|
viewModelOf(::AboutViewModel)
|
||||||
viewModelOf(::GroupViewModel)
|
viewModelOf(::GroupViewModel)
|
||||||
viewModelOf(::ReplaceRuleViewModel)
|
viewModelOf(::ReplaceRuleViewModel)
|
||||||
viewModelOf(::AllBookmarkViewModel)
|
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.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -16,11 +17,15 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|||||||
import androidx.compose.material3.FilledTonalIconButton
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.R
|
||||||
import io.legado.app.constant.AppConst.appInfo
|
import io.legado.app.constant.AppConst.appInfo
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
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.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.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.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
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.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutScreen(
|
fun AboutScreen(
|
||||||
versionName: String = appInfo.versionName,
|
state: AboutUiState,
|
||||||
|
onIntent: (AboutIntent) -> Unit,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onCheckUpdate: () -> Unit = {},
|
versionName: String = appInfo.versionName,
|
||||||
onOpenUrl: (String) -> Unit = {},
|
|
||||||
onShowMdFile: (String, String) -> Unit = { _, _ -> },
|
|
||||||
onSaveLog: () -> Unit = {},
|
|
||||||
onCreateHeapDump: () -> Unit = {},
|
|
||||||
onShowCrashLogs: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
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()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
@@ -88,14 +119,16 @@ fun AboutScreen(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentWidth(Alignment.CenterHorizontally))
|
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
TextCard(
|
TextCard(
|
||||||
text = versionName,
|
text = versionName,
|
||||||
cornerRadius = 8.dp,
|
cornerRadius = 8.dp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||||
.padding(vertical = 4.dp))
|
.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
AppText(
|
AppText(
|
||||||
text = stringResource(R.string.about_description),
|
text = stringResource(R.string.about_description),
|
||||||
style = LegadoTheme.typography.bodyLarge,
|
style = LegadoTheme.typography.bodyLarge,
|
||||||
@@ -103,25 +136,25 @@ fun AboutScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||||
.padding(bottom = 4.dp))
|
.padding(bottom = 4.dp)
|
||||||
Row (modifier = Modifier
|
)
|
||||||
.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center){
|
modifier = Modifier.fillMaxWidth(),
|
||||||
FilledTonalIconButton ( onClick = { onOpenUrl("https://example.com") } ) {
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_web_outline),
|
painter = painterResource(R.drawable.ic_web_outline),
|
||||||
contentDescription = stringResource(R.string.back)
|
contentDescription = stringResource(R.string.back)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
|
||||||
FilledTonalIconButton (onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") }) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_github),
|
painter = painterResource(R.drawable.ic_github),
|
||||||
contentDescription = stringResource(R.string.back)
|
contentDescription = stringResource(R.string.back)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
FilledTonalIconButton(onClick = { onIntent(AboutIntent.CheckUpdate) }) {
|
||||||
FilledTonalIconButton(onClick = onCheckUpdate) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_import),
|
painter = painterResource(R.drawable.ic_import),
|
||||||
contentDescription = stringResource(R.string.back)
|
contentDescription = stringResource(R.string.back)
|
||||||
@@ -132,58 +165,113 @@ fun AboutScreen(
|
|||||||
SplicedColumnGroup(
|
SplicedColumnGroup(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
title = ""
|
title = ""
|
||||||
){
|
) {
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.contributors),
|
title = stringResource(R.string.contributors),
|
||||||
onClick = {
|
onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }
|
||||||
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.privacy_policy),
|
title = stringResource(R.string.privacy_policy),
|
||||||
onClick = {
|
onClick = {
|
||||||
onShowMdFile("隐私政策", "privacyPolicy.md")
|
onIntent(
|
||||||
|
AboutIntent.ShowMdFile(
|
||||||
|
"隐私政策",
|
||||||
|
"privacyPolicy.md"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.license),
|
title = stringResource(R.string.license),
|
||||||
onClick = {
|
onClick = { onIntent(AboutIntent.ShowMdFile("许可证", "LICENSE.md")) }
|
||||||
onShowMdFile("许可证", "LICENSE.md")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.disclaimer),
|
title = stringResource(R.string.disclaimer),
|
||||||
onClick = {
|
onClick = { onIntent(AboutIntent.ShowMdFile("免责声明", "disclaimer.md")) }
|
||||||
onShowMdFile("免责声明", "disclaimer.md")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.crash_log),
|
title = stringResource(R.string.crash_log),
|
||||||
onClick = onShowCrashLogs
|
onClick = { onIntent(AboutIntent.ShowCrashLogs) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.save_log),
|
title = stringResource(R.string.save_log),
|
||||||
onClick = onSaveLog
|
onClick = { onIntent(AboutIntent.SaveLog) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.create_heap_dump),
|
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.book.isLocal
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.model.BookCover
|
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.config.coverConfig.CoverConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.LocalHazeState
|
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.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
|
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.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.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
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.isLocal
|
||||||
import io.legado.app.help.book.tryParesExportFileName
|
import io.legado.app.help.book.tryParesExportFileName
|
||||||
import io.legado.app.service.ExportBookService
|
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.changesource.ChangeSourceMigrationOptionsSheet
|
||||||
import io.legado.app.ui.book.info.ChangeSourceSheet
|
import io.legado.app.ui.book.info.ChangeSourceSheet
|
||||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
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.icon.AppIcons
|
||||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
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.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.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
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.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import androidx.navigation3.ui.NavDisplay
|
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.cache.manage.BookCacheManageRouteScreen
|
||||||
import io.legado.app.ui.book.explore.ExploreShowScreen
|
import io.legado.app.ui.book.explore.ExploreShowScreen
|
||||||
import io.legado.app.ui.book.import.local.ImportBookScreen
|
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.MainRouteRssRead
|
||||||
import io.legado.app.ui.rss.read.RssReadRouteScreen
|
import io.legado.app.ui.rss.read.RssReadRouteScreen
|
||||||
import io.legado.app.ui.rss.subscription.RuleSubScreen
|
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.startActivity
|
||||||
import io.legado.app.utils.startActivityForBook
|
import io.legado.app.utils.startActivityForBook
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@@ -127,6 +136,9 @@ fun MainActivity.mainEntryProvider(
|
|||||||
onNavigateToReadRecord = {
|
onNavigateToReadRecord = {
|
||||||
onNavigateToRoute(MainRouteReadRecord)
|
onNavigateToRoute(MainRouteReadRecord)
|
||||||
},
|
},
|
||||||
|
onNavigateToAbout = {
|
||||||
|
onNavigateToRoute(MainRouteAbout)
|
||||||
|
},
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||||
)
|
)
|
||||||
@@ -420,4 +432,27 @@ fun MainActivity.mainEntryProvider(
|
|||||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
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?,
|
val exploreUrl: String?,
|
||||||
) : MainRoute
|
) : MainRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object MainRouteAbout : MainRoute
|
||||||
|
|
||||||
object MainRouteConst {
|
object MainRouteConst {
|
||||||
const val ROUTE_MAIN = "main"
|
const val ROUTE_MAIN = "main"
|
||||||
const val ROUTE_SETTINGS = "settings"
|
const val ROUTE_SETTINGS = "settings"
|
||||||
@@ -103,4 +106,5 @@ object MainRouteConst {
|
|||||||
const val ROUTE_RULE_SUB = "rss/rule_sub"
|
const val ROUTE_RULE_SUB = "rss/rule_sub"
|
||||||
const val ROUTE_READ_RECORD = "read_record"
|
const val ROUTE_READ_RECORD = "read_record"
|
||||||
const val ROUTE_READ_RECORD_OVERVIEW = "read_record_overview"
|
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 -> {
|
MainRouteReadRecordOverview -> {
|
||||||
if (currentRoute == MainRouteHome || currentRoute == MainRouteReadRecord) {
|
if (currentRoute == MainRouteHome || currentRoute == MainRouteReadRecord) {
|
||||||
backStack.add(route)
|
backStack.add(route)
|
||||||
@@ -258,6 +268,8 @@ object MainNavigator {
|
|||||||
)
|
)
|
||||||
} ?: MainRouteHome
|
} ?: MainRouteHome
|
||||||
|
|
||||||
|
MainRouteConst.ROUTE_ABOUT -> MainRouteAbout
|
||||||
|
|
||||||
else -> MainRouteHome
|
else -> MainRouteHome
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ fun MainScreen(
|
|||||||
onNavigateToRssFavorites: () -> Unit,
|
onNavigateToRssFavorites: () -> Unit,
|
||||||
onNavigateToRuleSub: () -> Unit,
|
onNavigateToRuleSub: () -> Unit,
|
||||||
onNavigateToReadRecord: () -> Unit,
|
onNavigateToReadRecord: () -> Unit,
|
||||||
|
onNavigateToAbout: () -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
@@ -154,6 +155,7 @@ fun MainScreen(
|
|||||||
|
|
||||||
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
||||||
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
||||||
|
MainEffect.NavigateToAbout -> onNavigateToAbout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +233,7 @@ fun MainScreen(
|
|||||||
modifier = Modifier.padding(start = 20.dp),
|
modifier = Modifier.padding(start = 20.dp),
|
||||||
onClick = { onNavigateToSearch(null) },
|
onClick = { onNavigateToSearch(null) },
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
icon = { Icon(Icons.Default.Search, contentDescription = null) },
|
icon = { AppIcon(Icons.Default.Search, contentDescription = null) },
|
||||||
text = { AppText(stringResource(R.string.search)) }
|
text = { AppText(stringResource(R.string.search)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ class MainViewModel(
|
|||||||
|
|
||||||
PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
|
PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
|
||||||
|
|
||||||
|
PrefClickEvent.OpenAbout -> _effects.tryEmit(MainEffect.NavigateToAbout)
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +134,7 @@ sealed interface MainEffect {
|
|||||||
|
|
||||||
data object ExitApp : MainEffect
|
data object ExitApp : MainEffect
|
||||||
data object NavigateToReadRecord : MainEffect
|
data object NavigateToReadRecord : MainEffect
|
||||||
|
data object NavigateToAbout : MainEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseRuleEvent
|
import io.legado.app.base.BaseRuleEvent
|
||||||
import io.legado.app.data.entities.BookGroup
|
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.group.GroupEditSheet
|
||||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
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.importComponents.SourceInputDialog
|
||||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
|
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.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.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
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.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.width
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||||
import androidx.compose.material.icons.automirrored.filled.HelpOutline
|
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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import io.legado.app.R
|
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.bookmark.AllBookmarkActivity
|
||||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||||
import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
|
import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
|
||||||
@@ -206,7 +205,7 @@ fun MyScreen(
|
|||||||
title = stringResource(R.string.about),
|
title = stringResource(R.string.about),
|
||||||
imageVector = Icons.Default.Info,
|
imageVector = Icons.Default.Info,
|
||||||
onClick = {
|
onClick = {
|
||||||
onNavigate(PrefClickEvent.StartActivity(AboutActivity::class.java))
|
onNavigate(PrefClickEvent.OpenAbout)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ClickableSettingItem(
|
ClickableSettingItem(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ sealed class PrefClickEvent {
|
|||||||
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
||||||
object OpenReadRecord : PrefClickEvent()
|
object OpenReadRecord : PrefClickEvent()
|
||||||
object OpenBookCacheManage : PrefClickEvent()
|
object OpenBookCacheManage : PrefClickEvent()
|
||||||
|
object OpenAbout : PrefClickEvent()
|
||||||
object ToggleWebService : PrefClickEvent()
|
object ToggleWebService : PrefClickEvent()
|
||||||
object ExitApp : 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.main.MainDestination
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
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.MiuixIcons
|
||||||
import top.yukonga.miuix.kmp.icon.basic.Search
|
import top.yukonga.miuix.kmp.icon.basic.Search
|
||||||
import top.yukonga.miuix.kmp.icon.extended.Album
|
import top.yukonga.miuix.kmp.icon.extended.Album
|
||||||
@@ -102,25 +101,25 @@ object AppIcons {
|
|||||||
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
|
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
|
||||||
return when (destination) {
|
return when (destination) {
|
||||||
MainDestination.Bookshelf -> if (isMiuix) {
|
MainDestination.Bookshelf -> if (isMiuix) {
|
||||||
if (selected) MiuixIcons.Heavy.ContactsBook else MiuixIcons.Regular.ContactsBook
|
if (selected) MiuixIcons.Regular.ContactsBook else MiuixIcons.Regular.ContactsBook
|
||||||
} else {
|
} else {
|
||||||
if (selected) Icons.AutoMirrored.Filled.LibraryBooks else Icons.AutoMirrored.Outlined.LibraryBooks
|
if (selected) Icons.AutoMirrored.Filled.LibraryBooks else Icons.AutoMirrored.Outlined.LibraryBooks
|
||||||
}
|
}
|
||||||
|
|
||||||
MainDestination.Explore -> if (isMiuix) {
|
MainDestination.Explore -> if (isMiuix) {
|
||||||
if (selected) MiuixIcons.Heavy.Album else MiuixIcons.Regular.Album
|
if (selected) MiuixIcons.Regular.Album else MiuixIcons.Regular.Album
|
||||||
} else {
|
} else {
|
||||||
if (selected) Icons.Default.Explore else Icons.Outlined.Explore
|
if (selected) Icons.Default.Explore else Icons.Outlined.Explore
|
||||||
}
|
}
|
||||||
|
|
||||||
MainDestination.Rss -> if (isMiuix) {
|
MainDestination.Rss -> if (isMiuix) {
|
||||||
if (selected) MiuixIcons.Heavy.Favorites else MiuixIcons.Regular.Favorites
|
if (selected) MiuixIcons.Regular.Favorites else MiuixIcons.Regular.Favorites
|
||||||
} else {
|
} else {
|
||||||
if (selected) Icons.Default.RssFeed else Icons.Outlined.RssFeed
|
if (selected) Icons.Default.RssFeed else Icons.Outlined.RssFeed
|
||||||
}
|
}
|
||||||
|
|
||||||
MainDestination.My -> if (isMiuix) {
|
MainDestination.My -> if (isMiuix) {
|
||||||
if (selected) MiuixIcons.Heavy.Settings else MiuixIcons.Regular.Settings
|
if (selected) MiuixIcons.Regular.Settings else MiuixIcons.Regular.Settings
|
||||||
} else {
|
} else {
|
||||||
if (selected) Icons.Default.Person else Icons.Outlined.Person
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.DeleteSweep
|
import androidx.compose.material.icons.filled.DeleteSweep
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
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.modalBottomSheet.AppModalBottomSheet
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.utils.LogUtils
|
import io.legado.app.utils.LogUtils
|
||||||
@@ -47,62 +40,41 @@ fun AppLogSheet(
|
|||||||
|
|
||||||
AppModalBottomSheet(
|
AppModalBottomSheet(
|
||||||
show = show,
|
show = show,
|
||||||
onDismissRequest = onDismissRequest
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
title = stringResource(R.string.log),
|
||||||
Row(
|
endAction = {
|
||||||
modifier = Modifier
|
MediumIconButton(
|
||||||
.fillMaxWidth()
|
onClick = {
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
AppLog.clear()
|
||||||
verticalAlignment = Alignment.CenterVertically
|
logs = emptyList()
|
||||||
) {
|
},
|
||||||
AppText(
|
imageVector = Icons.Default.DeleteSweep
|
||||||
text = stringResource(R.string.log),
|
|
||||||
style = LegadoTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
IconButton(onClick = {
|
|
||||||
AppLog.clear()
|
|
||||||
logs = emptyList()
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Default.DeleteSweep, contentDescription = "Clear")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
LazyColumn(
|
if (logs.isEmpty()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
EmptyMessage(message = "暂无日志")
|
||||||
contentPadding = PaddingValues(bottom = 16.dp)
|
} else {
|
||||||
) {
|
LazyColumn(
|
||||||
items(logs) { item ->
|
modifier = Modifier.fillMaxWidth(),
|
||||||
LogItem(item) {
|
contentPadding = PaddingValues(bottom = 16.dp)
|
||||||
item.third?.let {
|
) {
|
||||||
showStackTrace = it.stackTraceToString()
|
items(logs) { item ->
|
||||||
|
LogItem(item) {
|
||||||
|
item.third?.let {
|
||||||
|
showStackTrace = it.stackTraceToString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogDetailSheet(
|
||||||
AppAlertDialog(
|
show = showStackTrace != null,
|
||||||
data = showStackTrace,
|
|
||||||
onDismissRequest = { showStackTrace = null },
|
|
||||||
title = "Log",
|
title = "Log",
|
||||||
content = { stackTrace ->
|
content = showStackTrace.orEmpty(),
|
||||||
SelectionContainer {
|
onDismissRequest = { showStackTrace = null }
|
||||||
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 }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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"
|
lifecycleViewmodelCompose = "2.10.0"
|
||||||
liveeventbus = "1.8.14"
|
liveeventbus = "1.8.14"
|
||||||
navigation3 = "1.1.1"
|
navigation3 = "1.1.1"
|
||||||
markdownCompose = "0.5.0"
|
markdownRenderer = "0.40.2"
|
||||||
markwon = "4.6.2"
|
markwon = "4.6.2"
|
||||||
material = "1.14.0-rc01"
|
material = "1.14.0-rc01"
|
||||||
material3 = "1.5.0-alpha19"
|
material3 = "1.5.0-alpha19"
|
||||||
@@ -85,7 +85,7 @@ biometricKtx = "1.1.0"
|
|||||||
reorderable = "3.1.0"
|
reorderable = "3.1.0"
|
||||||
haze = "1.7.2"
|
haze = "1.7.2"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
miuix = "0.9.0"
|
miuix = "0.9.1"
|
||||||
backdrop = "1.0.6"
|
backdrop = "1.0.6"
|
||||||
capsule = "2.1.3"
|
capsule = "2.1.3"
|
||||||
lyricViewx = "1.3.2"
|
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-core = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", 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-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
||||||
koin-core = { module = "io.insert-koin:koin-core" }
|
koin-core = { module = "io.insert-koin:koin-core" }
|
||||||
koin-android = { group = "io.insert-koin", name = "koin-android" }
|
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-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
|
||||||
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", 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" }
|
material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
|
||||||
media-media = { module = "androidx.media:media", version.ref = "media" }
|
media-media = { module = "androidx.media:media", version.ref = "media" }
|
||||||
media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
|
media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
|
||||||
|
|||||||
Reference in New Issue
Block a user