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