diff --git a/FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt b/FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt index d5f4592fa..5083041cb 100644 --- a/FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt +++ b/FloconAndroid/datastores-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt @@ -24,6 +24,10 @@ class FloconDatastorePreference( // no op } + override suspend fun remove(columnName: String) { + // no op + } + override suspend fun columns(): List { return emptyList() // no op } diff --git a/FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt b/FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt index efb24ef9a..80f8a671f 100644 --- a/FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt +++ b/FloconAndroid/datastores/src/commonMain/kotlin/io/github/openflocon/flocon/preferences/datastores/model/FloconDatastorePreference.kt @@ -58,6 +58,17 @@ class FloconDatastorePreference( } } + override suspend fun remove(columnName: String) { + try { + dataStore.edit { preferences -> + val key = preferences.asMap().keys.find { it.name == columnName } ?: return@edit + preferences.remove(key) + } + } catch (t: Throwable) { + FloconLogger.logError(t.message ?: "cannot remove datastore preference", t) + } + } + override suspend fun columns(): List { return try { dataStore.data.first().asMap().map { it.key.name } diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Protocol.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Protocol.kt index 151444d7e..1e4edfab4 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Protocol.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/Protocol.kt @@ -201,6 +201,7 @@ object Protocol { const val GetSharedPreferences = "get_shared_preferences" const val GetSharedPreferenceValue = "get_shared_preference_value" const val SetSharedPreferenceValue = "set_shared_preference_value" + const val RemoveSharedPreferenceValue = "remove_shared_preference_value" } } diff --git a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/preferences/SamplePreferences.kt b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/preferences/SamplePreferences.kt index 2a26e1760..a6b004140 100644 --- a/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/preferences/SamplePreferences.kt +++ b/FloconAndroid/sample-multiplatform/src/commonMain/kotlin/io/github/openflocon/flocon/myapplication/multi/preferences/SamplePreferences.kt @@ -20,12 +20,23 @@ object SamplePreferences : FloconPreference { private val values = mutableMapOf() + // default keys deleted from the desktop app, until they are set again + private val removed = mutableSetOf() + override suspend fun set(columnName: String, value: FloconPreferenceValue) { if (columnName == COUNTER) value.intValue?.let { counter = it } values[columnName] = value + removed -= columnName + } + + override suspend fun remove(columnName: String) { + if (columnName == COUNTER) counter = 0 + values -= columnName + removed += columnName } - override suspend fun columns(): List = (listOf(COUNTER, "username") + values.keys).distinct() + override suspend fun columns(): List = + (listOf(COUNTER, "username") + values.keys).distinct() - removed override suspend fun get(columnName: String): FloconPreferenceValue? = when (columnName) { COUNTER -> FloconPreferenceValue(intValue = counter) diff --git a/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt index ee935109f..6f801c7ad 100644 --- a/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt +++ b/FloconAndroid/sharedprefs-no-op/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt @@ -14,6 +14,12 @@ interface FloconPreference { value: FloconPreferenceValue, ) + /** + * Removes the key, when deleted from the desktop app. + * Does nothing by default, override it to support deleting a key. + */ + suspend fun remove(columnName: String) = Unit + suspend fun columns(): List suspend fun get( diff --git a/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt b/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt index 1bc2e3047..252fbb9b4 100644 --- a/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt +++ b/FloconAndroid/sharedprefs/src/androidMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPreference.kt @@ -36,6 +36,10 @@ data class FloconSharedPreference( editor.apply() } + override suspend fun remove(columnName: String) { + sharedPreferences.edit().remove(columnName).apply() + } + override suspend fun columns(): List { return sharedPreferences.all.keys.toList() } diff --git a/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt index ae7e4d6a2..150cbcb60 100644 --- a/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt +++ b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.kt @@ -20,6 +20,7 @@ import io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice.Preferen import io.github.openflocon.flocon.plugins.sharedprefs.model.fromdevice.SharedPreferenceValueResultDataModel import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceEditSharedPreferenceValueMessage import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceGetSharedPreferenceValueMessage +import io.github.openflocon.flocon.plugins.sharedprefs.model.todevice.ToDeviceRemoveSharedPreferenceValueMessage import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -110,6 +111,23 @@ internal class FloconSharedPrefsPluginImpl( FloconLogger.logError("SharedPreferences edit error", t) } } + + Protocol.ToDevice.SharedPreferences.Method.RemoveSharedPreferenceValue -> { + val message = encoder.decode(body) ?: return + val preference = preferences.value[message.sharedPreferenceName] ?: return + + try { + preference.remove(columnName = message.key) + + // then send the shared pref content + sendPreferenceValues( + preference = preference, + requestId = message.requestId, + ) + } catch (t: Throwable) { + FloconLogger.logError("SharedPreferences remove error", t) + } + } } } diff --git a/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt index ee935109f..6f801c7ad 100644 --- a/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt +++ b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/FloconPreference.kt @@ -14,6 +14,12 @@ interface FloconPreference { value: FloconPreferenceValue, ) + /** + * Removes the key, when deleted from the desktop app. + * Does nothing by default, override it to support deleting a key. + */ + suspend fun remove(columnName: String) = Unit + suspend fun columns(): List suspend fun get( diff --git a/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceRemoveSharedPreferenceValueMessage.kt b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceRemoveSharedPreferenceValueMessage.kt new file mode 100644 index 000000000..896c9ab72 --- /dev/null +++ b/FloconAndroid/sharedprefs/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/model/todevice/ToDeviceRemoveSharedPreferenceValueMessage.kt @@ -0,0 +1,10 @@ +package io.github.openflocon.flocon.plugins.sharedprefs.model.todevice + +import kotlinx.serialization.Serializable + +@Serializable +internal data class ToDeviceRemoveSharedPreferenceValueMessage( + val requestId: String, + val sharedPreferenceName: String, + val key: String, +) diff --git a/FloconAndroid/sharedprefs/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt b/FloconAndroid/sharedprefs/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt index 28a71b0eb..72239fd7e 100644 --- a/FloconAndroid/sharedprefs/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt +++ b/FloconAndroid/sharedprefs/src/iosMain/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPlugin.ios.kt @@ -51,4 +51,8 @@ private class UserDefaultsPreference( ?: return defaults.setObject(newValue, forKey = columnName) } + + override suspend fun remove(columnName: String) { + defaults.removeObjectForKey(columnName) + } } diff --git a/FloconAndroid/sharedprefs/src/jvmTest/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPluginTest.kt b/FloconAndroid/sharedprefs/src/jvmTest/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPluginTest.kt index 129f6fce6..1bef326b2 100644 --- a/FloconAndroid/sharedprefs/src/jvmTest/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPluginTest.kt +++ b/FloconAndroid/sharedprefs/src/jvmTest/kotlin/io/github/openflocon/flocon/plugins/sharedprefs/FloconSharedPrefsPluginTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals +import kotlin.test.assertFalse class FloconSharedPrefsPluginTest { @@ -26,10 +27,16 @@ class FloconSharedPrefsPluginTest { override val name: String, override val type: String = FloconPreferenceType.Custom, ) : FloconPreference { - val values = mutableMapOf("age" to FloconPreferenceValue(intValue = 34)) + val values = mutableMapOf( + "age" to FloconPreferenceValue(intValue = 34), + "token" to FloconPreferenceValue(stringValue = "abc"), + ) override suspend fun set(columnName: String, value: FloconPreferenceValue) { values[columnName] = value } + override suspend fun remove(columnName: String) { + values -= columnName + } override suspend fun columns(): List = values.keys.toList() override suspend fun get(columnName: String): FloconPreferenceValue? = values[columnName] } @@ -91,6 +98,40 @@ class FloconSharedPrefsPluginTest { assertContains(sender.messages.last().third, "{\"key\":\"age\",\"intValue\":35}") } + @Test + fun `removeSharedPreferenceValue removes the key and sends the values`() = runTest { + plugin.register(preference) + + plugin.onMessageReceived( + method = Protocol.ToDevice.SharedPreferences.Method.RemoveSharedPreferenceValue, + body = "{\"requestId\":\"1\",\"sharedPreferenceName\":\"user\",\"key\":\"token\"}", + ) + + assertFalse("token" in preference.values) + val (_, method, body) = sender.messages.last() + assertEquals(Protocol.FromDevice.SharedPreferences.Method.GetSharedPreferenceValue, method) + assertContains(body, "{\"key\":\"age\",\"intValue\":34}") + assertFalse(body.contains("token")) + } + + @Test + fun `removeSharedPreferenceValue keeps the key when remove is not implemented`() = runTest { + val readOnly = object : FloconPreference { + override val name: String = "readOnly" + override suspend fun set(columnName: String, value: FloconPreferenceValue) = Unit + override suspend fun columns(): List = listOf("age") + override suspend fun get(columnName: String) = FloconPreferenceValue(intValue = 34) + } + plugin.register(readOnly) + + plugin.onMessageReceived( + method = Protocol.ToDevice.SharedPreferences.Method.RemoveSharedPreferenceValue, + body = "{\"requestId\":\"1\",\"sharedPreferenceName\":\"readOnly\",\"key\":\"age\"}", + ) + + assertContains(sender.messages.last().third, "{\"key\":\"age\",\"intValue\":34}") + } + @Test fun `key is the plugin name sent by the desktop`() { assertEquals(Protocol.ToDevice.SharedPreferences.Plugin, plugin.key) diff --git a/FloconDesktop/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/FloconDesktop/composeApp/src/commonMain/composeResources/values-fr/strings.xml index 6001b1df1..8ccb6974d 100644 --- a/FloconDesktop/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/FloconDesktop/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -356,6 +356,9 @@ Annuler Modifier Copier + Supprimer + Supprimer « %1$s » ? + La clé est supprimée de l'app. Cette action est définitive. lecture seule %1$s est vide L'application n'a encore rien écrit dans ce fichier. diff --git a/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml b/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml index 7d12c7576..a3f71f55e 100644 --- a/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/FloconDesktop/composeApp/src/commonMain/composeResources/values/strings.xml @@ -429,6 +429,9 @@ Cancel Edit Copy + Delete + Delete "%1$s"? + The key is removed from the app. This can't be undone. read only %1$s is empty The app hasn't written anything in this file yet. diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/SharedPreferencesViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/SharedPreferencesViewModel.kt index 10fcd5f7e..a4e20b6ef 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/SharedPreferencesViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/SharedPreferencesViewModel.kt @@ -10,6 +10,7 @@ import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceRowDo import io.github.openflocon.domain.sharedpreference.usecase.EditSharedPrefFieldUseCase import io.github.openflocon.domain.sharedpreference.usecase.GetCurrentDeviceSharedPreferenceValuesUseCase import io.github.openflocon.domain.sharedpreference.usecase.ObserveCurrentDeviceSharedPreferenceValuesUseCase +import io.github.openflocon.domain.sharedpreference.usecase.RemoveSharedPrefFieldUseCase import io.github.openflocon.flocondesktop.features.sharedpreferences.delegate.SharedPrefSelectorDelegate import io.github.openflocon.flocondesktop.features.sharedpreferences.model.DeviceSharedPrefUiModel import io.github.openflocon.flocondesktop.features.sharedpreferences.model.SharedPreferencesRowUiModel @@ -39,6 +40,7 @@ class SharedPreferencesViewModel( private val sharedPrefSelectorDelegate: SharedPrefSelectorDelegate, private val observeCurrentDeviceSharedPreferenceValuesUseCase: ObserveCurrentDeviceSharedPreferenceValuesUseCase, private val editSharedPrefFieldUseCase: EditSharedPrefFieldUseCase, + private val removeSharedPrefFieldUseCase: RemoveSharedPrefFieldUseCase, private val getCurrentDeviceSharedPrefValuesUseCase: GetCurrentDeviceSharedPreferenceValuesUseCase, private val navigationState: MainFloconNavigationState, ) : ViewModel() { @@ -194,6 +196,13 @@ class SharedPreferencesViewModel( navigationState.navigate(SharedPreferencesRoutes.Edit(row.key)) } + // the device answers with the refreshed values, like after an edit + fun onDeleteClicked(row: SharedPreferencesRowUiModel) { + viewModelScope.launch(dispatcherProvider.viewModel) { + removeSharedPrefFieldUseCase(row.key) + } + } + fun onCopyClicked(row: SharedPreferencesRowUiModel) { viewModelScope.launch { copyToClipboard(row.value.asString()) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceDeleteDialog.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceDeleteDialog.kt new file mode 100644 index 000000000..fe1e7a1f5 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceDeleteDialog.kt @@ -0,0 +1,96 @@ +package io.github.openflocon.flocondesktop.features.sharedpreferences.view + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import flocondesktop.composeapp.generated.resources.Res +import flocondesktop.composeapp.generated.resources.preference_cancel +import flocondesktop.composeapp.generated.resources.preference_delete +import flocondesktop.composeapp.generated.resources.preference_delete_message +import flocondesktop.composeapp.generated.resources.preference_delete_title +import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.components.FloconButton +import io.github.openflocon.library.designsystem.components.FloconDialog +import io.github.openflocon.library.designsystem.components.FloconDialogHeader +import io.github.openflocon.library.designsystem.components.FloconOutlinedButton +import org.jetbrains.compose.resources.stringResource + +@Composable +fun SharedPreferenceDeleteDialog( + key: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + FloconDialog( + onDismissRequest = onDismiss, + modifier = modifier, + ) { + SharedPreferenceDeleteContent( + key = key, + onConfirm = onConfirm, + onDismiss = onDismiss, + ) + } +} + +@Composable +private fun SharedPreferenceDeleteContent( + key: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + Column { + FloconDialogHeader( + title = stringResource(Res.string.preference_delete_title, key), + modifier = Modifier.fillMaxWidth(), + ) + Column( + verticalArrangement = Arrangement.spacedBy(FloconTheme.spacing.medium), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = FloconTheme.spacing.large) + .padding(bottom = FloconTheme.spacing.medium), + ) { + Text( + text = stringResource(Res.string.preference_delete_message), + style = FloconTheme.typography.bodyMedium, + color = FloconTheme.colors.inkMuted, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(FloconTheme.spacing.small, Alignment.End), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + FloconOutlinedButton(onClick = onDismiss) { + Text(stringResource(Res.string.preference_cancel)) + } + FloconButton( + onClick = onConfirm, + containerColor = FloconTheme.colors.statusError, + ) { + Text(stringResource(Res.string.preference_delete)) + } + } + } + } +} + +@Preview +@Composable +private fun SharedPreferenceDeleteContentPreview() { + FloconTheme { + SharedPreferenceDeleteContent( + key = "user_token", + onConfirm = {}, + onDismiss = {}, + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceRow.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceRow.kt index 33462d03c..a97606b06 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceRow.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferenceRow.kt @@ -56,6 +56,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -85,10 +86,12 @@ import flocondesktop.composeapp.generated.resources.Res import flocondesktop.composeapp.generated.resources.preference_cancel import flocondesktop.composeapp.generated.resources.preference_copy import flocondesktop.composeapp.generated.resources.preference_decrement +import flocondesktop.composeapp.generated.resources.preference_delete import flocondesktop.composeapp.generated.resources.preference_edit import flocondesktop.composeapp.generated.resources.preference_increment import flocondesktop.composeapp.generated.resources.preference_read_only import flocondesktop.composeapp.generated.resources.preference_save +import io.github.openflocon.flocondesktop.common.ui.ContextualMenu import io.github.openflocon.flocondesktop.features.sharedpreferences.model.PreferenceValueTypeUiModel import io.github.openflocon.flocondesktop.features.sharedpreferences.model.SharedPreferencesRowUiModel import io.github.openflocon.flocondesktop.features.sharedpreferences.model.asString @@ -100,8 +103,10 @@ import io.github.openflocon.flocondesktop.features.sharedpreferences.model.previ import io.github.openflocon.flocondesktop.features.sharedpreferences.model.previewSharedPreferencesStringRowUiModel import io.github.openflocon.flocondesktop.features.sharedpreferences.model.previewSharedPreferencesStringSetRowUiModel import io.github.openflocon.library.designsystem.FloconTheme +import io.github.openflocon.library.designsystem.common.FloconContextMenuItem import io.github.openflocon.library.designsystem.components.FloconIcon import io.github.openflocon.library.designsystem.components.WithTooltip +import kotlinx.collections.immutable.persistentListOf import org.jetbrains.compose.resources.stringResource private val ActionSize = 30.dp @@ -112,6 +117,37 @@ private val MonoSmall = 12.sp @Composable fun SharedPreferenceRow( + model: SharedPreferencesRowUiModel, + isExpanded: Boolean, + onToggleExpanded: (key: String) -> Unit, + onValueChanged: (row: SharedPreferencesRowUiModel, valueString: String) -> Unit, + onEditClicked: (row: SharedPreferencesRowUiModel) -> Unit, + onCopyClicked: (row: SharedPreferencesRowUiModel) -> Unit, + onDeleteClicked: (row: SharedPreferencesRowUiModel) -> Unit, + modifier: Modifier = Modifier, +) { + val deleteLabel = stringResource(Res.string.preference_delete) + val onDelete by rememberUpdatedState(onDeleteClicked) + val menuItems = remember(model, deleteLabel) { + persistentListOf( + FloconContextMenuItem.Item(label = deleteLabel, onClick = { onDelete(model) }), + ) + } + ContextualMenu(items = menuItems) { + SharedPreferenceRowContent( + model = model, + isExpanded = isExpanded, + onToggleExpanded = onToggleExpanded, + onValueChanged = onValueChanged, + onEditClicked = onEditClicked, + onCopyClicked = onCopyClicked, + modifier = modifier, + ) + } +} + +@Composable +private fun SharedPreferenceRowContent( model: SharedPreferencesRowUiModel, isExpanded: Boolean, onToggleExpanded: (key: String) -> Unit, @@ -650,6 +686,7 @@ private fun PreviewRow(model: SharedPreferencesRowUiModel, isExpanded: Boolean = onValueChanged = { _, _ -> }, onEditClicked = {}, onCopyClicked = {}, + onDeleteClicked = {}, ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferencesScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferencesScreen.kt index ae354c855..7757cd2af 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferencesScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/sharedpreferences/view/SharedPreferencesScreen.kt @@ -81,6 +81,7 @@ fun SharedPreferencesScreen(modifier: Modifier = Modifier) { changeValue = viewModel::onValueChanged, onEditClicked = viewModel::onEditClicked, onCopyClicked = viewModel::onCopyClicked, + onDeleteClicked = viewModel::onDeleteClicked, autoUpdateState = autoUpdateState, onAutoUpdateChange = viewModel::onAutoUpdateChange, onAutoUpdateDelayChanged = viewModel::onAutoUpdateDelayChanged, @@ -96,6 +97,7 @@ fun SharedPrefScreen( changeValue: (SharedPreferencesRowUiModel, String) -> Unit, onEditClicked: (row: SharedPreferencesRowUiModel) -> Unit, onCopyClicked: (row: SharedPreferencesRowUiModel) -> Unit, + onDeleteClicked: (row: SharedPreferencesRowUiModel) -> Unit, autoUpdateState: PreferenceAutoUpdate, onAutoUpdateChange: (Boolean) -> Unit, onAutoUpdateDelayChanged: (delay: Int) -> Unit, @@ -112,6 +114,8 @@ fun SharedPrefScreen( } // UI only: the collapsible rows (json, long text) currently expanded, by key var expandedKeys by remember(deviceSharedPrefs) { mutableStateOf(emptySet()) } + // UI only: the row waiting for the delete confirmation + var rowToDelete by remember { mutableStateOf(null) } val lazyListState = rememberLazyListState() val scrollAdapter = rememberFloconScrollbarAdapter(lazyListState) @@ -180,6 +184,7 @@ fun SharedPrefScreen( onValueChanged = changeValue, onEditClicked = onEditClicked, onCopyClicked = onCopyClicked, + onDeleteClicked = { row -> rowToDelete = row }, ) } } @@ -208,6 +213,17 @@ fun SharedPrefScreen( } } } + + rowToDelete?.let { row -> + SharedPreferenceDeleteDialog( + key = row.key, + onConfirm = { + onDeleteClicked(row) + rowToDelete = null + }, + onDismiss = { rowToDelete = null }, + ) + } } @Composable @@ -254,6 +270,7 @@ private fun SharedPrefScreenPreview() { changeValue = { _, _ -> }, onEditClicked = {}, onCopyClicked = {}, + onDeleteClicked = {}, autoUpdateState = PreferenceAutoUpdate.Disabled, onAutoUpdateChange = {}, onAutoUpdateDelayChanged = {}, diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSource.kt index e2811f3f4..32409cd61 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSource.kt @@ -25,6 +25,12 @@ interface DeviceSharedPreferencesRemoteDataSource { value: SharedPreferenceRowDomainModel.Value, ) + suspend fun removeSharedPrefField( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + sharedPreference: DeviceSharedPreferenceDomainModel, + key: String, + ) + fun getPreferences(message: FloconIncomingMessageDomainModel): List fun getValues(message: FloconIncomingMessageDomainModel): SharedPreferenceValuesResponseDomainModel? diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/repository/SharedPreferencesRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/repository/SharedPreferencesRepositoryImpl.kt index 85a913299..7a25e1d0c 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/repository/SharedPreferencesRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/sharedpreference/repository/SharedPreferencesRepositoryImpl.kt @@ -128,4 +128,18 @@ class SharedPreferencesRepositoryImpl( ) } } + + override suspend fun removeSharedPrefField( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + sharedPreference: DeviceSharedPreferenceDomainModel, + key: String, + ) { + withContext(dispatcherProvider.data) { + remote.removeSharedPrefField( + deviceIdAndPackageName = deviceIdAndPackageName, + sharedPreference = sharedPreference, + key = key, + ) + } + } } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSourceImpl.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSourceImpl.kt index 722ced590..41b4e3858 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSourceImpl.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/datasource/DeviceSharedPreferencesRemoteDataSourceImpl.kt @@ -10,6 +10,7 @@ import com.flocon.data.remote.sharedpreference.mapper.toSharedPreferenceValuesRe import com.flocon.data.remote.sharedpreference.models.DeviceSharedPreferenceDataModel import com.flocon.data.remote.sharedpreference.models.ToDeviceEditSharedPreferenceValueMessage import com.flocon.data.remote.sharedpreference.models.ToDeviceGetSharedPreferenceValueMessage +import com.flocon.data.remote.sharedpreference.models.ToDeviceRemoveSharedPreferenceValueMessage import com.flocon.data.remote.sharedpreference.models.toDeviceSharedPreferenceDomain import io.github.openflocon.data.core.sharedpreference.datasource.DeviceSharedPreferencesRemoteDataSource import io.github.openflocon.domain.Protocol @@ -88,6 +89,28 @@ class DeviceSharedPreferencesRemoteDataSourceImpl( ) } + override suspend fun removeSharedPrefField( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + sharedPreference: DeviceSharedPreferenceDomainModel, + key: String, + ) { + val requestId = newRequestId() + server.sendMessageToClient( + deviceIdAndPackageName = deviceIdAndPackageName.toRemote(), + message = FloconOutgoingMessageDataModel( + plugin = Protocol.ToDevice.SharedPreferences.Plugin, + method = Protocol.ToDevice.SharedPreferences.Method.RemoveSharedPreferenceValue, + body = Json.encodeToString( + ToDeviceRemoveSharedPreferenceValueMessage( + requestId = requestId, + sharedPreferenceName = sharedPreference.id, + key = key, + ), + ), + ), + ) + } + override fun getPreferences(message: FloconIncomingMessageDomainModel): List = json.safeDecodeFromString>(message.body) ?.let { toDeviceSharedPreferenceDomain(it) } .orEmpty() diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/models/ToDeviceRemoveSharedPreferenceValueMessage.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/models/ToDeviceRemoveSharedPreferenceValueMessage.kt new file mode 100644 index 000000000..24ec6dfc8 --- /dev/null +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/sharedpreference/models/ToDeviceRemoveSharedPreferenceValueMessage.kt @@ -0,0 +1,10 @@ +package com.flocon.data.remote.sharedpreference.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ToDeviceRemoveSharedPreferenceValueMessage( + val requestId: String, + val sharedPreferenceName: String, + val key: String, +) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt index 53286d54c..d984ec620 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt @@ -201,6 +201,7 @@ object Protocol { const val GetSharedPreferences = "get_shared_preferences" const val GetSharedPreferenceValue = "get_shared_preference_value" const val SetSharedPreferenceValue = "set_shared_preference_value" + const val RemoveSharedPreferenceValue = "remove_shared_preference_value" } } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/DI.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/DI.kt index 47c0d20ad..9d823ddd0 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/DI.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/DI.kt @@ -7,6 +7,7 @@ import io.github.openflocon.domain.sharedpreference.usecase.GetCurrentDeviceShar import io.github.openflocon.domain.sharedpreference.usecase.ObserveCurrentDeviceSelectedSharedPreferenceUseCase import io.github.openflocon.domain.sharedpreference.usecase.ObserveCurrentDeviceSharedPreferenceValuesUseCase import io.github.openflocon.domain.sharedpreference.usecase.ObserveDeviceSharedPreferencesUseCase +import io.github.openflocon.domain.sharedpreference.usecase.RemoveSharedPrefFieldUseCase import io.github.openflocon.domain.sharedpreference.usecase.SelectCurrentDeviceSharedPreferenceUseCase import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module @@ -20,4 +21,5 @@ internal val sharedPreferencesModule = module { factoryOf(::GetCurrentDeviceSharedPreferenceValuesUseCase) factoryOf(::ObserveCurrentDeviceSharedPreferenceValuesUseCase) factoryOf(::EditSharedPrefFieldUseCase) + factoryOf(::RemoveSharedPrefFieldUseCase) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/repository/SharedPreferencesRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/repository/SharedPreferencesRepository.kt index d39745376..a1983cc85 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/repository/SharedPreferencesRepository.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/repository/SharedPreferencesRepository.kt @@ -39,4 +39,10 @@ interface SharedPreferencesRepository { key: String, value: SharedPreferenceRowDomainModel.Value, ) + + suspend fun removeSharedPrefField( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + sharedPreference: DeviceSharedPreferenceDomainModel, + key: String, + ) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/usecase/RemoveSharedPrefFieldUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/usecase/RemoveSharedPrefFieldUseCase.kt new file mode 100644 index 000000000..8d0a9656b --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/sharedpreference/usecase/RemoveSharedPrefFieldUseCase.kt @@ -0,0 +1,21 @@ +package io.github.openflocon.domain.sharedpreference.usecase + +import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.domain.sharedpreference.repository.SharedPreferencesRepository + +class RemoveSharedPrefFieldUseCase( + private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase, + private val getCurrentDeviceSelectedSharedPreferenceUseCase: GetCurrentDeviceSelectedSharedPreferenceUseCase, + private val sharedPreferencesRepository: SharedPreferencesRepository, +) { + suspend operator fun invoke(key: String) { + val current = getCurrentDeviceIdAndPackageNameUseCase() ?: return + val selectedSharedPreference = getCurrentDeviceSelectedSharedPreferenceUseCase() ?: return + + sharedPreferencesRepository.removeSharedPrefField( + deviceIdAndPackageName = current, + sharedPreference = selectedSharedPreference, + key = key, + ) + } +} diff --git a/FloconDesktop/mcp/src/desktopTest/kotlin/io/github/openflocon/mcp/sharedpreferences/SharedPreferencesMcpToolProviderTest.kt b/FloconDesktop/mcp/src/desktopTest/kotlin/io/github/openflocon/mcp/sharedpreferences/SharedPreferencesMcpToolProviderTest.kt index 90f0bfbb9..0f274710c 100644 --- a/FloconDesktop/mcp/src/desktopTest/kotlin/io/github/openflocon/mcp/sharedpreferences/SharedPreferencesMcpToolProviderTest.kt +++ b/FloconDesktop/mcp/src/desktopTest/kotlin/io/github/openflocon/mcp/sharedpreferences/SharedPreferencesMcpToolProviderTest.kt @@ -169,4 +169,10 @@ private class FakeSharedPreferencesRepository( key: String, value: Value, ) = error("not used") + + override suspend fun removeSharedPrefField( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + sharedPreference: DeviceSharedPreferenceDomainModel, + key: String, + ) = error("not used") } diff --git a/docs/sharedpref.md b/docs/sharedpref.md index c5eddca9a..8cc24fd95 100644 --- a/docs/sharedpref.md +++ b/docs/sharedpref.md @@ -17,6 +17,7 @@ Features: - **Auto-Discovery**: Automatically finds and lists default `SharedPreferences` files on Android. - **DataStore Support**: Seamlessly browse and mutate `PreferencesDataStore` keys. - **Live Mutation**: Edit booleans, numbers, strings, and JSON objects in real-time. +- **Delete a key**: Right-click a row and choose **Delete**, then confirm. The key is removed from the app (SharedPreferences, DataStore and iOS UserDefaults). --- @@ -80,6 +81,11 @@ class MyCustomPreference : FloconPreference { override suspend fun set(columnName: String, value: FloconPreferenceValue) { writeSecureKey(columnName, value.stringValue) } + + // optional: called when a key is deleted from the desktop (does nothing by default) + override suspend fun remove(columnName: String) { + deleteSecureKey(columnName) + } } floconRegisterPreference(MyCustomPreference()) diff --git a/protocol/shared-preferences.md b/protocol/shared-preferences.md index f4322c53a..bd5daa6c2 100644 --- a/protocol/shared-preferences.md +++ b/protocol/shared-preferences.md @@ -8,6 +8,7 @@ | Mobile → Desktop | `get_shared_preferences` | `List` | | Desktop → Mobile | `get_shared_preference_value` | `GetValuesRequest` | | Desktop → Mobile | `set_shared_preference_value` | `SetValueRequest` | +| Desktop → Mobile | `remove_shared_preference_value` | `RemoveValueRequest` | | Mobile → Desktop | `get_shared_preference_value` | `ValuesResult` | ## `get_shared_preferences` @@ -63,6 +64,22 @@ Updates one key, then the mobile answers with `get_shared_preference_value` (ful { "requestId": "9a7c…", "sharedPreferenceName": "session", "key": "isLogged", "booleanValue": false } ``` +## `remove_shared_preference_value` (Desktop → Mobile) + +Removes one key, then the mobile answers with `get_shared_preference_value` (full content). Older SDKs ignore this method. + +### `RemoveValueRequest` + +| Key | Type | Description | +|------------------------|----------|-------------------| +| `requestId` | `String` | Request id | +| `sharedPreferenceName` | `String` | Preference `name` | +| `key` | `String` | Key to remove | + +```json +{ "requestId": "9a7c…", "sharedPreferenceName": "session", "key": "token" } +``` + ## `get_shared_preference_value` (Mobile → Desktop) ### `ValuesResult`