[新增] 初步用 Compose 重写高级设置界面,可在高级-测试初步体验。
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
+44
-41
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user