diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt index fb367a7..dbb29b6 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt @@ -48,6 +48,7 @@ fun SettingsScreen( Column(Modifier.weight(1f)) { SettingsSectionContent( state = state, + windowClass = windowClass, section = state.selectedSection.takeUnless { it == SettingsSection.Overview } ?: SettingsSection.Preferences, onBack = {}, showBack = false, @@ -79,6 +80,7 @@ fun SettingsScreen( SettingsSection.Overview -> SettingsOverview(state, onSectionSelected, largeTitle = true) else -> SettingsSectionContent( state = state, + windowClass = windowClass, section = state.selectedSection, onBack = { onSectionSelected( @@ -118,6 +120,7 @@ fun SettingsScreen( @Composable private fun SettingsSectionContent( state: SettingsState, + windowClass: WindowClass, section: SettingsSection, onBack: () -> Unit, showBack: Boolean, @@ -157,7 +160,7 @@ private fun SettingsSectionContent( showBack = showBack, ) SettingsSection.Notifications -> NotificationSettings(state, onNotificationsChanged, onOpenNotificationSettings, onBack, showBack) - SettingsSection.Storage -> StorageSettings(state, onDeleteAllTransfers, onBack, showBack) + SettingsSection.Storage -> StorageSettings(state, windowClass, onDeleteAllTransfers, onBack, showBack) SettingsSection.About -> AboutSettings( state = state, onDiagnosticsChanged = onDiagnosticsChanged, diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt index 3dd7d67..7011977 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt @@ -13,17 +13,28 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.vnidrop.app.ui.components.AdaptiveDrawer +import com.vnidrop.app.ui.components.DestructiveButton +import com.vnidrop.app.ui.components.SecondaryButton import com.vnidrop.app.ui.state.formatBytes +import com.vnidrop.app.ui.state.WindowClass import com.vnidrop.app.ui.theme.LocalVniDropColors import org.jetbrains.compose.resources.stringResource import vnidrop.shared.generated.resources.Res +import vnidrop.shared.generated.resources.button_cancel import vnidrop.shared.generated.resources.storage_app_data import vnidrop.shared.generated.resources.storage_calculating import vnidrop.shared.generated.resources.storage_delete_transfers +import vnidrop.shared.generated.resources.storage_delete_transfers_description import vnidrop.shared.generated.resources.storage_deleting import vnidrop.shared.generated.resources.storage_total import vnidrop.shared.generated.resources.storage_received_files @@ -35,10 +46,12 @@ import vnidrop.shared.generated.resources.storage_footer @Composable internal fun StorageSettings( state: SettingsState, + windowClass: WindowClass, onDeleteAllTransfers: () -> Unit, onBack: () -> Unit, showBack: Boolean, ) { + var showDeleteConfirmation by rememberSaveable { mutableStateOf(false) } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { SettingsTopBar(stringResource(Res.string.storage_title), onBack, showBack) val storage = state.storage @@ -67,7 +80,7 @@ internal fun StorageSettings( } } Button( - onClick = onDeleteAllTransfers, + onClick = { showDeleteConfirmation = true }, enabled = !state.isDeletingTransfers, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), ) { @@ -79,6 +92,50 @@ internal fun StorageSettings( color = LocalVniDropColors.current.foregroundLighter, ) } + if (showDeleteConfirmation) { + AdaptiveDrawer( + windowClass = windowClass, + onDismissRequest = { showDeleteConfirmation = false }, + ) { + DeleteAllTransfersPanel( + onCancel = { showDeleteConfirmation = false }, + onConfirm = { + showDeleteConfirmation = false + onDeleteAllTransfers() + }, + ) + } + } +} + +@Composable +private fun DeleteAllTransfersPanel( + onCancel: () -> Unit, + onConfirm: () -> Unit, +) { + Column( + Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text( + stringResource(Res.string.storage_delete_transfers), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + ) + Text( + stringResource(Res.string.storage_delete_transfers_description), + color = LocalVniDropColors.current.foregroundLighter, + style = MaterialTheme.typography.bodyMedium, + ) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)) { + SecondaryButton(stringResource(Res.string.button_cancel), onClick = onCancel) + DestructiveButton( + stringResource(Res.string.storage_delete_transfers), + onClick = onConfirm, + modifier = Modifier.testTag("confirm-delete-all-transfers"), + ) + } + } } @Composable diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt index 2405ea3..aeffd9b 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt @@ -64,6 +64,7 @@ import com.vnidrop.app.ui.theme.VniDropTheme import com.vnidrop.app.ui.theme.LocalVniDropColors import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class) @@ -172,6 +173,45 @@ class FoundationComposeTest { runOnIdle { assertTrue(applied) } } + @Test + fun storageDeleteAllTransfersRequiresConfirmation() = runComposeUiTest { + var deleteRequested = false + setContent { + VniDropTheme(isDarkTheme = false) { + SettingsScreen( + state = SettingsState(selectedSection = SettingsSection.Storage), + windowClass = WindowClass.Desktop, + onSectionSelected = {}, + onUsernameChanged = {}, + onThemeModeChanged = {}, + onChooseFolder = {}, + onResetFolder = {}, + onNotificationsChanged = {}, + onOpenNotificationSettings = {}, + onDiagnosticsChanged = {}, + onBugWhatChanged = {}, + onBugExpectedChanged = {}, + onBugStepsChanged = {}, + onBugContactChanged = {}, + onBugIncludeLogsChanged = {}, + onSubmitBugReport = {}, + onDeleteAllTransfers = { deleteRequested = true }, + ) + } + } + + onNodeWithText("Delete all transfers").performClick() + onNodeWithText( + "This clears all sent and received transfer records from your history. Your received files are not deleted. " + + "Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. " + + "This can’t be undone.", + ).assertIsDisplayed() + runOnIdle { assertFalse(deleteRequested) } + + onNodeWithTag("confirm-delete-all-transfers").performClick() + runOnIdle { assertTrue(deleteRequested) } + } + @Test fun aboutSettingsShowsTheSharedProductAndPrivacyContent() = runComposeUiTest { setContent {