add settings backup

This commit is contained in:
javlon
2023-07-15 21:22:16 +02:00
parent 44a2b6db11
commit a8176e6589
11 changed files with 292 additions and 53 deletions

View File

@@ -13,5 +13,6 @@ class BackupEntry(
const val HISTORY = "history"
const val CATEGORIES = "categories"
const val FAVOURITES = "favourites"
const val SETTINGS = "settings"
}
}
}

View File

@@ -1,18 +1,36 @@
package org.koitharu.kotatsu.core.backup
import android.provider.Settings
import androidx.room.withTransaction
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.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ColorScheme
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
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 +85,13 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
return entry
}
suspend fun dumpSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
val json = JsonSerializer(settings).toJson()
entry.data.put(json)
return entry
}
fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject()
@@ -127,4 +152,67 @@ 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.listMode = item.getString("list_mode").getEnumValue(ListMode.GRID)
settings.theme = item.getInt("theme")
settings.colorScheme = item.getString("color_scheme").getEnumValue(ColorScheme.default)
settings.isAmoledTheme = item.getBoolean("is_amoled_theme")
settings.gridSize = item.getInt("grid_size")
settings.readerPageSwitch =
item.getJSONArray("reader_page_switch").mapJSONToSet<String, String> { it }
settings.isReaderTapsAdaptive = item.getBoolean("is_reader_taps_adaptive")
settings.isTrafficWarningEnabled = item.getBoolean("is_traffic_waring_enabled")
settings.isAllFavouritesVisible = item.getBoolean("is_all_favourites_visible")
settings.isTrackerEnabled = item.getBoolean("is_tracker_enabled")
settings.isTrackerNotificationsEnabled = item.getBoolean("is_tracker_notifications_enabled")
settings.notificationSound =
item.getString("notification_sound").toUriOrNull() ?: Settings.System.DEFAULT_NOTIFICATION_URI
settings.notificationVibrate = item.getBoolean("notification_vibrate")
settings.notificationLight = item.getBoolean("notification_light")
settings.readerAnimation = item.getBoolean("reader_animation")
settings.defaultReaderMode = item.getString("default_reader_node").getEnumValue(ReaderMode.STANDARD)
settings.isReaderModeDetectionEnabled = item.getBoolean("is_reader_mode_detection_enabled")
settings.isHistoryGroupingEnabled = item.getBoolean("is_history_grouping_enabled")
settings.isReadingIndicatorsEnabled = item.getBoolean("is_reading_indicators_enabled")
settings.isHistoryExcludeNsfw = item.getBoolean("is_history_exclude_nsfw")
settings.isIncognitoModeEnabled = item.getBoolean("is_incognito_mode_enabled")
settings.chaptersReverse = item.getBoolean("chapters_reverse")
settings.zoomMode = item.getString("zoom_mode").getEnumValue(ZoomMode.FIT_CENTER)
settings.trackSources = item.getJSONArray("track_sources").mapJSONToSet<String, String> { it }
settings.isLoggingEnabled = item.getBoolean("is_logging_enabled")
settings.isMirrorSwitchingAvailable = item.getBoolean("is_mirror_switching_available")
settings.isExitConfirmationEnabled = item.getBoolean("is_exit_confirmation_enabled")
settings.isDynamicShortcutsEnabled = item.getBoolean("is_dynamic_shortcuts_enabled")
settings.isUnstableUpdatesAllowed = item.getBoolean("is_unstable_updates_allowed")
settings.sourcesOrder = item.getJSONArray("sources_order").mapJSONToArray<String, String> { it }
settings.hiddenSources = item.getJSONArray("hidden_sources").mapJSONToSet<String, String> { it }
settings.isSourcesGridMode = item.getBoolean("is_sources_grid_mode")
settings.userSpecifiedMangaDirectories = item.getJSONArray("user_specified_manga_directions")
.mapJSONToSet<String, String> { it }.mapNotNullToSet { File(it).takeIfReadable() }
File(item.getStringOrNull("manga_storage_dir") ?: "").takeIfReadable()?.let {
settings.mangaStorageDir = it
}
settings.isDownloadsSlowdownEnabled = item.getBoolean("is_downloads_slowdown_enabled")
settings.isDownloadsWiFiOnly = item.getBoolean("is_downloads_wifi_only")
settings.isSuggestionsEnabled = item.getBoolean("is_suggestions_enabled")
settings.isSuggestionsExcludeNsfw = item.getBoolean("is_suggestions_exclude_nsfw")
settings.isSuggestionsNotificationAvailable = item.getBoolean("is_suggestions_notification_available")
settings.suggestionsTagsBlacklist =
item.getJSONArray("suggestions_tags_blacklist").mapJSONToSet<String, String> { it }
settings.isReaderBarEnabled = item.getBoolean("is_reader_bar_enabled")
settings.isReaderSliderEnabled = item.getBoolean("is_reader_slider_enabled")
settings.isImagesProxyEnabled = item.getBoolean("is_images_proxy_enabled")
settings.dnsOverHttps = item.getString("dns_over_https").getEnumValue(DoHProvider.NONE)
settings.isSSLBypassEnabled = item.getBoolean("is_ssl_bypass_enabled")
settings.localListOrder = item.getString("local_list_order").getEnumValue(SortOrder.NEWEST)
settings.isWebtoonZoomEnable = item.getBoolean("is_webtoon_zoom_enabled")
settings.readerAutoscrollSpeed = item.getFloatOrDefault("reader_autoscroll_speed", 0f)
}
}
return result
}
}

