This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
1 parent b86d90021c
commit 6c0757c91b
972 files changed
+6885 -7321

No files matched your search

@@ -0,0 +1,396 @@
package io.legato.kazusa.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 io.legato.kazusa.R
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.exception.NoStackTraceException
import io.legato.kazusa.help.AppWebDav
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.LocalConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.help.storage.Backup
import io.legato.kazusa.help.storage.BackupConfig
import io.legato.kazusa.help.storage.ImportOldData
import io.legato.kazusa.help.storage.Restore
import io.legato.kazusa.lib.dialogs.alert
import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.lib.permission.Permissions
import io.legato.kazusa.lib.permission.PermissionsCompat
import io.legato.kazusa.lib.prefs.fragment.PreferenceFragment
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.ui.about.AppLogDialog
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.widget.dialog.WaitDialog
import io.legato.kazusa.utils.FileDoc
import io.legato.kazusa.utils.applyTint
import io.legato.kazusa.utils.checkWrite
import io.legato.kazusa.utils.getPrefString
import io.legato.kazusa.utils.isContentScheme
import io.legato.kazusa.utils.launch
import io.legato.kazusa.utils.showDialogFragment
import io.legato.kazusa.utils.showHelp
import io.legato.kazusa.utils.toEditable
import io.legato.kazusa.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
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.webDavPassword)?.let {
it.setOnBindEditTextListener { editText ->
editText.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
}
}
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.webDavAccount, getPrefString(PreferKey.webDavAccount))
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName)
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
findPreference<io.legato.kazusa.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)
menu.applyTint(requireContext())
}
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.webDavAccount ->
if (value.isNullOrBlank()) {
preference.summary = getString(R.string.web_dav_account_s)
} else {
preference.summary = value
}
PreferKey.webDavPassword ->
if (value.isNullOrEmpty()) {
preference.summary = getString(R.string.web_dav_pw_s)
} else {
preference.summary = "*".repeat(value.length)
}
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_backup" -> backup()
"web_dav_restore" -> restore()
"import_old" -> restoreOld.launch()
}
return super.onPreferenceTreeClick(preference)
}
/**
* 备份忽略设置
*/
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()) {
coroutineContext.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()
}
}
@@ -0,0 +1,101 @@
package io.legato.kazusa.ui.config
import android.os.Bundle
import android.view.View
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.databinding.DialogCheckSourceConfigBinding
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.model.CheckSource
import io.legato.kazusa.utils.putPrefString
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import splitties.views.onClick
class CheckSourceConfig : BaseBottomSheetDialogFragment(R.layout.dialog_check_source_config) {
private val binding by viewBinding(DialogCheckSourceConfigBinding::bind)
//允许的最小超时时间,秒
private val minTimeout = 0L
override fun onStart() {
super.onStart()
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
//binding.toolBar.setBackgroundColor(primaryColor)
binding.run {
checkSearch.onClick {
if (!checkSearch.isChecked && !checkDiscovery.isChecked) {
checkDiscovery.isChecked = true
}
}
checkDiscovery.onClick {
if (!checkSearch.isChecked && !checkDiscovery.isChecked) {
checkSearch.isChecked = true
}
}
checkInfo.onClick {
if (!checkInfo.isChecked) {
checkCategory.isChecked = false
checkContent.isChecked = false
checkCategory.isEnabled = false
checkContent.isEnabled = false
} else {
checkCategory.isEnabled = true
}
}
checkCategory.onClick {
if (!checkCategory.isChecked) {
checkContent.isChecked = false
checkContent.isEnabled = false
} else {
checkContent.isEnabled = true
}
}
}
CheckSource.run {
binding.checkSourceTimeout.setText((timeout / 1000).toString())
binding.checkSearch.isChecked = checkSearch
binding.checkDiscovery.isChecked = checkDiscovery
binding.checkInfo.isChecked = checkInfo
binding.checkCategory.isChecked = checkCategory
binding.checkContent.isChecked = checkContent
binding.checkCategory.isEnabled = checkInfo
binding.checkContent.isEnabled = checkCategory
binding.tvCancel.onClick {
dismiss()
}
binding.tvOk.onClick {
val text = binding.checkSourceTimeout.text.toString()
when {
text.isBlank() -> {
toastOnUi("${getString(R.string.timeout)}${getString(R.string.cannot_empty)}")
return@onClick
}
text.toLong() <= minTimeout -> {
toastOnUi(
"${getString(R.string.timeout)}${getString(R.string.less_than)}${minTimeout}${
getString(
R.string.seconds
)
}"
)
return@onClick
}
else -> timeout = text.toLong() * 1000
}
checkSearch = binding.checkSearch.isChecked
checkDiscovery = binding.checkDiscovery.isChecked
checkInfo = binding.checkInfo.isChecked
checkCategory = binding.checkCategory.isChecked
checkContent = binding.checkContent.isChecked
putConfig()
putPrefString(PreferKey.checkSource, summary)
dismiss()
}
}
}
}
@@ -0,0 +1,51 @@
package io.legato.kazusa.ui.config
import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.databinding.ActivityConfigBinding
import io.legato.kazusa.utils.observeEvent
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
class ConfigActivity : VMBaseActivity<ActivityConfigBinding, ConfigViewModel>() {
override val binding by viewBinding(ActivityConfigBinding::inflate)
override val viewModel by viewModels<ConfigViewModel>()
override fun onActivityCreated(savedInstanceState: Bundle?) {
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.WELCOME_CONFIG -> replaceFragment<WelcomeConfigFragment>(configTag)
else -> finish()
}
}
override fun setTitle(resId: Int) {
super.setTitle(resId)
binding.titleBar.setTitle(resId)
}
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()
}
override fun observeLiveBus() {
super.observeLiveBus()
observeEvent<String>(EventBus.RECREATE) {
recreate()
}
}
}
@@ -0,0 +1,11 @@
package io.legato.kazusa.ui.config
object ConfigTag {
const val OTHER_CONFIG = "otherConfig"
const val THEME_CONFIG = "themeConfig"
const val BACKUP_CONFIG = "backupConfig"
const val COVER_CONFIG = "coverConfig"
const val WELCOME_CONFIG = "welcomeConfig"
}
@@ -0,0 +1,51 @@
package io.legato.kazusa.ui.config
import android.app.Application
import android.content.Context
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.data.appDb
import io.legato.kazusa.help.AppWebDav
import io.legato.kazusa.help.book.BookHelp
import io.legato.kazusa.utils.FileUtils
import io.legato.kazusa.utils.restart
import io.legato.kazusa.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)
}
}
}
@@ -0,0 +1,161 @@
package io.legato.kazusa.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.legato.kazusa.R
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.lib.prefs.SwitchPreference
import io.legato.kazusa.lib.prefs.fragment.PreferenceFragment
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.model.BookCover
import io.legato.kazusa.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.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)
}
}
}
}
@@ -0,0 +1,68 @@
package io.legato.kazusa.ui.config
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseDialogFragment
import io.legato.kazusa.databinding.DialogCoverRuleConfigBinding
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.model.BookCover
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.setLayout
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.views.onClick
class CoverRuleConfigDialog : BaseDialogFragment(R.layout.dialog_cover_rule_config) {
val binding by viewBinding(DialogCoverRuleConfigBinding::bind)
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
//binding.toolBar.setBackgroundColor(primaryColor)
initData()
binding.tvCancel.onClick {
dismissAllowingStateLoss()
}
binding.tvOk.onClick {
val enable = binding.cbEnable.isChecked
val searchUrl = binding.editSearchUrl.text?.toString()
val coverRule = binding.editCoverUrlRule.text?.toString()
if (searchUrl.isNullOrBlank() || coverRule.isNullOrBlank()) {
toastOnUi("搜索url和cover规则不能为空")
} else {
BookCover.CoverRule(enable, searchUrl, coverRule).let { config ->
BookCover.saveCoverRule(config)
}
dismissAllowingStateLoss()
}
}
binding.tvFooterLeft.onClick {
BookCover.delCoverRule()
dismissAllowingStateLoss()
}
}
private fun initData() {
lifecycleScope.launch {
val rule = withContext(IO) {
BookCover.getCoverRule()
}
Log.e("coverRule", GSON.toJson(rule))
binding.cbEnable.isChecked = rule.enable
binding.editSearchUrl.setText(rule.searchUrl)
binding.editCoverUrlRule.setText(rule.coverRule)
}
}
}
@@ -0,0 +1,128 @@
package io.legato.kazusa.ui.config
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.Toolbar
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
import io.legato.kazusa.databinding.DialogDirectLinkUploadConfigBinding
import io.legato.kazusa.help.DirectLinkUpload
import io.legato.kazusa.lib.dialogs.alert
import io.legato.kazusa.lib.dialogs.selector
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.applyTint
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.getClipText
import io.legato.kazusa.utils.sendToClip
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import splitties.init.appCtx
import splitties.views.onClick
class DirectLinkUploadConfig : BaseBottomSheetDialogFragment(R.layout.dialog_direct_link_upload_config),
Toolbar.OnMenuItemClickListener {
private val binding by viewBinding(DialogDirectLinkUploadConfigBinding::bind)
override fun onStart() {
super.onStart()
//setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
//binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.inflateMenu(R.menu.direct_link_upload_config)
binding.toolBar.menu.applyTint(requireContext())
binding.toolBar.setOnMenuItemClickListener(this)
upView(DirectLinkUpload.getRule())
binding.tvCancel.onClick {
dismiss()
}
binding.tvFooterLeft.onClick {
test()
}
binding.tvOk.onClick {
getRule()?.let { rule ->
DirectLinkUpload.putConfig(rule)
dismiss()
}
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_import_default -> importDefault()
R.id.menu_copy_rule -> getRule()?.let { rule ->
requireContext().sendToClip(GSON.toJson(rule))
}
R.id.menu_paste_rule -> runCatching {
requireContext().getClipText()!!.let {
val rule = GSON.fromJsonObject<DirectLinkUpload.Rule>(it).getOrThrow()
upView(rule)
}
}.onFailure {
toastOnUi("剪贴板为空或格式不对")
}
}
return true
}
private fun upView(rule: DirectLinkUpload.Rule) {
binding.editUploadUrl.setText(rule.uploadUrl)
binding.editDownloadUrlRule.setText(rule.downloadUrlRule)
binding.editSummary.setText(rule.summary)
binding.cbCompress.isChecked = rule.compress
}
private fun getRule(): DirectLinkUpload.Rule? {
val uploadUrl = binding.editUploadUrl.text?.toString()
val downloadUrlRule = binding.editDownloadUrlRule.text?.toString()
val summary = binding.editSummary.text?.toString()
val compress = binding.cbCompress.isChecked
if (uploadUrl.isNullOrBlank()) {
toastOnUi("上传Url不能为空")
return null
}
if (downloadUrlRule.isNullOrBlank()) {
toastOnUi("下载Url规则不能为空")
return null
}
if (summary.isNullOrBlank()) {
toastOnUi("注释不能为空")
return null
}
return DirectLinkUpload.Rule(uploadUrl, downloadUrlRule, summary, compress)
}
private fun importDefault() {
requireContext().selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
upView(rule)
}
}
private fun test() {
val rule = getRule() ?: return
execute {
DirectLinkUpload.upLoad("test.json", "{}", "application/json", rule)
}.onError {
alertTestResult(it.localizedMessage ?: "ERROR")
}.onSuccess { result ->
alertTestResult(result)
}
}
private fun alertTestResult(result: String) {
alert {
setTitle("result")
setMessage(result)
okButton()
negativeButton(R.string.copy_text) {
appCtx.sendToClip(result)
}
}
}
}
@@ -0,0 +1,333 @@
package io.legato.kazusa.ui.config
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
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.legato.kazusa.R
import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.help.AppFreezeMonitor
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.LocalConfig
import io.legato.kazusa.lib.dialogs.alert
import io.legato.kazusa.lib.prefs.fragment.PreferenceFragment
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.model.CheckSource
import io.legato.kazusa.model.ImageProvider
import io.legato.kazusa.receiver.SharedReceiverActivity
import io.legato.kazusa.service.WebService
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.widget.number.NumberPickerDialog
import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.getPrefBoolean
import io.legato.kazusa.utils.postEvent
import io.legato.kazusa.utils.putPrefBoolean
import io.legato.kazusa.utils.putPrefString
import io.legato.kazusa.utils.removePref
import io.legato.kazusa.utils.restart
import io.legato.kazusa.utils.showDialogFragment
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())
}
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 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()
}
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)
}
PreferKey.processText -> sharedPreferences?.let {
setProcessTextEnable(it.getBoolean(key, true))
}
PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, 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())
}
}
}
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
}
}
}
@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()
}
}
}
@@ -0,0 +1,358 @@
package io.legato.kazusa.ui.config
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.SeekBar
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import io.legato.kazusa.R
import io.legato.kazusa.base.AppContextWrapper
import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.databinding.DialogImageBlurringBinding
import io.legato.kazusa.help.LauncherIconHelp
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.ThemeConfig
import io.legato.kazusa.lib.dialogs.alert
import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.lib.prefs.ColorPreference
import io.legato.kazusa.lib.prefs.NameListPreference
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.ui.widget.number.NumberPickerDialog
import io.legato.kazusa.ui.widget.seekbar.SeekBarChangeListener
import io.legato.kazusa.utils.ColorUtils
import io.legato.kazusa.utils.FileUtils
import io.legato.kazusa.utils.MD5Utils
import io.legato.kazusa.utils.SelectImageContract
import io.legato.kazusa.utils.externalFiles
import io.legato.kazusa.utils.getPrefInt
import io.legato.kazusa.utils.getPrefString
import io.legato.kazusa.utils.inputStream
import io.legato.kazusa.utils.postEvent
import io.legato.kazusa.utils.putPrefInt
import io.legato.kazusa.utils.putPrefString
import io.legato.kazusa.utils.readUri
import io.legato.kazusa.utils.removePref
import io.legato.kazusa.utils.startActivity
import io.legato.kazusa.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 selectImage = registerForActivityResult(SelectImageContract()) {
it.uri?.let { uri ->
when (it.requestCode) {
requestCodeBgLight -> setBgFromUri(uri, PreferKey.bgImage) {
upTheme(false)
}
requestCodeBgDark -> setBgFromUri(uri, PreferKey.bgImageN) {
upTheme(true)
}
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme)
if (Build.VERSION.SDK_INT < 26) {
preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon)
}
upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage))
upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN))
//upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString())
upPreferenceSummary(PreferKey.fontScale)
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<NameListPreference>(PreferKey.themeMode)?.let {
it.setOnPreferenceChangeListener { _, _ ->
ThemeConfig.applyDayNight(requireContext())
true
}
}
}
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 onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
// menuInflater.inflate(R.menu.theme_config, menu)
// menu.applyTint(requireContext())
// }
// override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
// when (menuItem.itemId) {
// R.id.menu_theme_mode -> {
// AppConfig.isNightTheme = !AppConfig.isNightTheme
// ThemeConfig.applyDayNight(requireContext())
// return true
// }
// }
// return false
// }
//不使用菜单
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
sharedPreferences ?: return
when (key) {
PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key))
//PreferKey.transparentStatusBar -> recreateActivities()
//PreferKey.immNavigationBar -> recreateActivities()
PreferKey.cPrimary,
PreferKey.cAccent,
PreferKey.cBackground,
PreferKey.cBBackground -> {
upTheme(false)
}
PreferKey.cNPrimary,
PreferKey.cNAccent,
PreferKey.cNBackground,
PreferKey.cNBBackground -> {
upTheme(true)
}
PreferKey.themeMode -> {
ThemeConfig.applyDayNight(requireContext())
}
PreferKey.bgImage,
PreferKey.bgImageN -> {
upPreferenceSummary(key, getPrefString(key))
}
}
}
@SuppressLint("PrivateResource")
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (val key = preference.key) {
// PreferKey.barElevation -> NumberPickerDialog(requireContext())
// .setTitle(getString(R.string.bar_elevation))
// .setMaxValue(32)
// .setMinValue(0)
// .setValue(AppConfig.elevation)
// .setCustomButton((R.string.btn_default_s)) {
// AppConfig.elevation = AppConst.sysElevation
// recreateActivities()
// }
// .show {
// AppConfig.elevation = it
// recreateActivities()
// }
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)
recreateActivities()
}
.show {
putPrefInt(PreferKey.fontScale, it)
recreateActivities()
}
PreferKey.bgImage -> selectBgAction(false)
PreferKey.bgImageN -> selectBgAction(true)
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme",
"saveNightTheme" -> alertSaveTheme(key)
"coverConfig" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.COVER_CONFIG)
}
"welcomeStyle" -> startActivity<ConfigActivity> {
putExtra("configTag", ConfigTag.WELCOME_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" -> {
ThemeConfig.saveDayTheme(requireContext(), themeName)
}
"saveNightTheme" -> {
ThemeConfig.saveNightTheme(requireContext(), themeName)
}
}
}
}
cancelButton()
}
}
private fun selectBgAction(isNight: Boolean) {
val bgKey = if (isNight) PreferKey.bgImageN else PreferKey.bgImage
val blurringKey = if (isNight) PreferKey.bgImageNBlurring else PreferKey.bgImageBlurring
val actions = arrayListOf(
getString(R.string.background_image_blurring),
getString(R.string.select_image)
)
if (!getPrefString(bgKey).isNullOrEmpty()) {
actions.add(getString(R.string.delete))
}
context?.selector(items = actions) { _, i ->
when (i) {
0 -> alertImageBlurring(blurringKey) {
upTheme(isNight)
}
1 -> {
if (isNight) {
selectImage.launch(requestCodeBgDark)
} else {
selectImage.launch(requestCodeBgLight)
}
}
2 -> {
removePref(bgKey)
upTheme(isNight)
}
}
}
}
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.fontScale -> {
val fontScale = AppContextWrapper.getFontScale(requireContext())
preference.summary = getString(R.string.font_scale_summary, fontScale)
}
PreferKey.bgImage,
PreferKey.bgImageN -> preference.summary = if (value.isNullOrBlank()) {
getString(R.string.select_image)
} else {
value
}
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)
}
}
}
}
@@ -0,0 +1,121 @@
package io.legato.kazusa.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.recyclerview.widget.LinearLayoutManager
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
import io.legato.kazusa.base.adapter.ItemViewHolder
import io.legato.kazusa.base.adapter.RecyclerAdapter
import io.legato.kazusa.databinding.DialogRecyclerViewBinding
import io.legato.kazusa.databinding.ItemThemeConfigBinding
import io.legato.kazusa.help.config.ThemeConfig
import io.legato.kazusa.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
import io.legato.kazusa.utils.*
import io.legato.kazusa.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.addItemDecoration(VerticalDivider(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(ThemeConfig.configList)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_import -> {
requireContext().getClipText()?.let {
if (ThemeConfig.addConfig(it)) {
initData()
} else {
toastOnUi("格式不对,添加失败")
}
}
}
}
return true
}
fun delete(index: Int) {
alert(R.string.delete, R.string.sure_del) {
yesButton {
ThemeConfig.delConfig(index)
initData()
}
noButton()
}
}
fun share(index: Int) {
val json = GSON.toJson(ThemeConfig.configList[index])
requireContext().share(json, "主题分享")
}
inner class Adapter(context: Context) :
RecyclerAdapter<ThemeConfig.Config, ItemThemeConfigBinding>(context) {
override fun getViewBinding(parent: ViewGroup): ItemThemeConfigBinding {
return ItemThemeConfigBinding.inflate(inflater, parent, false)
}
override fun convert(
holder: ItemViewHolder,
binding: ItemThemeConfigBinding,
item: ThemeConfig.Config,
payloads: MutableList<Any>
) {
binding.apply {
tvName.text = item.themeName
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemThemeConfigBinding) {
binding.apply {
root.setOnClickListener {
ThemeConfig.applyConfig(context, ThemeConfig.configList[holder.layoutPosition])
}
ivShare.setOnClickListener {
share(holder.layoutPosition)
}
ivDelete.setOnClickListener {
delete(holder.layoutPosition)
}
}
}
}
}
@@ -0,0 +1,140 @@
package io.legato.kazusa.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.legato.kazusa.R
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.lib.prefs.fragment.PreferenceFragment
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.model.BookCover
import io.legato.kazusa.utils.*
import splitties.init.appCtx
import java.io.FileOutputStream
class WelcomeConfigFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val requestWelcomeImage = 221
private val requestWelcomeImageDark = 222
private val selectImage = registerForActivityResult(SelectImageContract()) {
it.uri?.let { uri ->
when (it.requestCode) {
requestWelcomeImage -> setCoverFromUri(PreferKey.welcomeImage, uri)
requestWelcomeImageDark -> setCoverFromUri(PreferKey.welcomeImageDark, uri)
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_welcome)
upPreferenceSummary(PreferKey.welcomeImage, getPrefString(PreferKey.welcomeImage))
upPreferenceSummary(PreferKey.welcomeImageDark, getPrefString(PreferKey.welcomeImageDark))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.setTitle(R.string.welcome_style)
//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.welcomeImage,
PreferKey.welcomeImageDark -> {
upPreferenceSummary(key, getPrefString(key))
}
}
}
@SuppressLint("PrivateResource")
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
PreferKey.welcomeImage ->
if (getPrefString(preference.key).isNullOrEmpty()) {
selectImage.launch(requestWelcomeImage)
} 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(requestWelcomeImage)
}
}
}
PreferKey.welcomeImageDark ->
if (getPrefString(preference.key).isNullOrEmpty()) {
selectImage.launch(requestWelcomeImageDark)
} 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(requestWelcomeImageDark)
}
}
}
}
return super.onPreferenceTreeClick(preference)
}
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.welcomeImage,
PreferKey.welcomeImageDark -> 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)
}.onFailure {
appCtx.toastOnUi(it.localizedMessage)
}
}
}
}