diff --git a/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt similarity index 50% rename from domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt rename to app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt index f480ee401..ec1e50c33 100644 --- a/domain/src/main/java/tachiyomi/domain/sync/SyncPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -1,5 +1,7 @@ -package tachiyomi.domain.sync +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 @@ -7,28 +9,8 @@ import java.util.UUID class SyncPreferences( private val preferenceStore: PreferenceStore, ) { - object Flags { - const val NONE = 0x0 - const val SYNC_ON_CHAPTER_READ = 0x1 - const val SYNC_ON_CHAPTER_OPEN = 0x2 - const val SYNC_ON_APP_START = 0x4 - const val SYNC_ON_APP_RESUME = 0x8 - const val SYNC_ON_LIBRARY_UPDATE = 0x10 - - const val Defaults = NONE - - fun values() = listOf( - NONE, - SYNC_ON_CHAPTER_READ, - SYNC_ON_CHAPTER_OPEN, - SYNC_ON_APP_START, - SYNC_ON_APP_RESUME, - SYNC_ON_LIBRARY_UPDATE, - ) - } - - fun syncHost() = preferenceStore.getString("sync_host", "https://sync.tachiyomi.org") - fun syncAPIKey() = preferenceStore.getString("sync_api_key", "") + 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) @@ -57,14 +39,12 @@ class SyncPreferences( return uniqueID } - fun syncFlags() = preferenceStore.getInt("sync_flags", Flags.Defaults) - fun isSyncEnabled(): Boolean { return syncService().get() != 0 } - fun getSyncOptions(): SyncOptions { - return SyncOptions( + fun getSyncSettings(): SyncSettings { + return SyncSettings( libraryEntries = preferenceStore.getBoolean("library_entries", true).get(), categories = preferenceStore.getBoolean("categories", true).get(), chapters = preferenceStore.getBoolean("chapters", true).get(), @@ -76,25 +56,37 @@ class SyncPreferences( ) } - fun setSyncOptions(syncOptions: SyncOptions) { - preferenceStore.getBoolean("library_entries", true).set(syncOptions.libraryEntries) - preferenceStore.getBoolean("categories", true).set(syncOptions.categories) - preferenceStore.getBoolean("chapters", true).set(syncOptions.chapters) - preferenceStore.getBoolean("tracking", true).set(syncOptions.tracking) - preferenceStore.getBoolean("history", true).set(syncOptions.history) - preferenceStore.getBoolean("appSettings", true).set(syncOptions.appSettings) - preferenceStore.getBoolean("sourceSettings", true).set(syncOptions.sourceSettings) - preferenceStore.getBoolean("privateSettings", true).set(syncOptions.privateSettings) + 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) } } - -data class SyncOptions( - 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/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/SyncOptionsScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncOptionsScreen.kt deleted file mode 100644 index 95d00e270..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncOptionsScreen.kt +++ /dev/null @@ -1,125 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.data - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -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.presentation.components.AppBar -import eu.kanade.presentation.util.Screen -import kotlinx.collections.immutable.PersistentSet -import kotlinx.collections.immutable.minus -import kotlinx.collections.immutable.plus -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.flow.update -import tachiyomi.domain.sync.SyncPreferences -import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.LabeledCheckbox -import tachiyomi.presentation.core.components.material.Scaffold -import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.i18n.stringResource -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class SyncOptionsScreen : 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 -> - Column( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), - ) { - LazyColumn( - modifier = Modifier - .weight(1f) - .padding(horizontal = MaterialTheme.padding.medium), - ) { - SyncChoices.forEach { (k, v) -> - item { - LabeledCheckbox( - label = stringResource(v), - checked = state.flags.contains(k), - onCheckedChange = { - model.toggleOptionFlag(k) - }, - ) - } - } - } - - HorizontalDivider() - } - } - } -} - -private class SyncOptionsScreenModel : StateScreenModel(State()) { - private val syncPreferences = Injekt.get() - - init { - loadInitialFlags() - } - - private fun loadInitialFlags() { - val savedFlags = syncPreferences.syncFlags().get() - val flagSet = SyncPreferences.Flags.values().filter { flag -> - savedFlags and flag > 0 - }.toSet().toPersistentSet() - - mutableState.update { State(flags = flagSet) } - } - - fun toggleOptionFlag(option: Int) { - mutableState.update { currentState -> - val newFlags = if (currentState.flags.contains(option)) { - currentState.flags - option - } else { - currentState.flags + option - } - saveFlags(newFlags) - currentState.copy(flags = newFlags) - } - } - - private fun saveFlags(flags: PersistentSet) { - val flagsInt = flags.fold(0) { acc, flag -> acc or flag } - syncPreferences.syncFlags().set(flagsInt) - } - - @Immutable - data class State( - val flags: PersistentSet = SyncChoices.keys.toPersistentSet(), - ) -} - -private val SyncChoices = mapOf( - SyncPreferences.Flags.SYNC_ON_CHAPTER_READ to MR.strings.sync_on_chapter_read, - SyncPreferences.Flags.SYNC_ON_CHAPTER_OPEN to MR.strings.sync_on_chapter_open, - SyncPreferences.Flags.SYNC_ON_APP_START to MR.strings.sync_on_app_start, - SyncPreferences.Flags.SYNC_ON_APP_RESUME to MR.strings.sync_on_app_resume, - SyncPreferences.Flags.SYNC_ON_LIBRARY_UPDATE to MR.strings.sync_on_library_update, -) 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 index a13aa5c28..86865113f 100644 --- 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 @@ -10,6 +10,8 @@ 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 @@ -17,8 +19,6 @@ 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.domain.sync.SyncOptions -import tachiyomi.domain.sync.SyncPreferences import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LazyColumnWithAction @@ -95,12 +95,12 @@ class SyncSettingsSelector : Screen() { private class SyncSettingsSelectorModel( val syncPreferences: SyncPreferences = Injekt.get(), ) : StateScreenModel( - State(syncOptionsToBackupOptions(syncPreferences.getSyncOptions())), + State(syncOptionsToBackupOptions(syncPreferences.getSyncSettings())), ) { fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) { mutableState.update { val updatedOptions = setter(it.options, enabled) - syncPreferences.setSyncOptions(backupOptionsToSyncOptions(updatedOptions)) + syncPreferences.setSyncSettings(backupOptionsToSyncOptions(updatedOptions)) it.copy(options = updatedOptions) } } @@ -113,21 +113,21 @@ private class SyncSettingsSelectorModel( data class State( val options: BackupOptions = BackupOptions(), ) companion object { - private fun syncOptionsToBackupOptions(syncOptions: SyncOptions): BackupOptions { + private fun syncOptionsToBackupOptions(syncSettings: SyncSettings): BackupOptions { return BackupOptions( - libraryEntries = syncOptions.libraryEntries, - categories = syncOptions.categories, - chapters = syncOptions.chapters, - tracking = syncOptions.tracking, - history = syncOptions.history, - appSettings = syncOptions.appSettings, - sourceSettings = syncOptions.sourceSettings, - privateSettings = syncOptions.privateSettings, + 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): SyncOptions { - return SyncOptions( + private fun backupOptionsToSyncOptions(backupOptions: BackupOptions): SyncSettings { + return SyncSettings( libraryEntries = backupOptions.libraryEntries, categories = backupOptions.categories, chapters = backupOptions.chapters, 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 }, + ) +}