From f21ff42e4c023304d4f2f29c4e5fc3c90324d8c2 Mon Sep 17 00:00:00 2001
From: HapeLee <1321903405@qq.com>
Date: Mon, 2 Feb 2026 21:15:04 +0800
Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E5=A2=9E]=20=E5=88=9D=E6=AD=A5?=
=?UTF-8?q?=E7=94=A8=20Compose=20=E9=87=8D=E5=86=99=E9=AB=98=E7=BA=A7?=
=?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=8F=AF=E5=9C=A8?=
=?UTF-8?q?=E9=AB=98=E7=BA=A7-=E6=B5=8B=E8=AF=95=E5=88=9D=E6=AD=A5?=
=?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/src/main/AndroidManifest.xml | 4 +
.../main/java/io/legado/app/di/appModule.kt | 2 +
.../io/legado/app/help/config/ThemeConfig.kt | 128 +++--
.../io/legado/app/ui/about/AboutScreen.kt | 2 +-
.../app/ui/book/toc/rule/TxtRuleScreen.kt | 2 -
.../app/ui/config/OtherConfigFragment.kt | 3 +-
.../io/legado/app/ui/config/PrefDelegate.kt | 53 ++
.../app/ui/config/TestConfigActivity.kt | 13 +
.../otherConfig/CheckSourceBottomSheet.kt | 153 ++++++
.../DirectLinkUploadBottomSheet.kt | 218 ++++++++
.../app/ui/config/otherConfig/OtherConfig.kt | 183 +++++++
.../config/otherConfig/OtherConfigScreen.kt | 512 ++++++++++++++++++
.../otherConfig/OtherConfigViewModel.kt | 157 ++++++
.../legado/app/ui/dict/rule/DictRuleScreen.kt | 2 -
.../java/io/legado/app/ui/main/my/MyScreen.kt | 2 +-
.../app/ui/replace/ReplaceRuleScreen.kt | 2 -
.../app/ui/widget/components/SettingItem.kt | 182 -------
.../widget/components/SplicedColumnGroup.kt | 4 +-
.../components/button/SmallTextButton.kt | 40 ++
.../checkBox/CheckboxGroupContainer.kt | 27 +
.../components/checkBox/CheckboxItem.kt | 57 ++
.../exportComponents/FilePickerSheet.kt | 85 +--
.../settingItem/ClickableSettingItem.kt | 35 ++
.../settingItem/InputSettingItem.kt | 92 ++++
.../components/settingItem/ListSettingItem.kt | 53 ++
.../components/settingItem/SettingItem.kt | 170 ++++++
.../settingItem/SliderSettingItem.kt | 130 +++++
.../settingItem/SwitchSettingItem.kt | 27 +
app/src/main/res/values-zh-rCN/strings.xml | 7 +-
app/src/main/res/xml/pref_config_other.xml | 5 +
30 files changed, 2074 insertions(+), 276 deletions(-)
create mode 100644 app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt
create mode 100644 app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt
create mode 100644 app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt
create mode 100644 app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt
create mode 100644 app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt
create mode 100644 app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt
create mode 100644 app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
delete mode 100644 app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxGroupContainer.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ae3c7e51c..3d4d585c7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -216,6 +216,10 @@
android:name=".ui.config.ConfigActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="standard" />
+
ReplaceEditViewModel(
diff --git a/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt b/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt
index 987ff6981..e4725866c 100644
--- a/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt
+++ b/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt
@@ -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
+ )
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
index 39d342224..01697e969 100644
--- a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
@@ -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
diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
index 8e6031bae..63221948d 100644
--- a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt
@@ -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)
diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
index c784dcd9c..6bfbc71d6 100644
--- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
+++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
@@ -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()
PreferKey.notificationsPost -> checkPermission(1)
PreferKey.ignoreBatteryPermission -> checkPermission(2)
}
diff --git a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt
new file mode 100644
index 000000000..11aa74b26
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt
@@ -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 prefDelegate(
+ key: String,
+ defaultValue: T,
+ onValueChange: ((T) -> Unit)? = null
+): ReadWriteProperty {
+ return object : ReadWriteProperty {
+ private var _value: MutableState = 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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt b/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt
new file mode 100644
index 000000000..25efeb9a0
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt
@@ -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() })
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt
new file mode 100644
index 000000000..748a003a3
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt
@@ -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))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt
new file mode 100644
index 000000000..b8f118b3d
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt
@@ -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(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(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)) }
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt
new file mode 100644
index 000000000..9043c59d2
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt
@@ -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"
+
+}
diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt
new file mode 100644
index 000000000..e9d17f146
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt
@@ -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))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
new file mode 100644
index 000000000..adf1290c3
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
@@ -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")
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
index 0f6da0659..b47443652 100644
--- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt
@@ -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)
diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt b/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
index a6d5daea4..86d64d155 100644
--- a/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
@@ -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)
diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
index f1a9145fe..4218fce29 100644
--- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
@@ -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)
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt
deleted file mode 100644
index 865c3f845..000000000
--- a/app/src/main/java/io/legado/app/ui/widget/components/SettingItem.kt
+++ /dev/null
@@ -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
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt b/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt
index 6228d812d..873db348d 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt
@@ -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)
)
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt
new file mode 100644
index 000000000..e26cd9f38
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt
@@ -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
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxGroupContainer.kt b/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxGroupContainer.kt
new file mode 100644
index 000000000..b49a72a1b
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxGroupContainer.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt
new file mode 100644
index 000000000..f341faed5
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt
@@ -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)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt
index e55b0e278..fe5ce8868 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt
@@ -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) -> Unit,
+ title: String = stringResource(R.string.select_operation),
+ onSelectSysDir: (() -> Unit)? = null,
+ onSelectSysFile: ((Array) -> Unit)? = null,
+ onManualInput: (() -> Unit)? = null,
onUpload: (() -> Unit)? = null,
allowExtensions: Array? = 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))
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt
new file mode 100644
index 000000000..ae8c7f4b0
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt
new file mode 100644
index 000000000..a6b62e88e
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt
@@ -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
+ }
+ )
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt
new file mode 100644
index 000000000..1bc13b705
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt
@@ -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,
+ entryValues: Array,
+ 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
+ )
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt
new file mode 100644
index 000000000..f6887f54c
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt
@@ -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)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
new file mode 100644
index 000000000..f38e6dbf2
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
@@ -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,
+ 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()) }
+ }
+ )
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt
new file mode 100644
index 000000000..3e7712028
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SwitchSettingItem.kt
@@ -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
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 2a47b3ed9..4daeea624 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -225,7 +225,7 @@
暂无任务
%1$d/%2$d
导入选择书籍
- 更新和搜索线程数(太多会卡顿)
+ 更新和搜索线程数
切换图标
已在书架
开始阅读
@@ -890,7 +890,7 @@
抗锯齿
绘制图片时抗锯齿
上传 URL
- 下载URL规则(downloadUrls)
+ 下载 URL 规则
响应时间排序
响应时间:%1$d ms
导出成功
@@ -1080,7 +1080,7 @@
分隔线颜色
移除重复标题
更新失败
- 阅读需要发送通知来显示朗读控制和下载进度,拒绝后将不再弹出。如有需要可至高级设置中再次申请。
+ 阅读需要发送通知来显示朗读控制和下载进度。
WebDav 书源比本地新,是否恢复
白名单(contentWhitelist)
黑名单(contentBlacklist)
@@ -1383,4 +1383,5 @@
当前%d, 输入 0 代表退出即刻清理。
启用滑动时动画
点击图片行为
+ 输出日志
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_config_other.xml b/app/src/main/res/xml/pref_config_other.xml
index 34d32d4af..735a0a5dd 100644
--- a/app/src/main/res/xml/pref_config_other.xml
+++ b/app/src/main/res/xml/pref_config_other.xml
@@ -2,6 +2,11 @@
+
+