fix: 修复CameraScreen编译错误,整合文件名模板功能

This commit is contained in:
Developer
2026-04-23 18:16:45 +08:00
parent 9455720516
commit 427e9166b3
7 changed files with 115 additions and 58 deletions

View File

@@ -33,6 +33,7 @@ class PreferencesManager(private val context: Context) {
private val KEY_MERGE_TITLE = stringPreferencesKey("merge_title") private val KEY_MERGE_TITLE = stringPreferencesKey("merge_title")
private val KEY_MERGE_CONTENT = stringPreferencesKey("merge_content") private val KEY_MERGE_CONTENT = stringPreferencesKey("merge_content")
private val KEY_RECORDER_NAME = stringPreferencesKey("recorder_name") private val KEY_RECORDER_NAME = stringPreferencesKey("recorder_name")
private val KEY_FILE_NAME_TEMPLATE = stringPreferencesKey("file_name_template")
} }
val watermarkStyle: Flow<WatermarkStyle> = context.dataStore.data.map { prefs -> val watermarkStyle: Flow<WatermarkStyle> = context.dataStore.data.map { prefs ->
@@ -104,6 +105,10 @@ class PreferencesManager(private val context: Context) {
prefs[KEY_RECORDER_NAME] ?: "" prefs[KEY_RECORDER_NAME] ?: ""
} }
val fileNameTemplate: Flow<String> = context.dataStore.data.map { prefs ->
prefs[KEY_FILE_NAME_TEMPLATE] ?: "{project}_{inspector}_{date}"
}
suspend fun setWatermarkStyle(style: WatermarkStyle) { suspend fun setWatermarkStyle(style: WatermarkStyle) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[KEY_WATERMARK_STYLE] = style.name prefs[KEY_WATERMARK_STYLE] = style.name
@@ -169,4 +174,10 @@ class PreferencesManager(private val context: Context) {
prefs[KEY_MERGE_CONTENT] = content prefs[KEY_MERGE_CONTENT] = content
} }
} }
suspend fun setFileNameTemplate(template: String) {
context.dataStore.edit { prefs ->
prefs[KEY_FILE_NAME_TEMPLATE] = template
}
}
} }

View File

@@ -49,9 +49,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -76,8 +77,8 @@ import com.inspection.camera.util.ImageProcessor
import com.inspection.camera.util.LocationHelper import com.inspection.camera.util.LocationHelper
import com.inspection.camera.util.PermissionManager import com.inspection.camera.util.PermissionManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.io.File import java.io.File
@@ -90,7 +91,7 @@ fun CameraScreen(
onNavigateToSettings: () -> Unit, onNavigateToSettings: () -> Unit,
onNavigateToMerge: (List<Uri>) -> Unit, onNavigateToMerge: (List<Uri>) -> Unit,
preferencesManager: PreferencesManager preferencesManager: PreferencesManager
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -100,25 +101,46 @@ fun CameraScreen(
var flashMode by remember { mutableIntStateOf(ImageCapture.FLASH_MODE_AUTO) } var flashMode by remember { mutableIntStateOf(ImageCapture.FLASH_MODE_AUTO) }
var locationText by remember { mutableStateOf("") } var locationText by remember { mutableStateOf("") }
var recorderName by remember { mutableStateOf("") } var recorderName by remember { mutableStateOf("") }
var fileNameTemplate by remember { mutableStateOf("{project}_{inspector}_{date}") }
var isLocationLoading by remember { mutableStateOf(true) } var isLocationLoading by remember { mutableStateOf(true) }
var currentWatermarkStyle by remember { mutableStateOf(WatermarkStyle.Default) } var currentWatermarkStyle by remember { mutableStateOf(WatermarkStyle.Default) }
var currentImageQuality by remember { mutableStateOf(ImageQuality.Standard) } var currentImageQuality by remember { mutableStateOf(ImageQuality.Standard) }
var locationMode by remember { mutableStateOf(LocationMode.Network) } var locationMode by remember { mutableStateOf(LocationMode.Network) }
var showPermissionDeniedDialog by remember { mutableStateOf(false) } var showPermissionDeniedDialog by remember { mutableStateOf(false) }
var flashVisible by remember { mutableStateOf(false) }
val capturedImages = remember { mutableStateListOf<Uri>() } val capturedImages = remember { mutableStateListOf<Uri>() }
val locationHelper = remember { LocationHelper(context) } val locationHelper = remember { LocationHelper(context) }
val shutterSound = remember { MediaActionSound() } val shutterSound = remember { MediaActionSound() }
var flashVisible by remember { mutableStateOf(false) }
val deviceId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
) ?: ""
val computedFileName by remember(recorderName, fileNameTemplate) {
derivedStateOf {
ImageProcessor.generateFileName(
template = fileNameTemplate,
projectName = "",
deviceId = deviceId,
inspectorName = recorderName
)
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
shutterSound.load(MediaActionSound.SHUTTER_CLICK) shutterSound.load(MediaActionSound.SHUTTER_CLICK)
} }
// 读取记录人信息(偏好)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
preferencesManager.recorderName.collect { recorderName = it } preferencesManager.recorderName.collect { recorderName = it }
} }
LaunchedEffect(Unit) {
preferencesManager.fileNameTemplate.collect { fileNameTemplate = it }
}
// 权限状态 // 权限状态
val permissionsState = rememberMultiplePermissionsState( val permissionsState = rememberMultiplePermissionsState(
permissions = listOf( permissions = listOf(
@@ -181,18 +203,18 @@ fun CameraScreen(
Log.d("CameraScreen", "Getting location with mode: $locationMode") Log.d("CameraScreen", "Getting location with mode: $locationMode")
val useNetwork = locationMode == LocationMode.Network val useNetwork = locationMode == LocationMode.Network
var result: String? = null var result: String? = null
result = withTimeoutOrNull(10000) { result = withTimeoutOrNull(10000) {
locationHelper.getLocationInfo(useNetwork) locationHelper.getLocationInfo(useNetwork)
} }
if (result.isNullOrBlank() && useNetwork) { if (result.isNullOrBlank() && useNetwork) {
Log.d("CameraScreen", "Network location failed, falling back to GPS") Log.d("CameraScreen", "Network location failed, falling back to GPS")
result = withTimeoutOrNull(5000) { result = withTimeoutOrNull(5000) {
locationHelper.getLocationInfo(false) locationHelper.getLocationInfo(false)
} }
} }
locationText = result ?: if (useNetwork) "定位失败" else "GPS定位失败" locationText = result ?: if (useNetwork) "定位失败" else "GPS定位失败"
Log.d("CameraScreen", "Location result: $locationText") Log.d("CameraScreen", "Location result: $locationText")
} catch (e: Exception) { } catch (e: Exception) {
@@ -205,14 +227,14 @@ fun CameraScreen(
isLocationLoading = false isLocationLoading = false
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
// 闪屏效果覆盖层(拍照成功时显示) // 闪屏效果覆盖层(拍照成功时显示)
if (flashVisible) { if (flashVisible) {
Box(modifier = Modifier Box(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.White.copy(alpha = 0.9f)) .background(Color.White.copy(alpha = 0.9f))
) )
} }
if (permissionsState.allPermissionsGranted) { if (permissionsState.allPermissionsGranted) {
CameraContent( CameraContent(
flashMode = flashMode, flashMode = flashMode,
@@ -221,6 +243,11 @@ fun CameraScreen(
if (!isCapturing) { if (!isCapturing) {
isCapturing = true isCapturing = true
scope.launch { scope.launch {
val location = try {
locationHelper.getCurrentLocation()
} catch (e: Exception) {
null
}
capturePhoto( capturePhoto(
context = context, context = context,
imageCapture = imageCapture, imageCapture = imageCapture,
@@ -228,10 +255,11 @@ fun CameraScreen(
watermarkStyle = currentWatermarkStyle, watermarkStyle = currentWatermarkStyle,
imageQuality = currentImageQuality, imageQuality = currentImageQuality,
locationText = getValidLocationTextForPhoto(locationText, locationHelper), locationText = getValidLocationTextForPhoto(locationText, locationHelper),
latitude = location?.latitude,
longitude = location?.longitude,
onComplete = { uri -> onComplete = { uri ->
capturedImages.add(uri) capturedImages.add(uri)
isCapturing = false isCapturing = false
}, },
onError = { onError = {
isCapturing = false isCapturing = false
@@ -241,7 +269,8 @@ fun CameraScreen(
flashVisible = true flashVisible = true
scope.launch { delay(150); flashVisible = false } scope.launch { delay(150); flashVisible = false }
}, },
recorderName = recorderName recorderName = recorderName,
fileName = computedFileName
) )
} }
} }
@@ -340,7 +369,7 @@ private fun CameraContent(
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try { try {
cameraProvider.unbindAll() cameraProvider.unbindAll()
cameraProvider.bindToLifecycle( cameraProvider.bindToLifecycle(
lifecycleOwner, lifecycleOwner,
@@ -372,11 +401,6 @@ private fun CameraContent(
} }
} }
// 强制重组:当 flashMode 变化时触发
LaunchedEffect(flashMode) {
// 闪光灯变化时确保相机已绑定
}
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
// 相机预览 // 相机预览
AndroidView( AndroidView(
@@ -498,9 +522,9 @@ private fun PermissionRequest(
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = if (hasPermanentlyDeniedPermission) text = if (hasPermanentlyDeniedPermission)
"权限被永久拒绝,请在设置中手动开启权限" "权限被永久拒绝,请在设置中手动开启权限"
else else
"请授予权限以使用拍照和地点水印功能", "请授予权限以使用拍照和地点水印功能",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
@@ -522,20 +546,18 @@ private suspend fun getValidLocationTextForPhoto(
locationHelper: LocationHelper locationHelper: LocationHelper
): String { ): String {
Log.d("CameraScreen", "getValidLocationTextForPhoto called, currentLocationText=$currentLocationText") Log.d("CameraScreen", "getValidLocationTextForPhoto called, currentLocationText=$currentLocationText")
// 检查当前定位文本是否有效
val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限", "GPS定位失败") val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限", "GPS定位失败")
val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText) val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText)
if (!isInvalid) { if (!isInvalid) {
Log.d("CameraScreen", "Using current location text: $currentLocationText") Log.d("CameraScreen", "Using current location text: $currentLocationText")
return currentLocationText return currentLocationText
} }
// 尝试快速获取当前位置(使用缓存)
Log.d("CameraScreen", "Requesting new location...") Log.d("CameraScreen", "Requesting new location...")
val location = locationHelper.getCurrentLocation() val location = locationHelper.getCurrentLocation()
val result = location?.let { val result = location?.let {
"${"%.4f".format(it.latitude)}, ${"%.4f".format(it.longitude)}" "${"%.4f".format(it.latitude)}, ${"%.4f".format(it.longitude)}"
} ?: "未知地点" } ?: "未知地点"
Log.d("CameraScreen", "Got location result: $result") Log.d("CameraScreen", "Got location result: $result")
@@ -549,10 +571,13 @@ private fun capturePhoto(
watermarkStyle: WatermarkStyle, watermarkStyle: WatermarkStyle,
imageQuality: ImageQuality, imageQuality: ImageQuality,
locationText: String, locationText: String,
latitude: Double? = null,
longitude: Double? = null,
onComplete: (Uri) -> Unit, onComplete: (Uri) -> Unit,
onError: () -> Unit = {}, onError: () -> Unit = {},
onFeedback: (() -> Unit)? = null, onFeedback: (() -> Unit)? = null,
recorderName: String = "" recorderName: String = "",
fileName: String = ""
) { ) {
Log.d("CameraScreen", "capturePhoto called, locationText=$locationText") Log.d("CameraScreen", "capturePhoto called, locationText=$locationText")
val photoFile = File( val photoFile = File(
@@ -563,9 +588,8 @@ private fun capturePhoto(
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
val executor = Executors.newSingleThreadExecutor() val executor = Executors.newSingleThreadExecutor()
// 拍照时立即播放快门声音和闪屏效果
onFeedback?.invoke() onFeedback?.invoke()
imageCapture.takePicture( imageCapture.takePicture(
outputOptions, outputOptions,
executor, executor,
@@ -578,9 +602,9 @@ private fun capturePhoto(
onError() onError()
return return
} }
try { try {
val timeText = ImageProcessor.getCurrentTimeText() val timeText = ImageProcessor.getCurrentTimeText()
Log.d("CameraScreen", "Adding watermark: timeText=$timeText, locationText=$locationText") Log.d("CameraScreen", "Adding watermark: timeText=$timeText, locationText=$locationText")
val watermarkedBitmap = ImageProcessor.addWatermark( val watermarkedBitmap = ImageProcessor.addWatermark(
bitmap, bitmap,
@@ -590,9 +614,10 @@ private fun capturePhoto(
recorderName recorderName
) )
// 保存到相册 val effectiveFileName = fileName.ifBlank {
val fileName = ImageProcessor.generateFileName("") ImageProcessor.generateFileName(inspectorName = recorderName)
val uri = ImageProcessor.saveToGallery(context, watermarkedBitmap, fileName, imageQuality.quality) }
val uri = ImageProcessor.saveToGallery(context, watermarkedBitmap, effectiveFileName, imageQuality.quality, latitude, longitude)
bitmap.recycle() bitmap.recycle()
watermarkedBitmap.recycle() watermarkedBitmap.recycle()

View File

@@ -76,10 +76,6 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
var isSelectionMode by remember { mutableStateOf(false) } var isSelectionMode by remember { mutableStateOf(false) }
var selectedImageUri by remember { mutableStateOf<Uri?>(null) } var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
LaunchedEffect(Unit) {
images = loadImagesFromGallery(context)
}
val sections: List<GallerySection> = remember(images) { val sections: List<GallerySection> = remember(images) {
categorizeImages(images) categorizeImages(images)
} }

View File

@@ -601,7 +601,7 @@ fun MergeScreen(
) )
} }
val fileName = ImageProcessor.generateFileName(title.ifBlank { "合成" }) val fileName = ImageProcessor.generateFileName(projectName = title.ifBlank { "合成" })
val uri = ImageProcessor.saveToGallery(context, bitmap, fileName) val uri = ImageProcessor.saveToGallery(context, bitmap, fileName)
uri?.let { onMergeComplete(it) } uri?.let { onMergeComplete(it) }
showSaveDialog = false showSaveDialog = false

View File

@@ -35,6 +35,8 @@ object ImageProcessor {
private val dateFormat = SimpleDateFormat("yyyy年M月d日 HH:mm:ss", Locale.getDefault()) private val dateFormat = SimpleDateFormat("yyyy年M月d日 HH:mm:ss", Locale.getDefault())
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault()) private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
private val dateOnlyFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
private val timeOnlyFormat = SimpleDateFormat("HHmmss", Locale.getDefault())
/** /**
* 尝试加载图片 Bitmap * 尝试加载图片 Bitmap
@@ -83,14 +85,39 @@ object ImageProcessor {
/** /**
* 生成文件名 * 生成文件名
* 支持模板:{project}_{device}_{inspector}_{date}_{time}.jpg
*/ */
fun generateFileName(theme: String): String { fun generateFileName(
val timeStr = fileNameFormat.format(Date()) template: String = "{project}_{inspector}_{date}",
return if (theme.isNotBlank()) { projectName: String = "",
"巡检报告_${theme}_$timeStr.jpg" deviceId: String = "",
} else { inspectorName: String = ""
"巡检报告_$timeStr.jpg" ): String {
val now = Date()
val dateStr = dateOnlyFormat.format(now)
val timeStr = timeOnlyFormat.format(now)
var fileName = template
.replace("{project}", projectName.ifBlank { "巡检" })
.replace("{device}", deviceId)
.replace("{inspector}", inspectorName)
.replace("{date}", dateStr)
.replace("{time}", timeStr)
// 清理可能的双下划线
fileName = fileName.replace("__", "_")
// 确保文件名不为空
if (fileName.isBlank() || fileName == ".jpg") {
fileName = "巡检_${dateStr}_${timeStr}"
} }
// 确保以 .jpg 结尾
if (!fileName.endsWith(".jpg", ignoreCase = true)) {
fileName += ".jpg"
}
return fileName
} }
/** /**

View File

@@ -24,16 +24,14 @@ class ImageProcessorTest {
@Test @Test
fun `test generateFileName with theme`() { fun `test generateFileName with theme`() {
val theme = "日常巡检" val theme = "日常巡检"
val result = ImageProcessor.generateFileName(theme) val result = ImageProcessor.generateFileName(projectName = theme)
assertTrue(result.contains("巡检报告_"))
assertTrue(result.contains(theme)) assertTrue(result.contains(theme))
assertTrue(result.endsWith(".jpg")) assertTrue(result.endsWith(".jpg"))
} }
@Test @Test
fun `test generateFileName without theme`() { fun `test generateFileName without theme`() {
val result = ImageProcessor.generateFileName("") val result = ImageProcessor.generateFileName(projectName = "")
assertTrue(result.contains("巡检报告_"))
assertTrue(result.endsWith(".jpg")) assertTrue(result.endsWith(".jpg"))
} }
@@ -49,7 +47,7 @@ class ImageProcessorTest {
@Test @Test
fun `test ImageQuality values`() { fun `test ImageQuality values`() {
assertEquals(95, ImageQuality.High.quality) assertEquals(100, ImageQuality.High.quality)
assertEquals(85, ImageQuality.Standard.quality) assertEquals(85, ImageQuality.Standard.quality)
assertEquals(70, ImageQuality.Low.quality) assertEquals(70, ImageQuality.Low.quality)
} }