View File

@@ -34,14 +34,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(

View File

@@ -1,11 +1,14 @@
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
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import java.util.ArrayList
class JsonSerializer private constructor(private val json: JSONObject) {
@@ -15,7 +18,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 +30,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 +42,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 +51,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 +68,91 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("state", e.state)
put("author", e.author)
put("source", e.source)
}
},
)
constructor(e: AppSettings) : this(
JSONObject().apply {
put("list_mode", e.listMode.name)
put("theme", e.theme)
put("color_scheme", e.colorScheme.name)
put("is_amoled_theme", e.isAmoledTheme)
put("grid_size", e.gridSize)
put("reader_page_switch", JSONArray(e.readerPageSwitch))
put("is_reader_taps_adaptive", e.isReaderTapsAdaptive)
put("is_traffic_waring_enabled", e.isTrafficWarningEnabled)
put("is_all_favourites_visible", e.isAllFavouritesVisible)
put("is_tracker_enabled", e.isTrackerEnabled)
put("is_tracker_notifications_enabled", e.isTrackerNotificationsEnabled)
put("notification_sound", e.notificationSound.toString())
put("notification_vibrate", e.notificationVibrate)
put("notification_light", e.notificationLight)
put("reader_animation", e.readerAnimation)
put("default_reader_node", e.defaultReaderMode.name)
put("is_reader_mode_detection_enabled", e.isReaderModeDetectionEnabled)
put("is_history_grouping_enabled", e.isHistoryGroupingEnabled)
put("is_reading_indicators_enabled", e.isReadingIndicatorsEnabled)
put("is_history_exclude_nsfw", e.isHistoryExcludeNsfw)
put("is_incognito_mode_enabled", e.isIncognitoModeEnabled) // maybe we should omit this
put("chapters_reverse", e.chaptersReverse)
put("zoom_mode", e.zoomMode)
put("track_sources", JSONArray(e.trackSources))
put("is_logging_enabled", e.isLoggingEnabled)
put("is_mirror_switching_available", e.isMirrorSwitchingAvailable)
put("is_exit_confirmation_enabled", e.isExitConfirmationEnabled)
put("is_dynamic_shortcuts_enabled", e.isDynamicShortcutsEnabled)
put("is_unstable_updates_allowed", e.isUnstableUpdatesAllowed)
put("sources_order", JSONArray(e.sourcesOrder))
put("hidden_sources", JSONArray(e.hiddenSources))
put("is_sources_grid_mode", e.isSourcesGridMode)
put(
"user_specified_manga_directions",
JSONArray(e.userSpecifiedMangaDirectories.map { it.absolutePath }),
)
put("manga_storage_dir", e.mangaStorageDir?.absolutePath)
put("is_downloads_slowdown_enabled", e.isDownloadsSlowdownEnabled)
put("is_downloads_wifi_only", e.isDownloadsWiFiOnly)
put("is_suggestions_enabled", e.isSuggestionsEnabled)
put("is_suggestions_exclude_nsfw", e.isSuggestionsExcludeNsfw)
put("is_suggestions_notification_available", e.isSuggestionsNotificationAvailable)
put("suggestions_tags_blacklist", JSONArray(e.suggestionsTagsBlacklist))
put("is_reader_bar_enabled", e.isReaderBarEnabled)
put("is_reader_slider_enabled", e.isReaderSliderEnabled)
put("is_images_proxy_enabled", e.isImagesProxyEnabled)
put("dns_over_https", e.dnsOverHttps.name)
put("is_ssl_bypass_enabled", e.isSSLBypassEnabled)
put("local_list_order", e.localListOrder.name)
put("is_webtoon_zoom_enabled", e.isWebtoonZoomEnable)
put("reader_autoscroll_speed", e.readerAutoscrollSpeed)
},
)
fun toJson(): JSONObject = json
}
}
// I have copied these extension functions from parser library,
// because, library doesn't support mapping primitive types (string, int, float ...),
// I didn't know where to put this extension functions :(
inline fun <K, T> JSONArray.mapJSONToArray(
block: (K) -> T,
): List<T> {
val len = length()
val result = ArrayList<T>(len)
for (i in 0 until len) {
val jo = get(i) as K
result.add(block(jo))
}
return result
}
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
}

