merge backup and sync screens

This commit is contained in:
Aria Moradi 2023-07-07 22:20:17 +03:30
parent 6d9f74d0bf
commit a1e9d34bb8
8 changed files with 219 additions and 267 deletions

View File

@ -17,7 +17,6 @@ import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.QueryStats
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -47,7 +46,6 @@ fun MoreScreen(
onClickCategories: () -> Unit,
onClickStats: () -> Unit,
onClickBackupAndRestore: () -> Unit,
onClickSync: () -> Unit,
onClickSettings: () -> Unit,
onClickAbout: () -> Unit,
) {
@ -143,18 +141,11 @@ fun MoreScreen(
}
item {
TextPreferenceWidget(
title = stringResource(R.string.label_backup),
title = stringResource(R.string.label_backup_and_sync),
icon = Icons.Outlined.SettingsBackupRestore,
onPreferenceClick = onClickBackupAndRestore,
)
}
item {
TextPreferenceWidget(
title = stringResource(R.string.label_sync),
icon = Icons.Outlined.Sync,
onPreferenceClick = onClickSync,
)
}
item { Divider() }

View File

@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.format.DateUtils
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@ -49,12 +50,15 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.util.isScrolledToEnd
@ -74,11 +78,107 @@ object SettingsBackupScreen : SearchableSettings {
val backupPreferences = Injekt.get<BackupPreferences>()
DiskUtil.RequestStoragePermission()
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
val syncService by syncPreferences.syncService().collectAsState()
return listOf(
getCreateBackupPref(),
getRestoreBackupPref(),
getManualBackupGroup(),
getAutomaticBackupGroup(backupPreferences = backupPreferences),
) + listOf(
Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_manual_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(),
title = stringResource(R.string.pref_sync_service),
entries = mapOf(
SyncManager.SyncService.NONE.value to stringResource(R.string.off),
SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi),
),
onValueChanged = { true },
),
),
),
) + getSyncServicePreferences(syncPreferences, syncService)
}
@Composable
private fun getManualBackupGroup(): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_manual_category),
preferenceItems = listOf(
getCreateBackupPref(),
getRestoreBackupPref(),
),
)
}
@Composable
private fun getAutomaticBackupGroup(
backupPreferences: BackupPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val backupDirPref = backupPreferences.backupsDirectory()
val backupDir by backupDirPref.collectAsState()
val pickBackupLocation = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { uri ->
if (uri != null) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
backupDirPref.set(file.uri.toString())
}
}
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
title = stringResource(R.string.pref_backup_interval),
entries = mapOf(
0 to stringResource(R.string.off),
6 to stringResource(R.string.update_6hour),
12 to stringResource(R.string.update_12hour),
24 to stringResource(R.string.update_24hour),
48 to stringResource(R.string.update_48hour),
168 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_backup_directory),
enabled = backupInterval != 0,
subtitle = remember(backupDir) {
(UniFile.fromUri(context, backupDir.toUri())?.filePath)?.let {
"$it/automatic"
}
} ?: stringResource(R.string.invalid_location, backupDir),
onClick = {
try {
pickBackupLocation.launch(null)
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
}
},
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
enabled = backupInterval != 0,
title = stringResource(R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
),
)
}
@ -343,73 +443,129 @@ object SettingsBackupScreen : SearchableSettings {
}
@Composable
private fun getAutomaticBackupGroup(
backupPreferences: BackupPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val backupIntervalPref = backupPreferences.backupInterval()
val backupInterval by backupIntervalPref.collectAsState()
val backupDirPref = backupPreferences.backupsDirectory()
val backupDir by backupDirPref.collectAsState()
val pickBackupLocation = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { uri ->
if (uri != null) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
backupDirPref.set(file.uri.toString())
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
val syncServiceType = SyncManager.SyncService.fromInt(syncService)
return when (syncServiceType) {
SyncManager.SyncService.NONE -> emptyList()
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
} +
if (syncServiceType == SyncManager.SyncService.NONE) {
emptyList()
} else {
listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
}
}
@Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
return listOf(
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_device_name),
subtitle = stringResource(R.string.pref_sync_device_name_summ),
pref = syncPreferences.deviceName(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_host),
subtitle = stringResource(R.string.pref_sync_host_summ),
pref = syncPreferences.syncHost(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_api_key),
subtitle = stringResource(R.string.pref_sync_api_key_summ),
pref = syncPreferences.syncAPIKey(),
),
)
}
@Composable
private fun getSyncNowPref(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
if (showDialog) {
SyncConfirmationDialog(
onConfirm = {
showDialog = false
scope.launch {
if (!SyncDataJob.isAnyJobRunning(context)) {
SyncDataJob.startNow(context)
} else {
context.toast(R.string.sync_in_progress)
}
}
},
onDismissRequest = { showDialog = false },
)
}
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_now_group_title),
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_sync_now),
subtitle = stringResource(R.string.pref_sync_now_subtitle),
onClick = {
showDialog = true
},
),
),
)
}
@Composable
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val syncIntervalPref = syncPreferences.syncInterval()
val lastSync by syncPreferences.syncLastSync().collectAsState()
val formattedLastSync = DateUtils.getRelativeTimeSpanString(lastSync.toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS)
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_backup_service_category),
title = stringResource(R.string.pref_sync_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = backupIntervalPref,
title = stringResource(R.string.pref_backup_interval),
pref = syncIntervalPref,
title = stringResource(R.string.pref_sync_interval),
entries = mapOf(
0 to stringResource(R.string.off),
6 to stringResource(R.string.update_6hour),
12 to stringResource(R.string.update_12hour),
24 to stringResource(R.string.update_24hour),
48 to stringResource(R.string.update_48hour),
168 to stringResource(R.string.update_weekly),
30 to stringResource(R.string.update_30min),
60 to stringResource(R.string.update_1hour),
180 to stringResource(R.string.update_3hour),
360 to stringResource(R.string.update_6hour),
720 to stringResource(R.string.update_12hour),
1440 to stringResource(R.string.update_24hour),
2880 to stringResource(R.string.update_48hour),
10080 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
SyncDataJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_backup_directory),
enabled = backupInterval != 0,
subtitle = remember(backupDir) {
(UniFile.fromUri(context, backupDir.toUri())?.filePath)?.let {
"$it/automatic"
}
} ?: stringResource(R.string.invalid_location, backupDir),
onClick = {
try {
pickBackupLocation.launch(null)
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
}
},
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
enabled = backupInterval != 0,
title = stringResource(R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.last_synchronization, formattedLastSync)),
),
)
}
@Composable
fun SyncConfirmationDialog(
onConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_sync_confirmation_title)) },
text = { Text(text = stringResource(R.string.pref_sync_confirmation_message)) },
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
}
private data class MissingRestoreComponents(

View File

@ -223,17 +223,11 @@ object SettingsMainScreen : Screen() {
screen = SettingsBrowseScreen,
),
Item(
titleRes = R.string.label_backup,
subtitleRes = R.string.pref_backup_summary,
titleRes = R.string.label_backup_and_sync,
subtitleRes = R.string.pref_backup_and_sync_summary,
icon = Icons.Outlined.SettingsBackupRestore,
screen = SettingsBackupScreen,
),
Item(
titleRes = R.string.label_sync,
subtitleRes = R.string.pref_sync_summary,
icon = Icons.Outlined.Sync,
screen = SettingsSyncScreen,
),
Item(
titleRes = R.string.pref_category_security,
subtitleRes = R.string.pref_security_summary,

View File

@ -291,7 +291,6 @@ private val settingScreens = listOf(
SettingsTrackingScreen,
SettingsBrowseScreen,
SettingsBackupScreen,
SettingsSyncScreen,
SettingsSecurityScreen,
SettingsAdvancedScreen,
)

View File

@ -1,180 +0,0 @@
package eu.kanade.presentation.more.settings.screen
import android.text.format.DateUtils
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cloud
import androidx.compose.material.icons.outlined.Devices
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.VpnKey
import androidx.compose.material3.AlertDialog
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager.SyncService
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import tachiyomi.domain.sync.SyncPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object SettingsSyncScreen : SearchableSettings {
@ReadOnlyComposable
@Composable
@StringRes
override fun getTitleRes() = R.string.label_sync
@Composable
override fun getPreferences(): List<Preference> {
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
val syncService by syncPreferences.syncService().collectAsState()
return listOf(
Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(),
title = stringResource(R.string.pref_sync_service),
entries = mapOf(
SyncService.NONE.value to stringResource(R.string.off),
SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi),
),
onValueChanged = { true },
),
) + getSyncServicePreferences(syncPreferences, syncService)
}
@Composable
private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
return when (SyncService.fromInt(syncService)) {
SyncService.NONE -> emptyList()
SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
} + getSyncNowPref() + getAutomaticSyncGroup(syncPreferences)
}
@Composable
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
return listOf(
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_device_name),
subtitle = stringResource(R.string.pref_sync_device_name_summ),
icon = Icons.Outlined.Devices,
pref = syncPreferences.deviceName(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_host),
subtitle = stringResource(R.string.pref_sync_host_summ),
icon = Icons.Outlined.Cloud,
pref = syncPreferences.syncHost(),
),
Preference.PreferenceItem.EditTextPreference(
title = stringResource(R.string.pref_sync_api_key),
subtitle = stringResource(R.string.pref_sync_api_key_summ),
icon = Icons.Outlined.VpnKey,
pref = syncPreferences.syncAPIKey(),
),
)
}
@Composable
private fun getSyncNowPref(): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
if (showDialog) {
SyncConfirmationDialog(
onConfirm = {
showDialog = false
scope.launch {
if (!SyncDataJob.isAnyJobRunning(context)) {
SyncDataJob.startNow(context)
} else {
context.toast(R.string.sync_in_progress)
}
}
},
onDismissRequest = { showDialog = false },
)
}
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_now_group_title),
preferenceItems = listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_sync_now),
subtitle = stringResource(R.string.pref_sync_now_subtitle),
onClick = {
showDialog = true
},
icon = Icons.Outlined.Sync,
),
),
)
}
@Composable
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
val syncIntervalPref = syncPreferences.syncInterval()
val lastSync by syncPreferences.syncLastSync().collectAsState()
val formattedLastSync = DateUtils.getRelativeTimeSpanString(lastSync.toEpochMilli(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS)
return Preference.PreferenceGroup(
title = stringResource(R.string.pref_sync_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = syncIntervalPref,
title = stringResource(R.string.pref_sync_interval),
entries = mapOf(
0 to stringResource(R.string.off),
30 to stringResource(R.string.update_30min),
60 to stringResource(R.string.update_1hour),
180 to stringResource(R.string.update_3hour),
360 to stringResource(R.string.update_6hour),
720 to stringResource(R.string.update_12hour),
1440 to stringResource(R.string.update_24hour),
2880 to stringResource(R.string.update_48hour),
10080 to stringResource(R.string.update_weekly),
),
onValueChanged = {
SyncDataJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.last_synchronization, formattedLastSync)),
),
)
}
@Composable
fun SyncConfirmationDialog(
onConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_sync_confirmation_title)) },
text = { Text(text = stringResource(R.string.pref_sync_confirmation_message)) },
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = stringResource(android.R.string.ok))
}
},
)
}
}

