fix(recorder): preserve record ID when editing photo of an existing record

This commit is contained in:
xfy 2026-07-24 11:16:44 +08:00
parent c7aa611e52
commit bff6e37908
5 changed files with 109 additions and 9 deletions

View File

@ -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()

View File

@ -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)
}
)
}

View File

@ -95,7 +95,7 @@ class DateRecorderRepository(
/**
* 删除指定相对路径的照片文件文件不存在时静默忽略
*/
private fun deletePhotoFile(relativePath: String) {
internal fun deletePhotoFile(relativePath: String) {
runCatching { absoluteFileOf(relativePath).delete() }
}

View File

@ -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(

View File

@ -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
}