Merge branch 'feature/backup-settings' of github.com:javlonrahimov/Kotatsu into javlonrahimov-feature/backup-settings
This commit is contained in:
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -4,6 +4,7 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="modules">
|
||||
|
||||
@@ -13,5 +13,6 @@ class BackupEntry(
|
||||
const val HISTORY = "history"
|
||||
const val CATEGORIES = "categories"
|
||||
const val FAVOURITES = "favourites"
|
||||
const val SETTINGS = "settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
@@ -12,7 +13,10 @@ import javax.inject.Inject
|
||||
|
||||
private const val PAGE_SIZE = 10
|
||||
|
||||
class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
||||
class BackupRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
suspend fun dumpHistory(): BackupEntry {
|
||||
var offset = 0
|
||||
@@ -67,6 +71,13 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
||||
return entry
|
||||
}
|
||||
|
||||
fun dumpSettings(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
|
||||
val json = JsonSerializer(settings.getAllValues()).toJson()
|
||||
entry.data.put(json)
|
||||
return entry
|
||||
}
|
||||
|
||||
fun createIndex(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
||||
val json = JSONObject()
|
||||
@@ -127,4 +138,14 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun restoreSettings(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
result += runCatchingCancellable {
|
||||
settings.restoreValuesFromMap(JsonDeserializer(item).toMap())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
@@ -34,14 +35,14 @@ class JsonDeserializer(private val json: JSONObject) {
|
||||
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
||||
state = json.getStringOrNull("state"),
|
||||
author = json.getStringOrNull("author"),
|
||||
source = json.getString("source")
|
||||
source = json.getString("source"),
|
||||
)
|
||||
|
||||
fun toTagEntity() = TagEntity(
|
||||
id = json.getLong("id"),
|
||||
title = json.getString("title"),
|
||||
key = json.getString("key"),
|
||||
source = json.getString("source")
|
||||
source = json.getString("source"),
|
||||
)
|
||||
|
||||
fun toHistoryEntity() = HistoryEntity(
|
||||
@@ -65,4 +66,29 @@ class JsonDeserializer(private val json: JSONObject) {
|
||||
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
|
||||
deletedAt = 0L,
|
||||
)
|
||||
|
||||
fun toMap(): Map<String, Any?> {
|
||||
val map = mutableMapOf<String, Any?>()
|
||||
val keys = json.keys()
|
||||
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
val value = json.get(key)
|
||||
map[key] = value
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <K, T> JSONArray.mapJSONToSet(block: (K) -> T): Set<T> {
|
||||
val len = length()
|
||||
val result = androidx.collection.ArraySet<T>(len)
|
||||
for (i in 0 until len) {
|
||||
val jo = get(i) as K
|
||||
result.add(block(jo))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
put("category_id", e.categoryId)
|
||||
put("sort_key", e.sortKey)
|
||||
put("created_at", e.createdAt)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
constructor(e: FavouriteCategoryEntity) : this(
|
||||
@@ -27,7 +27,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
put("order", e.order)
|
||||
put("track", e.track)
|
||||
put("show_in_lib", e.isVisibleInLibrary)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
constructor(e: HistoryEntity) : this(
|
||||
@@ -39,7 +39,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
put("page", e.page)
|
||||
put("scroll", e.scroll)
|
||||
put("percent", e.percent)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
constructor(e: TagEntity) : this(
|
||||
@@ -48,7 +48,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
put("title", e.title)
|
||||
put("key", e.key)
|
||||
put("source", e.source)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
constructor(e: MangaEntity) : this(
|
||||
@@ -65,8 +65,12 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
put("state", e.state)
|
||||
put("author", e.author)
|
||||
put("source", e.source)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
constructor(m: Map<String, *>) : this(
|
||||
JSONObject(m),
|
||||
)
|
||||
|
||||
fun toJson(): JSONObject = json
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import androidx.core.content.edit
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.backup.mapJSONToSet
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
@@ -380,6 +382,23 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
fun observe() = prefs.observe()
|
||||
|
||||
fun getAllValues(): Map<String, *> = prefs.all
|
||||
|
||||
fun restoreValuesFromMap(m: Map<String, *>) {
|
||||
prefs.edit {
|
||||
m.forEach { e ->
|
||||
when (e.value) {
|
||||
is Boolean -> putBoolean(e.key, e.value as Boolean)
|
||||
is Int -> putInt(e.key, e.value as Int)
|
||||
is Long -> putLong(e.key, e.value as Long)
|
||||
is Float -> putFloat(e.key, e.value as Float)
|
||||
is String -> putString(e.key, e.value as String)
|
||||
is JSONArray -> putStringSet(e.key, (e.value as JSONArray).mapJSONToSet<String, String> { it })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBackgroundNetworkRestricted(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.FloatRange
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import java.util.UUID
|
||||
@@ -40,3 +41,9 @@ fun CharSequence.sanitize(): CharSequence {
|
||||
}
|
||||
|
||||
fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF'
|
||||
|
||||
fun <E : Enum<E>> String.getEnumValue(defaultValue: E): E {
|
||||
return defaultValue.javaClass.enumConstants?.find {
|
||||
it.name == this
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
@@ -24,6 +26,7 @@ import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitStateAtLeast
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
@@ -61,6 +64,9 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
@Inject
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
|
||||
@Inject
|
||||
lateinit var activityRecreationHandle: ActivityRecreationHandle
|
||||
|
||||
private val backupSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument(),
|
||||
this,
|
||||
@@ -180,6 +186,19 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
|
||||
AppSettings.KEY_COLOR_THEME,
|
||||
AppSettings.KEY_THEME_AMOLED -> {
|
||||
postRestart()
|
||||
}
|
||||
|
||||
AppSettings.KEY_APP_LOCALE -> {
|
||||
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,4 +292,11 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun postRestart() {
|
||||
view?.postDelayed(400) {
|
||||
activityRecreationHandle.recreateAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipInput
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import java.io.*
|
||||
|
||||
class AppBackupAgent : BackupAgent() {
|
||||
@@ -31,7 +32,8 @@ class AppBackupAgent : BackupAgent() {
|
||||
|
||||
override fun onFullBackup(data: FullBackupDataOutput) {
|
||||
super.onFullBackup(data)
|
||||
val file = createBackupFile(this, BackupRepository(MangaDatabase(applicationContext)))
|
||||
val file =
|
||||
createBackupFile(this, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
|
||||
try {
|
||||
fullBackupFile(file, data)
|
||||
} finally {
|
||||
@@ -48,7 +50,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
mtime: Long
|
||||
) {
|
||||
if (destination?.name?.endsWith(".bk.zip") == true) {
|
||||
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext)))
|
||||
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
|
||||
destination.delete()
|
||||
} else {
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime)
|
||||
@@ -62,6 +64,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
}
|
||||
@@ -81,6 +84,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
repository.restoreSettings(backup.getEntry(BackupEntry.SETTINGS))
|
||||
}
|
||||
} finally {
|
||||
backup.close()
|
||||
@@ -102,4 +106,4 @@ class AppBackupAgent : BackupAgent() {
|
||||
bytes = read(buffer, 0, buffer.size.coerceAtMost(bytesLeft))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,15 @@ class BackupViewModel @Inject constructor(
|
||||
progress.value = 0f
|
||||
backup.put(repository.dumpHistory())
|
||||
|
||||
progress.value = 0.3f
|
||||
progress.value = 0.25f
|
||||
backup.put(repository.dumpCategories())
|
||||
|
||||
progress.value = 0.6f
|
||||
progress.value = 0.5f
|
||||
backup.put(repository.dumpFavourites())
|
||||
|
||||
progress.value = 0.9f
|
||||
progress.value = 0.75f
|
||||
backup.put(repository.dumpSettings())
|
||||
|
||||
backup.finish()
|
||||
progress.value = 1f
|
||||
backup.close()
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlin.math.roundToInt
|
||||
@AndroidEntryPoint
|
||||
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
|
||||
|
||||
private val viewModel: RestoreViewModel by viewModels()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
@@ -67,8 +68,10 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
private fun onRestoreDone(result: CompositeResult) {
|
||||
val builder = MaterialAlertDialogBuilder(context ?: return)
|
||||
when {
|
||||
result.isAllSuccess -> builder.setTitle(R.string.data_restored)
|
||||
.setMessage(R.string.data_restored_success)
|
||||
result.isAllSuccess -> {
|
||||
builder.setTitle(R.string.data_restored)
|
||||
.setMessage(R.string.data_restored_success)
|
||||
}
|
||||
|
||||
result.isAllFailed -> builder.setTitle(R.string.error)
|
||||
.setMessage(
|
||||
@@ -85,6 +88,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val ARG_FILE = "file"
|
||||
|
||||
@@ -50,12 +50,15 @@ class RestoreViewModel @Inject constructor(
|
||||
progress.value = 0f
|
||||
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
|
||||
progress.value = 0.3f
|
||||
progress.value = 0.25f
|
||||
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
|
||||
progress.value = 0.6f
|
||||
progress.value = 0.5f
|
||||
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
|
||||
progress.value = 0.75f
|
||||
result += repository.restoreSettings(backup.getEntry(BackupEntry.SETTINGS))
|
||||
|
||||
progress.value = 1f
|
||||
onRestoreDone.call(result)
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user