fix(recorder): preserve record ID when editing photo of an existing record
This commit is contained in:
parent
c7aa611e52
commit
bff6e37908
@ -18,6 +18,8 @@ class PhotoEditorActivity : BaseActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val sourcePath = intent?.getStringExtra(DateRecorderNav.EXTRA_TEMP_PHOTO_PATH)
|
||||
val recordId = intent?.getLongExtra(DateRecorderNav.EXTRA_RECORD_ID, -1L)
|
||||
?.takeIf { it >= 0 }
|
||||
requireNotNull(sourcePath) { "PhotoEditorActivity 必须接收 EXTRA_TEMP_PHOTO_PATH" }
|
||||
|
||||
setContent {
|
||||
@ -28,6 +30,9 @@ class PhotoEditorActivity : BaseActivity() {
|
||||
startActivityWithSlide(
|
||||
Intent(this, RecordEditActivity::class.java).apply {
|
||||
putExtra(DateRecorderNav.EXTRA_TEMP_PHOTO_PATH, finalPath)
|
||||
if (recordId != null) {
|
||||
putExtra(DateRecorderNav.EXTRA_RECORD_ID, recordId)
|
||||
}
|
||||
}
|
||||
)
|
||||
finishWithSlideBack()
|
||||
|
||||
@ -37,6 +37,7 @@ class RecordDetailActivity : BaseActivity() {
|
||||
startActivityWithSlide(
|
||||
Intent(this, PhotoEditorActivity::class.java).apply {
|
||||
putExtra(DateRecorderNav.EXTRA_TEMP_PHOTO_PATH, photoPath)
|
||||
putExtra(DateRecorderNav.EXTRA_RECORD_ID, recordId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ class DateRecorderRepository(
|
||||
/**
|
||||
* 删除指定相对路径的照片文件,文件不存在时静默忽略。
|
||||
*/
|
||||
private fun deletePhotoFile(relativePath: String) {
|
||||
internal fun deletePhotoFile(relativePath: String) {
|
||||
runCatching { absoluteFileOf(relativePath).delete() }
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ data class RecordEditUiState(
|
||||
* 记录编辑页面 ViewModel,处理新建与编辑两种模式。
|
||||
*
|
||||
* @param repository 数据仓库
|
||||
* @param photoPath 新建模式下的照片绝对路径(来自相机/编辑器),编辑模式为 null
|
||||
* @param photoPath 新建模式下的照片绝对路径,或编辑模式下重新编辑后的照片绝对路径(可空)
|
||||
* @param recordId 编辑模式下的已有记录 ID,新建模式为 null
|
||||
*/
|
||||
class RecordEditViewModel(
|
||||
@ -61,7 +61,8 @@ class RecordEditViewModel(
|
||||
|
||||
/** 用户是否手动编辑过标题;为 false 时标题随拍摄日期联动 */
|
||||
private var titleManuallyEdited = false
|
||||
|
||||
private var existingCreatedAt: Instant? = null
|
||||
private var originalPhotoRelativePath: String? = null
|
||||
init {
|
||||
if (recordId != null) {
|
||||
loadExistingRecord(recordId)
|
||||
@ -89,7 +90,13 @@ class RecordEditViewModel(
|
||||
viewModelScope.launch {
|
||||
val record = repository.observeById(id).first()
|
||||
if (record != null) {
|
||||
val absFile = repository.absoluteFileOf(record.photoPath)
|
||||
existingCreatedAt = record.createdAt
|
||||
originalPhotoRelativePath = record.photoPath
|
||||
val absFile = if (photoPath != null) {
|
||||
File(photoPath)
|
||||
} else {
|
||||
repository.absoluteFileOf(record.photoPath)
|
||||
}
|
||||
titleManuallyEdited = true
|
||||
_uiState.value = RecordEditUiState(
|
||||
loading = false,
|
||||
@ -150,9 +157,14 @@ class RecordEditViewModel(
|
||||
shootDate = state.shootDate,
|
||||
linkedDate = state.linkedDate,
|
||||
photoPath = relPath,
|
||||
createdAt = Instant.fromEpochMilliseconds(System.currentTimeMillis())
|
||||
createdAt = existingCreatedAt ?: Instant.fromEpochMilliseconds(System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
originalPhotoRelativePath?.let { oldPath ->
|
||||
if (oldPath != relPath) {
|
||||
repository.deletePhotoFile(oldPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repository.insert(
|
||||
DateRecord(
|
||||
|
||||
@ -1,21 +1,43 @@
|
||||
package plus.rua.project
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlinx.datetime.LocalDate
|
||||
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 kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* 新建记录标题预填与拍摄日期联动的单元测试。
|
||||
*
|
||||
* 新建模式的被测路径不会触发 DAO 调用,[FakeDateRecordDao] 返回空数据即可。
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class RecordEditViewModelTest {
|
||||
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun newViewModel(): RecordEditViewModel {
|
||||
val repository = DateRecorderRepository(FakeDateRecordDao(), File("build/tmp/record_edit_test"))
|
||||
return RecordEditViewModel(repository, photoPath = "/tmp/fake_photo.jpg", recordId = null)
|
||||
@ -51,12 +73,72 @@ class RecordEditViewModelTest {
|
||||
vm.onTitleChange("")
|
||||
assertFalse(vm.uiState.value.canSave)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun existingRecord_loadsRecordData_withoutNewPhotoPath() = runTest(testDispatcher) {
|
||||
val rootDir = File("build/tmp/record_edit_test")
|
||||
val existingRecord = DateRecord(
|
||||
id = 5L,
|
||||
title = "旧标题",
|
||||
note = "旧备注",
|
||||
shootDate = LocalDate(2026, 1, 1),
|
||||
linkedDate = null,
|
||||
photoPath = "Pictures/date_recorder/old.jpg",
|
||||
createdAt = Instant.fromEpochMilliseconds(100000L)
|
||||
)
|
||||
val dao = FakeDateRecordDao(existingRecord)
|
||||
val repository = DateRecorderRepository(dao, rootDir)
|
||||
|
||||
val vm = RecordEditViewModel(repository, photoPath = null, recordId = 5L)
|
||||
testScheduler.advanceUntilIdle()
|
||||
val state = vm.uiState.value
|
||||
assertEquals("旧标题", state.title)
|
||||
assertEquals("旧备注", state.note)
|
||||
assertTrue(state.isExistingRecord)
|
||||
assertEquals(File(rootDir, "Pictures/date_recorder/old.jpg").absolutePath, state.photoAbsolutePath)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun existingRecord_usesNewPhotoPath_whenPhotoPathProvided() = runTest(testDispatcher) {
|
||||
val rootDir = File("build/tmp/record_edit_test")
|
||||
val existingRecord = DateRecord(
|
||||
id = 5L,
|
||||
title = "旧标题",
|
||||
note = "旧备注",
|
||||
shootDate = LocalDate(2026, 1, 1),
|
||||
linkedDate = null,
|
||||
photoPath = "Pictures/date_recorder/old.jpg",
|
||||
createdAt = Instant.fromEpochMilliseconds(100000L)
|
||||
)
|
||||
val dao = FakeDateRecordDao(existingRecord)
|
||||
val repository = DateRecorderRepository(dao, rootDir)
|
||||
|
||||
val newPhotoFile = File(rootDir, "Pictures/date_recorder/edited.jpg")
|
||||
val vm = RecordEditViewModel(repository, photoPath = newPhotoFile.absolutePath, recordId = 5L)
|
||||
testScheduler.advanceUntilIdle()
|
||||
val state = vm.uiState.value
|
||||
assertEquals("旧标题", state.title)
|
||||
assertTrue(state.isExistingRecord)
|
||||
assertEquals(newPhotoFile.absolutePath, state.photoAbsolutePath)
|
||||
|
||||
vm.save()
|
||||
testScheduler.advanceUntilIdle()
|
||||
assertEquals(5L, dao.updatedRecord?.id)
|
||||
assertEquals("Pictures/date_recorder/edited.jpg", dao.updatedRecord?.photoPath)
|
||||
assertEquals(Instant.fromEpochMilliseconds(100000L), dao.updatedRecord?.createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeDateRecordDao : DateRecordDao {
|
||||
override fun getByIdFlow(id: Long): Flow<DateRecord?> = flowOf(null)
|
||||
override fun getAllFlow(): Flow<List<DateRecord>> = flowOf(emptyList())
|
||||
private class FakeDateRecordDao(
|
||||
private val record: DateRecord? = null
|
||||
) : DateRecordDao {
|
||||
var updatedRecord: DateRecord? = null
|
||||
override fun getByIdFlow(id: Long): Flow<DateRecord?> = flowOf(record)
|
||||
override fun getAllFlow(): Flow<List<DateRecord>> = flowOf(record?.let { listOf(it) } ?: emptyList())
|
||||
override suspend fun insert(record: DateRecord): Long = 0
|
||||
override suspend fun update(record: DateRecord) = Unit
|
||||
override suspend fun update(record: DateRecord) {
|
||||
this.updatedRecord = record
|
||||
}
|
||||
override suspend fun deleteByIds(ids: List<Long>): Int = 0
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user