From a2a631c141318c745eab3fd87ac36c67d724c418 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 23 Jul 2026 14:26:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=94=AF=E6=8C=81=E9=95=BF?= =?UTF-8?q?=E6=8C=89=E5=8D=95=E4=B8=AA=E8=AE=B0=E5=BD=95=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E5=A4=9A=E9=80=89=E6=A8=A1=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E9=80=89=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plus/rua/project/DateRecorderViewModel.kt | 10 +++ .../plus/rua/project/ui/DateRecorderScreen.kt | 26 +++++- .../rua/project/DateRecorderViewModelTest.kt | 87 +++++++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 core/src/test/kotlin/plus/rua/project/DateRecorderViewModelTest.kt diff --git a/core/src/main/kotlin/plus/rua/project/DateRecorderViewModel.kt b/core/src/main/kotlin/plus/rua/project/DateRecorderViewModel.kt index 7f2daa9..74f120c 100644 --- a/core/src/main/kotlin/plus/rua/project/DateRecorderViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/DateRecorderViewModel.kt @@ -130,6 +130,16 @@ class DateRecorderViewModel( _selectedIds.value = emptySet() } + /** + * 开启多选模式并选中指定的记录 ID(响应长按操作)。 + * + * @param id 长按选中的记录 ID + */ + fun startSelectionModeWith(id: Long) { + _selectionMode.value = true + _selectedIds.value = setOf(id) + } + /** * 切换某条记录的选中状态。 */ diff --git a/core/src/main/kotlin/plus/rua/project/ui/DateRecorderScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/DateRecorderScreen.kt index 67ac1be..ff55637 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DateRecorderScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DateRecorderScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -303,7 +305,8 @@ fun DateRecorderScreen( selectionMode = uiState.selectionMode, photoRoot = repo, onOpenRecord = onOpenRecord, - onToggleSelection = viewModel::toggleSelection + onToggleSelection = viewModel::toggleSelection, + onStartSelectionWith = viewModel::startSelectionModeWith ) } } @@ -341,7 +344,8 @@ private fun RecordGrid( selectionMode: Boolean, photoRoot: DateRecorderRepository, onOpenRecord: (Long) -> Unit, - onToggleSelection: (Long) -> Unit + onToggleSelection: (Long) -> Unit, + onStartSelectionWith: (Long) -> Unit ) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 100.dp), @@ -359,12 +363,20 @@ private fun RecordGrid( onClick = { if (selectionMode) onToggleSelection(record.id) else onOpenRecord(record.id) + }, + onLongClick = { + if (selectionMode) { + onToggleSelection(record.id) + } else { + onStartSelectionWith(record.id) + } } ) } } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun RecordCard( record: DateRecord, @@ -372,6 +384,7 @@ private fun RecordCard( isSelected: Boolean, selectionMode: Boolean, onClick: () -> Unit, + onLongClick: () -> Unit, modifier: Modifier = Modifier ) { val cardScale by animateFloatAsState( @@ -389,9 +402,9 @@ private fun RecordCard( animationSpec = tween(200), label = "record_card_border_color" ) + val cardShape = RoundedCornerShape(8.dp) Card( - onClick = onClick, - shape = RoundedCornerShape(8.dp), + shape = cardShape, border = BorderStroke(borderWidth, borderColor), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), modifier = modifier @@ -400,6 +413,11 @@ private fun RecordCard( scaleX = cardScale scaleY = cardScale } + .clip(cardShape) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick + ) ) { Box { AsyncImage( diff --git a/core/src/test/kotlin/plus/rua/project/DateRecorderViewModelTest.kt b/core/src/test/kotlin/plus/rua/project/DateRecorderViewModelTest.kt new file mode 100644 index 0000000..9991033 --- /dev/null +++ b/core/src/test/kotlin/plus/rua/project/DateRecorderViewModelTest.kt @@ -0,0 +1,87 @@ +package plus.rua.project + +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain + +/** + * [DateRecorderViewModel] 的多选模式与选择状态单元测试。 + */ +@OptIn(ExperimentalCoroutinesApi::class) +class DateRecorderViewModelTest { + + private val testDispatcher = UnconfinedTestDispatcher() + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + private fun newViewModel(): DateRecorderViewModel { + val repository = DateRecorderRepository(FakeDateRecorderDao(), File("build/tmp/date_recorder_vm_test")) + return DateRecorderViewModel(repository) + } + + @Test + fun startSelectionModeWith_enablesSelectionMode_andSelectsSpecifiedId() = runTest(testDispatcher) { + val vm = newViewModel() + backgroundScope.launch { vm.uiState.collect {} } + assertFalse(vm.uiState.value.selectionMode) + + vm.startSelectionModeWith(42L) + + assertTrue(vm.uiState.value.selectionMode) + assertEquals(setOf(42L), vm.uiState.value.selectedIds) + } + + @Test + fun toggleSelectionMode_togglesState_andClearsSelection() = runTest(testDispatcher) { + val vm = newViewModel() + backgroundScope.launch { vm.uiState.collect {} } + vm.startSelectionModeWith(100L) + assertTrue(vm.uiState.value.selectionMode) + + vm.toggleSelectionMode() + assertFalse(vm.uiState.value.selectionMode) + assertTrue(vm.uiState.value.selectedIds.isEmpty()) + } + + @Test + fun toggleSelection_addsAndRemovesId() = runTest(testDispatcher) { + val vm = newViewModel() + backgroundScope.launch { vm.uiState.collect {} } + vm.startSelectionModeWith(1L) + + vm.toggleSelection(2L) + assertEquals(setOf(1L, 2L), vm.uiState.value.selectedIds) + + vm.toggleSelection(1L) + assertEquals(setOf(2L), vm.uiState.value.selectedIds) + } +} + +private class FakeDateRecorderDao : DateRecordDao { + override fun getByIdFlow(id: Long): Flow = flowOf(null) + override fun getAllFlow(): Flow> = flowOf(emptyList()) + override suspend fun insert(record: DateRecord): Long = 0 + override suspend fun update(record: DateRecord) = Unit + override suspend fun deleteByIds(ids: List): Int = 0 +}