[修复] 修复Compose界面的背景图片显示,添加了一些界面设置选项进Compose设置页中

This commit is contained in:
HapeLee
2026-03-22 02:59:40 +08:00
parent dd46484805
commit 6a99e1023f
38 changed files with 745 additions and 182 deletions
@@ -3,15 +3,14 @@ package io.legado.app.base
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.WindowCompat
import io.legado.app.constant.EventBus
import io.legado.app.constant.Theme
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.OldThemeConfig
import io.legado.app.ui.theme.AppTheme
import io.legado.app.help.config.AppConfig
import io.legado.app.utils.disableAutoFill
import io.legado.app.utils.fullScreen
import io.legado.app.utils.observeEvent
@@ -40,9 +39,7 @@ abstract class BaseComposeActivity(
// Compose 入口
setContent {
AppTheme {
Surface() {
Content()
}
Content()
}
}
@@ -31,6 +31,7 @@ import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
import io.legado.app.ui.dict.rule.DictRuleViewModel
import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
@@ -92,6 +93,7 @@ val appModule = module {
viewModelOf(::OtherConfigViewModel)
viewModelOf(::ReadConfigViewModel)
viewModelOf(::CoverConfigViewModel)
viewModelOf(::ThemeConfigViewModel)
viewModelOf(::TocViewModel)
viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel)
@@ -17,7 +17,6 @@ import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -32,6 +31,7 @@ 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.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -55,7 +55,7 @@ fun AboutScreen(
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
@@ -188,4 +188,4 @@ fun AboutScreen(
}
}
}
}
}
@@ -26,7 +26,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -40,6 +39,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import io.legado.app.data.entities.Bookmark
import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
@@ -93,7 +93,7 @@ fun AllBookmarkScreen(
}
}
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
@@ -260,4 +260,4 @@ fun AllBookmarkScreen(
)
}
}
}
}
@@ -50,7 +50,6 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -83,6 +82,7 @@ import io.legado.app.model.BookShelfState
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SearchBarSection
@@ -286,7 +286,7 @@ fun ExploreShowScreen(
}
}
Scaffold(
AppScaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
@@ -762,4 +762,4 @@ fun LoadMoreFooter(
)
}
}
}
}
@@ -30,7 +30,6 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -47,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.button.TopBarButtonVariant
@@ -67,7 +67,7 @@ fun BookInfoEditScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
@@ -278,4 +278,4 @@ fun BookTypeDropdown(
}
}
}
}
}
@@ -40,7 +40,6 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
@@ -80,6 +79,7 @@ import cn.hutool.core.date.DateUtil
import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
@@ -142,7 +142,7 @@ fun ReadRecordScreen(
}
}
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
@@ -1019,4 +1019,4 @@ fun formatDuring(mss: Long): String {
val m = if (minutes > 0) "${minutes}分钟" else ""
val s = if (seconds > 0) "${seconds}" else ""
return if ("$d$h$m$s".isBlank()) "0秒" else "$d$h$m$s"
}
}
@@ -44,7 +44,6 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
@@ -66,6 +65,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.SearchContentHistory
import io.legado.app.ui.widget.components.AnimatedText
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
@@ -135,7 +135,7 @@ fun SearchContentScreen(
else -> null
}
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
@@ -465,4 +465,4 @@ fun SearchResultItem(
}
}
}
}
}
@@ -61,7 +61,6 @@ import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
@@ -98,6 +97,7 @@ import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SelectionBottomBar
@@ -295,7 +295,7 @@ fun TocScreen(
viewModel.clearSelection()
}
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
DynamicTopAppBar(
@@ -948,4 +948,4 @@ private fun StatusIcon(
}
}
}
}
}
@@ -14,10 +14,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHostState
@@ -50,11 +51,11 @@ import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.filePicker.FilePickerSheetMode
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.rules.RuleEditFields
import io.legado.app.ui.widget.components.rules.RuleEditSheet
import io.legado.app.ui.widget.components.rules.RuleListScaffold
@@ -84,9 +85,10 @@ fun TxtRuleScreen(
var showEditSheet by remember { mutableStateOf(false) }
var editingRule by remember { mutableStateOf<TxtTocRule?>(null) }
var showDeleteRuleDialog by remember { mutableStateOf<TxtTocRule?>(null) }
var showUrlInput by remember { mutableStateOf(false) }
var showFilePickerSheet by remember { mutableStateOf(false) }
var filePickerMode by remember { mutableStateOf(FilePickerSheetMode.EXPORT) }
var showImportSheet by remember { mutableStateOf(false) }
var showExportSheet by remember { mutableStateOf(false) }
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
viewModel.moveItemInList(from.index, to.index)
@@ -152,21 +154,38 @@ fun TxtRuleScreen(
)
}
if (showFilePickerSheet) {
if (showExportSheet) {
FilePickerSheet(
onDismissRequest = { showFilePickerSheet = false },
onDismissRequest = { showExportSheet = false },
title = stringResource(R.string.export),
onSelectSysDir = {
showFilePickerSheet = false
showExportSheet = false
exportDoc.launch("exportDictRule.json")
},
onUpload = {
showFilePickerSheet = false
showExportSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
},
allowExtensions = arrayOf("json")
)
}
if (showImportSheet) {
FilePickerSheet(
onDismissRequest = { showImportSheet = false },
title = stringResource(R.string.import_txt_toc_rule),
onSelectSysFile = { types ->
importDoc.launch(types)
showImportSheet = false
},
onManualInput = {
showUrlInput = true
showImportSheet = false
},
allowExtensions = arrayOf("json", "txt")
)
}
(importState as? BaseImportUiState.Success<TxtTocRule>)?.let { state ->
BatchImportDialog(
title = "导入词典规则",
@@ -279,7 +298,7 @@ fun TxtRuleScreen(
}),
ActionItem(
text = stringResource(R.string.export),
onClick = { showFilePickerSheet = true })
onClick = { showExportSheet = true })
),
onDeleteSelected = { ids ->
@Suppress("UNCHECKED_CAST")
@@ -292,19 +311,11 @@ fun TxtRuleScreen(
},
snackbarHostState = snackbarHostState,
dropDownMenuContent = { dismiss ->
DropdownMenuItem(
text = { Text("在线导入") },
onClick = { dismiss(); showUrlInput = true })
DropdownMenuItem(
text = { Text("本地导入") },
onClick = {
dismiss(); importDoc.launch(
arrayOf(
"text/plain",
"application/json"
)
)
})
RoundDropdownMenuItem(
text = { Text(stringResource(R.string.import_str)) },
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
)
}
) { paddingValues ->
Box(
@@ -53,11 +53,16 @@ fun <T> prefDelegate(
@Suppress("UNCHECKED_CAST")
private fun readInitialValue(): T {
return when (defaultValue) {
is String -> appCtx.getPrefString(key, defaultValue) as T
is Int -> appCtx.getPrefInt(key, defaultValue) as T
is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T
is Long -> appCtx.getPrefLong(key, defaultValue) as T
val prefs = appCtx.defaultSharedPreferences
return when {
defaultValue is String -> appCtx.getPrefString(key, defaultValue) as T
defaultValue == null && prefs.contains(key) -> {
appCtx.getPrefString(key, null) as T
}
defaultValue is Int -> appCtx.getPrefInt(key, defaultValue) as T
defaultValue is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T
defaultValue is Long -> appCtx.getPrefLong(key, defaultValue) as T
else -> defaultValue
}
}
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -21,13 +20,13 @@ import io.legado.app.base.BaseComposeActivity
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
import io.legado.app.ui.config.readConfig.ReadConfigScreen
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import kotlinx.serialization.Serializable
class TestConfigActivity : BaseComposeActivity() {
@@ -44,6 +43,9 @@ class TestConfigActivity : BaseComposeActivity() {
@Serializable
object CoverConfigRoute
@Serializable
object ThemeConfigRoute
@Composable
override fun Content() {
val navController = rememberNavController()
@@ -57,7 +59,8 @@ class TestConfigActivity : BaseComposeActivity() {
onBackClick = { finish() },
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
onNavigateToRead = { navController.navigate(ReadConfigRoute) },
onNavigateToCover = { navController.navigate(CoverConfigRoute) }
onNavigateToCover = { navController.navigate(CoverConfigRoute) },
onNavigateToTheme = { navController.navigate(ThemeConfigRoute) }
)
}
@@ -72,6 +75,10 @@ class TestConfigActivity : BaseComposeActivity() {
composable<CoverConfigRoute> {
CoverConfigScreen(onBackClick = { navController.popBackStack() })
}
composable<ThemeConfigRoute> {
ThemeConfigScreen(onBackClick = { navController.popBackStack() })
}
}
}
@@ -81,11 +88,12 @@ class TestConfigActivity : BaseComposeActivity() {
onBackClick: () -> Unit,
onNavigateToOther: () -> Unit,
onNavigateToRead: () -> Unit,
onNavigateToCover: () -> Unit
onNavigateToCover: () -> Unit,
onNavigateToTheme: () -> Unit
) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
@@ -105,10 +113,9 @@ class TestConfigActivity : BaseComposeActivity() {
.padding(16.dp)
) {
SplicedColumnGroup {
SwitchSettingItem(
title = "使用折叠应用栏",
checked = ThemeConfig.useFlexibleTopAppBar,
onCheckedChange = { ThemeConfig.useFlexibleTopAppBar = it }
ClickableSettingItem(
title = stringResource(R.string.theme_setting),
onClick = onNavigateToTheme
)
ClickableSettingItem(
title = stringResource(R.string.other_setting),
@@ -12,7 +12,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -27,6 +26,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -48,7 +48,7 @@ fun CoverConfigScreen(
var manageKey by remember { mutableStateOf<String?>(null) }
var showColorPickerByField by remember { mutableStateOf<String?>(null) }
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
@@ -282,4 +282,4 @@ fun CoverConfigScreen(
}
)
}
}
}
@@ -13,7 +13,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
@@ -30,6 +29,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.service.WebService
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -79,7 +79,7 @@ fun OtherConfigScreen(
}
}
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
@@ -505,4 +505,4 @@ fun ConfirmDialog(
}
}
)
}
}
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -20,6 +19,7 @@ import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -41,7 +41,7 @@ fun ReadConfigScreen(
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
var showPageKeySheet by remember { mutableStateOf(false) }
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
@@ -326,4 +326,4 @@ fun ReadConfigScreen(
if (showPageKeySheet) {
PageKeySheet(onDismissRequest = { showPageKeySheet = false })
}
}
}
@@ -0,0 +1,153 @@
package io.legado.app.ui.config.themeConfig
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import io.legado.app.R
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BackgroundImageManageSheet(
preferenceKey: String,
isDarkTheme: Boolean,
onDismissRequest: () -> Unit,
viewModel: ThemeConfigViewModel = koinViewModel()
) {
val scope = rememberCoroutineScope()
var showFilePicker by remember { mutableStateOf(false) }
val selectImage =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let {
scope.launch {
viewModel.setBackgroundFromUri(
uri = it,
preferenceKey = preferenceKey
)
}
}
}
val currentPath = if (isDarkTheme) {
ThemeConfig.bgImageDark
} else {
ThemeConfig.bgImageLight
}
GlassModalBottomSheet(
onDismissRequest = onDismissRequest
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
Text(
text = stringResource(R.string.background_image),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
if (currentPath.isNullOrBlank()) {
Surface(
onClick = { showFilePicker = true },
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.padding(16.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add",
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Surface(
shape = MaterialTheme.shapes.medium,
tonalElevation = 2.dp
) {
AsyncImage(
model = currentPath,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(9f / 16f),
contentScale = ContentScale.Crop
)
}
IconButton(
onClick = { viewModel.removeBackground(preferenceKey) },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
.size(32.dp)
) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Remove",
modifier = Modifier.size(20.dp)
)
}
}
}
}
}
}
if (showFilePicker) {
FilePickerSheet(
onDismissRequest = { showFilePicker = false },
onSelectSysFile = {
selectImage.launch("image/*")
showFilePicker = false
},
allowExtensions = arrayOf("jpg", "jpeg", "png", "webp")
)
}
}
@@ -1,7 +1,10 @@
package io.legado.app.ui.config.themeConfig
import android.util.Log
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.prefDelegate
import io.legado.app.utils.postEvent
object ThemeConfig {
@@ -19,16 +22,34 @@ object ThemeConfig {
var isPureBlack by prefDelegate(PreferKey.pureBlack, false)
var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null)
var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) {
postEvent(EventBus.RECREATE, false)
}
var bgImageDark by prefDelegate<String?>(PreferKey.bgImageN, null)
var bgImageDark by prefDelegate<String?>(PreferKey.bgImageN, null) {
postEvent(EventBus.RECREATE, false)
}
var bgImageBlurring by prefDelegate(PreferKey.bgImageBlurring, 0)
var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0)
fun hasImageBg(isDark: Boolean): Boolean {
return if (isDark) {
!bgImageDark.isNullOrEmpty()
val result = if (isDark) {
!bgImageDark.isNullOrBlank()
} else {
!bgImageLight.isNullOrEmpty()
!bgImageLight.isNullOrBlank()
}
Log.d(
"MainConfig",
"hasImageBg -> isDark=$isDark, " +
"bgImageDark=$bgImageDark, " +
"bgImageLight=$bgImageLight, " +
"result=$result"
)
return result
}
/*
@@ -0,0 +1,170 @@
package io.legado.app.ui.config.themeConfig
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThemeConfigScreen(
onBackClick: () -> Unit,
viewModel: ThemeConfigViewModel = koinViewModel()
) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
var manageKey by remember { mutableStateOf<Pair<String, Boolean>?>(null) }
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = { Text(stringResource(R.string.theme_setting)) },
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
SplicedColumnGroup {
SwitchSettingItem(
title = stringResource(R.string.pure_black),
checked = ThemeConfig.isPureBlack,
onCheckedChange = { ThemeConfig.isPureBlack = it }
)
DropdownListSettingItem(
title = stringResource(R.string.palette_style),
selectedValue = ThemeConfig.paletteStyle,
displayEntries = arrayOf(
"柔和", "中性", "鲜艳", "表现力", "彩虹",
"水果拼盘", "单色", "仿真", "内容取色"
),
entryValues = arrayOf(
"tonalSpot", "neutral", "vibrant", "expressive", "rainbow",
"fruitSalad", "monochrome", "fidelity", "content"
),
onValueChange = { ThemeConfig.paletteStyle = it }
)
}
SplicedColumnGroup(title = "Compose 相关") {
SwitchSettingItem(
title = "使用折叠应用栏",
checked = ThemeConfig.useFlexibleTopAppBar,
onCheckedChange = { ThemeConfig.useFlexibleTopAppBar = it }
)
SliderSettingItem(
title = stringResource(R.string.container_opacity),
description = stringResource(
R.string.container_opacity_summary,
ThemeConfig.containerOpacity
),
value = ThemeConfig.containerOpacity.toFloat(),
defaultValue = 100f,
valueRange = 0f..100f,
steps = 99,
onValueChange = { ThemeConfig.containerOpacity = it.toInt() }
)
SwitchSettingItem(
title = stringResource(R.string.is_blur_enable),
checked = ThemeConfig.enableBlur,
onCheckedChange = { ThemeConfig.enableBlur = it }
)
if (ThemeConfig.enableBlur) {
SwitchSettingItem(
title = stringResource(R.string.is_blur_progressive_enable),
checked = ThemeConfig.enableProgressiveBlur,
onCheckedChange = { ThemeConfig.enableProgressiveBlur = it }
)
}
}
SplicedColumnGroup(title = stringResource(R.string.day)) {
val hasLightBg = !ThemeConfig.bgImageLight.isNullOrBlank()
ClickableSettingItem(
title = stringResource(R.string.background_image),
description = if (hasLightBg) stringResource(R.string.click_to_delete) else stringResource(
R.string.select_image
),
onClick = { manageKey = Pair(PreferKey.bgImage, false) }
)
if (hasLightBg) {
SliderSettingItem(
title = stringResource(R.string.background_image_blurring),
value = ThemeConfig.bgImageBlurring.toFloat(),
defaultValue = 0f,
valueRange = 0f..100f,
steps = 99,
onValueChange = {
ThemeConfig.bgImageBlurring = it.toInt()
}
)
}
}
SplicedColumnGroup(title = stringResource(R.string.night)) {
val hasDarkBg = !ThemeConfig.bgImageDark.isNullOrBlank()
ClickableSettingItem(
title = stringResource(R.string.background_image),
description = if (hasDarkBg) stringResource(R.string.click_to_delete) else stringResource(
R.string.select_image
),
onClick = { manageKey = Pair(PreferKey.bgImageN, true) }
)
if (hasDarkBg) {
SliderSettingItem(
title = stringResource(R.string.background_image_blurring),
value = ThemeConfig.bgImageNBlurring.toFloat(),
defaultValue = 0f,
valueRange = 0f..100f,
steps = 99,
onValueChange = {
ThemeConfig.bgImageNBlurring = it.toInt()
}
)
}
}
}
}
manageKey?.let { (key, isDark) ->
BackgroundImageManageSheet(
preferenceKey = key,
isDarkTheme = isDark,
onDismissRequest = { manageKey = null }
)
}
}
@@ -0,0 +1,80 @@
package io.legado.app.ui.config.themeConfig
import android.net.Uri
import androidx.lifecycle.ViewModel
import io.legado.app.constant.PreferKey
import io.legado.app.utils.FileDoc
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.externalFiles
import io.legado.app.utils.inputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
class ThemeConfigViewModel : ViewModel() {
/**
* 设置背景图片
*/
suspend fun setBackgroundFromUri(
uri: Uri,
preferenceKey: String
) = withContext(Dispatchers.IO) {
kotlin.runCatching {
val fileDoc = FileDoc.fromUri(uri, false)
val suffix = fileDoc.name.substringAfterLast(".", "jpg")
// 计算MD5作为文件名
val md5 = uri.inputStream(appCtx).getOrThrow().use {
MD5Utils.md5Encode(it)
}
val fileName = "$md5.$suffix"
val folder = File(appCtx.externalFiles, preferenceKey)
val file = File(folder, fileName)
if (!file.exists()) {
FileUtils.createFileIfNotExist(file.absolutePath)
uri.inputStream(appCtx).getOrThrow().use { input ->
FileOutputStream(file).use { output ->
input.copyTo(output)
}
}
}
updateBackgroundPath(preferenceKey, file.absolutePath)
}.onFailure {
it.printStackTrace()
}
}
fun removeBackground(preferenceKey: String) {
updateBackgroundPath(preferenceKey, null)
}
/**
* 更新背景路径并清理旧文件
*/
private fun updateBackgroundPath(preferenceKey: String, newPath: String?) {
val oldPath = when (preferenceKey) {
PreferKey.bgImage -> ThemeConfig.bgImageLight
PreferKey.bgImageN -> ThemeConfig.bgImageDark
else -> null
}
if (oldPath != newPath && oldPath != null) {
val oldFile = File(oldPath)
if (oldFile.absolutePath.contains(appCtx.externalFiles.absolutePath)) {
oldFile.delete()
}
}
when (preferenceKey) {
PreferKey.bgImage -> ThemeConfig.bgImageLight = newPath
PreferKey.bgImageN -> ThemeConfig.bgImageDark = newPath
}
}
}
@@ -14,10 +14,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHostState
@@ -50,11 +51,11 @@ import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.filePicker.FilePickerSheetMode
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.rules.RuleEditFields
import io.legado.app.ui.widget.components.rules.RuleEditSheet
import io.legado.app.ui.widget.components.rules.RuleListScaffold
@@ -82,8 +83,11 @@ fun DictRuleScreen(
var editingRule by remember { mutableStateOf<DictRule?>(null) }
var showDeleteRuleDialog by remember { mutableStateOf<DictRule?>(null) }
var showUrlInput by remember { mutableStateOf(false) }
var showFilePickerSheet by remember { mutableStateOf(false) }
var filePickerMode by remember { mutableStateOf(FilePickerSheetMode.EXPORT) }
var showImportSheet by remember { mutableStateOf(false) }
var showExportSheet by remember { mutableStateOf(false) }
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
viewModel.moveItemInList(from.index, to.index)
@@ -149,21 +153,38 @@ fun DictRuleScreen(
)
}
if (showFilePickerSheet) {
if (showExportSheet) {
FilePickerSheet(
onDismissRequest = { showFilePickerSheet = false },
onDismissRequest = { showExportSheet = false },
title = stringResource(R.string.export),
onSelectSysDir = {
showFilePickerSheet = false
showExportSheet = false
exportDoc.launch("exportDictRule.json")
},
onUpload = {
showFilePickerSheet = false
showExportSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
},
allowExtensions = arrayOf("json")
)
}
if (showImportSheet) {
FilePickerSheet(
onDismissRequest = { showImportSheet = false },
title = stringResource(R.string.import_dict_rule),
onSelectSysFile = { types ->
importDoc.launch(types)
showImportSheet = false
},
onManualInput = {
showUrlInput = true
showImportSheet = false
},
allowExtensions = arrayOf("json", "txt")
)
}
(importState as? BaseImportUiState.Success<DictRule>)?.let { state ->
BatchImportDialog(
title = "导入词典规则",
@@ -288,19 +309,11 @@ fun DictRuleScreen(
},
snackbarHostState = snackbarHostState,
dropDownMenuContent = { dismiss ->
DropdownMenuItem(
text = { Text("在线导入") },
onClick = { dismiss(); showUrlInput = true })
DropdownMenuItem(
text = { Text("本地导入") },
onClick = {
dismiss(); importDoc.launch(
arrayOf(
"text/plain",
"application/json"
)
)
})
RoundDropdownMenuItem(
text = { Text(stringResource(R.string.import_str)) },
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
)
}
) { paddingValues ->
Box(
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -27,7 +26,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.WideNavigationRail
import androidx.compose.material3.WideNavigationRailItem
@@ -54,6 +52,7 @@ import io.legado.app.ui.main.explore.ExploreScreen
import io.legado.app.ui.main.my.MyScreen
import io.legado.app.ui.main.rss.RssScreen
import io.legado.app.ui.theme.regularHazeEffect
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassDefaults
import io.legado.app.utils.startActivity
import io.legado.app.utils.startActivityForBook
@@ -159,20 +158,21 @@ fun MainScreen(
}
}
Scaffold(
AppScaffold(
modifier = Modifier.weight(1f),
bottomBar = {
if (!useRail && MainConfig.showBottomView) {
val labelVisibilityMode = MainConfig.labelVisibilityMode
val isUnlabeled = labelVisibilityMode == "unlabeled"
NavigationBar(
modifier = Modifier
.regularHazeEffect(state = hazeState)
.heightIn(max = 84.dp),
.height(if (isUnlabeled) 64.dp else 80.dp),
containerColor = GlassDefaults.glassColor(
noBlurColor = BottomAppBarDefaults.containerColor,
blurAlpha = GlassDefaults.DefaultBlurAlpha
)
) {
val labelVisibilityMode = MainConfig.labelVisibilityMode
val alwaysShowLabel = when (labelVisibilityMode) {
"labeled" -> true
"selected" -> false
@@ -24,14 +24,12 @@ import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Source
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material.icons.filled.Web
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -52,6 +50,7 @@ import io.legado.app.ui.config.ConfigTag
import io.legado.app.ui.dict.rule.DictRuleActivity
import io.legado.app.ui.file.FileManageActivity
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -68,7 +67,7 @@ fun MyScreen(
val uiState by viewModel.uiState.collectAsState()
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
contentWindowInsets = WindowInsets.systemBars
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top),
@@ -230,7 +229,7 @@ fun MyScreen(
)
SettingItem(
title = stringResource(R.string.dict_rule),
imageVector = Icons.Default.Translate,
imageVector = Icons.AutoMirrored.Filled.Rule,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(DictRuleActivity::class.java)
@@ -281,4 +280,4 @@ fun MyScreen(
Spacer(Modifier.height(120.dp))
}
}
}
}
@@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -69,7 +70,6 @@ import io.legado.app.ui.widget.components.GroupManageBottomSheet
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.divider.PillDivider
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.filePicker.FilePickerSheetMode
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -107,8 +107,9 @@ fun ReplaceRuleScreen(
val hapticFeedback = LocalHapticFeedback.current
var showUrlInput by remember { mutableStateOf(false) }
var showFilePickerSheet by remember { mutableStateOf(false) }
var filePickerMode by remember { mutableStateOf(FilePickerSheetMode.EXPORT) }
var showImportSheet by remember { mutableStateOf(false) }
var showExportSheet by remember { mutableStateOf(false) }
var showDeleteRuleDialog by remember { mutableStateOf<ReplaceRule?>(null) }
var showGroupManageSheet by remember { mutableStateOf(false) }
@@ -164,15 +165,31 @@ fun ReplaceRuleScreen(
)
}
if (showFilePickerSheet) {
if (showImportSheet) {
FilePickerSheet(
onDismissRequest = { showFilePickerSheet = false },
onDismissRequest = { showImportSheet = false },
title = stringResource(R.string.import_replace_rule),
onSelectSysFile = { types ->
importDoc.launch(types)
showImportSheet = false
},
onManualInput = {
showUrlInput = true
showImportSheet = false
},
allowExtensions = arrayOf("json", "txt")
)
}
if (showExportSheet) {
FilePickerSheet(
onDismissRequest = { showExportSheet = false },
onSelectSysDir = {
showFilePickerSheet = false
showExportSheet = false
exportDoc.launch("exportReplaceRule.json")
},
onUpload = {
showFilePickerSheet = false
showExportSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
},
allowExtensions = arrayOf("json")
@@ -320,7 +337,7 @@ fun ReplaceRuleScreen(
),
ActionItem(
text = stringResource(R.string.export),
onClick = { showFilePickerSheet = true }
onClick = { showExportSheet = true }
)
),
onDeleteSelected = { ids ->
@@ -389,15 +406,9 @@ fun ReplaceRuleScreen(
snackbarHostState = snackbarHostState,
dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem(
text = { Text("在线导入") },
onClick = {
dismiss()
showUrlInput = true // 触发输入框
}
)
RoundDropdownMenuItem(
text = { Text("本地导入") },
onClick = { importDoc.launch(arrayOf("text/plain", "application/json")); dismiss() }
text = { Text(stringResource(R.string.import_str)) },
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
)
RoundDropdownMenuItem(
text = { Text("分组管理") },
@@ -52,7 +52,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition
@@ -78,6 +77,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.button.AlertButton
@@ -108,7 +108,7 @@ fun ReplaceEditScreen(
val isKeyboardVisible by keyboardAsState()
with(sharedTransitionScope) {
Scaffold(
AppScaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.sharedBounds(
@@ -487,4 +487,4 @@ fun QuickInputBar(
)
}
}
}
}
@@ -0,0 +1,42 @@
package io.legado.app.ui.theme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import io.legado.app.ui.config.themeConfig.ThemeConfig
@Composable
fun AppBackground(
darkTheme: Boolean,
content: @Composable () -> Unit
) {
val hasImageBg = ThemeConfig.hasImageBg(darkTheme)
val bgImagePath = if (darkTheme) ThemeConfig.bgImageDark else ThemeConfig.bgImageLight
val blur = if (darkTheme) {
ThemeConfig.bgImageNBlurring
} else {
ThemeConfig.bgImageBlurring
}
Box(modifier = Modifier.fillMaxSize()) {
if (hasImageBg && !bgImagePath.isNullOrBlank()) {
AsyncImage(
model = bgImagePath,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.blur(blur.dp),
//TODO:低版本安卓使用coil-transformations
contentScale = ContentScale.Crop
)
}
content()
}
}
@@ -20,17 +20,15 @@ fun AppTheme(
val context = LocalContext.current
val appThemeMode = ThemeResolver.resolveThemeMode(ThemeConfig.appTheme)
val isPureBlack = ThemeConfig.isPureBlack
val hasImageBg = ThemeConfig.hasImageBg(darkTheme)
val paletteStyle = ThemeConfig.paletteStyle
val colorScheme =
remember(context, appThemeMode, darkTheme, isPureBlack, hasImageBg, paletteStyle) {
remember(context, appThemeMode, darkTheme, isPureBlack, paletteStyle) {
ThemeManager.getColorScheme(
context = context,
mode = appThemeMode,
darkTheme = darkTheme,
isAmoled = isPureBlack,
isImageBg = hasImageBg,
paletteStyle = paletteStyle
)
}
@@ -39,7 +37,10 @@ fun AppTheme(
colorScheme = colorScheme,
typography = Typography(),
motionScheme = MotionScheme.expressive(),
shapes = Shapes(),
content = content
)
shapes = Shapes()
) {
AppBackground(darkTheme = darkTheme) {
content()
}
}
}
@@ -23,7 +23,6 @@ fun rememberOpaqueColorScheme(): ColorScheme {
mode = appThemeMode,
darkTheme = isDark,
isAmoled = isPureBlack,
isImageBg = hasImageBg,
paletteStyle = paletteStyle,
forceOpaque = true
)
@@ -44,7 +44,6 @@ object ThemeManager {
mode: AppThemeMode,
darkTheme: Boolean,
isAmoled: Boolean,
isImageBg: Boolean,
paletteStyle: String?,
forceOpaque: Boolean = false
): ColorScheme {
@@ -82,7 +81,7 @@ object ThemeManager {
)
}
if (!forceOpaque && (isImageBg || actualMode == AppThemeMode.Transparent)) {
if (!forceOpaque && actualMode == AppThemeMode.Transparent) {
return scheme.copy(
surface = Color.Transparent,
background = Color.Transparent,
@@ -0,0 +1,39 @@
package io.legado.app.ui.widget.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.FabPosition
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@Composable
fun AppScaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = Color.Transparent,
contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
modifier = modifier,
topBar = topBar,
bottomBar = bottomBar,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
containerColor = containerColor,
contentColor = contentColor,
contentWindowInsets = contentWindowInsets,
content = content
)
}
@@ -61,10 +61,9 @@ object GlassTopAppBarDefaults {
@Composable
fun glassColors(): TopAppBarColors {
val opacity = ThemeConfig.containerOpacity / 100f
val containerColor = GlassDefaults.glassColor(
noBlurColor = MaterialTheme.colorScheme.surface.copy(alpha = opacity),
noBlurColor = MaterialTheme.colorScheme.surface,
blurAlpha = GlassDefaults.TransparentAlpha
)
@@ -76,15 +75,17 @@ object GlassTopAppBarDefaults {
return TopAppBarDefaults.topAppBarColors(
containerColor = containerColor,
scrolledContainerColor = scrolledContainerColor
scrolledContainerColor = scrolledContainerColor,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@Composable
fun containerColor(): Color {
val opacity = ThemeConfig.containerOpacity / 100f
return GlassDefaults.glassColor(
noBlurColor = MaterialTheme.colorScheme.surface.copy(alpha = opacity),
noBlurColor = MaterialTheme.colorScheme.surface,
blurAlpha = GlassDefaults.TransparentAlpha
)
}
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -24,10 +23,8 @@ fun SectionHeader(
// 根容器的 Padding
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
Surface(
modifier = Modifier.fillMaxWidth()
) {
Row(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(contentPadding),
@@ -39,5 +36,5 @@ fun SectionHeader(
it()
}
}
}
}
@@ -1,5 +1,6 @@
package io.legado.app.ui.widget.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -11,6 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.card.GlassCard
@@ -39,10 +41,12 @@ fun SplicedColumnGroup(
GlassCard(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
content()
@@ -17,32 +17,25 @@ fun GlassCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
colors: CardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = ThemeConfig.containerOpacity / 100f),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
colors: CardColors? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
border: BorderStroke? = null,
content: @Composable ColumnScope.() -> Unit
) {
if (onClick != null) {
Card(
onClick = onClick,
modifier = modifier,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
content = content
)
} else {
Card(
modifier = modifier,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
content = content
)
}
val containerAlpha = ThemeConfig.containerOpacity / 100f
val finalColors = colors ?: CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = containerAlpha),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
Card(
onClick = onClick ?: {},
modifier = modifier,
shape = shape,
colors = finalColors,
elevation = elevation,
border = border,
content = content
)
}
@@ -88,7 +88,7 @@ fun FilePickerSheet(
onSelectSysFiles?.let {
FilePickerOptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = "系统多选",
text = "多选项目",
onClick = { it(typesOfExtensions(allowExtensions)) }
)
}
@@ -40,7 +40,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedToggleButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -61,6 +60,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.AnimatedText
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import kotlinx.coroutines.launch
@@ -141,7 +141,7 @@ fun <T> BatchImportDialog(
onDismissRequest = onDismissRequest,
containerColor = { it.surfaceContainer }
) {
Scaffold(
AppScaffold(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
@@ -19,7 +19,6 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.Icon
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -39,6 +38,7 @@ import androidx.compose.ui.zIndex
import dev.chrisbanes.haze.HazeState
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.SelectionBottomBar
@@ -86,9 +86,15 @@ fun <T> ListScaffold(
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
val hazeState = remember { HazeState() }
Scaffold(
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) },
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.padding(bottom = 72.dp)
)
},
topBar = {
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
DynamicTopAppBar(
@@ -136,4 +142,4 @@ fun <T> ListScaffold(
}
}
}
}
}
@@ -15,6 +15,7 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@@ -101,6 +102,7 @@ fun CompactSliderSettingItem(
var expanded by remember { mutableStateOf(false) }
var sliderValue by remember(value) { mutableFloatStateOf(value) }
var displayValue by remember(value) { mutableFloatStateOf(value) }
SettingItem(
title = title,
@@ -126,7 +128,7 @@ fun CompactSliderSettingItem(
cornerRadius = 8.dp,
horizontalPadding = 8.dp,
verticalPadding = 4.dp,
text = value.toInt().toString(),
text = displayValue.toInt().toString(),
backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
contentColor = MaterialTheme.colorScheme.onSurface
)
@@ -144,7 +146,10 @@ fun CompactSliderSettingItem(
value = sliderValue,
onValueChange = {
sliderValue = it
onValueChange(it)
displayValue = it
},
onValueChangeFinished = {
onValueChange(sliderValue)
},
valueRange = valueRange,
steps = steps,
@@ -152,6 +157,13 @@ fun CompactSliderSettingItem(
)
}
)
LaunchedEffect(value) {
if (!expanded) {
sliderValue = value
displayValue = value
}
}
}
@Composable
@@ -21,6 +21,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@@ -103,7 +104,7 @@ fun SwipeActionContainer(
content = {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surface,
color = Color.Transparent,
shape = RectangleShape
) {
content()