[新增] 初步用 Compose 重写高级设置界面,可在高级-测试初步体验。

This commit is contained in:
HapeLee
2026-02-02 21:15:04 +08:00
parent 62e87b0a60
commit f21ff42e4c
30 changed files with 2074 additions and 276 deletions
+4
View File
@@ -216,6 +216,10 @@
android:name=".ui.config.ConfigActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="standard" />
<activity
android:name=".ui.config.TestConfigActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="standard" />
<!-- 搜索界面 -->
<activity
android:name=".ui.book.search.SearchActivity"
@@ -13,6 +13,7 @@ import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import io.legado.app.ui.book.searchContent.SearchContentViewModel
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
import io.legado.app.ui.dict.rule.DictRuleViewModel
import io.legado.app.ui.main.my.MyViewModel
import io.legado.app.ui.replace.ReplaceEditRoute
@@ -43,6 +44,7 @@ val appModule = module {
viewModelOf(::ReplaceRuleViewModel)
viewModelOf(::AllBookmarkViewModel)
viewModelOf(::TxtTocRuleViewModel)
viewModelOf(::OtherConfigViewModel)
viewModel { (route: ReplaceEditRoute) ->
ReplaceEditViewModel(
@@ -190,48 +190,12 @@ object ThemeConfig {
}
fun saveDayTheme(context: Context, name: String) {
val primary =
context.getPrefInt(PreferKey.cPrimary, context.getCompatColor(R.color.md_brown_500))
val accent =
context.getPrefInt(PreferKey.cAccent, context.getCompatColor(R.color.md_red_600))
val background =
context.getPrefInt(PreferKey.cBackground, context.getCompatColor(R.color.md_grey_100))
val bBackground =
context.getPrefInt(PreferKey.cBBackground, context.getCompatColor(R.color.md_grey_200))
val config = Config(
themeName = name,
isNightTheme = false,
primaryColor = "#${primary.hexString}",
accentColor = "#${accent.hexString}",
backgroundColor = "#${background.hexString}",
bottomBackground = "#${bBackground.hexString}"
)
val config = getDayTheme(context, name)
addConfig(config)
}
fun saveNightTheme(context: Context, name: String) {
val primary =
context.getPrefInt(
PreferKey.cNPrimary,
context.getCompatColor(R.color.md_blue_grey_600)
)
val accent =
context.getPrefInt(
PreferKey.cNAccent,
context.getCompatColor(R.color.md_deep_orange_800)
)
val background =
context.getPrefInt(PreferKey.cNBackground, context.getCompatColor(R.color.md_grey_900))
val bBackground =
context.getPrefInt(PreferKey.cNBBackground, context.getCompatColor(R.color.md_grey_850))
val config = Config(
themeName = name,
isNightTheme = true,
primaryColor = "#${primary.hexString}",
accentColor = "#${accent.hexString}",
backgroundColor = "#${background.hexString}",
bottomBackground = "#${bBackground.hexString}"
)
val config = getNightTheme(context, name)
addConfig(config)
}
@@ -253,6 +217,77 @@ object ThemeConfig {
}
}
fun getDurConfig(context: Context): Config {
val isNight = AppConfig.isNightTheme
val name = if (isNight) {
"MD3-Night"
} else {
"MD3-Day"
}
return if (isNight) {
getNightTheme(context, name)
} else {
getDayTheme(context, name)
}
}
private fun getDayTheme(context: Context, name: String): Config {
val primary =
context.getPrefInt(PreferKey.cPrimary, context.getCompatColor(R.color.md_brown_500))
val accent =
context.getPrefInt(PreferKey.cAccent, context.getCompatColor(R.color.md_red_600))
val background =
context.getPrefInt(PreferKey.cBackground, context.getCompatColor(R.color.md_grey_100))
val bBackground =
context.getPrefInt(PreferKey.cBBackground, context.getCompatColor(R.color.md_grey_200))
val bgImgPath =
context.getPrefString(PreferKey.bgImage)
val bgImgBlur =
context.getPrefInt(PreferKey.bgImageBlurring, 0)
return Config(
themeName = name,
isNightTheme = false,
primaryColor = "#${primary.hexString}",
accentColor = "#${accent.hexString}",
backgroundColor = "#${background.hexString}",
bottomBackground = "#${bBackground.hexString}",
backgroundImgPath = bgImgPath,
backgroundImgBlur = bgImgBlur
)
}
private fun getNightTheme(context: Context, name: String): Config {
val primary =
context.getPrefInt(
PreferKey.cNPrimary,
context.getCompatColor(R.color.md_blue_grey_600)
)
val accent =
context.getPrefInt(
PreferKey.cNAccent,
context.getCompatColor(R.color.md_deep_orange_800)
)
val background =
context.getPrefInt(PreferKey.cNBackground, context.getCompatColor(R.color.md_grey_900))
val bBackground =
context.getPrefInt(PreferKey.cNBBackground, context.getCompatColor(R.color.md_grey_850))
val bgImgPath =
context.getPrefString(PreferKey.bgImageN)
val bgImgBlur =
context.getPrefInt(PreferKey.bgImageNBlurring, 0)
return Config(
themeName = name,
isNightTheme = true,
primaryColor = "#${primary.hexString}",
accentColor = "#${accent.hexString}",
backgroundColor = "#${background.hexString}",
bottomBackground = "#${bBackground.hexString}",
backgroundImgPath = bgImgPath,
backgroundImgBlur = bgImgBlur
)
}
@Keep
data class Config(
var themeName: String,
@@ -260,7 +295,9 @@ object ThemeConfig {
var primaryColor: String,
var accentColor: String,
var backgroundColor: String,
var bottomBackground: String
var bottomBackground: String,
var backgroundImgPath: String?,
var backgroundImgBlur: Int
) {
override fun hashCode(): Int {
@@ -276,10 +313,23 @@ object ThemeConfig {
&& other.accentColor == accentColor
&& other.backgroundColor == backgroundColor
&& other.bottomBackground == bottomBackground
&& other.backgroundImgPath == backgroundImgPath
&& other.backgroundImgBlur == backgroundImgBlur
}
return false
}
fun toMap() = mapOf(
"themeName" to themeName,
"isNightTheme" to isNightTheme,
"primaryColor" to primaryColor,
"accentColor" to accentColor,
"backgroundColor" to backgroundColor,
"bottomBackground" to bottomBackground,
"backgroundImgPath" to backgroundImgPath,
"backgroundImgBlur" to backgroundImgBlur
)
}
}
@@ -34,10 +34,10 @@ 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.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.SettingItem
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.SmallTopBarButton
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.settingItem.SettingItem
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -154,12 +154,10 @@ fun TxtRuleScreen(
FilePickerSheet(
sheetState = sheetState,
onDismissRequest = { showFilePickerSheet = false },
mode = filePickerMode,
onSelectSysDir = {
showFilePickerSheet = false
exportDoc.launch("exportDictRule.json")
},
onSelectSysFile = {},
onUpload = {
showFilePickerSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
@@ -42,6 +42,7 @@ import io.legado.app.utils.putPrefString
import io.legado.app.utils.removePref
import io.legado.app.utils.restart
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.startActivity
import splitties.init.appCtx
/**
@@ -168,7 +169,7 @@ class OtherConfigFragment : PreferenceFragment(),
PreferKey.clearWebViewData -> clearWebViewData()
"localPassword" -> alertLocalPassword()
PreferKey.shrinkDatabase -> shrinkDatabase()
"TEST" -> startActivity<TestConfigActivity>()
PreferKey.notificationsPost -> checkPermission(1)
PreferKey.ignoreBatteryPermission -> checkPermission(2)
}
@@ -0,0 +1,53 @@
package io.legado.app.ui.config
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefLong
import io.legado.app.utils.getPrefString
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.putPrefLong
import io.legado.app.utils.putPrefString
import splitties.init.appCtx
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun <T> prefDelegate(
key: String,
defaultValue: T,
onValueChange: ((T) -> Unit)? = null
): ReadWriteProperty<Any?, T> {
return object : ReadWriteProperty<Any?, T> {
private var _value: MutableState<T> = mutableStateOf(readInitialValue())
@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
else -> defaultValue
}
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return _value.value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (_value.value != value) {
when (value) {
is String -> appCtx.putPrefString(key, value)
is Int -> appCtx.putPrefInt(key, value)
is Boolean -> appCtx.putPrefBoolean(key, value)
is Long -> appCtx.putPrefLong(key, value)
}
_value.value = value
onValueChange?.invoke(value)
}
}
}
}
@@ -0,0 +1,13 @@
package io.legado.app.ui.config
import androidx.compose.runtime.Composable
import io.legado.app.base.BaseComposeActivity
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
class TestConfigActivity : BaseComposeActivity() {
@Composable
override fun Content() {
OtherConfigScreen(onBackClick = { finish() })
}
}
@@ -0,0 +1,153 @@
package io.legado.app.ui.config.otherConfig
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CheckSourceBottomSheet(
viewModel: OtherConfigViewModel = koinViewModel(),
onDismiss: () -> Unit
) {
val context = LocalContext.current
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
GlassModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 32.dp)
) {
Text(
text = stringResource(R.string.check_source_config),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 16.dp)
)
SliderSettingItem(
title = stringResource(R.string.check_source_timeout),
color = MaterialTheme.colorScheme.surface,
value = viewModel.checkSourceTimeout.toFloat(),
defaultValue = 180f,
onValueChange = { viewModel.checkSourceTimeout = it.toLong() },
valueRange = 0f..300f,
)
Spacer(modifier = Modifier.padding(8.dp))
CheckboxGroupContainer(columns = 2) {
item {
CheckboxItem(
title = stringResource(R.string.search),
checked = viewModel.checkSearch,
onCheckedChange = {
viewModel.checkSearch = it
if (!it && !viewModel.checkDiscovery) {
viewModel.checkDiscovery = true
}
}
)
}
item {
CheckboxItem(
title = stringResource(R.string.discovery),
checked = viewModel.checkDiscovery,
onCheckedChange = {
viewModel.checkDiscovery = it
if (!it && !viewModel.checkSearch) {
viewModel.checkSearch = true
}
}
)
}
}
CheckboxGroupContainer(columns = 3) {
item {
CheckboxItem(
title = stringResource(R.string.source_tab_info),
checked = viewModel.checkInfo,
onCheckedChange = {
viewModel.checkInfo = it
if (!it) {
viewModel.checkCategory = false
viewModel.checkContent = false
}
}
)
}
item {
CheckboxItem(
title = stringResource(R.string.chapter_list),
checked = viewModel.checkCategory,
enabled = viewModel.checkInfo,
onCheckedChange = {
viewModel.checkCategory = it
if (!it) viewModel.checkContent = false
}
)
}
item {
CheckboxItem(
title = stringResource(R.string.source_tab_content),
checked = viewModel.checkContent,
enabled = viewModel.checkCategory,
onCheckedChange = { viewModel.checkContent = it }
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
Button(onClick = {
if (viewModel.saveCheckSourceConfig()) {
onDismiss()
} else {
Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show()
}
}) {
Text(stringResource(R.string.ok))
}
}
}
}
}
@@ -0,0 +1,218 @@
package io.legado.app.ui.config.otherConfig
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.help.DirectLinkUpload
import io.legado.app.lib.dialogs.selector
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getClipText
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toastOnUi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DirectLinkUploadBottomSheet(
viewModel: OtherConfigViewModel,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var showMenu by remember { mutableStateOf(false) }
var showTestResult by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
viewModel.initDirectLinkRule()
}
GlassModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 24.dp)
.verticalScroll(rememberScrollState())
) {
CenterAlignedTopAppBar(
title = {
Text(
text = stringResource(R.string.direct_link_upload_config),
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { Text("导入默认") },
leadingIcon = { Icon(Icons.Default.Download, null) },
onClick = {
showMenu = false
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
viewModel.upView(rule)
}
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.copy_rule)) },
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
onClick = {
showMenu = false
val rule = DirectLinkUpload.Rule(
viewModel.uploadUrl,
viewModel.downloadUrlRule,
viewModel.summary,
viewModel.compress
)
context.sendToClip(GSON.toJson(rule))
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.paste_rule)) },
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
onClick = {
showMenu = false
runCatching {
context.getClipText()?.let {
val rule =
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
.getOrThrow()
viewModel.upView(rule)
}
}.onFailure {
context.toastOnUi("剪贴板为空或格式不对")
}
}
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = viewModel.uploadUrl,
onValueChange = { viewModel.uploadUrl = it },
label = { Text(stringResource(R.string.upload_url)) },
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = viewModel.downloadUrlRule,
onValueChange = { viewModel.downloadUrlRule = it },
label = { Text(stringResource(R.string.download_url_rule)) },
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = viewModel.summary,
onValueChange = { viewModel.summary = it },
label = { Text(stringResource(R.string.summary)) },
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(16.dp))
CheckboxItem(
title = stringResource(R.string.is_compress),
color = MaterialTheme.colorScheme.surface,
checked = viewModel.compress,
onCheckedChange = { viewModel.compress = it }
)
Spacer(Modifier.height(24.dp))
Row(modifier = Modifier.fillMaxWidth()) {
TextButton(onClick = {
viewModel.testRule { result -> showTestResult = result }
}) {
Text("测试")
}
Spacer(Modifier.weight(1f))
OutlinedButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
Button(onClick = {
if (viewModel.saveDirectLinkRule()) {
onDismiss()
} else {
context.toastOnUi("请填写完整信息")
}
}) {
Text(stringResource(R.string.ok))
}
}
}
}
showTestResult?.let { result ->
AlertDialog(
onDismissRequest = { showTestResult = null },
title = { Text("Result") },
text = { Text(result) },
confirmButton = {
TextButton(onClick = {
showTestResult = null
}) { Text(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { context.sendToClip(result) }) { Text(stringResource(R.string.copy_text)) }
}
)
}
}
@@ -0,0 +1,183 @@
package io.legado.app.ui.config.otherConfig
import io.legado.app.BuildConfig
import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.prefDelegate
object OtherConfig {
var language by prefDelegate(
PreferKey.language,
"auto"
)
var updateToVariant by prefDelegate(
PreferKey.updateToVariant,
"official_version"
)
var webServiceAutoStart by prefDelegate(
PreferKey.webServiceAutoStart,
false
)
var autoRefresh by prefDelegate(
PreferKey.autoRefresh,
false
)
var defaultToRead by prefDelegate(
PreferKey.defaultToRead,
false
)
var notificationsPost by prefDelegate(
PreferKey.notificationsPost,
true
)
var ignoreBatteryPermission by prefDelegate(
PreferKey.ignoreBatteryPermission,
true
)
var firebaseEnable by prefDelegate(
PreferKey.firebaseEnable,
true
)
var defaultBookTreeUri by prefDelegate(
PreferKey.defaultBookTreeUri,
""
)
var antiAlias by prefDelegate(
PreferKey.antiAlias,
false
)
var bitmapCacheSize by prefDelegate(
PreferKey.bitmapCacheSize,
50
)
var imageRetainNum by prefDelegate(
PreferKey.imageRetainNum,
0
)
var preDownloadNum by prefDelegate(
PreferKey.preDownloadNum,
10
)
var replaceEnableDefault by prefDelegate(
PreferKey.replaceEnableDefault,
true
)
var mediaButtonOnExit by prefDelegate(
PreferKey.mediaButtonOnExit,
true
)
var readAloudByMediaButton by prefDelegate(
PreferKey.readAloudByMediaButton,
false
)
var ignoreAudioFocus by prefDelegate(
PreferKey.ignoreAudioFocus,
false
)
var autoClearExpired by prefDelegate(
PreferKey.autoClearExpired,
true
)
var showAddToShelfAlert by prefDelegate(
PreferKey.showAddToShelfAlert,
true
)
var showMangaUi by prefDelegate(
PreferKey.showMangaUi,
true
)
var sharedElementEnterTransitionEnable by prefDelegate(
PreferKey.sharedElementEnterTransitionEnable,
false
)
var delayBookLoadEnable by prefDelegate(
PreferKey.delayBookLoadEnable,
true
)
private var _userAgent by prefDelegate(
PreferKey.userAgent,
""
)
var userAgent: String
get() = _userAgent.ifBlank {
defaultUserAgent
}
set(value) {
_userAgent = value
}
var webServiceWakeLock by prefDelegate(
PreferKey.webServiceWakeLock,
false
)
private var _sourceEditMaxLine by prefDelegate(
PreferKey.sourceEditMaxLine,
Int.MAX_VALUE
)
var sourceEditMaxLine: Int
get() = if (_sourceEditMaxLine < 10) Int.MAX_VALUE else _sourceEditMaxLine
set(value) {
_sourceEditMaxLine = value
}
var cronetEnable by prefDelegate(
PreferKey.cronet,
false
)
var webPort by prefDelegate(
PreferKey.webPort,
1122
)
var threadCount by prefDelegate(
PreferKey.threadCount,
16
)
var processText by prefDelegate(
PreferKey.processText,
true
)
var recordLog by prefDelegate(
PreferKey.recordLog,
false
)
var recordHeapDump by prefDelegate(
PreferKey.recordHeapDump,
false
)
private val defaultUserAgent: String
get() = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/${BuildConfig.Cronet_Main_Version} Safari/537.36"
}
@@ -0,0 +1,512 @@
package io.legado.app.ui.config.otherConfig
import android.Manifest
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.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
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
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.platform.LocalContext
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.service.WebService
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.SmallTopBarButton
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet
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.InputSettingItem
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.utils.restart
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OtherConfigScreen(
onBackClick: () -> Unit,
viewModel: OtherConfigViewModel = koinViewModel()
) {
val context = LocalContext.current
val notificationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
}
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showClearCacheDialog by remember { mutableStateOf(false) }
var showShrinkDbDialog by remember { mutableStateOf(false) }
var showClearWebViewDialog by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) }
var showCheckSourceSheet by remember { mutableStateOf(false) }
var showDirectLinkUploadSheet by remember { mutableStateOf(false) }
var tempPassword by remember { mutableStateOf("") }
var showFilePicker by remember { mutableStateOf(false) }
val selectDocTree = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
) { uri ->
uri?.let {
val modeFlags =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(it, modeFlags)
viewModel.updateLocalBookDir(it.toString())
}
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = {
Text(stringResource(R.string.other_setting))
},
scrollBehavior = scrollBehavior,
navigationIcon = {
SmallTopBarButton(onClick = onBackClick)
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
SplicedColumnGroup {
DropdownListSettingItem(
title = stringResource(R.string.language),
selectedValue = OtherConfig.language,
displayEntries = stringArrayResource(R.array.language),
entryValues = stringArrayResource(R.array.language_value),
onValueChange = { newValue ->
OtherConfig.language = newValue
context.restart()
}
)
DropdownListSettingItem(
title = stringResource(R.string.update_to_variant_title),
description = stringResource(R.string.update_to_variant_summary),
selectedValue = OtherConfig.updateToVariant,
displayEntries = stringArrayResource(R.array.default_app_variant),
entryValues = stringArrayResource(R.array.default_app_variant_value),
onValueChange = { OtherConfig.updateToVariant = it }
)
SwitchSettingItem(
title = stringResource(R.string.web_service_auto_start),
checked = OtherConfig.webServiceAutoStart,
onCheckedChange = { OtherConfig.webServiceAutoStart = it }
)
}
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
SwitchSettingItem(
title = stringResource(R.string.pt_auto_refresh),
description = stringResource(R.string.ps_auto_refresh),
checked = OtherConfig.autoRefresh,
onCheckedChange = { OtherConfig.autoRefresh = it }
)
SwitchSettingItem(
title = stringResource(R.string.pt_default_read),
description = stringResource(R.string.ps_default_read),
checked = OtherConfig.defaultToRead,
onCheckedChange = { OtherConfig.defaultToRead = it }
)
}
SplicedColumnGroup(title = stringResource(R.string.privacy)) {
ClickableSettingItem(
title = stringResource(R.string.notification_permission),
description = stringResource(R.string.notification_permission_rationale),
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
Toast.makeText(context, "无需申请", Toast.LENGTH_SHORT).show()
}
}
)
ClickableSettingItem(
title = stringResource(R.string.background_permission),
description = stringResource(R.string.ignore_battery_permission_rationale),
onClick = {
}
)
SwitchSettingItem(
title = stringResource(R.string.firebase_enable_title),
description = stringResource(R.string.firebase_enable_summary),
checked = OtherConfig.firebaseEnable,
onCheckedChange = { OtherConfig.firebaseEnable = it }
)
ClickableSettingItem(
title = stringResource(R.string.set_local_password),
description = stringResource(R.string.set_local_password_summary),
onClick = { showPasswordDialog = true }
)
}
SplicedColumnGroup(title = stringResource(R.string.read)) {
ClickableSettingItem(
title = stringResource(R.string.book_tree_uri_t),
description = OtherConfig.defaultBookTreeUri,
onClick = { showFilePicker = true }
)
SwitchSettingItem(
title = stringResource(R.string.anti_alias),
description = stringResource(R.string.pref_anti_alias_summary),
checked = OtherConfig.antiAlias,
onCheckedChange = { OtherConfig.antiAlias = it }
)
SliderSettingItem(
title = stringResource(R.string.bitmap_cache_size),
description = stringResource(R.string.bitmap_cache_size_summary),
value = OtherConfig.bitmapCacheSize.toFloat(),
defaultValue = 32f,
valueRange = 1f..2047f,
onValueChange = {
viewModel.updateBitmapCacheSize(it.toInt())
}
)
SliderSettingItem(
title = stringResource(R.string.image_retain_number),
description = stringResource(R.string.image_retain_number_summary),
value = OtherConfig.imageRetainNum.toFloat(),
defaultValue = 10f,
valueRange = 0f..100f,
onValueChange = { OtherConfig.imageRetainNum = it.toInt() }
)
SliderSettingItem(
title = stringResource(R.string.pre_download),
description = stringResource(R.string.pre_download_s),
value = OtherConfig.preDownloadNum.toFloat(),
defaultValue = 10f,
valueRange = 0f..100f,
onValueChange = { OtherConfig.preDownloadNum = it.toInt() }
)
SwitchSettingItem(
title = stringResource(R.string.replace_enable_default_t),
description = stringResource(R.string.replace_enable_default_s),
checked = OtherConfig.replaceEnableDefault,
onCheckedChange = { OtherConfig.replaceEnableDefault = it }
)
SwitchSettingItem(
title = stringResource(R.string.media_button_on_exit_title),
description = stringResource(R.string.media_button_on_exit_summary),
checked = OtherConfig.mediaButtonOnExit,
onCheckedChange = { OtherConfig.mediaButtonOnExit = it }
)
SwitchSettingItem(
title = stringResource(R.string.read_aloud_by_media_button_title),
description = stringResource(R.string.read_aloud_by_media_button_summary),
checked = OtherConfig.readAloudByMediaButton,
onCheckedChange = { OtherConfig.readAloudByMediaButton = it }
)
SwitchSettingItem(
title = stringResource(R.string.ignore_audio_focus_title),
description = stringResource(R.string.ignore_audio_focus_summary),
checked = OtherConfig.ignoreAudioFocus,
onCheckedChange = { OtherConfig.ignoreAudioFocus = it }
)
SwitchSettingItem(
title = stringResource(R.string.auto_clear_expired),
description = stringResource(R.string.auto_clear_expired_summary),
checked = OtherConfig.autoClearExpired,
onCheckedChange = { OtherConfig.autoClearExpired = it }
)
SwitchSettingItem(
title = stringResource(R.string.show_add_to_shelf_alert_title),
description = stringResource(R.string.show_add_to_shelf_alert_summary),
checked = OtherConfig.showAddToShelfAlert,
onCheckedChange = { OtherConfig.showAddToShelfAlert = it }
)
SwitchSettingItem(
title = stringResource(R.string.show_manga_ui),
checked = OtherConfig.showMangaUi,
onCheckedChange = { OtherConfig.showMangaUi = it }
)
}
SplicedColumnGroup(title = stringResource(R.string.other_setting)) {
SwitchSettingItem(
title = stringResource(R.string.use_animation),
description = stringResource(R.string.opt_animation),
checked = OtherConfig.sharedElementEnterTransitionEnable,
onCheckedChange = { OtherConfig.sharedElementEnterTransitionEnable = it }
)
SwitchSettingItem(
title = stringResource(R.string.delay_book_load),
description = stringResource(R.string.reduce_stutter),
checked = OtherConfig.delayBookLoadEnable,
onCheckedChange = { OtherConfig.delayBookLoadEnable = it }
)
InputSettingItem(
title = stringResource(R.string.user_agent),
value = OtherConfig.userAgent,
onConfirm = { viewModel.saveUserAgent(it) }
)
SwitchSettingItem(
title = stringResource(R.string.web_service_wake_lock),
description = stringResource(R.string.web_service_wake_lock_summary),
checked = OtherConfig.webServiceWakeLock,
onCheckedChange = { OtherConfig.webServiceWakeLock = it }
)
InputSettingItem(
title = stringResource(R.string.source_edit_text_max_line),
value = OtherConfig.sourceEditMaxLine.toString(),
defaultValue = 500.toString(),
onConfirm = { OtherConfig.sourceEditMaxLine = it.toIntOrNull() ?: 500 }
)
ClickableSettingItem(
title = stringResource(R.string.check_source_config),
onClick = { showCheckSourceSheet = true }
)
ClickableSettingItem(
title = stringResource(R.string.direct_link_upload_rule),
description = stringResource(R.string.direct_link_upload_rule_summary),
onClick = { showDirectLinkUploadSheet = true }
)
SwitchSettingItem(
title = "Cronet",
description = stringResource(R.string.pref_cronet_summary),
checked = OtherConfig.cronetEnable,
onCheckedChange = { OtherConfig.cronetEnable = it }
)
InputSettingItem(
title = stringResource(R.string.web_port_title),
value = OtherConfig.webPort.toString(),
onConfirm = { newValue ->
OtherConfig.webPort = newValue.toInt()
if (WebService.isRun) {
WebService.stop(context)
WebService.start(context)
}
}
)
ClickableSettingItem(
title = stringResource(R.string.clear_cache),
description = stringResource(R.string.clear_cache_summary),
onClick = { showClearCacheDialog = true }
)
ClickableSettingItem(
title = stringResource(R.string.clear_webview_data),
description = stringResource(R.string.clear_webview_data_summary),
onClick = { showClearWebViewDialog = true }
)
ClickableSettingItem(
title = stringResource(R.string.shrink_database),
description = stringResource(R.string.shrink_database_summary),
onClick = { showShrinkDbDialog = true }
)
SliderSettingItem(
title = stringResource(R.string.threads_num_title),
value = OtherConfig.threadCount.toFloat(),
defaultValue = 8f,
valueRange = 1f..256f,
onValueChange = { OtherConfig.threadCount = it.toInt() }
)
SwitchSettingItem(
title = stringResource(R.string.add_to_text_context_menu_t),
description = stringResource(R.string.add_to_text_context_menu_s),
checked = viewModel.isProcessTextEnabled(),
onCheckedChange = { viewModel.setProcessTextEnable(it) }
)
SwitchSettingItem(
title = stringResource(R.string.record_log),
description = stringResource(R.string.record_debug_log),
checked = OtherConfig.recordLog,
onCheckedChange = { OtherConfig.recordLog = it }
)
SwitchSettingItem(
title = stringResource(R.string.record_heap_dump_t),
description = stringResource(R.string.record_heap_dump_s),
checked = OtherConfig.recordHeapDump,
onCheckedChange = { OtherConfig.recordHeapDump = it }
)
}
}
if (showFilePicker) {
FilePickerSheet(
sheetState = rememberModalBottomSheetState(),
onDismissRequest = { showFilePicker = false },
onSelectSysDir = {
showFilePicker = false
try {
selectDocTree.launch(null)
} catch (e: Exception) {
}
}
)
}
if (showCheckSourceSheet) {
CheckSourceBottomSheet(
viewModel = viewModel,
onDismiss = { showCheckSourceSheet = false }
)
}
if (showDirectLinkUploadSheet) {
DirectLinkUploadBottomSheet(
viewModel = viewModel,
onDismiss = { showDirectLinkUploadSheet = false }
)
}
if (showClearCacheDialog) {
ConfirmDialog(
title = stringResource(R.string.clear_cache),
text = stringResource(R.string.sure_del),
onConfirm = {
viewModel.clearCache(context)
showClearCacheDialog = false
},
onDismiss = { showClearCacheDialog = false }
)
}
if (showShrinkDbDialog) {
ConfirmDialog(
title = stringResource(R.string.shrink_database),
text = stringResource(R.string.sure),
onConfirm = {
viewModel.shrinkDatabase()
showShrinkDbDialog = false
},
onDismiss = { showShrinkDbDialog = false }
)
}
if (showClearWebViewDialog) {
ConfirmDialog(
title = stringResource(R.string.clear_webview_data),
text = stringResource(R.string.sure_del),
onConfirm = {
viewModel.clearWebViewData(context)
showClearWebViewDialog = false
},
onDismiss = { showClearWebViewDialog = false }
)
}
if (showPasswordDialog) {
AlertDialog(
onDismissRequest = { showPasswordDialog = false },
title = { Text(stringResource(R.string.set_local_password)) },
text = {
TextField(
value = tempPassword,
onValueChange = { tempPassword = it },
label = { Text("Password") }
)
},
confirmButton = {
TextButton(onClick = {
viewModel.setLocalPassword(tempPassword)
showPasswordDialog = false
}) { Text(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { showPasswordDialog = false }) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
}
@Composable
fun ConfirmDialog(
title: String,
text: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { Text(text) },
confirmButton = {
OutlinedButton(onClick = onConfirm) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}
@@ -0,0 +1,157 @@
package io.legado.app.ui.config.otherConfig
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.help.DirectLinkUpload
import io.legado.app.help.book.BookHelp
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.model.CheckSource
import io.legado.app.model.ImageProvider
import io.legado.app.receiver.SharedReceiverActivity
import io.legado.app.utils.FileUtils
import io.legado.app.utils.putPrefString
import io.legado.app.utils.restart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import splitties.init.appCtx
class OtherConfigViewModel : ViewModel() {
private val packageManager = appCtx.packageManager
private val componentName = ComponentName(
appCtx,
SharedReceiverActivity::class.java.name
)
fun isProcessTextEnabled(): Boolean {
return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
fun setProcessTextEnable(enable: Boolean) {
val state = if (enable) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
packageManager.setComponentEnabledSetting(
componentName,
state,
PackageManager.DONT_KILL_APP
)
}
fun clearCache(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
BookHelp.clearCache()
FileUtils.delete(context.cacheDir.absolutePath)
context.externalCacheDir?.deleteRecursively()
context.cacheDir.deleteRecursively()
}
}
fun shrinkDatabase() {
viewModelScope.launch(Dispatchers.IO) {
appDb.openHelper.writableDatabase.execSQL("VACUUM")
}
}
fun clearWebViewData(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
FileUtils.delete(context.getDir("webview", Context.MODE_PRIVATE))
FileUtils.delete(context.getDir("hws_webview", Context.MODE_PRIVATE), true)
delay(3000)
appCtx.restart()
}
}
fun setLocalPassword(password: String) {
LocalConfig.password = password
}
fun saveUserAgent(input: String) {
OtherConfig.userAgent = input
AppConfig.userAgent = OtherConfig.userAgent
}
fun updateBitmapCacheSize(size: Int) {
AppConfig.bitmapCacheSize = size
ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
}
fun updateLocalBookDir(path: String) {
OtherConfig.defaultBookTreeUri = path
AppConfig.defaultBookTreeUri = OtherConfig.defaultBookTreeUri
}
var checkSourceTimeout by mutableStateOf((CheckSource.timeout / 1000))
var checkSearch by mutableStateOf(CheckSource.checkSearch)
var checkDiscovery by mutableStateOf(CheckSource.checkDiscovery)
var checkInfo by mutableStateOf(CheckSource.checkInfo)
var checkCategory by mutableStateOf(CheckSource.checkCategory)
var checkContent by mutableStateOf(CheckSource.checkContent)
fun saveCheckSourceConfig(): Boolean {
val timeoutLong = checkSourceTimeout
if (timeoutLong <= 0) return false // 验证失败
CheckSource.timeout = timeoutLong * 1000
CheckSource.checkSearch = checkSearch
CheckSource.checkDiscovery = checkDiscovery
CheckSource.checkInfo = checkInfo
CheckSource.checkCategory = checkCategory
CheckSource.checkContent = checkContent
CheckSource.putConfig()
appCtx.putPrefString(PreferKey.checkSource, CheckSource.summary)
return true
}
var uploadUrl by mutableStateOf("")
var downloadUrlRule by mutableStateOf("")
var summary by mutableStateOf("")
var compress by mutableStateOf(false)
fun initDirectLinkRule() {
val rule = DirectLinkUpload.getRule()
upView(rule)
}
fun upView(rule: DirectLinkUpload.Rule) {
uploadUrl = rule.uploadUrl
downloadUrlRule = rule.downloadUrlRule
summary = rule.summary
compress = rule.compress
}
fun saveDirectLinkRule(): Boolean {
if (uploadUrl.isBlank() || downloadUrlRule.isBlank() || summary.isBlank()) return false
val rule = DirectLinkUpload.Rule(uploadUrl, downloadUrlRule, summary, compress)
DirectLinkUpload.putConfig(rule)
return true
}
fun testRule(onResult: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val rule = DirectLinkUpload.Rule(uploadUrl, downloadUrlRule, summary, compress)
runCatching {
DirectLinkUpload.upLoad("test.json", "{}", "application/json", rule)
}.onSuccess {
onResult(it)
}.onFailure {
onResult(it.localizedMessage ?: "ERROR")
}
}
}
}
@@ -154,12 +154,10 @@ fun DictRuleScreen(
FilePickerSheet(
sheetState = sheetState,
onDismissRequest = { showFilePickerSheet = false },
mode = filePickerMode,
onSelectSysDir = {
showFilePickerSheet = false
exportDoc.launch("exportDictRule.json")
},
onSelectSysFile = {},
onUpload = {
showFilePickerSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
@@ -52,8 +52,8 @@ 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.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.SettingItem
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.settingItem.SettingItem
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -167,12 +167,10 @@ fun ReplaceRuleScreen(
FilePickerSheet(
sheetState = sheetState,
onDismissRequest = { showFilePickerSheet = false },
mode = filePickerMode,
onSelectSysDir = {
showFilePickerSheet = false
exportDoc.launch("exportReplaceRule.json")
},
onSelectSysFile = {},
onUpload = {
showFilePickerSheet = false
viewModel.uploadSelectedRules(selectedIds, rules)
@@ -1,182 +0,0 @@
package io.legado.app.ui.widget.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
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.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.card.GlassCard
@Preview(showBackground = true)
@Composable
fun PreviewSettingItemList() {
MaterialTheme {
Column(modifier = Modifier.padding(16.dp)) {
SettingItem(
title = "Only Title",
description = null,
option = null,
onClick = {}
)
SettingItem(
title = "Title + Description",
description = "This is a description",
option = null,
onClick = {}
)
SettingItem(
title = "Title + Option",
description = null,
option = "Dynamic option text",
onClick = {}
)
SettingItem(
title = "Title + Desc + Option",
description = "Description content here",
option = "Animated changing string",
onClick = {}
)
SettingItem(
imageVector = Icons.Default.Info,
title = "With Icon",
description = "Description",
option = "Option",
onClick = {}
)
SettingItem(
title = "With Trailing",
description = "Trailing icon on the right",
option = null,
trailingContent = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null
)
},
onClick = {}
)
}
}
}
@Composable
fun SettingItem(
modifier: Modifier = Modifier,
painter: Painter? = null,
imageVector: ImageVector? = null,
title: String,
description: String? = null,
option: String? = null,
trailingContent: @Composable (() -> Unit)? = null,
dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) {
var showMenu by remember { mutableStateOf(false) }
GlassCard(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
shape = RoundedCornerShape(8.dp)
) {
ListItem(
modifier = modifier
.combinedClickable(
onClick = onClick,
onLongClick = {
if (dropdownMenu != null) {
showMenu = true
onLongClick?.invoke()
}
}
),
leadingContent = {
when {
painter != null -> Icon(
painter = painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
imageVector != null -> Icon(
imageVector = imageVector,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
headlineContent = {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
option?.let {
Text(
text = it,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
},
trailingContent = trailingContent,
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
)
if (dropdownMenu != null) {
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
dropdownMenu {
showMenu = false
}
}
}
}
}
@@ -26,13 +26,13 @@ fun SplicedColumnGroup(
title: String = "",
content: @Composable ColumnScope.() -> Unit,
) {
Column(modifier = modifier.padding(vertical = 8.dp)) {
Column(modifier = modifier.padding(top = 4.dp, bottom = 12.dp)) {
if (title.isNotEmpty()) {
Text(
text = title,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 12.dp, bottom = 8.dp)
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
)
}
@@ -0,0 +1,40 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun SmallTextButton(
text: String,
icon: ImageVector,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
TextButton(
onClick = onClick,
modifier = modifier,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
shape = MaterialTheme.shapes.small
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
Text(
text = text,
style = MaterialTheme.typography.labelMedium
)
}
}
@@ -0,0 +1,27 @@
package io.legado.app.ui.widget.components.checkBox
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun CheckboxGroupContainer(
columns: Int = 3,
content: LazyGridScope.() -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
content = content
)
}
@@ -0,0 +1,57 @@
package io.legado.app.ui.widget.components.checkBox
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
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.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun CheckboxItem(
title: String,
color: Color = Color.Transparent,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
val alpha = if (enabled) 1f else 0.5f
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.background(
color = if (checked && enabled) MaterialTheme.colorScheme.secondaryContainer
else color,
shape = MaterialTheme.shapes.small
)
.clickable(enabled = enabled) { onCheckedChange(!checked) }
.padding(vertical = 12.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = checked,
onCheckedChange = null,
enabled = enabled,
modifier = Modifier.alpha(alpha)
)
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
modifier = Modifier
.padding(start = 12.dp)
.alpha(alpha)
)
}
}
@@ -3,7 +3,7 @@ package io.legado.app.ui.widget.components.exportComponents
import android.webkit.MimeTypeMap
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -14,8 +14,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.SaveAlt
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -40,9 +40,10 @@ enum class FilePickerSheetMode {
fun FilePickerSheet(
sheetState: SheetState,
onDismissRequest: () -> Unit,
mode: FilePickerSheetMode,
onSelectSysDir: () -> Unit,
onSelectSysFile: (Array<String>) -> Unit,
title: String = stringResource(R.string.select_operation),
onSelectSysDir: (() -> Unit)? = null,
onSelectSysFile: ((Array<String>) -> Unit)? = null,
onManualInput: (() -> Unit)? = null,
onUpload: (() -> Unit)? = null,
allowExtensions: Array<String>? = null,
) {
@@ -54,53 +55,55 @@ fun FilePickerSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.padding(bottom = 16.dp),
text = stringResource(R.string.select_operation),
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 20.dp)
)
Row(
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
maxItemsInEachRow = 2
) {
when (mode) {
FilePickerSheetMode.DIR -> {
FilePickerOptionCard(
icon = Icons.Default.FolderOpen,
text = stringResource(R.string.sys_folder_picker),
onClick = onSelectSysDir
)
}
FilePickerSheetMode.FILE -> {
FilePickerOptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = stringResource(R.string.sys_file_picker),
onClick = { onSelectSysFile(typesOfExtensions(allowExtensions)) }
)
}
FilePickerSheetMode.EXPORT -> {
FilePickerOptionCard(
icon = Icons.Default.SaveAlt,
text = stringResource(R.string.save_to_local),
onClick = onSelectSysDir
)
if (onUpload != null) {
FilePickerOptionCard(
icon = Icons.Default.CloudUpload,
text = stringResource(R.string.upload_url),
onClick = onUpload
)
}
}
onSelectSysDir?.let {
FilePickerOptionCard(
icon = Icons.Default.FolderOpen,
text = stringResource(R.string.sys_folder_picker),
onClick = it
)
}
onSelectSysFile?.let {
FilePickerOptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = stringResource(R.string.sys_file_picker),
onClick = { it(typesOfExtensions(allowExtensions)) }
)
}
onManualInput?.let {
FilePickerOptionCard(
icon = Icons.Default.EditNote,
text = stringResource(R.string.manual_input),
onClick = it
)
}
onUpload?.let {
FilePickerOptionCard(
icon = Icons.Default.CloudUpload,
text = stringResource(R.string.upload_url),
onClick = it
)
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
@@ -0,0 +1,35 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun ClickableSettingItem(
title: String,
modifier: Modifier = Modifier,
description: String? = null,
option: String? = null,
imageVector: ImageVector? = null,
onClick: () -> Unit
) {
SettingItem(
modifier = modifier,
title = title,
description = description,
option = option,
imageVector = imageVector,
trailingContent = {
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
onClick = onClick
)
}
@@ -0,0 +1,92 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.button.SmallTextButton
@Composable
fun InputSettingItem(
title: String,
value: String,
defaultValue: String? = "",
description: String? = null,
onConfirm: (String) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
val state = rememberTextFieldState(initialText = value)
LaunchedEffect(expanded) {
if (expanded) {
state.edit {
replace(0, length, value)
}
}
}
SettingItem(
title = title,
description = description,
option = value,
expanded = expanded,
onExpandChange = { expanded = it },
expandContent = {
TextField(
state = state,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 48.dp),
label = { Text(stringResource(R.string.edit)) },
contentPadding = PaddingValues(
top = 4.dp,
bottom = 4.dp,
start = 12.dp,
end = 12.dp
),
onKeyboardAction = {
onConfirm(state.text.toString())
expanded = false
}
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
SmallTextButton(
text = "默认",
icon = Icons.Default.Replay,
onClick = {
state.edit { replace(0, length, defaultValue.toString()) }
}
)
SmallTextButton(
text = "确认",
icon = Icons.Default.Check,
onClick = {
onConfirm(state.text.toString())
expanded = false
}
)
}
}
)
}
@@ -0,0 +1,53 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun DropdownListSettingItem(
title: String,
selectedValue: String,
displayEntries: Array<String>,
entryValues: Array<String>,
description: String? = null,
imageVector: ImageVector? = null,
onValueChange: (String) -> Unit
) {
val currentEntry = displayEntries.getOrNull(entryValues.indexOf(selectedValue)) ?: selectedValue
SettingItem(
title = title,
description = description,
option = currentEntry,
imageVector = imageVector,
onClick = { },
dropdownMenu = { onDismiss ->
displayEntries.forEachIndexed { index, display ->
DropdownMenuItem(
text = { Text(display) },
onClick = {
onValueChange(entryValues[index])
onDismiss()
},
trailingIcon = if (selectedValue == entryValues[index]) {
{
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
} else null
)
}
}
)
}
@@ -0,0 +1,170 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.card.GlassCard
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SettingItem(
modifier: Modifier = Modifier,
painter: Painter? = null,
imageVector: ImageVector? = null,
color: Color? = null,
title: String,
description: String? = null,
option: String? = null,
trailingContent: @Composable (() -> Unit)? = null,
dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
expanded: Boolean = false,
onExpandChange: ((Boolean) -> Unit)? = null,
expandContent: (@Composable ColumnScope.() -> Unit)? = null
) {
var showMenu by remember { mutableStateOf(false) }
val isExpandable = expandContent != null && onExpandChange != null
GlassCard(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
) {
Column {
ListItem(
modifier = modifier
.combinedClickable(
onClick = {
when {
dropdownMenu != null -> showMenu = true
isExpandable -> onExpandChange.invoke(!expanded)
else -> onClick?.invoke()
}
},
onLongClick = {
if (dropdownMenu != null) showMenu = true
onLongClick?.invoke()
}
),
leadingContent = if (painter != null || imageVector != null) {
{
if (painter != null) {
Icon(
painter = painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
} else if (imageVector != null) {
Icon(
imageVector = imageVector,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else null,
headlineContent = {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = if (description != null || option != null) {
{
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
description?.let {
Text(
it,
style = MaterialTheme.typography.bodySmallEmphasized,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
)
}
option?.let {
Text(
it,
style = MaterialTheme.typography.labelMediumEmphasized,
color = MaterialTheme.colorScheme.primary
)
}
}
}
} else null,
trailingContent = {
Box(contentAlignment = Alignment.Center) {
if (isExpandable && trailingContent == null) {
val rotation by animateFloatAsState(
if (expanded) 180f else 0f,
label = "arrow"
)
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(rotation)
)
} else {
trailingContent?.invoke()
}
dropdownMenu?.let { menu ->
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }) {
menu { showMenu = false }
}
}
}
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
if (isExpandable) {
AnimatedVisibility(
visible = expanded,
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 8.dp)
) {
expandContent.invoke(this)
}
}
}
}
}
}
@@ -0,0 +1,130 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.LinearScale
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.button.SmallTextButton
@Composable
fun SliderSettingItem(
title: String,
color: Color? = null,
value: Float,
defaultValue: Float,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
description: String? = null,
onValueChange: (Float) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var isInputMode by remember { mutableStateOf(false) }
var inputText by remember { mutableStateOf(value.toInt().toString()) }
val textFieldState = rememberTextFieldState(initialText = value.toInt().toString())
SettingItem(
title = title,
description = description,
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
option = value.toInt().toString(),
expanded = expanded,
onExpandChange = { expanded = it },
expandContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
AnimatedContent(
targetState = isInputMode,
label = "input_slider_switch"
) { targetInputMode ->
if (targetInputMode) {
TextField(
state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("输入数值 (${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 48.dp),
contentPadding = PaddingValues(
top = 4.dp,
bottom = 4.dp,
start = 12.dp,
end = 12.dp
),
inputTransformation = {
val newText = asCharSequence().toString()
newText.toFloatOrNull()?.let { num ->
onValueChange(
num.coerceIn(
valueRange.start,
valueRange.endInclusive
)
)
}
}
)
} else {
Slider(
value = value,
onValueChange = {
onValueChange(it)
textFieldState.edit { replace(0, length, it.toInt().toString()) }
},
valueRange = valueRange,
steps = steps,
modifier = Modifier
.fillMaxWidth()
)
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
SmallTextButton(
text = if (isInputMode) "滑块" else "输入",
icon = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
onClick = { isInputMode = !isInputMode }
)
Spacer(Modifier.width(8.dp))
SmallTextButton(
text = "默认",
icon = Icons.Default.RestartAlt,
onClick = {
onValueChange(defaultValue)
textFieldState.edit { replace(0, length, defaultValue.toInt().toString()) }
}
)
}
}
)
}
@@ -0,0 +1,27 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun SwitchSettingItem(
title: String,
description: String? = null,
checked: Boolean,
imageVector: ImageVector? = null,
onCheckedChange: (Boolean) -> Unit
) {
SettingItem(
title = title,
description = description,
imageVector = imageVector,
onClick = { onCheckedChange(!checked) },
trailingContent = {
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
)
}
+4 -3
View File
@@ -225,7 +225,7 @@
<string name="no_download">暂无任务</string>
<string name="download_count">%1$d/%2$d</string>
<string name="import_select_book">导入选择书籍</string>
<string name="threads_num_title">更新和搜索线程数(太多会卡顿)</string>
<string name="threads_num_title">更新和搜索线程数</string>
<string name="change_icon">切换图标</string>
<string name="remove_from_bookshelf">已在书架</string>
<string name="start_read">开始阅读</string>
@@ -890,7 +890,7 @@
<string name="anti_alias">抗锯齿</string>
<string name="pref_anti_alias_summary">绘制图片时抗锯齿</string>
<string name="upload_url">上传 URL</string>
<string name="download_url_rule">下载URL规则(downloadUrls)</string>
<string name="download_url_rule">下载 URL 规则</string>
<string name="sort_by_respondTime">响应时间排序</string>
<string name="respondTime">响应时间:%1$d ms</string>
<string name="export_success">导出成功</string>
@@ -1080,7 +1080,7 @@
<string name="tip_divider_color">分隔线颜色</string>
<string name="same_title_removed">移除重复标题</string>
<string name="update_book_fail">更新失败</string>
<string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度,拒绝后将不再弹出。如有需要可至高级设置中再次申请</string>
<string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度。</string>
<string name="webdav_after_local_restore_confirm">WebDav 书源比本地新,是否恢复</string>
<string name="c_whitelist">白名单(contentWhitelist)</string>
<string name="c_blacklist">黑名单(contentBlacklist)</string>
@@ -1383,4 +1383,5 @@
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
<string name="show_swipe_animation">启用滑动时动画</string>
<string name="click_image_way">点击图片行为</string>
<string name="show_web_log">输出日志</string>
</resources>
@@ -2,6 +2,11 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<io.legado.app.lib.prefs.Preference
android:key="TEST"
android:summary="这里存放由 Compose 重写的设置页"
android:title="测试" />
<io.legado.app.lib.prefs.NameListPreference
android:defaultValue="auto"
android:key="language"