tachiyomi/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt

371 lines
15 KiB
Kotlin
Raw Normal View History

package eu.kanade.presentation.more.settings.screen
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.google.accompanist.permissions.rememberPermissionState
import com.hippo.unifile.UniFile
import eu.kanade.domain.backup.service.BackupPreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SettingsBackupScreen : SearchableSettings {
@ReadOnlyComposable
@Composable
override fun getTitle(): String = stringResource(id = R.string.label_backup)
@Composable
override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
RequestStoragePermission()
return listOf(
getCreateBackupPref(),
getRestoreBackupPref(),
getAutomaticBackupGroup(backupPreferences = backupPreferences),
)
}
@Composable
private fun RequestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
@Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope()
val context = LocalContext.current
var flag by rememberSaveable { mutableStateOf(0) }
val chooseBackupDir = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/*"),
) {
if (it != null) {
context.contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
)
BackupCreatorJob.startNow(context, it, flag)
}
flag = 0
}
var showCreateDialog by rememberSaveable { mutableStateOf(false) }
if (showCreateDialog) {
CreateBackupDialog(
onConfirm = {
showCreateDialog = false
flag = it
chooseBackupDir.launch(Backup.getBackupFilename())
},
onDismissRequest = { showCreateDialog = false },
)
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(id = R.string.pref_create_backup),
subtitle = stringResource(id = R.string.pref_create_backup_summ),
onClick = {
scope.launch {
if (!BackupCreatorJob.isManualJobRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
showCreateDialog = true
} else {
context.toast(R.string.backup_in_progress)
}
}
},
)
}
@Composable
private fun CreateBackupDialog(
onConfirm: (flag: Int) -> Unit,
onDismissRequest: () -> Unit,
) {
val flags = remember { mutableStateListOf<Int>() }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(id = R.string.backup_choice)) },
text = {
val choices = remember {
mapOf(
BackupConst.BACKUP_CATEGORY to R.string.categories,
BackupConst.BACKUP_CHAPTER to R.string.chapters,
BackupConst.BACKUP_TRACK to R.string.track,
BackupConst.BACKUP_HISTORY to R.string.history,
)
}
Column {
CreateBackupDialogItem(
isSelected = true,
title = stringResource(id = R.string.manga),
)
choices.forEach { (k, v) ->
val isSelected = flags.contains(k)
CreateBackupDialogItem(
isSelected = isSelected,
title = stringResource(id = v),
modifier = Modifier.clickable {
if (isSelected) {
flags.remove(k)
} else {
flags.add(k)
}
},
)
}
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
val flag = flags.fold(initial = 0, operation = { a, b -> a or b })
onConfirm(flag)
},
) {
Text(text = stringResource(id = android.R.string.ok))
}
},
)
}
@Composable
private fun CreateBackupDialogItem(
modifier: Modifier = Modifier,
isSelected: Boolean,
title: String,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.fillMaxWidth(),
) {
Checkbox(
modifier = Modifier.heightIn(min = 48.dp),
checked = isSelected,
onCheckedChange = null,
)
Text(
text = title,
style = MaterialTheme.typography.bodyMedium.merge(),
modifier = Modifier.padding(start = 24.dp),
)
}
}
@Composable
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
val context = LocalContext.current
var error by remember { mutableStateOf<Any?>(null) }
if (error != null) {
val onDismissRequest = { error = null }
when (val err = error) {
is InvalidRestore -> {
val clipboard = LocalClipboardManager.current
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(id = R.string.invalid_backup_file)) },
text = { Text(text = err.message) },
dismissButton = {
TextButton(
onClick = {
clipboard.setText(AnnotatedString(err.message))
context.toast(R.string.copied_to_clipboard)
onDismissRequest()
},
) {
Text(text = stringResource(id = R.string.copy))
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(id = android.R.string.ok))
}
},
)
}
is MissingRestoreComponents -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(id = R.string.pref_restore_backup)) },
text = {
var msg = stringResource(id = R.string.backup_restore_content_full)
if (err.sources.isNotEmpty()) {
msg += "\n\n${stringResource(R.string.backup_restore_missing_sources)}\n${err.sources.joinToString("\n") { "- $it" }}"
}
if (err.sources.isNotEmpty()) {
msg += "\n\n${stringResource(R.string.backup_restore_missing_trackers)}\n${err.trackers.joinToString("\n") { "- $it" }}"
}
Text(text = msg)
},
confirmButton = {
TextButton(
onClick = {
BackupRestoreService.start(context, err.uri)
onDismissRequest()
},
) {
Text(text = stringResource(id = R.string.action_restore))
}
},
)
}
else -> error = null // Unknown
}
}
val chooseBackup = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
if (it != null) {
val results = try {
BackupFileValidator().validate(context, it)
} catch (e: Exception) {
error = InvalidRestore(e.message.toString())
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreService.start(context, it)
return@rememberLauncherForActivityResult
}
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
}
}
return Preference.PreferenceItem.TextPreference(
title = stringResource(id = R.string.pref_restore_backup),
subtitle = stringResource(id = R.string.pref_restore_backup_summ),
onClick = {
if (!BackupRestoreService.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
chooseBackup.launch("*/*")
} else {
context.toast(R.string.restore_in_progress)
}
},
)
}
@Composable
fun getAutomaticBackupGroup(
backupPreferences: BackupPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
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(id = R.string.pref_backup_service_category),
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(),
title = stringResource(id = R.string.pref_backup_interval),
entries = mapOf(
6 to stringResource(id = R.string.update_6hour),
12 to stringResource(id = R.string.update_12hour),
24 to stringResource(id = R.string.update_24hour),
48 to stringResource(id = R.string.update_48hour),
168 to stringResource(id = R.string.update_weekly),
),
onValueChanged = {
BackupCreatorJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.TextPreference(
title = stringResource(id = R.string.pref_backup_directory),
subtitle = remember(backupDir) {
UniFile.fromUri(context, backupDir.toUri()).filePath!! + "/automatic"
},
onClick = { pickBackupLocation.launch(null) },
),
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.numberOfBackups(),
title = stringResource(id = R.string.pref_backup_slots),
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
),
Preference.infoPreference(stringResource(id = R.string.backup_info)),
),
)
}
}
private data class MissingRestoreComponents(
val uri: Uri,
val sources: List<String>,
val trackers: List<String>,
)
data class InvalidRestore(
val message: String,
)