View File

@@ -72,14 +72,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
val theme: Int
var theme: Int
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
set(value) = prefs.edit { putString(KEY_THEME, value.toString()) }
val colorScheme: ColorScheme
var colorScheme: ColorScheme
get() = prefs.getEnumValue(KEY_COLOR_THEME, ColorScheme.default)
set(value) = prefs.edit { putEnumValue(KEY_COLOR_THEME, value) }
val isAmoledTheme: Boolean
var isAmoledTheme: Boolean
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
set(value) = prefs.edit { putBoolean(KEY_THEME_AMOLED, value) }
var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100)
@@ -96,11 +99,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
val readerPageSwitch: Set<String>
var readerPageSwitch: Set<String>
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
val isReaderTapsAdaptive: Boolean
set(value) = prefs.edit { putStringSet(KEY_READER_SWITCHERS, value) }
var isReaderTapsAdaptive: Boolean
get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
set(value) = prefs.edit { putBoolean(KEY_READER_TAPS_LTR, value) }
var isTrafficWarningEnabled: Boolean
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
@@ -110,41 +114,50 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
val isTrackerEnabled: Boolean
var isTrackerEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
set(value) = prefs.edit { putBoolean(KEY_TRACKER_ENABLED, value) }
val isTrackerNotificationsEnabled: Boolean
var isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
set(value) = prefs.edit { putBoolean(KEY_TRACKER_NOTIFICATIONS, value) }
var notificationSound: Uri
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
?: Settings.System.DEFAULT_NOTIFICATION_URI
set(value) = prefs.edit { putString(KEY_NOTIFICATIONS_SOUND, value.toString()) }
val notificationVibrate: Boolean
var notificationVibrate: Boolean
get() = prefs.getBoolean(KEY_NOTIFICATIONS_VIBRATE, false)
set(value) = prefs.edit { putBoolean(KEY_NOTIFICATIONS_VIBRATE, value) }
val notificationLight: Boolean
var notificationLight: Boolean
get() = prefs.getBoolean(KEY_NOTIFICATIONS_LIGHT, true)
set(value) = prefs.edit { putBoolean(KEY_NOTIFICATIONS_LIGHT, value) }
val readerAnimation: Boolean
var readerAnimation: Boolean
get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
set(value) = prefs.edit { putBoolean(KEY_READER_ANIMATION, value) }
val defaultReaderMode: ReaderMode
var defaultReaderMode: ReaderMode
get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
set(value) = prefs.edit { putEnumValue(KEY_READER_MODE, value) }
val isReaderModeDetectionEnabled: Boolean
var isReaderModeDetectionEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true)
set(value) = prefs.edit { putBoolean(KEY_READER_MODE_DETECT, value) }
var isHistoryGroupingEnabled: Boolean
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
val isReadingIndicatorsEnabled: Boolean
var isReadingIndicatorsEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
set(value) = prefs.edit { putBoolean(KEY_READING_INDICATORS, value) }
val isHistoryExcludeNsfw: Boolean
var isHistoryExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_HISTORY_EXCLUDE_NSFW, value) }
var isIncognitoModeEnabled: Boolean
get() = prefs.getBoolean(KEY_INCOGNITO_MODE, false)
@@ -154,34 +167,41 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_REVERSE_CHAPTERS, false)
set(value) = prefs.edit { putBoolean(KEY_REVERSE_CHAPTERS, value) }
val zoomMode: ZoomMode
var zoomMode: ZoomMode
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER)
set(value) = prefs.edit { putEnumValue(KEY_ZOOM_MODE, value) }
val trackSources: Set<String>
var trackSources: Set<String>
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY)
set(value) = prefs.edit { putStringSet(KEY_TRACK_SOURCES, value) }
var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) }
val isLoggingEnabled: Boolean
var isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
set(value) = prefs.edit { putBoolean(KEY_LOGGING_ENABLED, value) }
var isBiometricProtectionEnabled: Boolean
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
val isMirrorSwitchingAvailable: Boolean
var isMirrorSwitchingAvailable: Boolean
get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, true)
set(value) = prefs.edit { putBoolean(KEY_MIRROR_SWITCHING, value) }
val isExitConfirmationEnabled: Boolean
var isExitConfirmationEnabled: Boolean
get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false)
set(value) = prefs.edit { putBoolean(KEY_EXIT_CONFIRM, value) }
val isDynamicShortcutsEnabled: Boolean
var isDynamicShortcutsEnabled: Boolean
get() = prefs.getBoolean(KEY_SHORTCUTS, true)
set(value) = prefs.edit { putBoolean(KEY_SHORTCUTS, value) }
val isUnstableUpdatesAllowed: Boolean
var isUnstableUpdatesAllowed: Boolean
get() = prefs.getBoolean(KEY_UPDATES_UNSTABLE, false)
set(value) = prefs.edit { putBoolean(KEY_UPDATES_UNSTABLE, value) }
val isContentPrefetchEnabled: Boolean
get() {
@@ -262,23 +282,27 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
val isDownloadsSlowdownEnabled: Boolean
var isDownloadsSlowdownEnabled: Boolean
get() = prefs.getBoolean(KEY_DOWNLOADS_SLOWDOWN, false)
set(value) = prefs.edit { putBoolean(KEY_DOWNLOADS_SLOWDOWN, value) }
val isDownloadsWiFiOnly: Boolean
var isDownloadsWiFiOnly: Boolean
get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
set(value) = prefs.edit { putBoolean(KEY_DOWNLOADS_WIFI, value) }
var isSuggestionsEnabled: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) }
val isSuggestionsExcludeNsfw: Boolean
var isSuggestionsExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, value) }
val isSuggestionsNotificationAvailable: Boolean
var isSuggestionsNotificationAvailable: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, true)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, value) }
val suggestionsTagsBlacklist: Set<String>
var suggestionsTagsBlacklist: Set<String>
get() {
val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',')
if (string.isNullOrEmpty()) {
@@ -286,21 +310,27 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
return string.split(',').mapToSet { it.trim() }
}
set(value) = prefs.edit { putStringSet(KEY_SUGGESTIONS_EXCLUDE_TAGS, value) }
val isReaderBarEnabled: Boolean
var isReaderBarEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_BAR, true)
set(value) = prefs.edit { putBoolean(KEY_READER_BAR, value) }
val isReaderSliderEnabled: Boolean
var isReaderSliderEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
set(value) = prefs.edit { putBoolean(KEY_READER_SLIDER, value) }
val isImagesProxyEnabled: Boolean
var isImagesProxyEnabled: Boolean
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
set(value) = prefs.edit { putBoolean(KEY_IMAGES_PROXY, value) }
val dnsOverHttps: DoHProvider
var dnsOverHttps: DoHProvider
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
set(value) = prefs.edit { putEnumValue(KEY_DOH, value) }
val isSSLBypassEnabled: Boolean
var isSSLBypassEnabled: Boolean
get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
set(value) = prefs.edit { putBoolean(KEY_SSL_BYPASS, value) }
val proxyType: Proxy.Type
get() {
@@ -324,8 +354,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
val isWebtoonZoomEnable: Boolean
var isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_ZOOM, value) }
@get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float

View File

@@ -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
}

View File

@@ -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))
}
}
}
}

View File

@@ -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()

View File

@@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -12,16 +13,21 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
@Inject
lateinit var activityRecreationHandle: ActivityRecreationHandle
private val viewModel: RestoreViewModel by viewModels()
override fun onCreateViewBinding(
@@ -67,8 +73,11 @@ 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)
postRestart()
}
result.isAllFailed -> builder.setTitle(R.string.error)
.setMessage(
@@ -85,6 +94,12 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
dismiss()
}
private fun postRestart() {
view?.postDelayed(400) {
activityRecreationHandle.recreateAll()
}
}
companion object {
const val ARG_FILE = "file"

View 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 {