From ba63253fe2cfd245f977bc6127421c27f8a603c1 Mon Sep 17 00:00:00 2001 From: kaiserbh Date: Sat, 20 Jan 2024 17:10:23 +1100 Subject: [PATCH] chore: review pointers. --- .../eu/kanade/domain/sync/SyncPreferences.kt | 93 ++++++++++++ .../kanade/domain/sync/models/SyncSettings.kt | 12 ++ .../screen/data/SyncSettingsSelector.kt | 143 ++++++++++++++++++ .../screen/data/SyncTriggerOptionsScreen.kt | 101 +++++++++++++ .../data/sync/models/SyncTriggerOptions.kt | 72 +++++++++ 5 files changed, 421 insertions(+) create mode 100644 app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt create mode 100644 app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt diff --git a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt new file mode 100644 index 000000000..7add8fead --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -0,0 +1,93 @@ + +package eu.kanade.domain.sync + +import eu.kanade.domain.sync.models.SyncSettings +import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions +import tachiyomi.core.preference.Preference +import tachiyomi.core.preference.PreferenceStore +import java.util.UUID + +class SyncPreferences( + private val preferenceStore: PreferenceStore, +) { + fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org") + fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "") + fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L) + + fun syncInterval() = preferenceStore.getInt("sync_interval", 0) + fun syncService() = preferenceStore.getInt("sync_service", 0) + + fun googleDriveAccessToken() = preferenceStore.getString( + Preference.appStateKey("google_drive_access_token"), + "", + ) + + fun googleDriveRefreshToken() = preferenceStore.getString( + Preference.appStateKey("google_drive_refresh_token"), + "", + ) + + fun uniqueDeviceID(): String { + val uniqueIDPreference = preferenceStore.getString("unique_device_id", "") + + // Retrieve the current value of the preference + var uniqueID = uniqueIDPreference.get() + if (uniqueID.isBlank()) { + uniqueID = UUID.randomUUID().toString() + uniqueIDPreference.set(uniqueID) + } + + return uniqueID + } + + fun isSyncEnabled(): Boolean { + return syncService().get() != 0 + } + + fun getSyncSettings(): SyncSettings { + return SyncSettings( + libraryEntries = preferenceStore.getBoolean("library_entries", true).get(), + categories = preferenceStore.getBoolean("categories", true).get(), + chapters = preferenceStore.getBoolean("chapters", true).get(), + tracking = preferenceStore.getBoolean("tracking", true).get(), + history = preferenceStore.getBoolean("history", true).get(), + appSettings = preferenceStore.getBoolean("appSettings", true).get(), + sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(), + privateSettings = preferenceStore.getBoolean("privateSettings", true).get(), + ) + } + + fun setSyncSettings(syncSettings: SyncSettings) { + preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries) + preferenceStore.getBoolean("categories", true).set(syncSettings.categories) + preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters) + preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking) + preferenceStore.getBoolean("history", true).set(syncSettings.history) + preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings) + preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings) + preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings) + } + + fun getSyncTriggerOptions(): SyncTriggerOptions { + return SyncTriggerOptions( + syncOnChapterRead = preferenceStore.getBoolean("sync_on_chapter_read", false).get(), + syncOnChapterOpen = preferenceStore.getBoolean("sync_on_chapter_open", false).get(), + syncOnAppStart = preferenceStore.getBoolean("sync_on_app_start", false).get(), + syncOnAppResume = preferenceStore.getBoolean("sync_on_app_resume", false).get(), + syncOnLibraryUpdate = preferenceStore.getBoolean("sync_on_library_update", false).get(), + ) + } + + fun setSyncTriggerOptions(syncTriggerOptions: SyncTriggerOptions) { + preferenceStore.getBoolean("sync_on_chapter_read", false) + .set(syncTriggerOptions.syncOnChapterRead) + preferenceStore.getBoolean("sync_on_chapter_open", false) + .set(syncTriggerOptions.syncOnChapterOpen) + preferenceStore.getBoolean("sync_on_app_start", false) + .set(syncTriggerOptions.syncOnAppStart) + preferenceStore.getBoolean("sync_on_app_resume", false) + .set(syncTriggerOptions.syncOnAppResume) + preferenceStore.getBoolean("sync_on_library_update", false) + .set(syncTriggerOptions.syncOnLibraryUpdate) + } +} diff --git a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt new file mode 100644 index 000000000..da9da3e41 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.sync.models + +data class SyncSettings( + val libraryEntries: Boolean = true, + val categories: Boolean = true, + val chapters: Boolean = true, + val tracking: Boolean = true, + val history: Boolean = true, + val appSettings: Boolean = true, + val sourceSettings: Boolean = true, + val privateSettings: Boolean = false, +) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt new file mode 100644 index 000000000..6614caaad --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt @@ -0,0 +1,143 @@ + +package eu.kanade.presentation.more.settings.screen.data + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.domain.sync.models.SyncSettings +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.data.backup.create.BackupOptions +import eu.kanade.tachiyomi.data.sync.SyncDataJob +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.update +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.LazyColumnWithAction +import tachiyomi.presentation.core.components.SectionCard +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SyncSettingsSelector : Screen() { + @Composable + override fun Content() { + val context = LocalContext.current + val navigator = LocalNavigator.currentOrThrow + val model = rememberScreenModel { SyncSettingsSelectorModel() } + val state by model.state.collectAsState() + + Scaffold( + topBar = { + AppBar( + title = stringResource(MR.strings.pref_choose_what_to_sync), + navigateUp = navigator::pop, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.label_sync), + actionEnabled = state.options.anyEnabled(), + onClickAction = { + if (!SyncDataJob.isAnyJobRunning(context)) { + model.syncNow(context) + navigator.pop() + } else { + context.toast(MR.strings.sync_in_progress) + } + }, + ) { + item { + SectionCard(MR.strings.label_library) { + Options(BackupOptions.libraryOptions, state, model) + } + } + + item { + SectionCard(MR.strings.label_settings) { + Options(BackupOptions.settingsOptions, state, model) + } + } + } + } + } + + @Composable + private fun Options( + options: ImmutableList, + state: SyncSettingsSelectorModel.State, + model: SyncSettingsSelectorModel, + ) { + options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + enabled = option.enabled(state.options), + ) + } + } +} + +private class SyncSettingsSelectorModel( + val syncPreferences: SyncPreferences = Injekt.get(), +) : StateScreenModel( + State(syncOptionsToBackupOptions(syncPreferences.getSyncSettings())), +) { + fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { + mutableState.update { + val updatedOptions = setter(it.options, enabled) + syncPreferences.setSyncSettings(backupOptionsToSyncOptions(updatedOptions)) + it.copy(options = updatedOptions) + } + } + + fun syncNow(context: Context) { + SyncDataJob.startNow(context) + } + + @Immutable + data class State( + val options: BackupOptions = BackupOptions(), + ) companion object { + private fun syncOptionsToBackupOptions(syncSettings: SyncSettings): BackupOptions { + return BackupOptions( + libraryEntries = syncSettings.libraryEntries, + categories = syncSettings.categories, + chapters = syncSettings.chapters, + tracking = syncSettings.tracking, + history = syncSettings.history, + appSettings = syncSettings.appSettings, + sourceSettings = syncSettings.sourceSettings, + privateSettings = syncSettings.privateSettings, + ) + } + + private fun backupOptionsToSyncOptions(backupOptions: BackupOptions): SyncSettings { + return SyncSettings( + libraryEntries = backupOptions.libraryEntries, + categories = backupOptions.categories, + chapters = backupOptions.chapters, + tracking = backupOptions.tracking, + history = backupOptions.history, + appSettings = backupOptions.appSettings, + sourceSettings = backupOptions.sourceSettings, + privateSettings = backupOptions.privateSettings, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt new file mode 100644 index 000000000..27136eceb --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncTriggerOptionsScreen.kt @@ -0,0 +1,101 @@ +package eu.kanade.presentation.more.settings.screen.data + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.domain.sync.SyncPreferences +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.update +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.LazyColumnWithAction +import tachiyomi.presentation.core.components.SectionCard +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SyncTriggerOptionsScreen : Screen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val model = rememberScreenModel { SyncOptionsScreenModel() } + val state by model.state.collectAsState() + + Scaffold( + topBar = { + AppBar( + title = stringResource(MR.strings.pref_sync_options), + navigateUp = navigator::pop, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + LazyColumnWithAction( + contentPadding = contentPadding, + actionLabel = stringResource(MR.strings.action_save), + actionEnabled = state.options.anyEnabled(), + onClickAction = { + navigator.pop() + }, + ) { + item { + SectionCard(MR.strings.label_triggers) { + Options(SyncTriggerOptions.mainOptions, state, model) + } + } + } + } + } + + @Composable + private fun Options( + options: ImmutableList, + state: SyncOptionsScreenModel.State, + model: SyncOptionsScreenModel, + ) { + options.forEach { option -> + LabeledCheckbox( + label = stringResource(option.label), + checked = option.getter(state.options), + onCheckedChange = { + model.toggle(option.setter, it) + }, + enabled = option.enabled(state.options), + ) + } + } +} + +private class SyncOptionsScreenModel( + val syncPreferences: SyncPreferences = Injekt.get(), +) : StateScreenModel( + State( + syncPreferences.getSyncTriggerOptions(), + ), +) { + + fun toggle(setter: (SyncTriggerOptions, Boolean) -> SyncTriggerOptions, enabled: Boolean) { + mutableState.update { + val updatedTriggerOptions = setter(it.options, enabled) + syncPreferences.setSyncTriggerOptions(updatedTriggerOptions) + it.copy( + options = updatedTriggerOptions, + ) + } + } + + @Immutable + data class State( + val options: SyncTriggerOptions = SyncTriggerOptions(), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt new file mode 100644 index 000000000..b5dfaf001 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/models/SyncTriggerOptions.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.data.sync.models + +import dev.icerock.moko.resources.StringResource +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.i18n.MR + +data class SyncTriggerOptions( + val syncOnChapterRead: Boolean = false, + val syncOnChapterOpen: Boolean = false, + val syncOnAppStart: Boolean = false, + val syncOnAppResume: Boolean = false, + val syncOnLibraryUpdate: Boolean = false, +) { + fun asBooleanArray() = booleanArrayOf( + syncOnChapterRead, + syncOnChapterOpen, + syncOnAppStart, + syncOnAppResume, + syncOnLibraryUpdate, + ) + + fun anyEnabled() = syncOnChapterRead || + syncOnChapterOpen || + syncOnAppStart || + syncOnAppResume || + syncOnLibraryUpdate + + companion object { + val mainOptions = persistentListOf( + Entry( + label = MR.strings.sync_on_chapter_read, + getter = SyncTriggerOptions::syncOnChapterRead, + setter = { options, enabled -> options.copy(syncOnChapterRead = enabled) }, + ), + Entry( + label = MR.strings.sync_on_chapter_open, + getter = SyncTriggerOptions::syncOnChapterOpen, + setter = { options, enabled -> options.copy(syncOnChapterOpen = enabled) }, + ), + Entry( + label = MR.strings.sync_on_app_start, + getter = SyncTriggerOptions::syncOnAppStart, + setter = { options, enabled -> options.copy(syncOnAppStart = enabled) }, + ), + Entry( + label = MR.strings.sync_on_app_resume, + getter = SyncTriggerOptions::syncOnAppResume, + setter = { options, enabled -> options.copy(syncOnAppResume = enabled) }, + ), + Entry( + label = MR.strings.sync_on_library_update, + getter = SyncTriggerOptions::syncOnLibraryUpdate, + setter = { options, enabled -> options.copy(syncOnLibraryUpdate = enabled) }, + ), + ) + + fun fromBooleanArray(array: BooleanArray) = SyncTriggerOptions( + syncOnChapterRead = array[0], + syncOnChapterOpen = array[1], + syncOnAppStart = array[2], + syncOnAppResume = array[3], + syncOnLibraryUpdate = array[4], + ) + } + + data class Entry( + val label: StringResource, + val getter: (SyncTriggerOptions) -> Boolean, + val setter: (SyncTriggerOptions, Boolean) -> SyncTriggerOptions, + val enabled: (SyncTriggerOptions) -> Boolean = { true }, + ) +}