初步用Compose重构关于界面
增加了以后会使用的一些自定义Compose控件 尝试让机器人自动发布预发布版本 修复透明模式下搜索框"删除所有"文字不明显 #354 忙忙忙咩咩咩咩
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.utils.disableAutoFill
|
||||
import io.legado.app.utils.fullScreen
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.setStatusBarColorAuto
|
||||
import io.legado.app.utils.themeColor
|
||||
import io.legado.app.utils.windowSize
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseComposeActivity(
|
||||
val fullScreen: Boolean = true,
|
||||
private val toolBarTheme: Theme = Theme.Auto,
|
||||
private val transparent: Boolean = false,
|
||||
private val imageBg: Boolean = true
|
||||
) : AppCompatActivity() {
|
||||
|
||||
@Composable
|
||||
protected abstract fun Content()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initTheme()
|
||||
window.decorView.disableAutoFill()
|
||||
AppContextWrapper.applyLocaleAndFont(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Compose 入口
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSystemBar()
|
||||
|
||||
if (imageBg) {
|
||||
upBackgroundImage()
|
||||
}
|
||||
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
open fun setupSystemBar() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
if (fullScreen) fullScreen()
|
||||
|
||||
setStatusBarColorAuto(
|
||||
themeColor(com.google.android.material.R.attr.colorSurface),
|
||||
true,
|
||||
fullScreen
|
||||
)
|
||||
}
|
||||
|
||||
open fun upBackgroundImage() {
|
||||
try {
|
||||
ThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
||||
window.setBackgroundDrawable(it.toDrawable(resources))
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
open fun observeLiveBus() {
|
||||
observeEvent<String>(EventBus.RECREATE) {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
|
||||
open fun initTheme() {
|
||||
when (getPrefString("app_theme", "0")) {
|
||||
"0" -> {
|
||||
DynamicColors.applyToActivitiesIfAvailable(application)
|
||||
}
|
||||
"1" -> setTheme(R.style.Theme_Base_GR)
|
||||
"2" -> setTheme(R.style.Theme_Base_Lemon)
|
||||
"3" -> setTheme(R.style.Theme_Base_WH)
|
||||
"4" -> setTheme(R.style.Theme_Base_Elink)
|
||||
"5" -> setTheme(R.style.Theme_Base_Sora)
|
||||
"6" -> setTheme(R.style.Theme_Base_August)
|
||||
"7" -> setTheme(R.style.Theme_Base_Carlotta)
|
||||
"8" -> setTheme(R.style.Theme_Base_Koharu)
|
||||
"9" -> setTheme(R.style.Theme_Base_Yuuka)
|
||||
"10" -> setTheme(R.style.Theme_Base_Phoebe)
|
||||
"11" -> setTheme(R.style.Theme_Base_Mujika)
|
||||
"12" -> {
|
||||
if (AppConfig.customMode == "accent")
|
||||
setTheme(R.style.ThemeOverlay_WhiteBackground)
|
||||
|
||||
val colorImagePath = getPrefString(PreferKey.colorImage)
|
||||
if (!colorImagePath.isNullOrBlank()) {
|
||||
val file = File(colorImagePath)
|
||||
if (file.exists()) {
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
if (bitmap != null) {
|
||||
val colorAccuracy = true
|
||||
val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
|
||||
val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
|
||||
val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
|
||||
|
||||
val options = DynamicColorsOptions.Builder()
|
||||
.setContentBasedSource(scaledBitmap)
|
||||
.build()
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(application, options)
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
}else{
|
||||
DynamicColors.applyToActivitiesIfAvailable(
|
||||
application,
|
||||
DynamicColorsOptions.Builder()
|
||||
.setContentBasedSource(application.primaryColor)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"13" -> setTheme(R.style.AppTheme_Transparent)
|
||||
}
|
||||
|
||||
if (AppConfig.pureBlack)
|
||||
setTheme(R.style.ThemeOverlay_PureBlack)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val colorScheme = when {
|
||||
dynamicColor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ->
|
||||
if (darkTheme) dynamicDarkColorScheme(context)
|
||||
else dynamicLightColorScheme(context)
|
||||
|
||||
darkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography,
|
||||
shapes = AppShapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
// Material3 Typography
|
||||
val AppTypography = Typography()
|
||||
|
||||
// Material3 Shapes
|
||||
val AppShapes = Shapes()
|
||||
@@ -1,7 +1,5 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.filletBackground
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -9,21 +7,16 @@ import androidx.activity.compose.setContent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.core.app.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseActivity
|
||||
import io.legado.app.constant.AppConst.appInfo
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.databinding.ActivityAboutBinding
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.update.AppUpdate
|
||||
import io.legado.app.help.update.AppUpdateGitHub
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.utils.FileDoc
|
||||
@@ -40,18 +33,24 @@ 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 io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
class AboutActivity : BaseComposeActivity() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
AboutScreen(
|
||||
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)
|
||||
@@ -60,18 +59,6 @@ class AboutActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
AboutScreen(
|
||||
onCheckUpdate = { checkUpdate() },
|
||||
onOpenUrl = { openUrl(it) },
|
||||
onShowMdFile = { title, file -> showMdFile(title, file) },
|
||||
onSaveLog = { saveLog() },
|
||||
onCreateHeapDump = { createHeapDump() },
|
||||
onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
@@ -100,9 +87,9 @@ class AboutActivity : AppCompatActivity() {
|
||||
private fun checkUpdate() {
|
||||
waitDialog.show()
|
||||
AppUpdate.gitHubUpdate?.run {
|
||||
kotlin.check(lifecycleScope)
|
||||
check(lifecycleScope)
|
||||
.onSuccess {
|
||||
showDialogFragment(UpdateDialog(it))
|
||||
showDialogFragment(UpdateDialog(it, UpdateDialog.Mode.UPDATE))
|
||||
}.onError {
|
||||
appCtx.toastOnUi("${getString(R.string.check_update)}\n${it.localizedMessage}")
|
||||
}.onFinally {
|
||||
@@ -119,17 +106,17 @@ class AboutActivity : AppCompatActivity() {
|
||||
private fun saveLog() {
|
||||
Coroutine.async {
|
||||
val backupPath = AppConfig.backupPath ?: let {
|
||||
appCtx.toastOnUi("未设置备份目录")
|
||||
toastOnUi("未设置备份目录")
|
||||
return@async
|
||||
}
|
||||
if (!AppConfig.recordLog) {
|
||||
appCtx.toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
|
||||
toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
|
||||
delay(3000)
|
||||
}
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
copyLogs(doc)
|
||||
copyHeapDump(doc)
|
||||
appCtx.toastOnUi("已保存至备份目录")
|
||||
toastOnUi("已保存至备份目录")
|
||||
}.onError {
|
||||
AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
|
||||
}
|
||||
@@ -138,21 +125,21 @@ class AboutActivity : AppCompatActivity() {
|
||||
private fun createHeapDump() {
|
||||
Coroutine.async {
|
||||
val backupPath = AppConfig.backupPath ?: let {
|
||||
appCtx.toastOnUi("未设置备份目录")
|
||||
toastOnUi("未设置备份目录")
|
||||
return@async
|
||||
}
|
||||
if (!AppConfig.recordHeapDump) {
|
||||
appCtx.toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
|
||||
toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
|
||||
delay(3000)
|
||||
}
|
||||
appCtx.toastOnUi("开始创建堆转储")
|
||||
toastOnUi("开始创建堆转储")
|
||||
System.gc()
|
||||
CrashHandler.doHeapDump(true)
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
if (!copyHeapDump(doc)) {
|
||||
appCtx.toastOnUi("未找到堆转储文件")
|
||||
toastOnUi("未找到堆转储文件")
|
||||
} else {
|
||||
appCtx.toastOnUi("已保存至备份目录")
|
||||
toastOnUi("已保存至备份目录")
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
|
||||
@@ -160,7 +147,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun copyLogs(doc: FileDoc) {
|
||||
val cacheDir = appCtx.externalCache
|
||||
val cacheDir = externalCache
|
||||
val logFiles = File(cacheDir, "logs")
|
||||
val crashFiles = File(cacheDir, "crash")
|
||||
val logcatFile = File(cacheDir, "logcat.txt")
|
||||
@@ -182,7 +169,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun copyHeapDump(doc: FileDoc): Boolean {
|
||||
val heapFile = FileDoc.fromFile(File(appCtx.externalCache, "heapDump")).list()
|
||||
val heapFile = FileDoc.fromFile(File(externalCache, "heapDump")).list()
|
||||
?.firstOrNull() ?: return false
|
||||
doc.find("heapDump")?.delete()
|
||||
val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
|
||||
|
||||
@@ -1,47 +1,147 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DividerDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ButtonGroup
|
||||
import androidx.compose.material3.ButtonGroupDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.AppTheme
|
||||
import io.legado.app.constant.AppConst.appInfo
|
||||
import io.legado.app.ui.widget.compose.SettingItem
|
||||
import io.legado.app.ui.widget.compose.SplicedColumnGroup
|
||||
import io.legado.app.ui.widget.compose.TextCard
|
||||
|
||||
@Preview(showBackground = true, name = "Light Mode")
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
|
||||
@Composable
|
||||
fun PreviewAboutScreen() {
|
||||
AppTheme {
|
||||
AboutScreen(
|
||||
versionName = "1.0",
|
||||
onCheckUpdate = {},
|
||||
onOpenUrl = {},
|
||||
onShowMdFile = { _, _ -> },
|
||||
onSaveLog = {},
|
||||
onCreateHeapDump = {},
|
||||
onShowCrashLogs = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ButtonGroup(
|
||||
onOpenUrl: (String) -> Unit,
|
||||
onCheckUpdate: () -> Unit
|
||||
) {
|
||||
|
||||
ButtonGroup(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
overflowIndicator = { menuState ->
|
||||
ButtonGroupDefaults.OverflowIndicator(menuState = menuState)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
clickableItem(
|
||||
onClick = { onOpenUrl("https://example.com") },
|
||||
weight = 1f,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_web_outline),
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
)
|
||||
|
||||
clickableItem(
|
||||
onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") },
|
||||
weight = 1f,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_github),
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
)
|
||||
|
||||
clickableItem(
|
||||
onClick = { onCheckUpdate() },
|
||||
weight = 1f,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_import),
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AboutScreen(
|
||||
versionName: String = appInfo.versionName,
|
||||
onNavigateBack: () -> Unit = {},
|
||||
onCheckUpdate: () -> Unit = {},
|
||||
onOpenUrl: (String) -> Unit = {},
|
||||
onShowMdFile: (String, String) -> Unit = { _, _ -> },
|
||||
onSaveLog: () -> Unit = {},
|
||||
onCreateHeapDump: () -> Unit = {},
|
||||
onShowCrashLogs: () -> Unit = {},
|
||||
versionName: String = "1.0.0"
|
||||
onShowCrashLogs: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val version = remember { appInfo.versionName }
|
||||
LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.about)) }
|
||||
title = { Text(stringResource(R.string.about)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
@@ -51,57 +151,109 @@ fun AboutScreen(
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(text = "版本:$version", style = MaterialTheme.typography.bodyLarge)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = onCheckUpdate) {
|
||||
Text(stringResource(R.string.check_update))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = { onOpenUrl(context.getString(R.string.github_url)) }) {
|
||||
Text(stringResource(R.string.github_url))
|
||||
}
|
||||
|
||||
Button(onClick = { onOpenUrl(context.getString(R.string.legado_url)) }) {
|
||||
Text(stringResource(R.string.web_service))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
Modifier.padding(vertical = 8.dp),
|
||||
DividerDefaults.Thickness,
|
||||
DividerDefaults.color
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Text(text = "文档与政策", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(onClick = { onShowMdFile(context.getString(R.string.privacy_policy), "privacyPolicy.md") }) {
|
||||
Text(stringResource(R.string.privacy_policy))
|
||||
}
|
||||
TextButton(onClick = { onShowMdFile(context.getString(R.string.license), "LICENSE.md") }) {
|
||||
Text(stringResource(R.string.license))
|
||||
}
|
||||
TextButton(onClick = { onShowMdFile(context.getString(R.string.disclaimer), "disclaimer.md") }) {
|
||||
Text(stringResource(R.string.disclaimer))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
Modifier.padding(vertical = 8.dp),
|
||||
DividerDefaults.Thickness,
|
||||
DividerDefaults.color
|
||||
Text(
|
||||
text = stringResource(R.string.app_name), style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally))
|
||||
TextCard(
|
||||
text = versionName,
|
||||
backgroundColor = colorScheme.tertiaryContainer,
|
||||
contentColor = colorScheme.onTertiaryContainer,
|
||||
cornerRadius = 8.dp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_description), style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally))
|
||||
Row (modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center){
|
||||
FilledTonalIconButton ( onClick = { onOpenUrl("https://example.com") } ) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_web_outline),
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
|
||||
Text(text = "开发工具", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(onClick = onShowCrashLogs) {
|
||||
Text(stringResource(R.string.crash_log))
|
||||
}
|
||||
TextButton(onClick = onSaveLog) {
|
||||
Text(stringResource(R.string.save_log))
|
||||
}
|
||||
TextButton(onClick = onCreateHeapDump) {
|
||||
Text(stringResource(R.string.create_heap_dump))
|
||||
FilledTonalIconButton (onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_github),
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
|
||||
FilledTonalIconButton (onClick = { onCheckUpdate }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_import),
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SplicedColumnGroup(
|
||||
title = stringResource(R.string.about),
|
||||
content = listOf(
|
||||
{
|
||||
SettingItem(
|
||||
modifier = Modifier.background(colorScheme.surfaceBright),
|
||||
title = stringResource(R.string.contributors),
|
||||
description = "",
|
||||
onClick = {
|
||||
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
description = "",
|
||||
onClick = {
|
||||
onShowMdFile("隐私政策", "privacyPolicy.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.license),
|
||||
description = "",
|
||||
onClick = {
|
||||
onShowMdFile("许可证", "LICENSE.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.disclaimer),
|
||||
description = "",
|
||||
onClick = {
|
||||
onShowMdFile("免责声明", "disclaimer.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.crash_log),
|
||||
description = "",
|
||||
onClick = onShowCrashLogs
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.save_log),
|
||||
description = "",
|
||||
onClick = onSaveLog
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.create_heap_dump),
|
||||
description = "",
|
||||
onClick = onCreateHeapDump
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
@Composable
|
||||
fun AnimatedText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
for (i in text.indices) {
|
||||
val char = text[i]
|
||||
Box {
|
||||
AnimatedContent(
|
||||
targetState = char,
|
||||
transitionSpec = {
|
||||
(slideInVertically(initialOffsetY = { it })).togetherWith(
|
||||
slideOutVertically(targetOffsetY = { -it })
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
) {
|
||||
Text(
|
||||
style = style,
|
||||
color = color,
|
||||
softWrap = softWrap,
|
||||
text = it.toString(),
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
overflow = overflow,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onTextLayout = onTextLayout
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画文本控件
|
||||
*
|
||||
* 区别于 AnimatedText, 该控件在文本变化时,提供整行的滑动动画效果
|
||||
*/
|
||||
@Composable
|
||||
fun AnimatedTextLine(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
var currentText by remember { mutableStateOf(text) }
|
||||
SideEffect { currentText = text }
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = currentText,
|
||||
transitionSpec = {
|
||||
(slideInVertically(initialOffsetY = { it })).togetherWith(
|
||||
slideOutVertically(targetOffsetY = { -it })
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
) { text ->
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
style = style,
|
||||
color = color,
|
||||
softWrap = softWrap,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
overflow = overflow,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onTextLayout = onTextLayout
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.base.AppTypography
|
||||
|
||||
@Composable
|
||||
fun SettingItem(
|
||||
modifier: Modifier = Modifier,
|
||||
painter: Painter? = null,
|
||||
title: String,
|
||||
option: String? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
description: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
painter?.let {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 8.dp).size(24.dp),
|
||||
painter = it,
|
||||
tint = colorScheme.onSurfaceVariant,
|
||||
contentDescription = "Icon"
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = colorScheme.onSurface,
|
||||
style = AppTypography.titleMedium,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
style = AppTypography.labelMedium
|
||||
)
|
||||
option?.let {
|
||||
AnimatedText(
|
||||
text = it,
|
||||
style = AppTypography.labelMedium,
|
||||
color = colorScheme.primary,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
trailingContent?.let { composable ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
.width(55.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
composable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
|
||||
*
|
||||
* @param title the title of the group
|
||||
* @param content a list of composable that will be displayed in the group
|
||||
* @param modifier Modifier
|
||||
*/
|
||||
@Composable
|
||||
fun SplicedColumnGroup(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = "",
|
||||
content: List<@Composable () -> Unit>,
|
||||
) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
val cornerRadius = 16.dp
|
||||
val connectionRadius = 5.dp
|
||||
// Define shapes for different positions.
|
||||
val topShape = RoundedCornerShape(
|
||||
topStart = cornerRadius,
|
||||
topEnd = cornerRadius,
|
||||
bottomStart = connectionRadius,
|
||||
bottomEnd = connectionRadius
|
||||
)
|
||||
val middleShape = RoundedCornerShape(connectionRadius)
|
||||
val bottomShape = RoundedCornerShape(
|
||||
topStart = connectionRadius,
|
||||
topEnd = connectionRadius,
|
||||
bottomStart = cornerRadius,
|
||||
bottomEnd = cornerRadius
|
||||
)
|
||||
val singleShape = RoundedCornerShape(cornerRadius)
|
||||
|
||||
Column(modifier = modifier.padding(vertical = 8.dp)) {
|
||||
// Group title
|
||||
if (title != "")
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
|
||||
// The container for setting items.
|
||||
Column(
|
||||
modifier = Modifier.clip(
|
||||
// Clip the whole column to ensure content stays within the rounded bounds.
|
||||
if (content.size == 1) singleShape else RoundedCornerShape(cornerRadius)
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
content.forEachIndexed { index, itemContent ->
|
||||
// Determine the shape based on the pkg's position.
|
||||
val shape = when {
|
||||
content.size == 1 -> singleShape
|
||||
index == 0 -> topShape
|
||||
index == content.size - 1 -> bottomShape
|
||||
else -> middleShape
|
||||
}
|
||||
|
||||
// Apply background with the correct shape to the pkg.
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceBright, shape)
|
||||
) {
|
||||
itemContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewTextCard() {
|
||||
MaterialTheme {
|
||||
TextCard(
|
||||
text = "v1.0.0"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = colorScheme.tertiaryContainer,
|
||||
contentColor: Color = colorScheme.onTertiaryContainer,
|
||||
cornerRadius: Dp = 12.dp,
|
||||
paddingHorizontal: Dp = 8.dp,
|
||||
paddingVertical: Dp = 0.dp,
|
||||
textSize: TextUnit = 10.sp,
|
||||
bold: Boolean = true,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (onClick != null) Modifier.clickable { onClick() }
|
||||
else Modifier
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = backgroundColor,
|
||||
contentColor = contentColor
|
||||
),
|
||||
shape = RoundedCornerShape(cornerRadius),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = paddingHorizontal,
|
||||
vertical = paddingVertical
|
||||
),
|
||||
fontSize = textSize,
|
||||
fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user