[新增] 初步用 Compose 重写高级设置界面,可在高级-测试初步体验。
This commit is contained in:
@@ -216,6 +216,10 @@
|
|||||||
android:name=".ui.config.ConfigActivity"
|
android:name=".ui.config.ConfigActivity"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:launchMode="standard" />
|
android:launchMode="standard" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.config.TestConfigActivity"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:launchMode="standard" />
|
||||||
<!-- 搜索界面 -->
|
<!-- 搜索界面 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.book.search.SearchActivity"
|
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.readRecord.ReadRecordViewModel
|
||||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||||
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
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.dict.rule.DictRuleViewModel
|
||||||
import io.legado.app.ui.main.my.MyViewModel
|
import io.legado.app.ui.main.my.MyViewModel
|
||||||
import io.legado.app.ui.replace.ReplaceEditRoute
|
import io.legado.app.ui.replace.ReplaceEditRoute
|
||||||
@@ -43,6 +44,7 @@ val appModule = module {
|
|||||||
viewModelOf(::ReplaceRuleViewModel)
|
viewModelOf(::ReplaceRuleViewModel)
|
||||||
viewModelOf(::AllBookmarkViewModel)
|
viewModelOf(::AllBookmarkViewModel)
|
||||||
viewModelOf(::TxtTocRuleViewModel)
|
viewModelOf(::TxtTocRuleViewModel)
|
||||||
|
viewModelOf(::OtherConfigViewModel)
|
||||||
|
|
||||||
viewModel { (route: ReplaceEditRoute) ->
|
viewModel { (route: ReplaceEditRoute) ->
|
||||||
ReplaceEditViewModel(
|
ReplaceEditViewModel(
|
||||||
|
|||||||
@@ -190,48 +190,12 @@ object ThemeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveDayTheme(context: Context, name: String) {
|
fun saveDayTheme(context: Context, name: String) {
|
||||||
val primary =
|
val config = getDayTheme(context, name)
|
||||||
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}"
|
|
||||||
)
|
|
||||||
addConfig(config)
|
addConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveNightTheme(context: Context, name: String) {
|
fun saveNightTheme(context: Context, name: String) {
|
||||||
val primary =
|
val config = getNightTheme(context, name)
|
||||||
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}"
|
|
||||||
)
|
|
||||||
addConfig(config)
|
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
|
@Keep
|
||||||
data class Config(
|
data class Config(
|
||||||
var themeName: String,
|
var themeName: String,
|
||||||
@@ -260,7 +295,9 @@ object ThemeConfig {
|
|||||||
var primaryColor: String,
|
var primaryColor: String,
|
||||||
var accentColor: String,
|
var accentColor: String,
|
||||||
var backgroundColor: String,
|
var backgroundColor: String,
|
||||||
var bottomBackground: String
|
var bottomBackground: String,
|
||||||
|
var backgroundImgPath: String?,
|
||||||
|
var backgroundImgBlur: Int
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -276,10 +313,23 @@ object ThemeConfig {
|
|||||||
&& other.accentColor == accentColor
|
&& other.accentColor == accentColor
|
||||||
&& other.backgroundColor == backgroundColor
|
&& other.backgroundColor == backgroundColor
|
||||||
&& other.bottomBackground == bottomBackground
|
&& other.bottomBackground == bottomBackground
|
||||||
|
&& other.backgroundImgPath == backgroundImgPath
|
||||||
|
&& other.backgroundImgBlur == backgroundImgBlur
|
||||||
}
|
}
|
||||||
return false
|
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.R
|
||||||
import io.legado.app.constant.AppConst.appInfo
|
import io.legado.app.constant.AppConst.appInfo
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
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.SplicedColumnGroup
|
||||||
import io.legado.app.ui.widget.components.button.SmallTopBarButton
|
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.card.TextCard
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SettingItem
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -154,12 +154,10 @@ fun TxtRuleScreen(
|
|||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
mode = filePickerMode,
|
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
exportDoc.launch("exportDictRule.json")
|
exportDoc.launch("exportDictRule.json")
|
||||||
},
|
},
|
||||||
onSelectSysFile = {},
|
|
||||||
onUpload = {
|
onUpload = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
viewModel.uploadSelectedRules(selectedIds, rules)
|
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.removePref
|
||||||
import io.legado.app.utils.restart
|
import io.legado.app.utils.restart
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +169,7 @@ class OtherConfigFragment : PreferenceFragment(),
|
|||||||
PreferKey.clearWebViewData -> clearWebViewData()
|
PreferKey.clearWebViewData -> clearWebViewData()
|
||||||
"localPassword" -> alertLocalPassword()
|
"localPassword" -> alertLocalPassword()
|
||||||
PreferKey.shrinkDatabase -> shrinkDatabase()
|
PreferKey.shrinkDatabase -> shrinkDatabase()
|
||||||
|
"TEST" -> startActivity<TestConfigActivity>()
|
||||||
PreferKey.notificationsPost -> checkPermission(1)
|
PreferKey.notificationsPost -> checkPermission(1)
|
||||||
PreferKey.ignoreBatteryPermission -> checkPermission(2)
|
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(
|
FilePickerSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
mode = filePickerMode,
|
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
exportDoc.launch("exportDictRule.json")
|
exportDoc.launch("exportDictRule.json")
|
||||||
},
|
},
|
||||||
onSelectSysFile = {},
|
|
||||||
onUpload = {
|
onUpload = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
viewModel.uploadSelectedRules(selectedIds, rules)
|
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.file.FileManageActivity
|
||||||
import io.legado.app.ui.replace.ReplaceRuleActivity
|
import io.legado.app.ui.replace.ReplaceRuleActivity
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
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.SplicedColumnGroup
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SettingItem
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
|||||||
@@ -167,12 +167,10 @@ fun ReplaceRuleScreen(
|
|||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
mode = filePickerMode,
|
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
exportDoc.launch("exportReplaceRule.json")
|
exportDoc.launch("exportReplaceRule.json")
|
||||||
},
|
},
|
||||||
onSelectSysFile = {},
|
|
||||||
onUpload = {
|
onUpload = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
viewModel.uploadSelectedRules(selectedIds, rules)
|
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 = "",
|
title: String = "",
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier.padding(vertical = 8.dp)) {
|
Column(modifier = modifier.padding(top = 4.dp, bottom = 12.dp)) {
|
||||||
if (title.isNotEmpty()) {
|
if (title.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-27
@@ -3,7 +3,7 @@ package io.legado.app.ui.widget.components.exportComponents
|
|||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||||
import androidx.compose.material.icons.filled.CloudUpload
|
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.FolderOpen
|
||||||
import androidx.compose.material.icons.filled.SaveAlt
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -40,9 +40,10 @@ enum class FilePickerSheetMode {
|
|||||||
fun FilePickerSheet(
|
fun FilePickerSheet(
|
||||||
sheetState: SheetState,
|
sheetState: SheetState,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
mode: FilePickerSheetMode,
|
title: String = stringResource(R.string.select_operation),
|
||||||
onSelectSysDir: () -> Unit,
|
onSelectSysDir: (() -> Unit)? = null,
|
||||||
onSelectSysFile: (Array<String>) -> Unit,
|
onSelectSysFile: ((Array<String>) -> Unit)? = null,
|
||||||
|
onManualInput: (() -> Unit)? = null,
|
||||||
onUpload: (() -> Unit)? = null,
|
onUpload: (() -> Unit)? = null,
|
||||||
allowExtensions: Array<String>? = null,
|
allowExtensions: Array<String>? = null,
|
||||||
) {
|
) {
|
||||||
@@ -54,53 +55,55 @@ fun FilePickerSheet(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
text = title,
|
||||||
text = stringResource(R.string.select_operation),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 20.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
FlowRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
maxItemsInEachRow = 2
|
||||||
) {
|
) {
|
||||||
when (mode) {
|
|
||||||
FilePickerSheetMode.DIR -> {
|
onSelectSysDir?.let {
|
||||||
FilePickerOptionCard(
|
FilePickerOptionCard(
|
||||||
icon = Icons.Default.FolderOpen,
|
icon = Icons.Default.FolderOpen,
|
||||||
text = stringResource(R.string.sys_folder_picker),
|
text = stringResource(R.string.sys_folder_picker),
|
||||||
onClick = onSelectSysDir
|
onClick = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FilePickerSheetMode.FILE -> {
|
|
||||||
|
onSelectSysFile?.let {
|
||||||
FilePickerOptionCard(
|
FilePickerOptionCard(
|
||||||
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
|
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||||
text = stringResource(R.string.sys_file_picker),
|
text = stringResource(R.string.sys_file_picker),
|
||||||
onClick = { onSelectSysFile(typesOfExtensions(allowExtensions)) }
|
onClick = { it(typesOfExtensions(allowExtensions)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FilePickerSheetMode.EXPORT -> {
|
|
||||||
FilePickerOptionCard(
|
|
||||||
icon = Icons.Default.SaveAlt,
|
|
||||||
text = stringResource(R.string.save_to_local),
|
|
||||||
onClick = onSelectSysDir
|
|
||||||
)
|
|
||||||
|
|
||||||
if (onUpload != null) {
|
onManualInput?.let {
|
||||||
|
FilePickerOptionCard(
|
||||||
|
icon = Icons.Default.EditNote,
|
||||||
|
text = stringResource(R.string.manual_input),
|
||||||
|
onClick = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpload?.let {
|
||||||
FilePickerOptionCard(
|
FilePickerOptionCard(
|
||||||
icon = Icons.Default.CloudUpload,
|
icon = Icons.Default.CloudUpload,
|
||||||
text = stringResource(R.string.upload_url),
|
text = stringResource(R.string.upload_url),
|
||||||
onClick = onUpload
|
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="no_download">暂无任务</string>
|
||||||
<string name="download_count">%1$d/%2$d</string>
|
<string name="download_count">%1$d/%2$d</string>
|
||||||
<string name="import_select_book">导入选择书籍</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="change_icon">切换图标</string>
|
||||||
<string name="remove_from_bookshelf">已在书架</string>
|
<string name="remove_from_bookshelf">已在书架</string>
|
||||||
<string name="start_read">开始阅读</string>
|
<string name="start_read">开始阅读</string>
|
||||||
@@ -890,7 +890,7 @@
|
|||||||
<string name="anti_alias">抗锯齿</string>
|
<string name="anti_alias">抗锯齿</string>
|
||||||
<string name="pref_anti_alias_summary">绘制图片时抗锯齿</string>
|
<string name="pref_anti_alias_summary">绘制图片时抗锯齿</string>
|
||||||
<string name="upload_url">上传 URL</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="sort_by_respondTime">响应时间排序</string>
|
||||||
<string name="respondTime">响应时间:%1$d ms</string>
|
<string name="respondTime">响应时间:%1$d ms</string>
|
||||||
<string name="export_success">导出成功</string>
|
<string name="export_success">导出成功</string>
|
||||||
@@ -1080,7 +1080,7 @@
|
|||||||
<string name="tip_divider_color">分隔线颜色</string>
|
<string name="tip_divider_color">分隔线颜色</string>
|
||||||
<string name="same_title_removed">移除重复标题</string>
|
<string name="same_title_removed">移除重复标题</string>
|
||||||
<string name="update_book_fail">更新失败</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="webdav_after_local_restore_confirm">WebDav 书源比本地新,是否恢复</string>
|
||||||
<string name="c_whitelist">白名单(contentWhitelist)</string>
|
<string name="c_whitelist">白名单(contentWhitelist)</string>
|
||||||
<string name="c_blacklist">黑名单(contentBlacklist)</string>
|
<string name="c_blacklist">黑名单(contentBlacklist)</string>
|
||||||
@@ -1383,4 +1383,5 @@
|
|||||||
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
|
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
|
||||||
<string name="show_swipe_animation">启用滑动时动画</string>
|
<string name="show_swipe_animation">启用滑动时动画</string>
|
||||||
<string name="click_image_way">点击图片行为</string>
|
<string name="click_image_way">点击图片行为</string>
|
||||||
|
<string name="show_web_log">输出日志</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
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
|
<io.legado.app.lib.prefs.NameListPreference
|
||||||
android:defaultValue="auto"
|
android:defaultValue="auto"
|
||||||
android:key="language"
|
android:key="language"
|
||||||
|
|||||||
Reference in New Issue
Block a user