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

1
.idea/gradle.xml generated
View File

@@ -4,6 +4,7 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" /> <option name="gradleJvm" value="jbr-17" />
<option name="modules"> <option name="modules">

View File

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

View File

@@ -1,18 +1,36 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import android.provider.Settings
import androidx.room.withTransaction import androidx.room.withTransaction
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase 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.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.json.mapJSON
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import javax.inject.Inject import javax.inject.Inject
private const val PAGE_SIZE = 10 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 { suspend fun dumpHistory(): BackupEntry {
var offset = 0 var offset = 0
@@ -67,6 +85,13 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
return entry 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 { fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray()) val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject() val json = JSONObject()
@@ -127,4 +152,67 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
} }
return result 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"), largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"), state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"), author = json.getStringOrNull("author"),
source = json.getString("source") source = json.getString("source"),
) )
fun toTagEntity() = TagEntity( fun toTagEntity() = TagEntity(
id = json.getLong("id"), id = json.getLong("id"),
title = json.getString("title"), title = json.getString("title"),
key = json.getString("key"), key = json.getString("key"),
source = json.getString("source") source = json.getString("source"),
) )
fun toHistoryEntity() = HistoryEntity( fun toHistoryEntity() = HistoryEntity(

View File

@@ -1,11 +1,14 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity 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.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import java.util.ArrayList
class JsonSerializer private constructor(private val json: JSONObject) { 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("category_id", e.categoryId)
put("sort_key", e.sortKey) put("sort_key", e.sortKey)
put("created_at", e.createdAt) put("created_at", e.createdAt)
} },
) )
constructor(e: FavouriteCategoryEntity) : this( constructor(e: FavouriteCategoryEntity) : this(
@@ -27,7 +30,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("order", e.order) put("order", e.order)
put("track", e.track) put("track", e.track)
put("show_in_lib", e.isVisibleInLibrary) put("show_in_lib", e.isVisibleInLibrary)
} },
) )
constructor(e: HistoryEntity) : this( constructor(e: HistoryEntity) : this(
@@ -39,7 +42,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("page", e.page) put("page", e.page)
put("scroll", e.scroll) put("scroll", e.scroll)
put("percent", e.percent) put("percent", e.percent)
} },
) )
constructor(e: TagEntity) : this( constructor(e: TagEntity) : this(
@@ -48,7 +51,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("title", e.title) put("title", e.title)
put("key", e.key) put("key", e.key)
put("source", e.source) put("source", e.source)
} },
) )
constructor(e: MangaEntity) : this( constructor(e: MangaEntity) : this(
@@ -65,8 +68,91 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("state", e.state) put("state", e.state)
put("author", e.author) put("author", e.author)
put("source", e.source) 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 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) get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) } 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 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) 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) get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
set(value) = prefs.edit { putBoolean(KEY_THEME_AMOLED, value) }
var gridSize: Int var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100) 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) get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
set(value) = prefs.edit { putStringSet(KEY_READER_SWITCHERS, value) }
val isReaderTapsAdaptive: Boolean var isReaderTapsAdaptive: Boolean
get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false) get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
set(value) = prefs.edit { putBoolean(KEY_READER_TAPS_LTR, value) }
var isTrafficWarningEnabled: Boolean var isTrafficWarningEnabled: Boolean
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true) 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) get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) } set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
val isTrackerEnabled: Boolean var isTrackerEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) 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) get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
set(value) = prefs.edit { putBoolean(KEY_TRACKER_NOTIFICATIONS, value) }
var notificationSound: Uri var notificationSound: Uri
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull() get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
?: Settings.System.DEFAULT_NOTIFICATION_URI ?: Settings.System.DEFAULT_NOTIFICATION_URI
set(value) = prefs.edit { putString(KEY_NOTIFICATIONS_SOUND, value.toString()) } set(value) = prefs.edit { putString(KEY_NOTIFICATIONS_SOUND, value.toString()) }
val notificationVibrate: Boolean var notificationVibrate: Boolean
get() = prefs.getBoolean(KEY_NOTIFICATIONS_VIBRATE, false) 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) 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) 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) 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) get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true)
set(value) = prefs.edit { putBoolean(KEY_READER_MODE_DETECT, value) }
var isHistoryGroupingEnabled: Boolean var isHistoryGroupingEnabled: Boolean
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true) get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) } set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
val isReadingIndicatorsEnabled: Boolean var isReadingIndicatorsEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_INDICATORS, true) 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) get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_HISTORY_EXCLUDE_NSFW, value) }
var isIncognitoModeEnabled: Boolean var isIncognitoModeEnabled: Boolean
get() = prefs.getBoolean(KEY_INCOGNITO_MODE, false) 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) get() = prefs.getBoolean(KEY_REVERSE_CHAPTERS, false)
set(value) = prefs.edit { putBoolean(KEY_REVERSE_CHAPTERS, value) } set(value) = prefs.edit { putBoolean(KEY_REVERSE_CHAPTERS, value) }
val zoomMode: ZoomMode var zoomMode: ZoomMode
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER) 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) get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY)
set(value) = prefs.edit { putStringSet(KEY_TRACK_SOURCES, value) }
var appPassword: String? var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null) get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) } 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) get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
set(value) = prefs.edit { putBoolean(KEY_LOGGING_ENABLED, value) }
var isBiometricProtectionEnabled: Boolean var isBiometricProtectionEnabled: Boolean
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true) get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) } set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
val isMirrorSwitchingAvailable: Boolean var isMirrorSwitchingAvailable: Boolean
get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, true) 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) 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) 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) get() = prefs.getBoolean(KEY_UPDATES_UNSTABLE, false)
set(value) = prefs.edit { putBoolean(KEY_UPDATES_UNSTABLE, value) }
val isContentPrefetchEnabled: Boolean val isContentPrefetchEnabled: Boolean
get() { 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) 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) get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
set(value) = prefs.edit { putBoolean(KEY_DOWNLOADS_WIFI, value) }
var isSuggestionsEnabled: Boolean var isSuggestionsEnabled: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS, false) get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) } set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) }
val isSuggestionsExcludeNsfw: Boolean var isSuggestionsExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false) 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) 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() { get() {
val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',') val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',')
if (string.isNullOrEmpty()) { if (string.isNullOrEmpty()) {
@@ -286,21 +310,27 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
} }
return string.split(',').mapToSet { it.trim() } 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) 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) 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) 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) 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) get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
set(value) = prefs.edit { putBoolean(KEY_SSL_BYPASS, value) }
val proxyType: Proxy.Type val proxyType: Proxy.Type
get() { get() {
@@ -324,8 +354,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST) get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) } set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
val isWebtoonZoomEnable: Boolean var isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true) get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_ZOOM, value) }
@get:FloatRange(from = 0.0, to = 1.0) @get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float var readerAutoscrollSpeed: Float

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.content.SharedPreferences
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import java.util.UUID import java.util.UUID
@@ -40,3 +41,9 @@ fun CharSequence.sanitize(): CharSequence {
} }
fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF' 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.BackupZipInput
import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import java.io.* import java.io.*
class AppBackupAgent : BackupAgent() { class AppBackupAgent : BackupAgent() {
@@ -31,7 +32,8 @@ class AppBackupAgent : BackupAgent() {
override fun onFullBackup(data: FullBackupDataOutput) { override fun onFullBackup(data: FullBackupDataOutput) {
super.onFullBackup(data) super.onFullBackup(data)
val file = createBackupFile(this, BackupRepository(MangaDatabase(applicationContext))) val file =
createBackupFile(this, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
try { try {
fullBackupFile(file, data) fullBackupFile(file, data)
} finally { } finally {
@@ -48,7 +50,7 @@ class AppBackupAgent : BackupAgent() {
mtime: Long mtime: Long
) { ) {
if (destination?.name?.endsWith(".bk.zip") == true) { 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() destination.delete()
} else { } else {
super.onRestoreFile(data, size, destination, type, mode, mtime) super.onRestoreFile(data, size, destination, type, mode, mtime)
@@ -62,6 +64,7 @@ class AppBackupAgent : BackupAgent() {
backup.put(repository.dumpHistory()) backup.put(repository.dumpHistory())
backup.put(repository.dumpCategories()) backup.put(repository.dumpCategories())
backup.put(repository.dumpFavourites()) backup.put(repository.dumpFavourites())
backup.put(repository.dumpSettings())
backup.finish() backup.finish()
backup.file backup.file
} }
@@ -81,6 +84,7 @@ class AppBackupAgent : BackupAgent() {
repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY)) repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES)) repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES)) repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
repository.restoreSettings(backup.getEntry(BackupEntry.SETTINGS))
} }
} finally { } finally {
backup.close() backup.close()
@@ -102,4 +106,4 @@ class AppBackupAgent : BackupAgent() {
bytes = read(buffer, 0, buffer.size.coerceAtMost(bytesLeft)) bytes = read(buffer, 0, buffer.size.coerceAtMost(bytesLeft))
} }
} }
} }

View File

@@ -29,13 +29,15 @@ class BackupViewModel @Inject constructor(
progress.value = 0f progress.value = 0f
backup.put(repository.dumpHistory()) backup.put(repository.dumpHistory())
progress.value = 0.3f progress.value = 0.25f
backup.put(repository.dumpCategories()) backup.put(repository.dumpCategories())
progress.value = 0.6f progress.value = 0.5f
backup.put(repository.dumpFavourites()) backup.put(repository.dumpFavourites())
progress.value = 0.9f progress.value = 0.75f
backup.put(repository.dumpSettings())
backup.finish() backup.finish()
progress.value = 1f progress.value = 1f
backup.close() backup.close()

View File

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

View File

@@ -50,12 +50,15 @@ class RestoreViewModel @Inject constructor(
progress.value = 0f progress.value = 0f
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY)) result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
progress.value = 0.3f progress.value = 0.25f
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES)) result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
progress.value = 0.6f progress.value = 0.5f
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES)) result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
progress.value = 0.75f
result += repository.restoreSettings(backup.getEntry(BackupEntry.SETTINGS))
progress.value = 1f progress.value = 1f
onRestoreDone.call(result) onRestoreDone.call(result)
} finally { } finally {