fix: 修复CameraScreen编译错误,整合文件名模板功能
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user