2022-10-15 17:38:01 +02:00
|
|
|
package eu.kanade.presentation.more.settings.screen
|
|
|
|
|
2022-10-20 23:48:13 +02:00
|
|
|
import android.content.ActivityNotFoundException
|
|
|
|
import android.content.Context
|
2022-10-15 17:38:01 +02:00
|
|
|
import android.content.Intent
|
|
|
|
import android.net.Uri
|
|
|
|
import android.widget.Toast
|
|
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
2022-10-15 17:58:24 +02:00
|
|
|
import androidx.annotation.StringRes
|
2022-10-15 17:38:01 +02:00
|
|
|
import androidx.compose.foundation.clickable
|
2022-10-18 15:35:10 +02:00
|
|
|
import androidx.compose.foundation.layout.Box
|
2022-10-15 17:38:01 +02:00
|
|
|
import androidx.compose.foundation.layout.Row
|
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
|
import androidx.compose.foundation.layout.heightIn
|
|
|
|
import androidx.compose.foundation.layout.padding
|
2022-10-18 15:35:10 +02:00
|
|
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
2022-10-15 17:38:01 +02:00
|
|
|
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.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.saveable.rememberSaveable
|
|
|
|
import androidx.compose.runtime.setValue
|
2022-10-16 18:52:34 +02:00
|
|
|
import androidx.compose.runtime.toMutableStateList
|
2022-10-15 17:38:01 +02:00
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
import androidx.compose.ui.platform.LocalContext
|
|
|
|
import androidx.compose.ui.res.stringResource
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
import androidx.core.net.toUri
|
|
|
|
import com.hippo.unifile.UniFile
|
2023-02-26 22:16:49 +01:00
|
|
|
import eu.kanade.presentation.extensions.RequestStoragePermission
|
2022-10-15 17:38:01 +02:00
|
|
|
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
|
2022-11-24 04:28:25 +01:00
|
|
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
2022-10-15 17:38:01 +02:00
|
|
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
2022-11-05 16:56:31 +01:00
|
|
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
2022-10-15 17:38:01 +02:00
|
|
|
import eu.kanade.tachiyomi.util.system.toast
|
|
|
|
import kotlinx.coroutines.launch
|
2023-03-05 18:11:47 +01:00
|
|
|
import tachiyomi.domain.service.BackupPreferences
|
2023-02-18 22:33:03 +01:00
|
|
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
2023-02-18 21:52:52 +01:00
|
|
|
import tachiyomi.presentation.core.components.material.Divider
|
2023-02-18 22:33:03 +01:00
|
|
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
|
|
|
import tachiyomi.presentation.core.util.isScrolledToStart
|
2022-10-15 17:38:01 +02:00
|
|
|
import uy.kohesive.injekt.Injekt
|
|
|
|
import uy.kohesive.injekt.api.get
|
|
|
|
|
2022-11-23 15:14:55 +01:00
|
|
|
object SettingsBackupScreen : SearchableSettings {
|
2022-10-15 17:58:24 +02:00
|
|
|
|
2022-10-15 17:38:01 +02:00
|
|
|
@ReadOnlyComposable
|
|
|
|
@Composable
|
2022-10-15 17:58:24 +02:00
|
|
|
@StringRes
|
|
|
|
override fun getTitleRes() = R.string.label_backup
|
2022-10-15 17:38:01 +02:00
|
|
|
|
|
|
|
@Composable
|
|
|
|
override fun getPreferences(): List<Preference> {
|
|
|
|
val backupPreferences = Injekt.get<BackupPreferences>()
|
|
|
|
|
2022-11-24 04:28:25 +01:00
|
|
|
DiskUtil.RequestStoragePermission()
|
2022-10-15 17:38:01 +02:00
|
|
|
|
|
|
|
return listOf(
|
|
|
|
getCreateBackupPref(),
|
|
|
|
getRestoreBackupPref(),
|
|
|
|
getAutomaticBackupGroup(backupPreferences = backupPreferences),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@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
|
2022-10-20 23:48:13 +02:00
|
|
|
try {
|
|
|
|
chooseBackupDir.launch(Backup.getBackupFilename())
|
|
|
|
} catch (e: ActivityNotFoundException) {
|
|
|
|
flag = 0
|
|
|
|
context.toast(R.string.file_picker_error)
|
|
|
|
}
|
2022-10-15 17:38:01 +02:00
|
|
|
},
|
|
|
|
onDismissRequest = { showCreateDialog = false },
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return Preference.PreferenceItem.TextPreference(
|
2022-10-15 17:58:24 +02:00
|
|
|
title = stringResource(R.string.pref_create_backup),
|
|
|
|
subtitle = stringResource(R.string.pref_create_backup_summ),
|
2022-10-15 17:38:01 +02:00
|
|
|
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,
|
|
|
|
) {
|
2022-10-16 18:52:34 +02:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
val flags = remember { choices.keys.toMutableStateList() }
|
2022-10-15 17:38:01 +02:00
|
|
|
AlertDialog(
|
|
|
|
onDismissRequest = onDismissRequest,
|
2022-10-15 17:58:24 +02:00
|
|
|
title = { Text(text = stringResource(R.string.backup_choice)) },
|
2022-10-15 17:38:01 +02:00
|
|
|
text = {
|
2022-10-18 15:35:10 +02:00
|
|
|
Box {
|
|
|
|
val state = rememberLazyListState()
|
|
|
|
ScrollbarLazyColumn(state = state) {
|
|
|
|
item {
|
|
|
|
CreateBackupDialogItem(
|
|
|
|
isSelected = true,
|
|
|
|
title = stringResource(R.string.manga),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
choices.forEach { (k, v) ->
|
|
|
|
item {
|
|
|
|
val isSelected = flags.contains(k)
|
|
|
|
CreateBackupDialogItem(
|
|
|
|
isSelected = isSelected,
|
|
|
|
title = stringResource(v),
|
|
|
|
modifier = Modifier.clickable {
|
|
|
|
if (isSelected) {
|
|
|
|
flags.remove(k)
|
|
|
|
} else {
|
|
|
|
flags.add(k)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
2022-10-18 15:35:10 +02:00
|
|
|
if (!state.isScrolledToStart()) Divider(modifier = Modifier.align(Alignment.TopCenter))
|
|
|
|
if (!state.isScrolledToEnd()) Divider(modifier = Modifier.align(Alignment.BottomCenter))
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
dismissButton = {
|
|
|
|
TextButton(onClick = onDismissRequest) {
|
2022-10-29 17:43:51 +02:00
|
|
|
Text(text = stringResource(R.string.action_cancel))
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
confirmButton = {
|
|
|
|
TextButton(
|
|
|
|
onClick = {
|
|
|
|
val flag = flags.fold(initial = 0, operation = { a, b -> a or b })
|
|
|
|
onConfirm(flag)
|
|
|
|
},
|
|
|
|
) {
|
2022-10-15 17:58:24 +02:00
|
|
|
Text(text = stringResource(android.R.string.ok))
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@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 -> {
|
|
|
|
AlertDialog(
|
|
|
|
onDismissRequest = onDismissRequest,
|
2022-10-15 17:58:24 +02:00
|
|
|
title = { Text(text = stringResource(R.string.invalid_backup_file)) },
|
2022-10-28 00:02:53 +02:00
|
|
|
text = { Text(text = "${err.uri}\n\n${err.message}") },
|
2022-10-15 17:38:01 +02:00
|
|
|
dismissButton = {
|
|
|
|
TextButton(
|
|
|
|
onClick = {
|
2022-11-05 16:56:31 +01:00
|
|
|
context.copyToClipboard(err.message, err.message)
|
2022-10-15 17:38:01 +02:00
|
|
|
onDismissRequest()
|
|
|
|
},
|
|
|
|
) {
|
2022-10-29 17:43:51 +02:00
|
|
|
Text(text = stringResource(android.R.string.copy))
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
confirmButton = {
|
|
|
|
TextButton(onClick = onDismissRequest) {
|
2022-10-15 17:58:24 +02:00
|
|
|
Text(text = stringResource(android.R.string.ok))
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
is MissingRestoreComponents -> {
|
|
|
|
AlertDialog(
|
|
|
|
onDismissRequest = onDismissRequest,
|
2022-10-15 17:58:24 +02:00
|
|
|
title = { Text(text = stringResource(R.string.pref_restore_backup)) },
|
2022-10-15 17:38:01 +02:00
|
|
|
text = {
|
2022-10-20 23:48:13 +02:00
|
|
|
val msg = buildString {
|
|
|
|
append(stringResource(R.string.backup_restore_content_full))
|
|
|
|
if (err.sources.isNotEmpty()) {
|
|
|
|
append("\n\n").append(stringResource(R.string.backup_restore_missing_sources))
|
|
|
|
err.sources.joinTo(this, separator = "\n- ", prefix = "\n- ")
|
|
|
|
}
|
|
|
|
if (err.trackers.isNotEmpty()) {
|
|
|
|
append("\n\n").append(stringResource(R.string.backup_restore_missing_trackers))
|
|
|
|
err.trackers.joinTo(this, separator = "\n- ", prefix = "\n- ")
|
|
|
|
}
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
Text(text = msg)
|
|
|
|
},
|
|
|
|
confirmButton = {
|
|
|
|
TextButton(
|
|
|
|
onClick = {
|
|
|
|
BackupRestoreService.start(context, err.uri)
|
|
|
|
onDismissRequest()
|
|
|
|
},
|
|
|
|
) {
|
2022-10-15 17:58:24 +02:00
|
|
|
Text(text = stringResource(R.string.action_restore))
|
2022-10-15 17:38:01 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
else -> error = null // Unknown
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-20 23:48:13 +02:00
|
|
|
val chooseBackup = rememberLauncherForActivityResult(
|
|
|
|
object : ActivityResultContracts.GetContent() {
|
|
|
|
override fun createIntent(context: Context, input: String): Intent {
|
|
|
|
val intent = super.createIntent(context, input)
|
|
|
|
return Intent.createChooser(intent, context.getString(R.string.file_select_backup))
|
|
|
|
}
|
|
|
|
},
|
|
|
|
) {
|
2022-10-15 17:38:01 +02:00
|
|
|
if (it != null) {
|
|
|
|
val results = try {
|
|
|
|
BackupFileValidator().validate(context, it)
|
|
|
|
} catch (e: Exception) {
|
2022-10-28 00:02:53 +02:00
|
|
|
error = InvalidRestore(it, e.message.toString())
|
2022-10-15 17:38:01 +02:00
|
|
|
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(
|
2022-10-15 17:58:24 +02:00
|
|
|
title = stringResource(R.string.pref_restore_backup),
|
|
|
|
subtitle = stringResource(R.string.pref_restore_backup_summ),
|
2022-10-15 17:38:01 +02:00
|
|
|
onClick = {
|
|
|
|
if (!BackupRestoreService.isRunning(context)) {
|
|
|
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
|
|
|
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
|
|
|
|
}
|
2022-10-20 23:48:13 +02:00
|
|
|
// no need to catch because it's wrapped with a chooser
|
2022-10-15 17:38:01 +02:00
|
|
|
chooseBackup.launch("*/*")
|
|
|
|
} else {
|
|
|
|
context.toast(R.string.restore_in_progress)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
fun getAutomaticBackupGroup(
|
|
|
|
backupPreferences: BackupPreferences,
|
|
|
|
): Preference.PreferenceGroup {
|
|
|
|
val context = LocalContext.current
|
2022-10-21 04:56:27 +02:00
|
|
|
val backupIntervalPref = backupPreferences.backupInterval()
|
|
|
|
val backupInterval by backupIntervalPref.collectAsState()
|
2022-10-15 17:38:01 +02:00
|
|
|
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(
|
2022-10-15 17:58:24 +02:00
|
|
|
title = stringResource(R.string.pref_backup_service_category),
|
2022-10-15 17:38:01 +02:00
|
|
|
preferenceItems = listOf(
|
|
|
|
Preference.PreferenceItem.ListPreference(
|
2022-10-21 04:56:27 +02:00
|
|
|
pref = backupIntervalPref,
|
2022-10-15 17:58:24 +02:00
|
|
|
title = stringResource(R.string.pref_backup_interval),
|
2022-10-15 17:38:01 +02:00
|
|
|
entries = mapOf(
|
2022-10-21 04:56:27 +02:00
|
|
|
0 to stringResource(R.string.off),
|
2022-10-15 17:58:24 +02:00
|
|
|
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),
|
2022-10-15 17:38:01 +02:00
|
|
|
),
|
|
|
|
onValueChanged = {
|
|
|
|
BackupCreatorJob.setupTask(context, it)
|
|
|
|
true
|
|
|
|
},
|
|
|
|
),
|
|
|
|
Preference.PreferenceItem.TextPreference(
|
2022-10-15 17:58:24 +02:00
|
|
|
title = stringResource(R.string.pref_backup_directory),
|
2022-10-21 04:56:27 +02:00
|
|
|
enabled = backupInterval != 0,
|
2022-10-15 17:38:01 +02:00
|
|
|
subtitle = remember(backupDir) {
|
2022-10-21 04:34:27 +02:00
|
|
|
(UniFile.fromUri(context, backupDir.toUri())?.filePath)?.let {
|
|
|
|
"$it/automatic"
|
|
|
|
}
|
|
|
|
} ?: stringResource(R.string.invalid_location, backupDir),
|
2022-10-20 23:48:13 +02:00
|
|
|
onClick = {
|
|
|
|
try {
|
|
|
|
pickBackupLocation.launch(null)
|
|
|
|
} catch (e: ActivityNotFoundException) {
|
|
|
|
context.toast(R.string.file_picker_error)
|
|
|
|
}
|
|
|
|
},
|
2022-10-15 17:38:01 +02:00
|
|
|
),
|
|
|
|
Preference.PreferenceItem.ListPreference(
|
|
|
|
pref = backupPreferences.numberOfBackups(),
|
2022-10-21 04:56:27 +02:00
|
|
|
enabled = backupInterval != 0,
|
2022-10-15 17:58:24 +02:00
|
|
|
title = stringResource(R.string.pref_backup_slots),
|
2022-10-15 17:38:01 +02:00
|
|
|
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
|
|
|
|
),
|
2022-10-16 21:32:48 +02:00
|
|
|
Preference.PreferenceItem.InfoPreference(stringResource(R.string.backup_info)),
|
2022-10-15 17:38:01 +02:00
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private data class MissingRestoreComponents(
|
|
|
|
val uri: Uri,
|
|
|
|
val sources: List<String>,
|
|
|
|
val trackers: List<String>,
|
|
|
|
)
|
|
|
|
|
|
|
|
data class InvalidRestore(
|
2022-10-28 00:02:53 +02:00
|
|
|
val uri: Uri,
|
2022-10-15 17:38:01 +02:00
|
|
|
val message: String,
|
|
|
|
)
|