Compare commits
No commits in common. "4d755dce315af7744479e3e8f61fa07631789c39" and "c3e4dc0e79f337a9be0dc0f89c92256e4a06f6b2" have entirely different histories.
4d755dce31
...
c3e4dc0e79
4 changed files with 4 additions and 60 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
|
- Gesture controls: drag to position, pinch to resize, two-finger rotate
|
||||||
- Slider panel for precise control of blur, falloff, size, angle, aspect ratio
|
- Slider panel for precise control of blur, falloff, size, angle, aspect ratio
|
||||||
- Multi-lens support on devices with multiple back cameras
|
- Multi-lens support on devices with multiple back cameras
|
||||||
- EXIF GPS tagging with user-toggleable opt-out (persisted across restarts)
|
- EXIF GPS tagging from device location
|
||||||
- Saves processed images to MediaStore (scoped storage)
|
- Saves processed images to MediaStore (scoped storage)
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
@ -97,5 +97,6 @@ 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.
|
- `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.
|
- 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.
|
- 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.
|
- Fragment shader uses `int` uniform branching in GLSL ES 1.00 — works but could be cleaner with ES 3.00.
|
||||||
|
|
|
||||||
21
LICENSE
21
LICENSE
|
|
@ -1,21 +0,0 @@
|
||||||
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,8 +39,6 @@ import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.FlipCameraAndroid
|
import androidx.compose.material.icons.filled.FlipCameraAndroid
|
||||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
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.material.icons.filled.Tune
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -109,7 +107,6 @@ fun CameraScreen(
|
||||||
val lastSavedUri by viewModel.lastSavedUri.collectAsState()
|
val lastSavedUri by viewModel.lastSavedUri.collectAsState()
|
||||||
val lastThumbnailBitmap by viewModel.lastThumbnailBitmap.collectAsState()
|
val lastThumbnailBitmap by viewModel.lastThumbnailBitmap.collectAsState()
|
||||||
val galleryPreviewBitmap by viewModel.galleryPreviewBitmap.collectAsState()
|
val galleryPreviewBitmap by viewModel.galleryPreviewBitmap.collectAsState()
|
||||||
val geotagEnabled by viewModel.geotagEnabled.collectAsState()
|
|
||||||
val isGalleryPreview = galleryPreviewBitmap != null
|
val isGalleryPreview = galleryPreviewBitmap != null
|
||||||
|
|
||||||
val zoomRatio by viewModel.cameraManager.zoomRatio.collectAsState()
|
val zoomRatio by viewModel.cameraManager.zoomRatio.collectAsState()
|
||||||
|
|
@ -257,20 +254,6 @@ fun CameraScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
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) {
|
if (!isGalleryPreview) {
|
||||||
// Camera flip button
|
// Camera flip button
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package no.naiv.tiltshift.ui
|
package no.naiv.tiltshift.ui
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
@ -41,16 +40,12 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CameraViewModel"
|
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. */
|
/** Max dimension for the preview source bitmap to keep effect computation fast. */
|
||||||
private const val PREVIEW_MAX_DIMENSION = 1024
|
private const val PREVIEW_MAX_DIMENSION = 1024
|
||||||
/** Debounce delay before recomputing preview to reduce GC pressure during slider drags. */
|
/** Debounce delay before recomputing preview to reduce GC pressure during slider drags. */
|
||||||
private const val PREVIEW_DEBOUNCE_MS = 80L
|
private const val PREVIEW_DEBOUNCE_MS = 80L
|
||||||
}
|
}
|
||||||
|
|
||||||
private val prefs = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
val cameraManager = CameraManager(application)
|
val cameraManager = CameraManager(application)
|
||||||
val photoSaver = PhotoSaver(application)
|
val photoSaver = PhotoSaver(application)
|
||||||
val captureHandler = ImageCaptureHandler(application, photoSaver)
|
val captureHandler = ImageCaptureHandler(application, photoSaver)
|
||||||
|
|
@ -103,20 +98,6 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
||||||
|
|
||||||
val isGalleryPreview: Boolean get() = _galleryBitmap.value != null
|
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
|
// Device state
|
||||||
private val _currentRotation = MutableStateFlow(Surface.ROTATION_0)
|
private val _currentRotation = MutableStateFlow(Surface.ROTATION_0)
|
||||||
val currentRotation: StateFlow<Int> = _currentRotation.asStateFlow()
|
val currentRotation: StateFlow<Int> = _currentRotation.asStateFlow()
|
||||||
|
|
@ -260,7 +241,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
||||||
val result = captureHandler.processExistingImage(
|
val result = captureHandler.processExistingImage(
|
||||||
imageUri = uri,
|
imageUri = uri,
|
||||||
blurParams = _blurParams.value,
|
blurParams = _blurParams.value,
|
||||||
location = effectiveLocation
|
location = _currentLocation.value
|
||||||
)
|
)
|
||||||
handleSaveResult(result)
|
handleSaveResult(result)
|
||||||
cancelGalleryPreview()
|
cancelGalleryPreview()
|
||||||
|
|
@ -282,7 +263,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
||||||
executor = cameraManager.getExecutor(),
|
executor = cameraManager.getExecutor(),
|
||||||
blurParams = _blurParams.value,
|
blurParams = _blurParams.value,
|
||||||
deviceRotation = _currentRotation.value,
|
deviceRotation = _currentRotation.value,
|
||||||
location = effectiveLocation,
|
location = _currentLocation.value,
|
||||||
isFrontCamera = cameraManager.isFrontCamera.value
|
isFrontCamera = cameraManager.isFrontCamera.value
|
||||||
)
|
)
|
||||||
handleSaveResult(result)
|
handleSaveResult(result)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue