feat: add cross device sync (#1005)
* feat: add cross device sync. * chore: add google api. * chore: add SY specifics. * feat: add backupSource, backupPref, and "SY" backupSavedSearches. I forgot to add the data into the merging logic, So remote always have empty value :(. Better late than never. * feat(sync): Allow to choose what to sync. Various improvement and added the option to choose what they want to sync. Added sync library button to LibraryTab as well. Signed-off-by: KaiserBh <kaiserbh@proton.me> * oops. Signed-off-by: KaiserBh <kaiserbh@proton.me> * refactor: fix up the sync triggers, and update imports. * refactor * chore: review pointers. * refactor: update imports * refactor: add more error guard for gdrive. Also changed it to be app specific, we don't want them to use sync data from SY or other forks as some of the model and backup is different. So if people using other forks they should use the same data and not mismatch. * fix: conflict and refactor. * refactor: update imports. * chore: fix some of detekt error. * refactor: add breaks and max retries. I think we were reaching deadlock or infinite loop causing the sync to go forever. * feat: db changes to accommodate new syncing logic. Using timestamp to sync is a bit skewed due to system clock etc and therefore there was a lot of issues with it such as removing a manga that shouldn't have been removed. Marking chapters as unread even though it was marked as a read. Hopefully by using versioning system it should eliminate those issues. * chore: add migrations * chore: version and is_syncing fields. * chore: add SY only stuff. * fix: oops wrong index. Signed-off-by: KaiserBh <kaiserbh@proton.me> * chore: review pointers. Signed-off-by: KaiserBh <kaiserbh@proton.me> * chore: remove the option to reset timestamp We don't need this anymore since we are utilizing versioning system. Signed-off-by: KaiserBh <kaiserbh@proton.me> * refactor: Forgot to use the new versioning system. I forgot to cherry pick this from mihon branch. * chore: remove isSyncing from Chapter/Manga model. Signed-off-by: KaiserBh <kaiserbh@proton.me> * chore: remove unused import. Signed-off-by: KaiserBh <kaiserbh@proton.me> * chore: remove isSyncing leftover. Signed-off-by: KaiserBh <kaiserbh@proton.me> * chore: remove isSyncing. Signed-off-by: KaiserBh <kaiserbh@proton.me> * refactor: make sure the manga version is bumped. When there is changes in the chapters table such as reading or updating last read page we should bump the manga version. This way the manga is synced properly as in the History and last_read history is synced properly. This should fix the sorting issue. Signed-off-by: KaiserBh <kaiserbh@proton.me> --------- Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
@@ -38,6 +38,7 @@ fun LibraryToolbar(
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickGlobalUpdate: () -> Unit,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
onClickSyncNow: () -> Unit,
|
||||
// SY -->
|
||||
onClickSyncExh: (() -> Unit)?,
|
||||
// SY <--
|
||||
@@ -60,6 +61,7 @@ fun LibraryToolbar(
|
||||
onClickRefresh = onClickRefresh,
|
||||
onClickGlobalUpdate = onClickGlobalUpdate,
|
||||
onClickOpenRandomManga = onClickOpenRandomManga,
|
||||
onClickSyncNow = onClickSyncNow,
|
||||
// SY -->
|
||||
onClickSyncExh = onClickSyncExh,
|
||||
// SY <--
|
||||
@@ -77,6 +79,7 @@ private fun LibraryRegularToolbar(
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickGlobalUpdate: () -> Unit,
|
||||
onClickOpenRandomManga: () -> Unit,
|
||||
onClickSyncNow: () -> Unit,
|
||||
// SY -->
|
||||
onClickSyncExh: (() -> Unit)?,
|
||||
// SY <--
|
||||
@@ -125,7 +128,10 @@ private fun LibraryRegularToolbar(
|
||||
title = stringResource(MR.strings.action_open_random_manga),
|
||||
onClick = onClickOpenRandomManga,
|
||||
),
|
||||
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.sync_library),
|
||||
onClick = onClickSyncNow,
|
||||
),
|
||||
).builder().apply {
|
||||
// SY -->
|
||||
if (onClickSyncExh != null) {
|
||||
|
||||
-1
@@ -60,7 +60,6 @@ import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
|
||||
+236
-1
@@ -15,16 +15,19 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -35,10 +38,13 @@ import androidx.core.net.toUri
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||
import eu.kanade.presentation.more.settings.screen.data.SyncSettingsSelector
|
||||
import eu.kanade.presentation.more.settings.screen.data.SyncTriggerOptionsScreen
|
||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
@@ -46,10 +52,15 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.displayablePath
|
||||
@@ -91,13 +102,16 @@ object SettingsDataScreen : SearchableSettings {
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||
|
||||
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
||||
val syncService by syncPreferences.syncService().collectAsState()
|
||||
|
||||
return persistentListOf(
|
||||
getStorageLocationPref(storagePreferences = storagePreferences),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
|
||||
|
||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||
getDataGroup(),
|
||||
)
|
||||
) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -330,4 +344,225 @@ object SettingsDataScreen : SearchableSettings {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||
return listOf(
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_sync_service_category),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = syncPreferences.syncService(),
|
||||
title = stringResource(MR.strings.pref_sync_service),
|
||||
entries = persistentMapOf(
|
||||
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
|
||||
SyncManager.SyncService.SYNCYOMI.value to stringResource(MR.strings.syncyomi),
|
||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(MR.strings.google_drive),
|
||||
),
|
||||
onValueChanged = { true },
|
||||
),
|
||||
),
|
||||
),
|
||||
) + getSyncServicePreferences(syncPreferences, syncService)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
|
||||
|
||||
val basePreferences = getBasePreferences(syncServiceType, syncPreferences)
|
||||
|
||||
return if (syncServiceType != SyncManager.SyncService.NONE) {
|
||||
basePreferences + getAdditionalPreferences(syncPreferences)
|
||||
} else {
|
||||
basePreferences
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBasePreferences(
|
||||
syncServiceType: SyncManager.SyncService,
|
||||
syncPreferences: SyncPreferences,
|
||||
): List<Preference> {
|
||||
return when (syncServiceType) {
|
||||
SyncManager.SyncService.NONE -> emptyList()
|
||||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||
return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGoogleDrivePreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_google_drive_sign_in),
|
||||
onClick = {
|
||||
val intent = googleDriveSync.getSignInIntent()
|
||||
context.startActivity(intent)
|
||||
},
|
||||
),
|
||||
getGoogleDrivePurge(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val googleDriveSync = remember { GoogleDriveSyncService(context) }
|
||||
var showPurgeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showPurgeDialog) {
|
||||
PurgeConfirmationDialog(
|
||||
onConfirm = {
|
||||
showPurgeDialog = false
|
||||
scope.launch {
|
||||
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
|
||||
when (result) {
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(
|
||||
MR.strings.google_drive_not_signed_in,
|
||||
duration = 5000,
|
||||
)
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(
|
||||
MR.strings.google_drive_sync_data_not_found,
|
||||
duration = 5000,
|
||||
)
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(
|
||||
MR.strings.google_drive_sync_data_purged,
|
||||
duration = 5000,
|
||||
)
|
||||
GoogleDriveSyncService.DeleteSyncDataStatus.ERROR -> context.toast(
|
||||
MR.strings.google_drive_sync_data_purge_error,
|
||||
duration = 10000,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = { showPurgeDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_google_drive_purge_sync_data),
|
||||
onClick = { showPurgeDialog = true },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PurgeConfirmationDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) },
|
||||
text = { Text(text = stringResource(MR.strings.pref_purge_confirmation_message)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||
val scope = rememberCoroutineScope()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
title = stringResource(MR.strings.pref_sync_host),
|
||||
subtitle = stringResource(MR.strings.pref_sync_host_summ),
|
||||
pref = syncPreferences.clientHost(),
|
||||
onValueChanged = { newValue ->
|
||||
scope.launch {
|
||||
// Trim spaces at the beginning and end, then remove trailing slash if present
|
||||
val trimmedValue = newValue.trim()
|
||||
val modifiedValue = trimmedValue.trimEnd { it == '/' }
|
||||
syncPreferences.clientHost().set(modifiedValue)
|
||||
}
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
title = stringResource(MR.strings.pref_sync_api_key),
|
||||
subtitle = stringResource(MR.strings.pref_sync_api_key_summ),
|
||||
pref = syncPreferences.clientAPIKey(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_sync_now_group_title),
|
||||
preferenceItems = persistentListOf(
|
||||
getSyncOptionsPref(),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_sync_now),
|
||||
subtitle = stringResource(MR.strings.pref_sync_now_subtitle),
|
||||
onClick = {
|
||||
navigator.push(SyncSettingsSelector())
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.pref_sync_options),
|
||||
subtitle = stringResource(MR.strings.pref_sync_options_summ),
|
||||
onClick = { navigator.push(SyncTriggerOptionsScreen()) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val syncIntervalPref = syncPreferences.syncInterval()
|
||||
val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_sync_automatic_category),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = syncIntervalPref,
|
||||
title = stringResource(MR.strings.pref_sync_interval),
|
||||
entries = persistentMapOf(
|
||||
0 to stringResource(MR.strings.off),
|
||||
30 to stringResource(MR.strings.update_30min),
|
||||
60 to stringResource(MR.strings.update_1hour),
|
||||
180 to stringResource(MR.strings.update_3hour),
|
||||
360 to stringResource(MR.strings.update_6hour),
|
||||
720 to stringResource(MR.strings.update_12hour),
|
||||
1440 to stringResource(MR.strings.update_24hour),
|
||||
2880 to stringResource(MR.strings.update_48hour),
|
||||
10080 to stringResource(MR.strings.update_weekly),
|
||||
),
|
||||
onValueChanged = {
|
||||
SyncDataJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(
|
||||
stringResource(MR.strings.last_synchronization, relativeTimeSpanString(lastSync)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
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<BackupOptions.Entry>,
|
||||
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<SyncSettingsSelectorModel.State>(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -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<SyncTriggerOptions.Entry>,
|
||||
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<SyncOptionsScreenModel.State>(
|
||||
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(),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user