diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt index 40dffef..c7ac8a8 100644 --- a/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt @@ -16,8 +16,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.app.NavUtils import androidx.preference.PreferenceManager +import net.helcel.cowspent.R import net.helcel.cowspent.android.about.AboutActivity import net.helcel.cowspent.android.account.AccountActivity +import net.helcel.cowspent.android.helper.showToast +import net.helcel.cowspent.model.DBAccountProject +import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper +import net.helcel.cowspent.persistence.CowspentServerSyncHelper import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.util.ColorUtils @@ -34,6 +39,17 @@ class PreferencesActivity : AppCompatActivity() { } } + private fun restoreDeletedProjects(projects: List) { + if (projects.isEmpty()) return + val syncHelper = CowspentSQLiteOpenHelper.getInstance(applicationContext).cowspentServerSyncHelper + syncHelper.restoreAccountProjects(projects) + if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) { + syncHelper.runAccountProjectsSync() + } + setResult(RESULT_OK) + showToast(this, getString(R.string.settings_restore_deleted_projects_done)) + } + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -74,7 +90,8 @@ class PreferencesActivity : AppCompatActivity() { startActivity(Intent(this, AboutActivity::class.java)) }, onColorSelected = { appColor = it }, - onNightModeChanged = { nightMode = it } + onNightModeChanged = { nightMode = it }, + onRestoreDeletedProjects = { restoreDeletedProjects(it) } ) } } diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt index 9e7aa73..6d75c03 100644 --- a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.RadioButton import androidx.compose.material.Scaffold import androidx.compose.material.Slider +import androidx.compose.material.Checkbox import androidx.compose.material.Switch import androidx.compose.material.SwitchDefaults import androidx.compose.material.Text @@ -34,11 +35,14 @@ import androidx.compose.material.icons.filled.Brightness2 import androidx.compose.material.icons.filled.Group import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.filled.Restore import androidx.compose.material.icons.filled.Sync import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -47,6 +51,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -56,6 +61,9 @@ import androidx.core.content.edit import androidx.preference.PreferenceManager import net.helcel.cowspent.R import net.helcel.cowspent.android.helper.ColorPicker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.helcel.cowspent.model.DBAccountProject import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentServerSyncHelper import net.helcel.cowspent.util.ColorUtils @@ -68,7 +76,8 @@ fun SettingsScreen( onAccountSettingsClick: () -> Unit, onAboutClick: () -> Unit, onColorSelected: (Int) -> Unit, - onNightModeChanged: (String) -> Unit = {} + onNightModeChanged: (String) -> Unit = {}, + onRestoreDeletedProjects: (List) -> Unit = {} ) { val context = LocalContext.current val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } @@ -92,6 +101,16 @@ fun SettingsScreen( val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context) + var deletedProjects by remember { mutableStateOf(emptyList()) } + var showRestoreDialog by remember { mutableStateOf(false) } + + // Off the main thread: this reads the account projects table. + LaunchedEffect(Unit) { + deletedProjects = withContext(Dispatchers.IO) { + CowspentSQLiteOpenHelper.getInstance(context).cowspentServerSyncHelper.deletedAccountProjects() + } + } + // States for preferences var nightMode by remember(keyNightMode) { mutableStateOf(sharedPreferences.getString(keyNightMode, "-1") ?: "-1") @@ -257,6 +276,32 @@ fun SettingsScreen( onClick = onAccountSettingsClick ) + // Nothing held back means nothing to undo, so the row is not offered at all. + if (deletedProjects.isNotEmpty()) { + SettingsPreference( + title = stringResource(R.string.settings_restore_deleted_projects), + summary = pluralStringResource( + R.plurals.settings_restore_deleted_projects_summary, + deletedProjects.size, + deletedProjects.size + ), + icon = Icons.Default.Restore, + onClick = { showRestoreDialog = true } + ) + } + + if (showRestoreDialog) { + RestoreDeletedProjectsDialog( + projects = deletedProjects, + onDismiss = { showRestoreDialog = false }, + onConfirm = { chosen -> + showRestoreDialog = false + onRestoreDeletedProjects(chosen) + deletedProjects = deletedProjects - chosen.toSet() + } + ) + } + // Other SettingsCategory(stringResource(R.string.settings_other)) @@ -349,6 +394,60 @@ fun SettingsScreen( } } +@Composable +fun RestoreDeletedProjectsDialog( + projects: List, + onDismiss: () -> Unit, + onConfirm: (List) -> Unit +) { + val selected = remember(projects) { mutableStateListOf().apply { addAll(projects) } } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.settings_restore_deleted_projects)) }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + projects.forEach { project -> + val isSelected = project in selected + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { + if (isSelected) selected.remove(project) else selected.add(project) + } + ) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = isSelected, onCheckedChange = null) + Spacer(Modifier.width(16.dp)) + Column { + Text( + text = project.name.ifEmpty { project.remoteId }, + style = MaterialTheme.typography.subtitle1 + ) + Text(text = project.ncUrl, style = MaterialTheme.typography.caption) + } + } + } + } + }, + confirmButton = { + TextButton( + onClick = { onConfirm(selected.toList()) }, + enabled = selected.isNotEmpty() + ) { + Text(stringResource(R.string.settings_restore_deleted_projects_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.simple_cancel)) } + } + ) +} + @Composable fun SettingsCategory(title: String) { Text( diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt index d354ed4..b53218a 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -6,6 +6,7 @@ import android.content.SharedPreferences import android.net.ConnectivityManager import android.util.Log import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread import androidx.core.content.edit import androidx.core.graphics.toColorInt import androidx.preference.PreferenceManager @@ -27,6 +28,7 @@ import net.helcel.cowspent.R import net.helcel.cowspent.android.account.AccountActivity import net.helcel.cowspent.android.main.BillsListViewActivity import net.helcel.cowspent.android.main.MainConstants +import net.helcel.cowspent.model.DBAccountProject import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBProject @@ -1475,6 +1477,21 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen } } + @WorkerThread + fun deletedAccountProjects(): List { + val forgotten = forgottenAccountProjects(preferences) + if (forgotten.isEmpty()) return emptyList() + return dbHelper.accountProjects.filter { accountProjectKey(it.remoteId, it.ncUrl) in forgotten } + } + + fun restoreAccountProjects(projects: List) { + if (projects.isEmpty()) return + val restored = projects.map { accountProjectKey(it.remoteId, it.ncUrl) }.toSet() + preferences.edit { + putStringSet(FORGOTTEN_ACCOUNT_PROJECTS, forgottenAccountProjects(preferences) - restored) + } + } + fun runAccountProjectsSync() { Log.d(TAG, "Account projects sync requested; ${if (syncAccountProjectsActive) "sync active" else "sync NOT active"}) ...") updateNetworkStatus() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3fa089c..17cddf8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -137,6 +137,9 @@ Theme Offline mode Only sync manually. + Restore deleted projects + Restoring from the account + Restore Custom color Color Selection Show archived projects @@ -291,4 +294,9 @@ Project %1$s Share %1$s + + + %d project the account offers is kept off this device. + %d projects the account offers are kept off this device. +