Convert create backup dialog to a screen

Allows us more flexibility in adding more options/explanations in the future.
This commit is contained in:
arkon 2023-11-05 17:13:51 -05:00
parent 634ceeec50
commit 00b2853d3d
8 changed files with 209 additions and 174 deletions

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -13,11 +12,9 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -27,41 +24,34 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.permissions.PermissionRequestHelper import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrolledToStart
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -131,124 +121,11 @@ object SettingsDataScreen : SearchableSettings {
@Composable @Composable
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference { private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
val scope = rememberCoroutineScope() val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
var flag by rememberSaveable { mutableIntStateOf(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,
)
BackupCreateJob.startNow(context, it, flag)
}
flag = 0
}
var showCreateDialog by rememberSaveable { mutableStateOf(false) }
if (showCreateDialog) {
CreateBackupDialog(
onConfirm = {
showCreateDialog = false
flag = it
try {
chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
flag = 0
context.toast(R.string.file_picker_error)
}
},
onDismissRequest = { showCreateDialog = false },
)
}
return Preference.PreferenceItem.TextPreference( return Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_create_backup), title = stringResource(R.string.pref_create_backup),
subtitle = stringResource(R.string.pref_create_backup_summ), subtitle = stringResource(R.string.pref_create_backup_summ),
onClick = { onClick = { navigator.push(CreateBackupScreen()) },
scope.launch {
if (!BackupCreateJob.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 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,
BackupConst.BACKUP_APP_PREFS to R.string.app_settings,
BackupConst.BACKUP_SOURCE_PREFS to R.string.source_settings,
)
}
val flags = remember { choices.keys.toMutableStateList() }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.backup_choice)) },
text = {
Box {
val state = rememberLazyListState()
ScrollbarLazyColumn(state = state) {
item {
LabeledCheckbox(
label = stringResource(R.string.manga),
checked = true,
onCheckedChange = {},
)
}
choices.forEach { (k, v) ->
item {
val isSelected = flags.contains(k)
LabeledCheckbox(
label = stringResource(v),
checked = isSelected,
onCheckedChange = {
if (it) {
flags.add(k)
} else {
flags.remove(k)
}
},
)
}
}
}
if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(R.string.action_cancel))
}
},
confirmButton = {
TextButton(
onClick = {
val flag = flags.fold(initial = 0, operation = { a, b -> a or b })
onConfirm(flag)
},
) {
Text(text = stringResource(R.string.action_ok))
}
},
) )
} }
@ -336,7 +213,7 @@ object SettingsDataScreen : SearchableSettings {
}, },
) { ) {
if (it == null) { if (it == null) {
error = InvalidRestore(message = context.getString(R.string.file_null_uri_error)) context.toast(R.string.file_null_uri_error)
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
} }

View File

@ -0,0 +1,168 @@
package eu.kanade.presentation.more.settings.screen.data
import android.content.ActivityNotFoundException
import android.content.Context
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.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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 androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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 eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
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.flow.update
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
class CreateBackupScreen : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { CreateBackupScreenModel() }
val state by model.state.collectAsState()
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,
)
model.createBackup(context, it)
navigator.pop()
}
}
Scaffold(
topBar = {
AppBar(
title = stringResource(R.string.pref_create_backup),
navigateUp = navigator::pop,
scrollBehavior = it,
)
},
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
) {
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = MaterialTheme.padding.medium),
) {
item {
LabeledCheckbox(
label = stringResource(R.string.manga),
checked = true,
onCheckedChange = {},
enabled = false,
)
}
BackupChoices.forEach { (k, v) ->
item {
LabeledCheckbox(
label = stringResource(v),
checked = state.flags.contains(k),
onCheckedChange = {
model.toggleFlag(k)
},
)
}
}
}
HorizontalDivider()
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
onClick = {
if (!BackupCreateJob.isManualJobRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
try {
chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(R.string.file_picker_error)
}
} else {
context.toast(R.string.backup_in_progress)
}
},
) {
Text(
text = stringResource(R.string.action_create),
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
}
}
}
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
fun toggleFlag(flag: Int) {
mutableState.update {
if (it.flags.contains(flag)) {
it.copy(flags = it.flags - flag)
} else {
it.copy(flags = it.flags + flag)
}
}
}
fun createBackup(context: Context, uri: Uri) {
val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b })
BackupCreateJob.startNow(context, uri, flags)
}
@Immutable
data class State(
val flags: Set<Int> = BackupChoices.keys,
)
}
private val BackupChoices = mapOf(
BackupCreateFlags.BACKUP_CATEGORY to R.string.categories,
BackupCreateFlags.BACKUP_CHAPTER to R.string.chapters,
BackupCreateFlags.BACKUP_TRACK to R.string.track,
BackupCreateFlags.BACKUP_HISTORY to R.string.history,
BackupCreateFlags.BACKUP_APP_PREFS to R.string.app_settings,
BackupCreateFlags.BACKUP_SOURCE_PREFS to R.string.source_settings,
)

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.data.backup
// Filter options
internal object BackupConst {
const val BACKUP_CATEGORY = 0x1
const val BACKUP_CATEGORY_MASK = 0x1
const val BACKUP_CHAPTER = 0x2
const val BACKUP_CHAPTER_MASK = 0x2
const val BACKUP_HISTORY = 0x4
const val BACKUP_HISTORY_MASK = 0x4
const val BACKUP_TRACK = 0x8
const val BACKUP_TRACK_MASK = 0x8
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_APP_PREFS_MASK = 0x10
const val BACKUP_SOURCE_PREFS = 0x20
const val BACKUP_SOURCE_PREFS_MASK = 0x20
const val BACKUP_ALL = 0x3F
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.data.backup
internal object BackupCreateFlags {
const val BACKUP_CATEGORY = 0x1
const val BACKUP_CHAPTER = 0x2
const val BACKUP_HISTORY = 0x4
const val BACKUP_TRACK = 0x8
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_SOURCE_PREFS = 0x20
const val AutomaticDefaults = BACKUP_CATEGORY or
BACKUP_CHAPTER or
BACKUP_HISTORY or
BACKUP_TRACK or
BACKUP_APP_PREFS or
BACKUP_SOURCE_PREFS
}

View File

@ -41,7 +41,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: backupPreferences.backupsDirectory().get().toUri() ?: backupPreferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL) val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
try { try {
setForeground(getForegroundInfo()) setForeground(getForegroundInfo())

View File

@ -5,18 +5,12 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupChapter
@ -161,7 +155,7 @@ class BackupCreator(
*/ */
private suspend fun backupCategories(options: Int): List<BackupCategory> { private suspend fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup // Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { return if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
getCategories.await() getCategories.await()
.filterNot(Category::isSystemCategory) .filterNot(Category::isSystemCategory)
.map(backupCategoryMapper) .map(backupCategoryMapper)
@ -188,7 +182,7 @@ class BackupCreator(
val mangaObject = BackupManga.copyFrom(manga) val mangaObject = BackupManga.copyFrom(manga)
// Check if user wants chapter information in backup // Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { if (options and BACKUP_CHAPTER == BACKUP_CHAPTER) {
// Backup all the chapters // Backup all the chapters
handler.awaitList { handler.awaitList {
chaptersQueries.getChaptersByMangaId( chaptersQueries.getChaptersByMangaId(
@ -202,7 +196,7 @@ class BackupCreator(
} }
// Check if user wants category information in backup // Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
// Backup categories for this manga // Backup categories for this manga
val categoriesForManga = getCategories.await(manga.id) val categoriesForManga = getCategories.await(manga.id)
if (categoriesForManga.isNotEmpty()) { if (categoriesForManga.isNotEmpty()) {
@ -211,7 +205,7 @@ class BackupCreator(
} }
// Check if user wants track information in backup // Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { if (options and BACKUP_TRACK == BACKUP_TRACK) {
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) } val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
if (tracks.isNotEmpty()) { if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks mangaObject.tracking = tracks
@ -219,7 +213,7 @@ class BackupCreator(
} }
// Check if user wants history information in backup // Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { if (options and BACKUP_HISTORY == BACKUP_HISTORY) {
val historyByMangaId = getHistory.await(manga.id) val historyByMangaId = getHistory.await(manga.id)
if (historyByMangaId.isNotEmpty()) { if (historyByMangaId.isNotEmpty()) {
val history = historyByMangaId.map { history -> val history = historyByMangaId.map { history ->
@ -236,13 +230,13 @@ class BackupCreator(
} }
private fun backupAppPreferences(flags: Int): List<BackupPreference> { private fun backupAppPreferences(flags: Int): List<BackupPreference> {
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList() if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
return preferenceStore.getAll().toBackupPreferences() return preferenceStore.getAll().toBackupPreferences()
} }
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> { private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList() if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
return sourceManager.getCatalogueSources() return sourceManager.getCatalogueSources()
.filterIsInstance<ConfigurableSource>() .filterIsInstance<ConfigurableSource>()

View File

@ -484,6 +484,7 @@
<string name="pref_backup_directory">Backup location</string> <string name="pref_backup_directory">Backup location</string>
<string name="pref_backup_interval">Automatic backup frequency</string> <string name="pref_backup_interval">Automatic backup frequency</string>
<string name="pref_backup_slots">Maximum automatic backups</string> <string name="pref_backup_slots">Maximum automatic backups</string>
<string name="action_create">Create</string>
<string name="backup_created">Backup created</string> <string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</string> <string name="invalid_backup_file">Invalid backup file</string>
<string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</string> <string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</string>
@ -880,7 +881,7 @@
<string name="file_select_cover">Select cover image</string> <string name="file_select_cover">Select cover image</string>
<string name="file_select_backup">Select backup file</string> <string name="file_select_backup">Select backup file</string>
<string name="file_picker_error">No file picker app found</string> <string name="file_picker_error">No file picker app found</string>
<string name="file_null_uri_error">File picker failed to return file to app</string> <string name="file_null_uri_error">No file selected</string>
<!--UpdateCheck--> <!--UpdateCheck-->
<string name="update_check_confirm">Download</string> <string name="update_check_confirm">Download</string>

View File

@ -21,6 +21,7 @@ fun LabeledCheckbox(
label: String, label: String,
checked: Boolean, checked: Boolean,
onCheckedChange: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
) { ) {
Row( Row(
modifier = modifier modifier = modifier
@ -37,6 +38,7 @@ fun LabeledCheckbox(
Checkbox( Checkbox(
checked = checked, checked = checked,
onCheckedChange = null, onCheckedChange = null,
enabled = enabled,
) )
Text(text = label) Text(text = label)