View File

@ -72,7 +72,6 @@ object MoreTab : Tab {
onClickCategories = { navigator.push(CategoryScreen()) },
onClickStats = { navigator.push(StatsScreen()) },
onClickBackupAndRestore = { navigator.push(SettingsScreen.toBackupScreen()) },
onClickSync = { navigator.push(SettingsScreen.toSyncScreen()) },
onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
)

View File

@ -16,7 +16,6 @@ import eu.kanade.presentation.more.settings.screen.AboutScreen
import eu.kanade.presentation.more.settings.screen.SettingsAppearanceScreen
import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
import eu.kanade.presentation.more.settings.screen.SettingsSyncScreen
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.Screen
@ -25,7 +24,6 @@ import tachiyomi.presentation.core.components.TwoPanelBox
class SettingsScreen private constructor(
val toBackup: Boolean,
val toSync: Boolean,
val toAbout: Boolean,
) : Screen() {
@ -36,8 +34,6 @@ class SettingsScreen private constructor(
Navigator(
screen = if (toBackup) {
SettingsBackupScreen
} else if (toSync) {
SettingsSyncScreen
} else if (toAbout) {
AboutScreen
} else {
@ -60,8 +56,6 @@ class SettingsScreen private constructor(
Navigator(
screen = if (toBackup) {
SettingsBackupScreen
} else if (toSync) {
SettingsSyncScreen
} else if (toAbout) {
AboutScreen
} else {
@ -85,12 +79,10 @@ class SettingsScreen private constructor(
}
companion object {
fun toMainScreen() = SettingsScreen(toBackup = false, toSync = false, toAbout = false)
fun toMainScreen() = SettingsScreen(toBackup = false, toAbout = false)
fun toBackupScreen() = SettingsScreen(toBackup = true, toSync = false, toAbout = false)
fun toBackupScreen() = SettingsScreen(toBackup = true, toAbout = false)
fun toSyncScreen() = SettingsScreen(toBackup = false, toSync = true, toAbout = false)
fun toAboutScreen() = SettingsScreen(toBackup = false, toSync = false, toAbout = true)
fun toAboutScreen() = SettingsScreen(toBackup = false, toAbout = true)
}
}

View File

@ -22,7 +22,8 @@
<string name="label_recent_updates">Updates</string>
<string name="label_recent_manga">History</string>
<string name="label_sources">Sources</string>
<string name="label_backup">Backup and restore</string>
<string name="label_backup">Backup</string>
<string name="label_backup_and_sync">Backup and Sync</string>
<string name="label_sync">Sync</string>
<string name="label_stats">Statistics</string>
<string name="label_migration">Migrate</string>
@ -179,7 +180,7 @@
<string name="pref_downloads_summary">Automatic download, download ahead</string>
<string name="pref_tracking_summary">One-way progress sync, enhanced sync</string>
<string name="pref_browse_summary">Sources, extensions, global search</string>
<string name="pref_backup_summary">Manual &amp; automatic backups</string>
<string name="pref_backup_and_sync_summary">Manual &amp; automatic backups and sync</string>
<string name="pref_security_summary">App lock, secure screen</string>
<string name="pref_advanced_summary">Dump crash logs, battery optimizations</string>
@ -499,6 +500,7 @@
<string name="pref_restore_backup_summ">Restore library from backup file</string>
<string name="pref_backup_directory">Backup location</string>
<string name="pref_backup_service_category">Automatic backups</string>
<string name="pref_backup_manual_category">Manual backups</string>
<string name="pref_backup_interval">Backup frequency</string>
<string name="pref_backup_slots">Maximum backups</string>
<string name="backup_created">Backup created</string>
@ -537,7 +539,6 @@
<string name="pref_sync_host_summ">Enter the host address for synchronizing your library</string>
<string name="pref_sync_api_key">API key</string>
<string name="pref_sync_api_key_summ">Enter the API key to synchronize your library</string>
<string name="pref_sync_summary">Sync your library with a remote server</string>
<string name="pref_sync_now_group_title">Sync Actions</string>
<string name="pref_sync_now">Sync now</string>
<string name="pref_sync_confirmation_title">Sync confirmation</string>