Restore deleted

This commit is contained in:
2026-09-10 01:29:37 +02:00
parent 98b7bcaaf7
commit b7037d8074
4 changed files with 143 additions and 2 deletions
@@ -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<DBAccountProject>) {
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) }
)
}
}
@@ -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<DBAccountProject>) -> 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<DBAccountProject>()) }
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<DBAccountProject>,
onDismiss: () -> Unit,
onConfirm: (List<DBAccountProject>) -> Unit
) {
val selected = remember(projects) { mutableStateListOf<DBAccountProject>().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(
@@ -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<DBAccountProject> {
val forgotten = forgottenAccountProjects(preferences)
if (forgotten.isEmpty()) return emptyList()
return dbHelper.accountProjects.filter { accountProjectKey(it.remoteId, it.ncUrl) in forgotten }
}
fun restoreAccountProjects(projects: List<DBAccountProject>) {
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()
+8
View File
@@ -137,6 +137,9 @@
<string name="settings_night_mode">Theme</string>
<string name="settings_offline_mode">Offline mode</string>
<string name="settings_offline_mode_summary">Only sync manually.</string>
<string name="settings_restore_deleted_projects">Restore deleted projects</string>
<string name="settings_restore_deleted_projects_done">Restoring from the account</string>
<string name="settings_restore_deleted_projects_confirm">Restore</string>
<string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string>
@@ -291,4 +294,9 @@
<string name="share_intent_title">Project %1$s</string>
<string name="share_chooser_title">Share %1$s</string>
<plurals name="settings_restore_deleted_projects_summary">
<item quantity="one">%d project the account offers is kept off this device.</item>
<item quantity="other">%d projects the account offers are kept off this device.</item>
</plurals>
</resources>