Compare commits
2 commits
c3e4dc0e79
...
4d755dce31
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d755dce31 | |||
| e9bef0607f |
4 changed files with 60 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.
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Ole-Morten Duesund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -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