Add GPS geotagging toggle to camera top bar
Users can now opt out of embedding GPS coordinates in photos. The toggle is persisted via SharedPreferences and defaults to enabled. When disabled, effectiveLocation returns null so no EXIF GPS tags are written. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3e4dc0e79
commit
e9bef0607f
3 changed files with 39 additions and 4 deletions
|
|
@ -10,7 +10,7 @@ Android camera app that applies a real-time tilt-shift (miniature/diorama) blur
|
|||
- Gesture controls: drag to position, pinch to resize, two-finger rotate
|
||||
- Slider panel for precise control of blur, falloff, size, angle, aspect ratio
|
||||
- Multi-lens support on devices with multiple back cameras
|
||||
- EXIF GPS tagging from device location
|
||||
- EXIF GPS tagging with user-toggleable opt-out (persisted across restarts)
|
||||
- Saves processed images to MediaStore (scoped storage)
|
||||
|
||||
## Architecture
|
||||
|
|
@ -97,6 +97,5 @@ Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after
|
|||
|
||||
- `minSdk = 35` (Android 15) — intentional for personal use. Lower to 26-29 if distributing.
|
||||
- Accompanist Permissions (`0.36.0`) is deprecated; should migrate to first-party `activity-compose` API.
|
||||
- No user-facing toggle to disable GPS tagging — location is embedded whenever permission is granted.
|
||||
- Dependencies are pinned to late-2024 versions; periodic bumps recommended.
|
||||
- Fragment shader uses `int` uniform branching in GLSL ES 1.00 — works but could be cleaner with ES 3.00.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import androidx.compose.material.icons.filled.Close
|
|||
import androidx.compose.material.icons.filled.FlipCameraAndroid
|
||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import androidx.compose.material.icons.filled.LocationOff
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -107,6 +109,7 @@ fun CameraScreen(
|
|||
val lastSavedUri by viewModel.lastSavedUri.collectAsState()
|
||||
val lastThumbnailBitmap by viewModel.lastThumbnailBitmap.collectAsState()
|
||||
val galleryPreviewBitmap by viewModel.galleryPreviewBitmap.collectAsState()
|
||||
val geotagEnabled by viewModel.geotagEnabled.collectAsState()
|
||||
val isGalleryPreview = galleryPreviewBitmap != null
|
||||
|
||||
val zoomRatio by viewModel.cameraManager.zoomRatio.collectAsState()
|
||||
|
|
@ -254,6 +257,20 @@ fun CameraScreen(
|
|||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// GPS geotagging toggle
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.toggleGeotag()
|
||||
viewModel.haptics.tick()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (geotagEnabled) Icons.Default.LocationOn else Icons.Default.LocationOff,
|
||||
contentDescription = if (geotagEnabled) "Disable GPS geotagging" else "Enable GPS geotagging",
|
||||
tint = if (geotagEnabled) Color.White else Color.White.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
|
||||
if (!isGalleryPreview) {
|
||||
// Camera flip button
|
||||
IconButton(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package no.naiv.tiltshift.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
|
|
@ -40,12 +41,16 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
companion object {
|
||||
private const val TAG = "CameraViewModel"
|
||||
private const val PREFS_NAME = "tiltshift_prefs"
|
||||
private const val KEY_GEOTAG_ENABLED = "geotag_enabled"
|
||||
/** Max dimension for the preview source bitmap to keep effect computation fast. */
|
||||
private const val PREVIEW_MAX_DIMENSION = 1024
|
||||
/** Debounce delay before recomputing preview to reduce GC pressure during slider drags. */
|
||||
private const val PREVIEW_DEBOUNCE_MS = 80L
|
||||
}
|
||||
|
||||
private val prefs = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val cameraManager = CameraManager(application)
|
||||
val photoSaver = PhotoSaver(application)
|
||||
val captureHandler = ImageCaptureHandler(application, photoSaver)
|
||||
|
|
@ -98,6 +103,20 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
val isGalleryPreview: Boolean get() = _galleryBitmap.value != null
|
||||
|
||||
// GPS geotagging toggle (persisted)
|
||||
private val _geotagEnabled = MutableStateFlow(prefs.getBoolean(KEY_GEOTAG_ENABLED, true))
|
||||
val geotagEnabled: StateFlow<Boolean> = _geotagEnabled.asStateFlow()
|
||||
|
||||
fun toggleGeotag() {
|
||||
val newValue = !_geotagEnabled.value
|
||||
_geotagEnabled.value = newValue
|
||||
prefs.edit().putBoolean(KEY_GEOTAG_ENABLED, newValue).apply()
|
||||
}
|
||||
|
||||
/** Returns current location only if geotagging is enabled. */
|
||||
private val effectiveLocation: Location?
|
||||
get() = if (_geotagEnabled.value) _currentLocation.value else null
|
||||
|
||||
// Device state
|
||||
private val _currentRotation = MutableStateFlow(Surface.ROTATION_0)
|
||||
val currentRotation: StateFlow<Int> = _currentRotation.asStateFlow()
|
||||
|
|
@ -241,7 +260,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
val result = captureHandler.processExistingImage(
|
||||
imageUri = uri,
|
||||
blurParams = _blurParams.value,
|
||||
location = _currentLocation.value
|
||||
location = effectiveLocation
|
||||
)
|
||||
handleSaveResult(result)
|
||||
cancelGalleryPreview()
|
||||
|
|
@ -263,7 +282,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
executor = cameraManager.getExecutor(),
|
||||
blurParams = _blurParams.value,
|
||||
deviceRotation = _currentRotation.value,
|
||||
location = _currentLocation.value,
|
||||
location = effectiveLocation,
|
||||
isFrontCamera = cameraManager.isFrontCamera.value
|
||||
)
|
||||
handleSaveResult(result)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue