diff --git a/CLAUDE.md b/CLAUDE.md index d9a4edf..22a6b5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt index 6cb7705..80ed80b 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt @@ -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( diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt index 9595e21..cb4d9da 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt @@ -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 = _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 = _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)