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

View File

@@ -76,10 +76,6 @@ fun GalleryScreen(onNavigateBack: () -> Unit) {
var isSelectionMode by remember { mutableStateOf(false) }
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
LaunchedEffect(Unit) {
images = loadImagesFromGallery(context)
}
val sections: List<GallerySection> = remember(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)
uri?.let { onMergeComplete(it) }
showSaveDialog = false

View File

@@ -35,6 +35,8 @@ object ImageProcessor {
private val dateFormat = SimpleDateFormat("yyyy年M月d日 HH:mm:ss", Locale.getDefault())
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
private val dateOnlyFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
private val timeOnlyFormat = SimpleDateFormat("HHmmss", Locale.getDefault())
/**
* 尝试加载图片 Bitmap
@@ -83,14 +85,39 @@ object ImageProcessor {
/**
* 生成文件名
* 支持模板:{project}_{device}_{inspector}_{date}_{time}.jpg
*/
fun generateFileName(theme: String): String {
val timeStr = fileNameFormat.format(Date())
return if (theme.isNotBlank()) {
"巡检报告_${theme}_$timeStr.jpg"
} else {
"巡检报告_$timeStr.jpg"
fun generateFileName(
template: String = "{project}_{inspector}_{date}",
projectName: String = "",
deviceId: String = "",
inspectorName: String = ""
): 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
fun `test generateFileName with theme`() {
val theme = "日常巡检"
val result = ImageProcessor.generateFileName(theme)
assertTrue(result.contains("巡检报告_"))
val result = ImageProcessor.generateFileName(projectName = theme)
assertTrue(result.contains(theme))
assertTrue(result.endsWith(".jpg"))
}
@Test
fun `test generateFileName without theme`() {
val result = ImageProcessor.generateFileName("")
assertTrue(result.contains("巡检报告_"))
val result = ImageProcessor.generateFileName(projectName = "")
assertTrue(result.endsWith(".jpg"))
}
@@ -49,7 +47,7 @@ class ImageProcessorTest {
@Test
fun `test ImageQuality values`() {
assertEquals(95, ImageQuality.High.quality)
assertEquals(100, ImageQuality.High.quality)
assertEquals(85, ImageQuality.Standard.quality)
assertEquals(70, ImageQuality.Low.quality)
}