[优化] 导航系统换成Navigation3,老旧设置页替换为新的Compose界面

This commit is contained in:
HapeLee
2026-03-27 01:30:13 +08:00
parent fafc852840
commit 75a3752079
20 changed files with 632 additions and 2132 deletions
+5 -20
View File
@@ -38,25 +38,11 @@ Design 3 风格重构版本。
## 🛠️ 核心功能
1. **自定义书源** 强大的爬虫引擎,规则简单易懂,支持自定义抓取网页数据
2. **多格式支持** 支持本地 TXT、EPUB 阅读,智能扫描本地文件
3. **高度自定义** 切换字体、背景、行距、段距、加粗、简繁转换等
4. **订阅内容** 不止是书,可以订阅任何你想看的内容
5. **净化替换** 强力去除广告,替换正文内容
6. **翻页模式:** 覆盖、仿真、滑动、滚动等多种模式随心切换。
7. **完全开源:** 无广告,持续迭代优化。
---
## 🤝 社区与支持
由于本项目的开发者比较忙,暂时没有独立社区。
以下为原项目的社区:
* **官方 Wiki:** [书源规则教程](https://mgz0227.github.io/The-tutorial-of-Legado/)
* **交流社区:** [Telegram 频道](https://t.me/legado_channels) | [Discord](https://discord.gg/VtUfRyzRXn) | [语雀 Wiki](https://www.yuque.com/legado/wiki/community)
* **相关文档:** [免责声明](https://gedoor.github.io/Disclaimer)
1. **多格式支持** 支持本地 TXT、EPUB 阅读,智能扫描本地文件
2. **高度自定义** 切换字体、背景、行距、段距、加粗、简繁转换等
3. **净化替换** 强力去除广告,替换正文内容
4. **翻页模式** 覆盖、仿真、滑动、滚动等多种模式随心切换
5. **完全开源** 无广告,持续迭代优化
---
@@ -78,6 +64,5 @@ Design 3 风格重构版本。
## ⚠️ 免责声明
本项目为 **Legado(阅读)** 的非官方分支,仅用于学习、交流与个人使用。
项目内容不代表原作者立场,与原项目作者不存在任何隶属或授权关系。
+1 -10
View File
@@ -211,15 +211,6 @@
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!-- 配置界面 -->
<activity
android:name=".ui.config.ConfigActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="standard" />
<activity
android:name=".ui.config.TestConfigActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="standard" />
<!-- 搜索界面 -->
<activity
android:name=".ui.book.search.SearchActivity"
@@ -595,4 +586,4 @@
</intent>
</queries>
</manifest>
</manifest>
@@ -52,6 +52,7 @@ object PreferKey {
const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait"
const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape"
const val bookshelfLayoutCompact = "bookshelfLayoutCompact"
const val bookshelfShowDivider = "bookshelfShowDivider"
const val bookshelfGridLayout = "bookshelfGridLayout"
const val bookshelfSort = "bookshelfSort"
const val bookExportFileName = "bookExportFileName"
@@ -168,19 +168,10 @@ object OldThemeConfig {
fun applyConfig(context: Context, config: Config) {
try {
val primary = config.primaryColor.toColorInt()
val accent = config.accentColor.toColorInt()
val background = config.backgroundColor.toColorInt()
val bBackground = config.bottomBackground.toColorInt()
if (config.isNightTheme) {
context.putPrefInt(PreferKey.cNPrimary, primary)
context.putPrefInt(PreferKey.cNAccent, accent)
context.putPrefInt(PreferKey.cNBackground, background)
context.putPrefInt(PreferKey.cNBBackground, bBackground)
} else {
context.putPrefInt(PreferKey.cPrimary, primary)
context.putPrefInt(PreferKey.cAccent, accent)
context.putPrefInt(PreferKey.cBackground, background)
context.putPrefInt(PreferKey.cBBackground, bBackground)
}
AppConfig.isNightTheme = config.isNightTheme
applyDayNight(context)
@@ -332,4 +323,4 @@ object OldThemeConfig {
}
}
}
@@ -1,414 +0,0 @@
package io.legado.app.ui.config
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.text.InputType
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.MenuProvider
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import io.legado.app.R
import io.legado.app.constant.AppLog
import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogWebdavAuthBinding
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.AppWebDav.testWebDav
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.BackupConfig
import io.legado.app.help.storage.ImportOldData
import io.legado.app.help.storage.Restore
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.prefs.fragment.PreferenceFragment
import io.legado.app.ui.about.AppLogDialog
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.FileDoc
import io.legado.app.utils.checkWrite
import io.legado.app.utils.getPrefString
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.launch
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.showHelp
import io.legado.app.utils.toEditable
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import kotlin.coroutines.coroutineContext
import androidx.core.content.edit
import kotlinx.coroutines.currentCoroutineContext
class BackupConfigFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener,
MenuProvider {
private val viewModel by activityViewModels<ConfigViewModel>()
private val waitDialog by lazy { WaitDialog(requireContext()) }
private var backupJob: Job? = null
private var restoreJob: Job? = null
private val selectBackupPath = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
if (uri.isContentScheme()) {
AppConfig.backupPath = uri.toString()
} else {
AppConfig.backupPath = uri.path
}
}
}
private val backupDir = registerForActivityResult(HandleFileContract()) { result ->
result.uri?.let { uri ->
if (uri.isContentScheme()) {
AppConfig.backupPath = uri.toString()
backup(uri.toString())
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
backup(path)
}
}
}
}
private val restoreDoc = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
waitDialog.setText("恢复中…")
waitDialog.show()
val task = Coroutine.async {
Restore.restore(appCtx, uri)
}.onFinally {
waitDialog.dismiss()
}
waitDialog.setOnCancelListener {
task.cancel()
}
}
}
private val restoreOld = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
ImportOldData.importUri(appCtx, uri)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_backup)
findPreference<EditTextPreference>(PreferKey.webDavDir)?.let {
it.setOnBindEditTextListener { editText ->
editText.text = AppConfig.webDavDir?.toEditable()
}
}
findPreference<EditTextPreference>(PreferKey.webDavDeviceName)?.let {
it.setOnBindEditTextListener { editText ->
editText.text = AppConfig.webDavDeviceName?.toEditable()
}
}
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName)
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
?.onLongClick {
restoreFromLocal()
true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.setTitle(R.string.backup_restore)
preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
//listView.setEdgeEffectColor(primaryColor)
activity?.addMenuProvider(this, viewLifecycleOwner)
if (!LocalConfig.backupHelpVersionIsLast) {
showHelp("webDavHelp")
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.backup_restore, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.menu_help -> {
showHelp("webDavHelp")
return true
}
R.id.menu_log -> showDialogFragment<AppLogDialog>()
}
return false
}
override fun onDestroy() {
super.onDestroy()
preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
PreferKey.backupPath -> upPreferenceSummary(key, getPrefString(key))
PreferKey.webDavUrl,
PreferKey.webDavAccount,
PreferKey.webDavPassword,
PreferKey.webDavDir -> listView.post {
upPreferenceSummary(key, appCtx.getPrefString(key))
viewModel.upWebDavConfig()
}
PreferKey.webDavDeviceName -> upPreferenceSummary(key, getPrefString(key))
}
}
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.webDavUrl ->
if (value.isNullOrBlank()) {
preference.summary = getString(R.string.web_dav_url_s)
} else {
preference.summary = value
}
PreferKey.webDavDir -> preference.summary = when (value) {
null -> "legado"
else -> value
}
else -> {
if (preference is ListPreference) {
val index = preference.findIndexOfValue(value)
// Set the summary to reflect the new value.
preference.summary = if (index >= 0) preference.entries[index] else null
} else {
preference.summary = value
}
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
PreferKey.backupPath -> selectBackupPath.launch()
PreferKey.restoreIgnore -> backupIgnore()
"web_dav_test" -> {
lifecycleScope.launch {
testWebDav()
}
}
"web_dav_auth" -> {
showWebDavAuthDialog()
}
"web_dav_backup" -> backup()
"web_dav_restore" -> restore()
"import_old" -> restoreOld.launch()
}
return super.onPreferenceTreeClick(preference)
}
private fun showWebDavAuthDialog() {
val binding = DialogWebdavAuthBinding.inflate(layoutInflater).apply {
editAccount.setText(PreferenceManager.getDefaultSharedPreferences(requireContext()).getString(PreferKey.webDavAccount, ""))
editPassword.setText(PreferenceManager.getDefaultSharedPreferences(requireContext()).getString(PreferKey.webDavPassword, ""))
editPassword.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
alert(titleResource = R.string.web_dav_account) {
customView { binding.root }
okButton {
val account = binding.editAccount.text.toString()
val password = binding.editPassword.text.toString()
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit {
putString(PreferKey.webDavAccount, account)
.putString(PreferKey.webDavPassword, password)
}
upPreferenceSummary(PreferKey.webDavAccount, account)
upPreferenceSummary(PreferKey.webDavPassword, password)
lifecycleScope.launch {
testWebDav()
}
}
cancelButton()
}
}
/**
* 备份忽略设置
*/
private fun backupIgnore() {
val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) {
BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false
}
alert(R.string.restore_ignore) {
multiChoiceItems(BackupConfig.ignoreTitle, checkedItems) { _, which, isChecked ->
BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[which]] = isChecked
}
onDismiss {
BackupConfig.saveIgnoreConfig()
}
}
}
fun backup() {
val backupPath = AppConfig.backupPath
if (backupPath.isNullOrEmpty()) {
backupDir.launch()
} else {
if (backupPath.isContentScheme()) {
lifecycleScope.launch {
val canWrite = withContext(IO) {
FileDoc.fromDir(backupPath).checkWrite()
}
if (canWrite) {
backup(backupPath)
} else {
backupDir.launch()
}
}
} else {
backupUsePermission(backupPath)
}
}
}
private fun backup(backupPath: String) {
waitDialog.setText("备份中…")
waitDialog.setOnCancelListener {
backupJob?.cancel()
}
waitDialog.show()
backupJob?.cancel()
backupJob = lifecycleScope.launch {
try {
Backup.backupLocked(requireContext(), backupPath)
appCtx.toastOnUi(R.string.backup_success)
} catch (e: Throwable) {
ensureActive()
AppLog.put("备份出错\n${e.localizedMessage}", e)
appCtx.toastOnUi(
appCtx.getString(
R.string.backup_fail,
e.localizedMessage
)
)
} finally {
ensureActive()
waitDialog.dismiss()
}
}
}
private fun backupUsePermission(path: String) {
PermissionsCompat.Builder()
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
backup(path)
}
.request()
}
fun restore() {
waitDialog.setText(R.string.loading)
waitDialog.setOnCancelListener {
restoreJob?.cancel()
}
waitDialog.show()
Coroutine.async {
restoreJob = coroutineContext[Job]
showRestoreDialog(requireContext())
}.onError {
AppLog.put("恢复备份出错WebDavError\n${it.localizedMessage}", it)
if (context == null) {
return@onError
}
alert {
setTitle(R.string.restore)
setMessage("WebDavError\n${it.localizedMessage}\n将从本地备份恢复。")
okButton {
restoreFromLocal()
}
cancelButton()
}
}.onFinally {
waitDialog.dismiss()
}
}
private suspend fun showRestoreDialog(context: Context) {
val names = withContext(IO) { AppWebDav.getBackupNames() }
if (AppWebDav.isJianGuoYun && names.size > 700) {
context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
}
if (names.isNotEmpty()) {
currentCoroutineContext().ensureActive()
withContext(Main) {
context.selector(
title = context.getString(R.string.select_restore_file),
items = names
) { _, index ->
if (index in 0 until names.size) {
listView.post {
restoreWebDav(names[index])
}
}
}
}
} else {
throw NoStackTraceException("Web dav no back up file")
}
}
private fun restoreWebDav(name: String) {
waitDialog.setText("恢复中…")
waitDialog.show()
val task = Coroutine.async {
AppWebDav.restoreWebDav(name)
}.onError {
AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it)
appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}")
}.onFinally {
waitDialog.dismiss()
}
waitDialog.setOnCancelListener {
task.cancel()
}
}
private fun restoreFromLocal() {
restoreDoc.launch {
title = getString(R.string.select_restore_file)
mode = HandleFileContract.FILE
allowExtensions = arrayOf("zip")
}
}
override fun onDestroyView() {
super.onDestroyView()
waitDialog.dismiss()
}
}
@@ -1,46 +0,0 @@
package io.legado.app.ui.config
import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityConfigBinding
import io.legado.app.utils.viewbindingdelegate.viewBinding
class ConfigActivity : VMBaseActivity<ActivityConfigBinding, ConfigViewModel>() {
override val binding by viewBinding(ActivityConfigBinding::inflate)
override val viewModel by viewModels<ConfigViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(binding.topBar)
when (val configTag = intent.getStringExtra("configTag")) {
ConfigTag.OTHER_CONFIG -> replaceFragment<OtherConfigFragment>(configTag)
ConfigTag.THEME_CONFIG -> replaceFragment<ThemeConfigFragment>(configTag)
ConfigTag.BACKUP_CONFIG -> replaceFragment<BackupConfigFragment>(configTag)
ConfigTag.COVER_CONFIG -> replaceFragment<CoverConfigFragment>(configTag)
ConfigTag.READ_CONFIG -> replaceFragment<ReadConfigFragment>(configTag)
else -> finish()
}
}
override fun setTitle(resId: Int) {
super.setTitle(resId)
binding.topBar.setTitle(resId)
binding.topBar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
inline fun <reified T : Fragment> replaceFragment(configTag: String) {
intent.putExtra("configTag", configTag)
@Suppress("DEPRECATION")
val configFragment = supportFragmentManager.findFragmentByTag(configTag)
?: T::class.java.newInstance()
supportFragmentManager.beginTransaction()
.replace(R.id.configFrameLayout, configFragment, configTag)
.commit()
}
}
@@ -0,0 +1,78 @@
package io.legado.app.ui.config
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.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfigNavScreen(
onBackClick: () -> Unit,
onNavigateToOther: () -> Unit,
onNavigateToRead: () -> Unit,
onNavigateToCover: () -> Unit,
onNavigateToTheme: () -> Unit,
onNavigateToBackup: () -> Unit
) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = { Text(stringResource(R.string.setting)) },
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
SplicedColumnGroup {
ClickableSettingItem(
title = stringResource(R.string.theme_setting),
onClick = onNavigateToTheme
)
ClickableSettingItem(
title = stringResource(R.string.other_setting),
onClick = onNavigateToOther
)
ClickableSettingItem(
title = stringResource(R.string.read_config),
onClick = onNavigateToRead
)
ClickableSettingItem(
title = stringResource(R.string.cover_config),
onClick = onNavigateToCover
)
ClickableSettingItem(
title = stringResource(R.string.backup_restore),
onClick = onNavigateToBackup
)
}
}
}
}
@@ -1,51 +0,0 @@
package io.legado.app.ui.config
import android.app.Application
import android.content.Context
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp
import io.legado.app.utils.FileUtils
import io.legado.app.utils.restart
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.delay
import splitties.init.appCtx
class ConfigViewModel(application: Application) : BaseViewModel(application) {
fun upWebDavConfig() {
execute {
AppWebDav.upConfig()
}
}
fun clearCache() {
execute {
BookHelp.clearCache()
FileUtils.delete(context.cacheDir.absolutePath)
}.onSuccess {
context.toastOnUi(R.string.clear_cache_success)
}
}
fun clearWebViewData() {
execute {
FileUtils.delete(context.getDir("webview", Context.MODE_PRIVATE))
FileUtils.delete(context.getDir("hws_webview", Context.MODE_PRIVATE), true)
context.toastOnUi(R.string.clear_webview_data_success)
delay(3000)
appCtx.restart()
}
}
fun shrinkDatabase() {
execute {
appDb.openHelper.writableDatabase.execSQL("VACUUM")
}.onSuccess {
context.toastOnUi(R.string.success)
}
}
}
@@ -1,168 +0,0 @@
package io.legado.app.ui.config
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.prefs.SwitchPreference
import io.legado.app.lib.prefs.fragment.PreferenceFragment
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.model.BookCover
import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.FileOutputStream
class CoverConfigFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeCover = 111
private val requestCodeCoverDark = 112
private val selectImage = registerForActivityResult(SelectImageContract()) {
it.uri?.let { uri ->
when (it.requestCode) {
requestCodeCover -> setCoverFromUri(PreferKey.defaultCover, uri)
requestCodeCoverDark -> setCoverFromUri(PreferKey.defaultCoverDark, uri)
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_cover)
upPreferenceSummary(PreferKey.defaultCover, getPrefString(PreferKey.defaultCover))
upPreferenceSummary(PreferKey.defaultCoverDark, getPrefString(PreferKey.defaultCoverDark))
findPreference<SwitchPreference>(PreferKey.coverShowAuthor)
?.isEnabled = getPrefBoolean(PreferKey.coverShowName)
findPreference<SwitchPreference>(PreferKey.coverShowAuthorN)
?.isEnabled = getPrefBoolean(PreferKey.coverShowNameN)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.setTitle(R.string.cover_config)
//listView.setEdgeEffectColor(primaryColor)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}
override fun onDestroy() {
super.onDestroy()
preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
sharedPreferences ?: return
when (key) {
PreferKey.defaultCover,
PreferKey.defaultCoverDark -> {
upPreferenceSummary(key, getPrefString(key))
}
PreferKey.coverShowName -> {
findPreference<SwitchPreference>(PreferKey.coverShowAuthor)
?.isEnabled = getPrefBoolean(key)
BookCover.upDefaultCover()
}
PreferKey.coverShowNameN -> {
findPreference<SwitchPreference>(PreferKey.coverShowAuthorN)
?.isEnabled = getPrefBoolean(key)
BookCover.upDefaultCover()
}
PreferKey.coverTextColor,
PreferKey.coverTextColorN,
PreferKey.coverShadowColor,
PreferKey.coverShadowColorN,
PreferKey.coverDefaultColor,
PreferKey.coverShowShadow,
PreferKey.coverShowStroke,
PreferKey.coverShowAuthor,
PreferKey.coverShowAuthorN -> {
BookCover.upDefaultCover()
}
}
}
@SuppressLint("PrivateResource")
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
"coverRule" -> showDialogFragment(CoverRuleConfigDialog())
PreferKey.defaultCover ->
if (getPrefString(preference.key).isNullOrEmpty()) {
selectImage.launch(requestCodeCover)
} else {
context?.selector(
items = arrayListOf(
getString(R.string.delete),
getString(R.string.select_image)
)
) { _, i ->
if (i == 0) {
removePref(preference.key)
BookCover.upDefaultCover()
} else {
selectImage.launch(requestCodeCover)
}
}
}
PreferKey.defaultCoverDark ->
if (getPrefString(preference.key).isNullOrEmpty()) {
selectImage.launch(requestCodeCoverDark)
} else {
context?.selector(
items = arrayListOf(
getString(R.string.delete),
getString(R.string.select_image)
)
) { _, i ->
if (i == 0) {
removePref(preference.key)
BookCover.upDefaultCover()
} else {
selectImage.launch(requestCodeCoverDark)
}
}
}
}
return super.onPreferenceTreeClick(preference)
}
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.defaultCover,
PreferKey.defaultCoverDark -> preference.summary = if (value.isNullOrBlank()) {
getString(R.string.select_image)
} else {
value
}
else -> preference.summary = value
}
}
private fun setCoverFromUri(preferenceKey: String, uri: Uri) {
readUri(uri) { fileDoc, inputStream ->
kotlin.runCatching {
var file = requireContext().externalFiles
val suffix = fileDoc.name.substringAfterLast(".")
val fileName = uri.inputStream(requireContext()).getOrThrow().use {
MD5Utils.md5Encode(it) + ".$suffix"
}
file = FileUtils.createFileIfNotExist(file, "covers", fileName)
FileOutputStream(file).use {
inputStream.copyTo(it)
}
putPrefString(preferenceKey, file.absolutePath)
BookCover.upDefaultCover()
}.onFailure {
appCtx.toastOnUi(it.localizedMessage)
}
}
}
}
@@ -1,401 +0,0 @@
package io.legado.app.ui.config
//import io.legado.app.lib.theme.primaryColor
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.postDelayed
import androidx.fragment.app.activityViewModels
import androidx.preference.ListPreference
import androidx.preference.Preference
import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppFreezeMonitor
import io.legado.app.help.DispatchersMonitor
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.prefs.fragment.PreferenceFragment
import io.legado.app.model.CheckSource
import io.legado.app.model.ImageProvider
import io.legado.app.receiver.SharedReceiverActivity
import io.legado.app.service.WebService
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.utils.FirebaseManager
import io.legado.app.utils.LogUtils
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefBoolean
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
/**
* 其它设置
*/
class OtherConfigFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val viewModel by activityViewModels<ConfigViewModel>()
private val packageManager = appCtx.packageManager
private val componentName = ComponentName(
appCtx,
SharedReceiverActivity::class.java.name
)
private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
it.uri?.let { treeUri ->
AppConfig.defaultBookTreeUri = treeUri.toString()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean(PreferKey.processText, isProcessTextEnabled())
addPreferencesFromResource(R.xml.pref_config_other)
upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
upPreferenceSummary(PreferKey.preDownloadNum, AppConfig.preDownloadNum.toString())
upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString())
upPreferenceSummary(PreferKey.webPort, AppConfig.webPort.toString())
AppConfig.defaultBookTreeUri?.let {
upPreferenceSummary(PreferKey.defaultBookTreeUri, it)
}
upPreferenceSummary(PreferKey.checkSource, CheckSource.summary)
upPreferenceSummary(PreferKey.bitmapCacheSize, AppConfig.bitmapCacheSize.toString())
upPreferenceSummary(PreferKey.imageRetainNum, AppConfig.imageRetainNum.toString())
upPreferenceSummary(PreferKey.sourceEditMaxLine, AppConfig.sourceEditMaxLine.toString())
updatePermissionSummary()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.setTitle(R.string.other_setting)
preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
//listView.setEdgeEffectColor(primaryColor)
}
override fun onDestroy() {
super.onDestroy()
preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onResume() {
super.onResume()
updatePermissionSummary()
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
PreferKey.userAgent -> showUserAgentDialog()
PreferKey.defaultBookTreeUri -> localBookTreeSelect.launch {
title = getString(R.string.select_book_folder)
mode = HandleFileContract.DIR_SYS
}
PreferKey.preDownloadNum -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.pre_download))
.setMaxValue(9999)
.setMinValue(0)
.setValue(AppConfig.preDownloadNum)
.show {
AppConfig.preDownloadNum = it
}
PreferKey.threadCount -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.threads_num_title))
.setMaxValue(999)
.setMinValue(1)
.setValue(AppConfig.threadCount)
.show {
AppConfig.threadCount = it
}
PreferKey.webPort -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.web_port_title))
.setMaxValue(60000)
.setMinValue(1024)
.setValue(AppConfig.webPort)
.show {
AppConfig.webPort = it
}
PreferKey.cleanCache -> clearCache()
PreferKey.uploadRule -> showDialogFragment<DirectLinkUploadConfig>()
PreferKey.checkSource -> showDialogFragment<CheckSourceConfig>()
PreferKey.bitmapCacheSize -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.bitmap_cache_size))
.setMaxValue(2047)
.setMinValue(1)
.setValue(AppConfig.bitmapCacheSize)
.show {
AppConfig.bitmapCacheSize = it
ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
}
}
PreferKey.imageRetainNum -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.image_retain_number))
.setMaxValue(999)
.setMinValue(0)
.setValue(AppConfig.imageRetainNum)
.show {
AppConfig.imageRetainNum = it
}
PreferKey.sourceEditMaxLine -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.source_edit_text_max_line))
.setMaxValue(Int.MAX_VALUE)
.setMinValue(10)
.setValue(AppConfig.sourceEditMaxLine)
.show {
AppConfig.sourceEditMaxLine = it
}
}
PreferKey.clearWebViewData -> clearWebViewData()
"localPassword" -> alertLocalPassword()
PreferKey.shrinkDatabase -> shrinkDatabase()
"TEST" -> startActivity<TestConfigActivity>()
PreferKey.notificationsPost -> checkPermission(1)
PreferKey.ignoreBatteryPermission -> checkPermission(2)
}
return super.onPreferenceTreeClick(preference)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
PreferKey.preDownloadNum -> {
upPreferenceSummary(key, AppConfig.preDownloadNum.toString())
}
PreferKey.threadCount -> {
upPreferenceSummary(key, AppConfig.threadCount.toString())
postEvent(PreferKey.threadCount, "")
}
PreferKey.webPort -> {
upPreferenceSummary(key, AppConfig.webPort.toString())
if (WebService.isRun) {
WebService.stop(requireContext())
WebService.start(requireContext())
}
}
PreferKey.defaultBookTreeUri -> {
upPreferenceSummary(key, AppConfig.defaultBookTreeUri)
}
PreferKey.recordLog -> {
AppConfig.recordLog = appCtx.getPrefBoolean(PreferKey.recordLog)
LogUtils.upLevel()
LogUtils.logDeviceInfo()
LiveEventBus.config().enableLogger(AppConfig.recordLog)
AppFreezeMonitor.init(appCtx)
DispatchersMonitor.init()
}
PreferKey.processText -> sharedPreferences?.let {
setProcessTextEnable(it.getBoolean(key, true))
}
PreferKey.language -> listView.postDelayed(1000) {
appCtx.restart()
}
PreferKey.userAgent -> listView.post {
upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
}
PreferKey.checkSource -> listView.post {
upPreferenceSummary(PreferKey.checkSource, CheckSource.summary)
}
PreferKey.bitmapCacheSize -> {
upPreferenceSummary(key, AppConfig.bitmapCacheSize.toString())
}
PreferKey.imageRetainNum -> {
upPreferenceSummary(key, AppConfig.imageRetainNum.toString())
}
PreferKey.sourceEditMaxLine -> {
upPreferenceSummary(key, AppConfig.sourceEditMaxLine.toString())
}
PreferKey.firebaseEnable -> {
sharedPreferences?.let {
FirebaseManager.setEnabled(requireContext(), it.getBoolean(key, true))
}
}
}
}
private fun updatePermissionSummary() {
val notificationsGranted: Boolean =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
requireContext(), android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
upPreferenceSummary(
PreferKey.notificationsPost,
if (notificationsGranted) "已获取" else "未获取"
)
val pm = requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
val batteryGranted: Boolean =
pm.isIgnoringBatteryOptimizations(requireContext().packageName)
upPreferenceSummary(
PreferKey.ignoreBatteryPermission,
if (batteryGranted) "已获取" else "未获取"
)
}
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.preDownloadNum -> preference.summary =
getString(R.string.pre_download_s, value)
PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value)
PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value)
PreferKey.bitmapCacheSize -> preference.summary =
getString(R.string.bitmap_cache_size_summary, value)
PreferKey.imageRetainNum -> preference.summary =
getString(R.string.image_retain_number_summary, value)
PreferKey.sourceEditMaxLine -> preference.summary =
getString(R.string.source_edit_max_line_summary, value)
else -> if (preference is ListPreference) {
val index = preference.findIndexOfValue(value)
// Set the summary to reflect the new value.
preference.summary = if (index >= 0) preference.entries[index] else null
} else {
preference.summary = value
}
}
}
private fun checkPermission(int: Int) {
when (int) {
1 -> {
PermissionsCompat.Builder()
.addPermissions(Permissions.POST_NOTIFICATIONS)
.rationale(R.string.notification_permission_rationale)
.onGranted { updatePermissionSummary() }
.request()
}
2 -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PermissionsCompat.Builder()
.addPermissions(Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
.rationale(R.string.ignore_battery_permission_rationale)
.request()
}
}
}
}
@SuppressLint("InflateParams")
private fun showUserAgentDialog() {
alert(getString(R.string.user_agent)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = getString(R.string.user_agent)
editView.setText(AppConfig.userAgent)
}
customView { alertBinding.root }
okButton {
val userAgent = alertBinding.editView.text?.toString()
if (userAgent.isNullOrBlank()) {
removePref(PreferKey.userAgent)
} else {
putPrefString(PreferKey.userAgent, userAgent)
}
}
cancelButton()
}
}
private fun clearCache() {
requireContext().alert(
titleResource = R.string.clear_cache,
messageResource = R.string.sure_del
) {
okButton {
viewModel.clearCache()
}
noButton()
}
}
private fun shrinkDatabase() {
alert(R.string.sure, R.string.shrink_database) {
okButton {
viewModel.shrinkDatabase()
}
noButton()
}
}
private fun clearWebViewData() {
alert(R.string.clear_webview_data, R.string.sure_del) {
okButton {
viewModel.clearWebViewData()
}
noButton()
}
}
private fun isProcessTextEnabled(): Boolean {
return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
private fun setProcessTextEnable(enable: Boolean) {
if (enable) {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
} else {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
}
}
private fun alertLocalPassword() {
context?.alert(R.string.set_local_password, R.string.set_local_password_summary) {
val editTextBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "password"
}
customView {
editTextBinding.root
}
okButton {
LocalConfig.password = editTextBinding.editView.text.toString()
}
cancelButton()
}
}
}
@@ -1,175 +0,0 @@
package io.legado.app.ui.config
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import android.view.ViewConfiguration
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.config.PageKeyDialog
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
import io.legado.app.utils.removePref
class ReadConfigFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val slopSquare by lazy { ViewConfiguration.get(requireContext()).scaledTouchSlop }
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_read)
upPreferenceSummary(PreferKey.menuAlpha, AppConfig.menuAlpha.toString())
upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
if (!CanvasRecorderFactory.isSupport) {
removePref(PreferKey.optimizeRender)
preferenceScreen.removePreferenceRecursively(PreferKey.optimizeRender)
}
val pref = findPreference<Preference>("clickRegionalConfig")
pref?.isEnabled = false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.setTitle(R.string.read)
}
override fun onResume() {
super.onResume()
preferenceManager
.sharedPreferences
?.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
preferenceManager
.sharedPreferences
?.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
) {
when (key) {
PreferKey.readBodyToLh -> activity?.recreate()
PreferKey.hideStatusBar -> {
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
postEvent(EventBus.UP_CONFIG, arrayListOf(0, 2))
}
PreferKey.hideNavigationBar -> {
ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar)
postEvent(EventBus.UP_CONFIG, arrayListOf(0, 2))
}
PreferKey.keepLight -> postEvent(key, true)
PreferKey.readSliderMode -> postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
PreferKey.screenOrientation -> {
(activity as? ReadBookActivity)?.setOrientation()
}
PreferKey.textFullJustify,
PreferKey.textBottomJustify,
PreferKey.useZhLayout -> {
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
}
PreferKey.showBrightnessView -> {
postEvent(PreferKey.showBrightnessView, "")
}
PreferKey.expandTextMenu -> {
(activity as? ReadBookActivity)?.textActionMenu?.upMenu()
}
PreferKey.doublePageHorizontal -> {
ChapterProvider.upLayout()
ReadBook.loadContent(false)
}
PreferKey.showReadTitleAddition,
PreferKey.readBarStyleFollowPage -> {
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
}
PreferKey.progressBarBehavior -> {
postEvent(EventBus.UP_SEEK_BAR, true)
}
PreferKey.noAnimScrollPage -> {
ReadBook.callBack?.upPageAnim()
}
PreferKey.optimizeRender -> {
ChapterProvider.upStyle()
ReadBook.callBack?.upPageAnim(true)
ReadBook.loadContent(false)
}
PreferKey.paddingDisplayCutouts -> {
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
"customPageKey" -> PageKeyDialog(requireContext()).show()
"clickRegionalConfig" -> {
(activity as? ReadBookActivity)?.showClickRegionalConfig()
}
PreferKey.menuAlpha -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.menu_alpha))
.setMaxValue(100)
.setMinValue(0)
.setValue(AppConfig.menuAlpha)
.show {
AppConfig.menuAlpha = it
upPreferenceSummary(PreferKey.menuAlpha, it.toString())
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
}
}
PreferKey.pageTouchSlop -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.page_touch_slop_dialog_title))
.setMaxValue(9999)
.setMinValue(0)
.setValue(AppConfig.pageTouchSlop)
.show {
AppConfig.pageTouchSlop = it
postEvent(EventBus.UP_CONFIG, arrayListOf(4))
}
}
}
return super.onPreferenceTreeClick(preference)
}
@Suppress("SameParameterValue")
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.menuAlpha -> preference.summary =
getString(R.string.menu_alpha_sum, AppConfig.menuAlpha)
PreferKey.pageTouchSlop -> preference.summary =
getString(R.string.page_touch_slop_summary, value)
}
}
}
@@ -1,153 +0,0 @@
package io.legado.app.ui.config
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.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import io.legado.app.R
import io.legado.app.base.BaseComposeActivity
import io.legado.app.ui.config.backupConfig.BackupConfigScreen
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
import io.legado.app.ui.config.readConfig.ReadConfigScreen
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import kotlinx.serialization.Serializable
class TestConfigActivity : BaseComposeActivity() {
@Serializable
object ConfigNavRoute
@Serializable
object OtherConfigRoute
@Serializable
object ReadConfigRoute
@Serializable
object CoverConfigRoute
@Serializable
object ThemeConfigRoute
@Serializable
object BackupConfigRoute
@Composable
override fun Content() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = ConfigNavRoute
) {
composable<ConfigNavRoute> {
ConfigNavScreen(
onBackClick = { finish() },
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
onNavigateToRead = { navController.navigate(ReadConfigRoute) },
onNavigateToCover = { navController.navigate(CoverConfigRoute) },
onNavigateToTheme = { navController.navigate(ThemeConfigRoute) },
onNavigateToBackup = { navController.navigate(BackupConfigRoute) }
)
}
composable<OtherConfigRoute> {
OtherConfigScreen(onBackClick = { navController.popBackStack() })
}
composable<ReadConfigRoute> {
ReadConfigScreen(onBackClick = { navController.popBackStack() })
}
composable<CoverConfigRoute> {
CoverConfigScreen(onBackClick = { navController.popBackStack() })
}
composable<ThemeConfigRoute> {
ThemeConfigScreen(
onBackClick = { navController.popBackStack() }
)
}
composable<BackupConfigRoute> {
BackupConfigScreen(onBackClick = { navController.popBackStack() })
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfigNavScreen(
onBackClick: () -> Unit,
onNavigateToOther: () -> Unit,
onNavigateToRead: () -> Unit,
onNavigateToCover: () -> Unit,
onNavigateToTheme: () -> Unit,
onNavigateToBackup: () -> Unit
) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = { Text("设置") },
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
SplicedColumnGroup {
ClickableSettingItem(
title = stringResource(R.string.theme_setting),
onClick = onNavigateToTheme
)
ClickableSettingItem(
title = stringResource(R.string.other_setting),
onClick = onNavigateToOther
)
ClickableSettingItem(
title = stringResource(R.string.read_config),
onClick = onNavigateToRead
)
ClickableSettingItem(
title = stringResource(R.string.cover_config),
onClick = onNavigateToCover
)
ClickableSettingItem(
title = stringResource(R.string.backup_restore),
onClick = onNavigateToBackup
)
}
}
}
}
}
@@ -1,479 +0,0 @@
package io.legado.app.ui.config
//import io.legado.app.lib.theme.primaryColor
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.SeekBar
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import io.legado.app.R
import io.legado.app.base.AppContextWrapper
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogImageBlurringBinding
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.prefs.ColorPreference
import io.legado.app.lib.prefs.ImagePreviewPreference
import io.legado.app.lib.prefs.NameListPreference
import io.legado.app.lib.prefs.ThemeCardPreference
import io.legado.app.lib.prefs.ThemeModePreference
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.externalFiles
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import io.legado.app.utils.inputStream
import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.putPrefString
import io.legado.app.utils.readUri
import io.legado.app.utils.removePref
import io.legado.app.utils.restart
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi
import splitties.init.appCtx
import java.io.FileOutputStream
@Suppress("SameParameterValue")
class ThemeConfigFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener
//MenuProvider
{
private val requestCodeBgLight = 121
private val requestCodeBgDark = 122
private val requestCodeColorImage = 123
private val selectImage = registerForActivityResult(SelectImageContract()) {
it.uri?.let { uri ->
when (it.requestCode) {
requestCodeBgLight -> setBgFromUri(uri, PreferKey.bgImage) {
upTheme(false)
}
requestCodeBgDark -> setBgFromUri(uri, PreferKey.bgImageN) {
upTheme(true)
}
requestCodeColorImage -> setBgFromUri(uri, PreferKey.colorImage) {
}
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme)
upPreferenceSummary(PreferKey.fontScale)
upPreferenceSummary(PreferKey.containerOpacity)
findPreference<ColorPreference>(PreferKey.cBackground)?.let {
it.onSaveColor = { color ->
if (!ColorUtils.isColorLight(color)) {
toastOnUi(R.string.day_background_too_dark)
true
} else {
false
}
}
}
findPreference<ColorPreference>(PreferKey.cNBackground)?.let {
it.onSaveColor = { color ->
if (ColorUtils.isColorLight(color)) {
toastOnUi(R.string.night_background_too_light)
true
} else {
false
}
}
}
findPreference<ThemeModePreference>(PreferKey.themeMode)?.let {
it.setOnPreferenceChangeListener { _, _ ->
true
}
}
val themePref = findPreference<ThemeCardPreference>(PreferKey.themePref)
val colorImage = findPreference<ImagePreviewPreference>(PreferKey.colorImage)
val colorPrimary = findPreference<ColorPreference>("colorPrimary")
val customMode = findPreference<NameListPreference>("customMode")
val currentTheme = getPrefString("app_theme")
colorPrimary?.isVisible = currentTheme == "12"
colorImage?.isVisible = currentTheme == "12"
customMode?.isVisible = currentTheme == "12"
themePref?.let {
it.setOnPreferenceChangeListener { _, _ ->
true
}
}
themePref?.setOnPreferenceChangeListener { _, newValue ->
colorPrimary?.isVisible = newValue == "12"
colorImage?.isVisible = newValue == "12"
customMode?.isVisible = newValue == "12"
true
}
// TODO:删掉种子色了喵
val hasColorImage = !getPrefString(PreferKey.colorImage).isNullOrBlank()
colorPrimary?.isEnabled = !hasColorImage
if (hasColorImage)
{
upPreferenceSummary("colorPrimary", getString(R.string.seed_photo_alart))
upPreferenceSummary(PreferKey.colorImage, getString(R.string.click_to_delete))
}
upPreferenceSummary(PreferKey.bgImage)
upPreferenceSummary(PreferKey.bgImageN)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.setTitle(R.string.theme_setting)
//listView.setEdgeEffectColor(primaryColor)
//activity?.addMenuProvider(this, viewLifecycleOwner)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}
override fun onDestroy() {
super.onDestroy()
preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
sharedPreferences ?: return
when (key) {
PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key))
//PreferKey.transparentStatusBar -> recreateActivities()
//PreferKey.immNavigationBar -> recreateActivities()
PreferKey.themePref -> {
//recreateActivities()
}
PreferKey.paletteStyle, PreferKey.pureBlack, PreferKey.swipeAnimation -> {
Handler(Looper.getMainLooper()).postDelayed({
recreateActivities()
}, 100)
}
PreferKey.enableBlur -> {
ThemeConfig.enableBlur = PreferKey.enableBlur.toBoolean()
}
PreferKey.enableProgressiveBlur -> {
ThemeConfig.enableProgressiveBlur = PreferKey.enableProgressiveBlur.toBoolean()
}
PreferKey.customMode -> handleRestartRequired()
PreferKey.isPredictiveBackEnabled -> {
toastOnUi("重启以应用")
}
PreferKey.cPrimary -> {
val color = getPrefInt(key, ThemeStore.primaryColor(requireContext()))
ThemeStore.editTheme(requireContext())
.primaryColor(color)
.apply()
DynamicColors.applyToActivitiesIfAvailable(requireContext().applicationContext as Application, DynamicColorsOptions.Builder()
.setContentBasedSource(requireContext().primaryColor)
.build())
Handler(Looper.getMainLooper()).postDelayed({
recreateActivities()
}, 100)
}
PreferKey.cBackground,
PreferKey.cBBackground -> {
upTheme(false)
}
//PreferKey.cNPrimary,
PreferKey.cNBackground,
PreferKey.cNBBackground -> {
upTheme(true)
}
PreferKey.themeMode -> {
//recreateActivities()
}
PreferKey.bgImage,
PreferKey.bgImageN -> {
upPreferenceSummary(key, getString(R.string.click_to_delete))
}
PreferKey.colorImage -> handleRestartRequired()
PreferKey.showDiscovery, PreferKey.showRss,
PreferKey.showStatusBar, PreferKey.showBottomView, PreferKey.tabletInterface,
PreferKey.labelVisibilityMode -> postEvent(EventBus.NOTIFY_MAIN, true)
}
}
@SuppressLint("PrivateResource")
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (val key = preference.key) {
PreferKey.fontScale -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.font_scale))
.setMaxValue(16)
.setMinValue(8)
.setValue(10)
.setCustomButton((R.string.btn_default_s)) {
putPrefInt(PreferKey.fontScale, 0)
Handler(Looper.getMainLooper()).postDelayed({
recreateActivities()
}, 100)
}
.show {
putPrefInt(PreferKey.fontScale, it)
Handler(Looper.getMainLooper()).postDelayed({
recreateActivities()
}, 100)
}
PreferKey.containerOpacity -> NumberPickerDialog(requireContext())
.setTitle(getString(R.string.container_opacity))
.setMaxValue(100)
.setMinValue(0)
.setValue(ThemeConfig.containerOpacity)
.setCustomButton((R.string.btn_default_s)) {
ThemeConfig.containerOpacity = 100
Handler(Looper.getMainLooper()).postDelayed({
upPreferenceSummary(PreferKey.containerOpacity)
}, 100)
}
.show {
ThemeConfig.containerOpacity = it
Handler(Looper.getMainLooper()).postDelayed({
upPreferenceSummary(PreferKey.containerOpacity)
}, 100)
}
PreferKey.bgImage -> selectBgAction(false)
PreferKey.bgImageN -> selectBgAction(true)
"colorImage" -> selectBgAction(null)
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme",
"saveNightTheme" -> alertSaveTheme(key)
"coverConfig" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.COVER_CONFIG)
}
}
return super.onPreferenceTreeClick(preference)
}
@SuppressLint("InflateParams")
private fun alertSaveTheme(key: String) {
alert(R.string.theme_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "name"
}
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let { themeName ->
when (key) {
"saveDayTheme" -> {
OldThemeConfig.saveDayTheme(requireContext(), themeName)
}
"saveNightTheme" -> {
OldThemeConfig.saveNightTheme(requireContext(), themeName)
}
}
}
}
cancelButton()
}
}
private fun selectBgAction(isNight: Boolean?) {
val bgKey = when (isNight) {
true -> PreferKey.bgImageN
false -> PreferKey.bgImage
null -> PreferKey.colorImage
}
val blurringKey = when (isNight) {
true -> PreferKey.bgImageNBlurring
false -> PreferKey.bgImageBlurring
else -> null
}
val actions = mutableListOf<String>()
if (isNight != null) {
actions.add(getString(R.string.background_image_blurring))
}
actions.add(getString(R.string.select_image))
if (!getPrefString(bgKey).isNullOrEmpty()) {
actions.add(getString(R.string.delete))
}
context?.selector(items = actions) { _, i ->
when {
isNight != null && i == 0 -> {
alertImageBlurring(blurringKey!!) {
upTheme(isNight)
}
}
(isNight == null && i == 0) || (isNight != null && i == 1) -> {
when (isNight) {
true -> selectImage.launch(requestCodeBgDark)
false -> selectImage.launch(requestCodeBgLight)
null -> selectImage.launch(requestCodeColorImage)
}
}
(isNight == null && i == 1) || (isNight != null && i == 2) -> {
removePref(bgKey)
if (isNight != null) {
upTheme(isNight)
}
}
}
}
}
private fun handleRestartRequired() {
alert(getString(R.string.restart_required_message)) {
okButton {
Handler(Looper.getMainLooper()).postDelayed({
requireContext().restart()
}, 100)
}
cancelButton {
toastOnUi(R.string.restart_later_message)
}
}
}
private fun alertImageBlurring(preferKey: String, success: () -> Unit) {
alert(R.string.background_image_blurring) {
val alertBinding = DialogImageBlurringBinding.inflate(layoutInflater).apply {
getPrefInt(preferKey, 0).let {
seekBar.progress = it
textViewValue.text = it.toString()
}
seekBar.setOnSeekBarChangeListener(object : SeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progress: Int,
fromUser: Boolean
) {
textViewValue.text = progress.toString()
}
})
}
customView { alertBinding.root }
okButton {
alertBinding.seekBar.progress.let {
putPrefInt(preferKey, it)
success.invoke()
}
}
cancelButton()
}
}
private fun upTheme(isNightTheme: Boolean) {
if (AppConfig.isNightTheme == isNightTheme) {
listView.post {
//ThemeConfig.applyTheme(requireContext())
recreateActivities()
}
}
}
private fun recreateActivities() {
postEvent(EventBus.RECREATE, "")
}
private fun upPreferenceSummary(preferenceKey: String, value: String? = null) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
//PreferKey.barElevation -> preference.summary =
// getString(R.string.bar_elevation_s, value)
PreferKey.containerOpacity -> {
preference.summary =
getString(R.string.container_opacity_summary, AppConfig.containerOpacity)
}
PreferKey.fontScale -> {
val fontScale = AppContextWrapper.getFontScale(requireContext())
preference.summary = getString(R.string.font_scale_summary, fontScale)
}
PreferKey.bgImage,
PreferKey.bgImageN -> {
val hasImage = !getPrefString(preferenceKey).isNullOrBlank()
preference.summary = if (hasImage) {
getString(R.string.click_to_delete)
} else {
getString(R.string.select_image)
}
}
else -> preference.summary = value
}
}
private fun setBgFromUri(uri: Uri, preferenceKey: String, success: () -> Unit) {
readUri(uri) { fileDoc, inputStream ->
kotlin.runCatching {
var file = requireContext().externalFiles
val suffix = fileDoc.name.substringAfterLast(".")
val fileName = uri.inputStream(requireContext()).getOrThrow().use {
MD5Utils.md5Encode(it) + ".$suffix"
}
file = FileUtils.createFileIfNotExist(file, preferenceKey, fileName)
FileOutputStream(file).use {
inputStream.copyTo(it)
}
putPrefString(preferenceKey, file.absolutePath)
success()
}.onFailure {
appCtx.toastOnUi(it.localizedMessage)
}
}
}
}
@@ -1,129 +0,0 @@
package io.legado.app.ui.config
import android.content.Context
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.toColorInt
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.BaseBottomSheetDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemThemeConfigBinding
import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
class ThemeListDialog : BaseBottomSheetDialogFragment(R.layout.dialog_recycler_view),
Toolbar.OnMenuItemClickListener {
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
private val adapter by lazy { Adapter(requireContext()) }
override fun onStart() {
super.onStart()
//setLayout(0.9f, 0.9f)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
//binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.theme_list)
initView()
initMenu()
initData()
}
private fun initView() = binding.run {
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter
}
private fun initMenu() = binding.run {
toolBar.setOnMenuItemClickListener(this@ThemeListDialog)
toolBar.inflateMenu(R.menu.theme_list)
toolBar.menu.applyTint(requireContext())
}
fun initData() {
adapter.setItems(OldThemeConfig.configList)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_import -> {
requireContext().getClipText()?.let {
if (OldThemeConfig.addConfig(it)) {
initData()
} else {
toastOnUi("格式不对,添加失败")
}
}
}
}
return true
}
fun delete(index: Int) {
alert(R.string.delete, R.string.sure_del) {
yesButton {
OldThemeConfig.delConfig(index)
initData()
}
noButton()
}
}
fun share(index: Int) {
val json = GSON.toJson(OldThemeConfig.configList[index])
requireContext().share(json, "主题分享")
}
inner class Adapter(context: Context) :
RecyclerAdapter<OldThemeConfig.Config, ItemThemeConfigBinding>(context) {
override fun getViewBinding(parent: ViewGroup): ItemThemeConfigBinding {
return ItemThemeConfigBinding.inflate(inflater, parent, false)
}
override fun convert(
holder: ItemViewHolder,
binding: ItemThemeConfigBinding,
item: OldThemeConfig.Config,
payloads: MutableList<Any>
) {
binding.apply {
tvName.text = item.themeName
if (item.primaryColor.toColorInt() == context.primaryColor)
cdRoot.setCardBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSecondaryContainer))
else
cdRoot.setCardBackgroundColor(context.themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemThemeConfigBinding) {
binding.apply {
root.setOnClickListener {
OldThemeConfig.applyConfig(
context,
OldThemeConfig.configList[holder.layoutPosition]
)
}
ivShare.setOnClickListener {
share(holder.layoutPosition)
}
ivDelete.setOnClickListener {
delete(holder.layoutPosition)
}
}
}
}
}
@@ -11,6 +11,12 @@ import splitties.init.appCtx
class BackupConfigViewModel : ViewModel() {
private suspend fun syncWebDavConfig() {
withContext(Dispatchers.IO) {
AppWebDav.upConfig()
}
}
fun setWebDavAccount(account: String, password: String) {
BackupConfig.webDavAccount = account
BackupConfig.webDavPassword = password
@@ -25,6 +31,7 @@ class BackupConfigViewModel : ViewModel() {
}
suspend fun testWebDav(): Boolean {
syncWebDavConfig()
return withContext(Dispatchers.IO) {
try {
AppWebDav.testWebDav()
@@ -51,6 +58,7 @@ class BackupConfigViewModel : ViewModel() {
}
suspend fun getBackupNames(): List<String> {
syncWebDavConfig()
return withContext(Dispatchers.IO) {
AppWebDav.getBackupNames()
}
@@ -59,6 +67,7 @@ class BackupConfigViewModel : ViewModel() {
fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
AppWebDav.upConfig()
AppWebDav.restoreWebDav(name)
withContext(Dispatchers.Main) {
onSuccess()
@@ -42,6 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
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.ToggleButton
@@ -108,6 +109,9 @@ fun ThemeConfigScreen(
var showRestartDialog by remember { mutableStateOf(false) }
var showColorPicker by remember { mutableStateOf(false) }
var showLauncherIconPicker by remember { mutableStateOf(false) }
var showThemeListDialog by remember { mutableStateOf(false) }
var saveThemeKey by remember { mutableStateOf<String?>(null) }
var themeName by remember { mutableStateOf("") }
val fontScaleValue = remember { mutableFloatStateOf(ThemeConfig.fontScale.toFloat()) }
val primaryColorValue = remember { mutableIntStateOf(ThemeConfig.cPrimary) }
@@ -339,6 +343,22 @@ fun ThemeConfigScreen(
)
}
SplicedColumnGroup(title = stringResource(R.string.share)) {
ClickableSettingItem(
title = stringResource(R.string.theme_list),
description = stringResource(R.string.theme_list_summary),
onClick = { showThemeListDialog = true }
)
ClickableSettingItem(
title = stringResource(R.string.save_theme_config),
description = stringResource(R.string.save_day_theme_summary),
onClick = {
saveThemeKey = "saveDayTheme"
themeName = ""
}
)
}
SplicedColumnGroup(title = stringResource(R.string.compose_related)) {
SwitchSettingItem(
title = stringResource(R.string.use_flexible_top_bar),
@@ -483,6 +503,45 @@ fun ThemeConfigScreen(
}
)
}
if (showThemeListDialog) {
ThemeListDialog(
onDismissRequest = { showThemeListDialog = false }
)
}
saveThemeKey?.let { key ->
AlertDialog(
onDismissRequest = { saveThemeKey = null },
title = { Text(stringResource(R.string.theme_name)) },
text = {
OutlinedTextField(
value = themeName,
onValueChange = { themeName = it },
label = { Text("name") },
singleLine = true
)
},
confirmButton = {
TextButton(
onClick = {
when (key) {
"saveDayTheme" -> OldThemeConfig.saveDayTheme(context, themeName)
"saveNightTheme" -> OldThemeConfig.saveNightTheme(context, themeName)
}
saveThemeKey = null
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { saveThemeKey = null }) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -0,0 +1,184 @@
package io.legado.app.ui.config.themeConfig
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileDownload
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import io.legado.app.R
import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import io.legado.app.utils.GSON
import io.legado.app.utils.getClipText
import io.legado.app.utils.share
import io.legado.app.utils.toastOnUi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThemeListDialog(
onDismissRequest: () -> Unit
) {
val context = LocalContext.current
var listVersion by remember { mutableIntStateOf(0) }
var deleteIndex by remember { mutableStateOf<Int?>(null) }
val themeList = remember(listVersion) { OldThemeConfig.configList.toList() }
GlassModalBottomSheet(
onDismissRequest = onDismissRequest
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.theme_list),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = {
val clipText = context.getClipText()
if (clipText != null && OldThemeConfig.addConfig(clipText)) {
listVersion++
} else {
context.toastOnUi("Import failed")
}
}
) {
Icon(
imageVector = Icons.Default.FileDownload,
contentDescription = stringResource(R.string.import_theme)
)
}
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
itemsIndexed(themeList, key = { _, item -> item.themeName }) { index, item ->
GlassCard(
onClick = {
OldThemeConfig.applyConfig(context, item)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = if (item.primaryColor.toColorInt() == context.primaryColor) {
MaterialTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surfaceContainer
}
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.padding(end = 12.dp)
.background(
color = Color(
item.primaryColor.toColorInt()
),
shape = MaterialTheme.shapes.large
)
.padding(8.dp)
)
Text(
text = item.themeName,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = {
val json = GSON.toJson(item)
context.share(json, "主题分享")
}
) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = stringResource(R.string.share)
)
}
IconButton(onClick = { deleteIndex = index }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.delete)
)
}
}
}
}
}
}
}
deleteIndex?.let { index ->
AlertDialog(
onDismissRequest = { deleteIndex = null },
title = { Text(stringResource(R.string.delete)) },
text = { Text(stringResource(R.string.sure_del)) },
confirmButton = {
TextButton(
onClick = {
OldThemeConfig.delConfig(index)
listVersion++
deleteIndex = null
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { deleteIndex = null }) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
}
@@ -1,11 +1,27 @@
package io.legado.app.ui.main
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.base.BaseComposeActivity
@@ -23,15 +39,24 @@ import io.legado.app.service.WebService
import io.legado.app.ui.about.CrashLogsDialog
import io.legado.app.ui.about.UpdateDialog
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.config.ConfigNavScreen
import io.legado.app.ui.config.ConfigTag
import io.legado.app.ui.config.backupConfig.BackupConfigScreen
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
import io.legado.app.ui.config.mainConfig.MainConfig
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
import io.legado.app.ui.config.readConfig.ReadConfigScreen
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
import io.legado.app.ui.welcome.WelcomeActivity
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.startActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -41,7 +66,60 @@ import kotlin.coroutines.suspendCoroutine
*/
open class MainActivity : BaseComposeActivity() {
companion object {
const val EXTRA_START_ROUTE = "startRoute"
private const val ROUTE_MAIN = "main"
private const val ROUTE_SETTINGS = "settings"
private const val ROUTE_SETTINGS_OTHER = "settings/other"
private const val ROUTE_SETTINGS_READ = "settings/read"
private const val ROUTE_SETTINGS_COVER = "settings/cover"
private const val ROUTE_SETTINGS_THEME = "settings/theme"
private const val ROUTE_SETTINGS_BACKUP = "settings/backup"
fun createIntent(context: Context, configTag: String? = null): Intent {
return Intent(context, MainActivity::class.java).apply {
putExtra(EXTRA_START_ROUTE, routeForConfigTag(configTag))
}
}
private fun routeForConfigTag(configTag: String?): String {
return when (configTag) {
ConfigTag.OTHER_CONFIG -> ROUTE_SETTINGS_OTHER
ConfigTag.READ_CONFIG -> ROUTE_SETTINGS_READ
ConfigTag.COVER_CONFIG -> ROUTE_SETTINGS_COVER
ConfigTag.THEME_CONFIG -> ROUTE_SETTINGS_THEME
ConfigTag.BACKUP_CONFIG -> ROUTE_SETTINGS_BACKUP
else -> ROUTE_SETTINGS
}
}
}
private val viewModel by viewModel<MainViewModel>()
private val routeEvents = MutableSharedFlow<MainRoute>(extraBufferCapacity = 1)
@Serializable
private sealed interface MainRoute : NavKey
@Serializable
private data object MainRouteHome : MainRoute
@Serializable
private data object MainRouteSettings : MainRoute
@Serializable
private data object MainRouteSettingsOther : MainRoute
@Serializable
private data object MainRouteSettingsRead : MainRoute
@Serializable
private data object MainRouteSettingsCover : MainRoute
@Serializable
private data object MainRouteSettingsTheme : MainRoute
@Serializable
private data object MainRouteSettingsBackup : MainRoute
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@@ -70,6 +148,12 @@ open class MainActivity : BaseComposeActivity() {
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
routeEvents.tryEmit(resolveStartRoute(intent))
}
@Composable
override fun Content() {
val orientation = resources.configuration.orientation
@@ -84,7 +168,169 @@ open class MainActivity : BaseComposeActivity() {
else -> false
}
MainScreen(useRail = useRail)
val backStack = rememberNavBackStack(resolveStartRoute(intent))
LaunchedEffect(backStack) {
routeEvents.collect { route ->
navigateToRoute(backStack, route)
}
}
NavDisplay(
backStack = backStack,
transitionSpec = {
(slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
durationMillis = 260,
easing = FastOutSlowInEasing
),
initialOffset = { fullWidth -> fullWidth / 10 }
) + fadeIn(
animationSpec = tween(
durationMillis = 220,
easing = LinearOutSlowInEasing
)
)) togetherWith ExitTransition.None
},
popTransitionSpec = {
(slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
durationMillis = 240,
easing = FastOutSlowInEasing
),
initialOffset = { fullWidth -> -fullWidth / 8 }
) + fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = LinearOutSlowInEasing
)
)) togetherWith
(scaleOut(
targetScale = 0.92f,
animationSpec = tween(
durationMillis = 220,
easing = FastOutSlowInEasing
)
) + fadeOut(
animationSpec = tween(durationMillis = 180)
))
},
predictivePopTransitionSpec = { _ ->
(slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
durationMillis = 240,
easing = FastOutSlowInEasing
),
initialOffset = { fullWidth -> -fullWidth / 8 }
) + fadeIn(
animationSpec = tween(
durationMillis = 200,
easing = LinearOutSlowInEasing
)
)) togetherWith
(scaleOut(
targetScale = 0.92f,
animationSpec = tween(
durationMillis = 220,
easing = FastOutSlowInEasing
)
) + fadeOut(
animationSpec = tween(durationMillis = 180)
))
},
onBack = {
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
finish()
}
},
entryProvider = entryProvider {
entry<MainRouteHome> {
MainScreen(
useRail = useRail,
onOpenSettings = {
navigateToRoute(backStack, MainRouteSettings)
}
)
}
entry<MainRouteSettings> {
ConfigNavScreen(
onBackClick = { navigateBack(backStack) },
onNavigateToOther = { backStack.add(MainRouteSettingsOther) },
onNavigateToRead = { backStack.add(MainRouteSettingsRead) },
onNavigateToCover = { backStack.add(MainRouteSettingsCover) },
onNavigateToTheme = { backStack.add(MainRouteSettingsTheme) },
onNavigateToBackup = { backStack.add(MainRouteSettingsBackup) }
)
}
entry<MainRouteSettingsOther> {
OtherConfigScreen(onBackClick = { navigateBack(backStack) })
}
entry<MainRouteSettingsRead> {
ReadConfigScreen(onBackClick = { navigateBack(backStack) })
}
entry<MainRouteSettingsCover> {
CoverConfigScreen(onBackClick = { navigateBack(backStack) })
}
entry<MainRouteSettingsTheme> {
ThemeConfigScreen(onBackClick = { navigateBack(backStack) })
}
entry<MainRouteSettingsBackup> {
BackupConfigScreen(onBackClick = { navigateBack(backStack) })
}
}
)
}
private fun navigateToRoute(backStack: MutableList<NavKey>, route: MainRoute) {
val currentRoute = backStack.lastOrNull()
if (currentRoute == route) return
when (route) {
MainRouteHome -> {
backStack.clear()
backStack.add(MainRouteHome)
}
MainRouteSettings -> {
if (currentRoute == MainRouteHome) {
backStack.add(MainRouteSettings)
} else {
backStack.clear()
backStack.add(MainRouteHome)
backStack.add(MainRouteSettings)
}
}
MainRouteSettingsOther,
MainRouteSettingsRead,
MainRouteSettingsCover,
MainRouteSettingsTheme,
MainRouteSettingsBackup -> {
backStack.clear()
backStack.add(MainRouteHome)
backStack.add(MainRouteSettings)
backStack.add(route)
}
}
}
private fun navigateBack(backStack: MutableList<NavKey>) {
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
finish()
}
}
private fun checkStartupRoute(): Boolean {
@@ -197,6 +443,23 @@ open class MainActivity : BaseComposeActivity() {
}
}
private fun resolveStartRoute(intent: Intent?): MainRoute {
return resolveStartRoute(intent?.getStringExtra(EXTRA_START_ROUTE))
}
private fun resolveStartRoute(route: String?): MainRoute {
return when (route) {
"main" -> MainRouteHome
"settings" -> MainRouteSettings
"settings/other" -> MainRouteSettingsOther
"settings/read" -> MainRouteSettingsRead
"settings/cover" -> MainRouteSettingsCover
"settings/theme" -> MainRouteSettingsTheme
"settings/backup" -> MainRouteSettingsBackup
else -> MainRouteHome
}
}
}
class LauncherW : MainActivity()
@@ -78,7 +78,8 @@ import org.koin.androidx.compose.koinViewModel
@Composable
fun MainScreen(
viewModel: MainViewModel = koinViewModel(),
useRail: Boolean
useRail: Boolean,
onOpenSettings: () -> Unit
) {
val context = LocalContext.current
val uiState by viewModel.state.collectAsStateWithLifecycle()
@@ -362,6 +363,7 @@ fun MainScreen(
MainDestination.Rss -> RssScreen()
MainDestination.My -> MyScreen(
viewModel = koinViewModel(),
onOpenSettings = onOpenSettings,
onNavigate = { event ->
viewModel.onPrefClickEvent(context, event)
}
@@ -15,13 +15,11 @@ import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.automirrored.filled.Rule
import androidx.compose.material.icons.filled.Backup
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.FindReplace
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Source
import androidx.compose.material.icons.filled.Web
@@ -45,8 +43,6 @@ import io.legado.app.ui.book.bookmark.AllBookmarkActivity
import io.legado.app.ui.book.readRecord.ReadRecordActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigTag
import io.legado.app.ui.dict.rule.DictRuleActivity
import io.legado.app.ui.file.FileManageActivity
import io.legado.app.ui.replace.ReplaceRuleActivity
@@ -61,6 +57,7 @@ import io.legado.app.ui.widget.components.settingItem.SettingItem
@Composable
fun MyScreen(
viewModel: MyViewModel,
onOpenSettings: () -> Unit,
onNavigate: (PrefClickEvent) -> Unit
) {
@@ -91,27 +88,20 @@ fun MyScreen(
) { padding ->
Column(
modifier = Modifier
.padding(
top = padding.calculateTopPadding()
)
.verticalScroll(rememberScrollState()),
//TODO:暂时这样解决,改天加模糊
) {
Spacer(
modifier = Modifier
.padding(
top = padding.calculateTopPadding()
)
)
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = ""
) {
SettingItem(
title = stringResource(R.string.book_source_manage),
description = stringResource(R.string.book_source_manage_desc),
imageVector = Icons.Default.Source,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(BookSourceActivity::class.java)
)
}
)
SettingItem(
title = stringResource(R.string.web_service),
description = if (uiState.isWebServiceRun)
@@ -149,66 +139,21 @@ fun MyScreen(
)
}
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = stringResource(R.string.setting)) {
SettingItem(
title = stringResource(R.string.backup_restore),
description = stringResource(R.string.web_dav_set_import_old),
imageVector = Icons.Default.Backup,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.BACKUP_CONFIG
)
)
}
)
SettingItem(
title = stringResource(R.string.theme_setting),
description = stringResource(R.string.theme_setting_s),
imageVector = Icons.Default.Palette,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.THEME_CONFIG
)
)
}
)
SettingItem(
title = stringResource(R.string.read),
description = stringResource(R.string.global_read_setting),
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.READ_CONFIG
)
)
}
)
SettingItem(
title = stringResource(R.string.other_setting),
description = stringResource(R.string.other_setting_s),
imageVector = Icons.Default.Settings,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(
ConfigActivity::class.java,
ConfigTag.OTHER_CONFIG
)
)
}
)
}
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = "规则"
) {
SettingItem(
title = stringResource(R.string.book_source_manage),
description = stringResource(R.string.book_source_manage_desc),
imageVector = Icons.Default.Source,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(BookSourceActivity::class.java)
)
}
)
SettingItem(
title = stringResource(R.string.replace_purify),
imageVector = Icons.Default.FindReplace,
@@ -229,7 +174,7 @@ fun MyScreen(
)
SettingItem(
title = stringResource(R.string.dict_rule),
imageVector = Icons.AutoMirrored.Filled.Rule,
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(DictRuleActivity::class.java)
@@ -237,9 +182,17 @@ fun MyScreen(
}
)
}
SplicedColumnGroup(
modifier = Modifier.padding(horizontal = 16.dp),
title = stringResource(R.string.other)) {
SettingItem(
title = stringResource(R.string.setting),
imageVector = Icons.Default.Settings,
onClick = {
onOpenSettings()
}
)
SettingItem(
title = stringResource(R.string.bookmark),
imageVector = Icons.Default.Bookmark,