diff --git a/core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ExifEdit.kt b/core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ExifEdit.kt
new file mode 100644
index 0000000000..b8afb0b966
--- /dev/null
+++ b/core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ExifEdit.kt
@@ -0,0 +1,137 @@
+package ru.tech.imageresizershrinker.core.resources.icons
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Icons.Outlined.ExifEdit: ImageVector by lazy {
+ ImageVector.Builder(
+ name = "Outlined.ExifEdit",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f
+ ).apply {
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(17.138f, 9.874f)
+ lineToRelative(-7.023f, -7.023f)
+ curveTo(9.803f, 2.539f, 9.413f, 2.383f, 9.023f, 2.383f)
+ horizontalLineTo(3.561f)
+ curveTo(2.702f, 2.383f, 2f, 3.085f, 2f, 3.944f)
+ verticalLineToRelative(5.462f)
+ curveToRelative(0f, 0.39f, 0.156f, 0.78f, 0.468f, 1.092f)
+ lineToRelative(7.023f, 7.023f)
+ curveToRelative(0.312f, 0.312f, 0.702f, 0.468f, 1.092f, 0.468f)
+ reflectiveCurveToRelative(0.78f, -0.156f, 1.092f, -0.468f)
+ lineToRelative(5.462f, -5.462f)
+ curveToRelative(0.312f, -0.312f, 0.468f, -0.702f, 0.468f, -1.092f)
+ curveTo(17.606f, 10.576f, 17.45f, 10.186f, 17.138f, 9.874f)
+ close()
+ moveTo(10.584f, 16.429f)
+ lineToRelative(-7.023f, -7.023f)
+ verticalLineTo(3.944f)
+ horizontalLineTo(9.023f)
+ lineToRelative(7.023f, 7.023f)
+ lineTo(10.584f, 16.429f)
+ close()
+ }
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(5.322f, 4.724f)
+ curveToRelative(0.523f, 0f, 0.981f, 0.458f, 0.981f, 0.981f)
+ reflectiveCurveTo(5.845f, 6.686f, 5.322f, 6.686f)
+ reflectiveCurveTo(4.341f, 6.228f, 4.341f, 5.705f)
+ reflectiveCurveTo(4.799f, 4.724f, 5.322f, 4.724f)
+ }
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(9.605f, 12.27f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(2.302f, -2.302f)
+ lineToRelative(0.576f, 0.576f)
+ lineToRelative(-2.302f, 2.302f)
+ }
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(13.058f, 12.27f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(-0.384f, 0.384f)
+ lineToRelative(0.576f, 0.576f)
+ lineToRelative(-0.576f, 0.576f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(-0.767f, 0.767f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(2.302f, -2.302f)
+ lineToRelative(1.151f, 1.151f)
+ close()
+ }
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(8.117f, 7.329f)
+ lineToRelative(0.576f, 0.576f)
+ lineToRelative(0.576f, -0.576f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(-0.904f, 0.904f)
+ lineToRelative(-0.495f, 0.495f)
+ lineToRelative(-0.904f, 0.904f)
+ lineToRelative(0.576f, 0.576f)
+ lineToRelative(0.576f, 0.576f)
+ lineToRelative(0.576f, -0.576f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(0.192f, -0.192f)
+ lineToRelative(0.136f, -0.136f)
+ lineToRelative(0.576f, 0.576f)
+ lineToRelative(0.495f, -0.495f)
+ lineToRelative(-0.576f, -0.576f)
+ lineToRelative(0.136f, -0.136f)
+ close()
+ }
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(9.612f, 7.659f)
+ lineTo(8.917f, 9.28f)
+ lineTo(7.295f, 9.975f)
+ lineToRelative(0.463f, 0.463f)
+ lineToRelative(0.811f, -0.347f)
+ lineToRelative(-0.347f, 0.811f)
+ lineToRelative(0.463f, 0.463f)
+ lineToRelative(0.695f, -1.621f)
+ lineToRelative(1.621f, -0.695f)
+ lineToRelative(-0.463f, -0.463f)
+ lineTo(9.728f, 8.933f)
+ lineToRelative(0.347f, -0.811f)
+ lineTo(9.612f, 7.659f)
+ close()
+ }
+ path(fill = SolidColor(Color(0xFF000000))) {
+ moveTo(21.886f, 14.399f)
+ curveToRelative(-0.076f, -0.186f, -0.182f, -0.355f, -0.317f, -0.507f)
+ lineToRelative(-0.937f, -0.937f)
+ curveToRelative(-0.152f, -0.152f, -0.321f, -0.266f, -0.507f, -0.342f)
+ curveTo(19.94f, 12.538f, 19.746f, 12.5f, 19.543f, 12.5f)
+ curveToRelative(-0.186f, 0f, -0.371f, 0.034f, -0.557f, 0.101f)
+ curveToRelative(-0.186f, 0.068f, -0.355f, 0.177f, -0.507f, 0.329f)
+ lineToRelative(-5.293f, 5.268f)
+ curveToRelative(-0.101f, 0.101f, -0.177f, 0.215f, -0.228f, 0.342f)
+ reflectiveCurveToRelative(-0.076f, 0.258f, -0.076f, 0.393f)
+ verticalLineToRelative(1.671f)
+ curveToRelative(0f, 0.287f, 0.097f, 0.528f, 0.291f, 0.722f)
+ curveToRelative(0.194f, 0.194f, 0.435f, 0.291f, 0.722f, 0.291f)
+ horizontalLineToRelative(1.671f)
+ curveToRelative(0.135f, 0f, 0.266f, -0.025f, 0.392f, -0.076f)
+ curveToRelative(0.127f, -0.051f, 0.241f, -0.127f, 0.342f, -0.228f)
+ lineToRelative(5.268f, -5.268f)
+ curveToRelative(0.152f, -0.152f, 0.262f, -0.325f, 0.329f, -0.519f)
+ curveTo(21.966f, 15.332f, 22f, 15.142f, 22f, 14.957f)
+ reflectiveCurveTo(21.962f, 14.585f, 21.886f, 14.399f)
+ close()
+ moveTo(15.365f, 20.098f)
+ horizontalLineTo(14.402f)
+ verticalLineToRelative(-0.962f)
+ lineToRelative(3.09f, -3.064f)
+ lineToRelative(0.481f, 0.456f)
+ lineToRelative(0.456f, 0.481f)
+ lineTo(15.365f, 20.098f)
+ close()
+ }
+ }.build()
+}
diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml
index 9eb4138e73..c9c1283421 100644
--- a/core/resources/src/main/res/values/strings.xml
+++ b/core/resources/src/main/res/values/strings.xml
@@ -1560,4 +1560,7 @@
Pages Selection
Tool Exit Confirmation
If you have unsaved changes while using particular tools and try to close it, then confirm dialog will be shown
+ Edit EXIF
+ Change metadata of single image without recompression
+ Tap to edit available tags
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ClipboardUtils.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ClipboardUtils.kt
index ee8e09e261..3f7d2c5995 100644
--- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ClipboardUtils.kt
+++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ClipboardUtils.kt
@@ -112,9 +112,13 @@ fun rememberClipboardText(): State {
return clip
}
-fun ClipboardManager?.clipList(): List = this?.primaryClip?.clipList() ?: emptyList()
+fun ClipboardManager?.clipList(): List = runCatching {
+ this?.primaryClip?.clipList()
+}.getOrNull() ?: emptyList()
-fun ClipboardManager?.clipText(): String = this?.primaryClip?.getItemAt(0)?.text?.toString() ?: ""
+fun ClipboardManager?.clipText(): String = runCatching {
+ this?.primaryClip?.getItemAt(0)?.text?.toString()
+}.getOrNull() ?: ""
fun ClipData.clipList() = List(
size = itemCount,
@@ -123,12 +127,15 @@ fun ClipData.clipList() = List(
}
).filterNotNull()
-fun List.toClipData(): ClipData? {
+fun List.toClipData(
+ description: String = "Images"
+): ClipData? {
if (this.isEmpty()) return null
return ClipData(
ClipDescription(
- "Images", arrayOf("image/*")
+ description,
+ arrayOf("image/*")
),
ClipData.Item(this.first())
).apply {
@@ -139,11 +146,12 @@ fun List.toClipData(): ClipData? {
}
fun Uri.asClip(
- context: Context
+ context: Context,
+ label: String = "Image"
): ClipEntry = ClipEntry(
ClipData.newUri(
context.contentResolver,
- "IMAGE",
+ label,
this
)
)
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt
index 417b6dae5e..8d2dd17285 100644
--- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt
+++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt
@@ -710,78 +710,102 @@ sealed class Screen(
subtitle = 0
)
+ @Serializable
+ data class EditExif(
+ val uri: KUri? = null,
+ ) : Screen(
+ id = 37,
+ title = R.string.edit_exif_screen,
+ subtitle = R.string.edit_exif_screen_sub
+ )
+
companion object {
val typedEntries by lazy {
listOf(
- listOf(
- SingleEdit(),
- ResizeAndConvert(),
- FormatConversion(),
- Crop(),
- WeightResize(),
- LimitResize(),
- DeleteExif(),
- ) to Triple(
- R.string.edit,
- Icons.Rounded.MiniEditLarge,
- Icons.Outlined.MiniEditLarge
+ ScreenGroup(
+ entries = listOf(
+ SingleEdit(),
+ ResizeAndConvert(),
+ FormatConversion(),
+ Crop(),
+ WeightResize(),
+ LimitResize(),
+ EditExif(),
+ DeleteExif(),
+ ),
+ title = R.string.edit,
+ selectedIcon = Icons.Rounded.MiniEditLarge,
+ baseIcon = Icons.Outlined.MiniEditLarge
),
- listOf(
- Filter(),
- Draw(),
- EraseBackground(),
- MarkupLayers(),
- CollageMaker(),
- ImageStitching(),
- ImageStacking(),
- ImageSplitting(),
- Watermarking(),
- GradientMaker(),
- NoiseGeneration,
- ) to Triple(
- R.string.create,
- Icons.Filled.AutoAwesome,
- Icons.Outlined.AutoAwesome
+ ScreenGroup(
+ entries = listOf(
+ Filter(),
+ Draw(),
+ EraseBackground(),
+ MarkupLayers(),
+ CollageMaker(),
+ ImageStitching(),
+ ImageStacking(),
+ ImageSplitting(),
+ Watermarking(),
+ GradientMaker(),
+ NoiseGeneration,
+ ),
+ title = R.string.create,
+ selectedIcon = Icons.Filled.AutoAwesome,
+ baseIcon = Icons.Outlined.AutoAwesome
),
- listOf(
- PickColorFromImage(),
- RecognizeText(),
- Compare(),
- ImagePreview(),
- Base64Tools(),
- SvgMaker(),
- GeneratePalette(),
- LoadNetImage(),
- ) to Triple(
- R.string.image,
- Icons.Filled.FilterHdr,
- Icons.Outlined.FilterHdr
+ ScreenGroup(
+ entries = listOf(
+ PickColorFromImage(),
+ RecognizeText(),
+ Compare(),
+ ImagePreview(),
+ Base64Tools(),
+ SvgMaker(),
+ GeneratePalette(),
+ LoadNetImage(),
+ ),
+ title = R.string.image,
+ selectedIcon = Icons.Filled.FilterHdr,
+ baseIcon = Icons.Outlined.FilterHdr
),
- listOf(
- PdfTools(),
- DocumentScanner,
- ScanQrCode(),
- ColorTools,
- GifTools(),
- Cipher(),
- ChecksumTools(),
- Zip(),
- JxlTools(),
- ApngTools(),
- WebpTools()
- ) to Triple(
- R.string.tools,
- Icons.Rounded.Toolbox,
- Icons.Outlined.Toolbox
+ ScreenGroup(
+ entries = listOf(
+ PdfTools(),
+ DocumentScanner,
+ ScanQrCode(),
+ ColorTools,
+ GifTools(),
+ Cipher(),
+ ChecksumTools(),
+ Zip(),
+ JxlTools(),
+ ApngTools(),
+ WebpTools()
+ ),
+ title = R.string.tools,
+ selectedIcon = Icons.Rounded.Toolbox,
+ baseIcon = Icons.Outlined.Toolbox
)
)
}
+
val entries by lazy {
- typedEntries.flatMap { it.first }.sortedBy { it.id }
+ typedEntries.flatMap { it.entries }.sortedBy { it.id }
}
- const val FEATURES_COUNT = 60
+ const val FEATURES_COUNT = 61
}
}
+data class ScreenGroup(
+ val entries: List,
+ @StringRes val title: Int,
+ val selectedIcon: ImageVector,
+ val baseIcon: ImageVector
+) {
+ fun icon(isSelected: Boolean) = if (isSelected) selectedIcon else baseIcon
+}
+
private typealias KUri = @Serializable(UriSerializer::class) Uri
\ No newline at end of file
diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/ScreenUtils.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/ScreenUtils.kt
index 78de6ceb94..0b49085781 100644
--- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/ScreenUtils.kt
+++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/ScreenUtils.kt
@@ -47,6 +47,7 @@ import ru.tech.imageresizershrinker.core.resources.icons.CropSmall
import ru.tech.imageresizershrinker.core.resources.icons.Draw
import ru.tech.imageresizershrinker.core.resources.icons.Encrypted
import ru.tech.imageresizershrinker.core.resources.icons.Exif
+import ru.tech.imageresizershrinker.core.resources.icons.ExifEdit
import ru.tech.imageresizershrinker.core.resources.icons.ImageCombine
import ru.tech.imageresizershrinker.core.resources.icons.ImageConvert
import ru.tech.imageresizershrinker.core.resources.icons.ImageDownload
@@ -111,6 +112,7 @@ internal fun Screen.simpleName(): String? = when (this) {
is Screen.Base64Tools -> "Base64_Tools"
is Screen.ChecksumTools -> "Checksum_Tools"
is Screen.MeshGradients -> "Mesh_Gradients"
+ is Screen.EditExif -> "Edit_EXIF"
}
internal fun Screen.icon(): ImageVector? = when (this) {
@@ -157,6 +159,7 @@ internal fun Screen.icon(): ImageVector? = when (this) {
is Screen.MarkupLayers -> Icons.Outlined.Stack
is Screen.Base64Tools -> Icons.Outlined.Base64
is Screen.ChecksumTools -> Icons.Rounded.Tag
+ is Screen.EditExif -> Icons.Outlined.ExifEdit
}
internal object UriSerializer : KSerializer {
diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/image/Picture.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/image/Picture.kt
index a06ccb165b..9159bb6362 100644
--- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/image/Picture.kt
+++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/image/Picture.kt
@@ -68,7 +68,7 @@ import ru.tech.imageresizershrinker.core.ui.widget.modifier.transparencyChecker
fun Picture(
model: Any?,
modifier: Modifier = Modifier,
- transformations: List = emptyList(),
+ transformations: List? = null,
manualImageLoader: ImageLoader? = null,
contentDescription: String? = null,
shape: Shape = RectangleShape,
@@ -131,7 +131,7 @@ fun Picture(
.crossfade(crossfadeEnabled)
.allowHardware(allowHardware)
.transformations(
- transformations + hdrTransformation
+ (transformations ?: emptyList()) + hdrTransformation
)
.build()
}
diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/sheets/PickImageFromUrisSheet.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/sheets/PickImageFromUrisSheet.kt
index 87cc66b2ce..7adaf37167 100644
--- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/sheets/PickImageFromUrisSheet.kt
+++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/sheets/PickImageFromUrisSheet.kt
@@ -68,7 +68,7 @@ import ru.tech.imageresizershrinker.core.ui.widget.text.TitleItem
fun PickImageFromUrisSheet(
visible: Boolean,
onDismiss: () -> Unit,
- transformations: List,
+ transformations: List? = null,
uris: List?,
selectedUri: Uri?,
onUriRemoved: (Uri) -> Unit,
diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt
index 4e9a9d47d5..db3b38af47 100644
--- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt
+++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt
@@ -141,6 +141,7 @@ internal fun List.screenList(
),
Screen.SvgMaker(uris),
Screen.Zip(uris),
+ Screen.EditExif(uris.firstOrNull()),
Screen.DeleteExif(uris),
Screen.LimitResize(uris)
).let {
diff --git a/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifContent.kt b/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifContent.kt
index 74cbbc9613..1921d91c57 100644
--- a/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifContent.kt
+++ b/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifContent.kt
@@ -35,7 +35,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import ru.tech.imageresizershrinker.core.domain.image.model.ImageInfo
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.resources.icons.Exif
import ru.tech.imageresizershrinker.core.resources.icons.MiniEdit
@@ -277,11 +276,6 @@ fun DeleteExifContent(
)
PickImageFromUrisSheet(
- transformations = listOf(
- component.imageInfoTransformationFactory(
- imageInfo = ImageInfo()
- )
- ),
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false
diff --git a/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/screenLogic/DeleteExifComponent.kt b/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/screenLogic/DeleteExifComponent.kt
index 32b33f9698..094ea72bdb 100644
--- a/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/screenLogic/DeleteExifComponent.kt
+++ b/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/screenLogic/DeleteExifComponent.kt
@@ -41,7 +41,6 @@ import ru.tech.imageresizershrinker.core.domain.saving.FilenameCreator
import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget
import ru.tech.imageresizershrinker.core.domain.saving.model.SaveResult
import ru.tech.imageresizershrinker.core.domain.utils.smartJob
-import ru.tech.imageresizershrinker.core.ui.transformation.ImageInfoTransformation
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.core.ui.utils.state.update
@@ -56,7 +55,6 @@ class DeleteExifComponent @AssistedInject internal constructor(
private val imageScaler: ImageScaler,
private val shareProvider: ShareProvider,
private val filenameCreator: FilenameCreator,
- val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
diff --git a/feature/edit-exif/.gitignore b/feature/edit-exif/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/feature/edit-exif/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/edit-exif/build.gradle.kts b/feature/edit-exif/build.gradle.kts
new file mode 100644
index 0000000000..193bc0e2da
--- /dev/null
+++ b/feature/edit-exif/build.gradle.kts
@@ -0,0 +1,25 @@
+/*
+ * ImageToolbox is an image editor for android
+ * Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * You should have received a copy of the Apache License
+ * along with this program. If not, see .
+ */
+
+plugins {
+ alias(libs.plugins.image.toolbox.library)
+ alias(libs.plugins.image.toolbox.feature)
+ alias(libs.plugins.image.toolbox.hilt)
+ alias(libs.plugins.image.toolbox.compose)
+}
+
+android.namespace = "ru.tech.imageresizershrinker.feature.edit_exif"
\ No newline at end of file
diff --git a/feature/edit-exif/src/main/AndroidManifest.xml b/feature/edit-exif/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..44008a4332
--- /dev/null
+++ b/feature/edit-exif/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/EditExifContent.kt b/feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/EditExifContent.kt
new file mode 100644
index 0000000000..ae2dc0cc5e
--- /dev/null
+++ b/feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/EditExifContent.kt
@@ -0,0 +1,281 @@
+/*
+ * ImageToolbox is an image editor for android
+ * Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * You should have received a copy of the Apache License
+ * along with this program. If not, see .
+ */
+
+package ru.tech.imageresizershrinker.feature.edit_exif.presentation
+
+import android.net.Uri
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil3.toBitmap
+import ru.tech.imageresizershrinker.core.data.utils.safeAspectRatio
+import ru.tech.imageresizershrinker.core.resources.R
+import ru.tech.imageresizershrinker.core.resources.icons.Exif
+import ru.tech.imageresizershrinker.core.resources.icons.MiniEdit
+import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
+import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
+import ru.tech.imageresizershrinker.core.ui.utils.helper.ImageUtils.fileSize
+import ru.tech.imageresizershrinker.core.ui.utils.helper.asClip
+import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
+import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalComponentActivity
+import ru.tech.imageresizershrinker.core.ui.utils.provider.rememberLocalEssentials
+import ru.tech.imageresizershrinker.core.ui.widget.AdaptiveLayoutScreen
+import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
+import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton
+import ru.tech.imageresizershrinker.core.ui.widget.buttons.ZoomButton
+import ru.tech.imageresizershrinker.core.ui.widget.controls.FormatExifWarning
+import ru.tech.imageresizershrinker.core.ui.widget.dialogs.ExitWithoutSavingDialog
+import ru.tech.imageresizershrinker.core.ui.widget.dialogs.LoadingDialog
+import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeImagePickingDialog
+import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
+import ru.tech.imageresizershrinker.core.ui.widget.image.AutoFilePicker
+import ru.tech.imageresizershrinker.core.ui.widget.image.ImageNotPickedWidget
+import ru.tech.imageresizershrinker.core.ui.widget.image.Picture
+import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
+import ru.tech.imageresizershrinker.core.ui.widget.other.LoadingIndicator
+import ru.tech.imageresizershrinker.core.ui.widget.other.TopAppBarEmoji
+import ru.tech.imageresizershrinker.core.ui.widget.preferences.PreferenceItem
+import ru.tech.imageresizershrinker.core.ui.widget.sheets.EditExifSheet
+import ru.tech.imageresizershrinker.core.ui.widget.sheets.ProcessImagesPreferenceSheet
+import ru.tech.imageresizershrinker.core.ui.widget.sheets.ZoomModalSheet
+import ru.tech.imageresizershrinker.core.ui.widget.text.TopAppBarTitle
+import ru.tech.imageresizershrinker.core.ui.widget.utils.AutoContentBasedColors
+import ru.tech.imageresizershrinker.feature.edit_exif.presentation.screenLogic.EditExifComponent
+
+@Composable
+fun EditExifContent(
+ component: EditExifComponent,
+) {
+ val context = LocalComponentActivity.current
+
+ val essentials = rememberLocalEssentials()
+ val showConfetti: () -> Unit = essentials::showConfetti
+
+ AutoContentBasedColors(component.uri)
+
+ var showOriginal by rememberSaveable { mutableStateOf(false) }
+ var showExitDialog by rememberSaveable { mutableStateOf(false) }
+
+ val imagePicker = rememberImagePicker(onSuccess = component::setUri)
+ val pickImage = imagePicker::pickImage
+
+ AutoFilePicker(
+ onAutoPick = pickImage,
+ isPickedAlready = component.initialUri != null
+ )
+
+ val saveBitmap: (oneTimeSaveLocationUri: String?) -> Unit = {
+ component.saveBitmap(
+ oneTimeSaveLocationUri = it,
+ onComplete = essentials::parseSaveResult
+ )
+ }
+
+ val isPortrait by isPortraitOrientationAsState()
+
+ var showZoomSheet by rememberSaveable { mutableStateOf(false) }
+
+ ZoomModalSheet(
+ data = component.uri,
+ visible = showZoomSheet,
+ onDismiss = {
+ showZoomSheet = false
+ }
+ )
+
+ val onBack = {
+ if (component.haveChanges) showExitDialog = true
+ else component.onGoBack()
+ }
+
+ AdaptiveLayoutScreen(
+ shouldDisableBackHandler = !component.haveChanges,
+ title = {
+ TopAppBarTitle(
+ title = stringResource(R.string.edit_exif_screen),
+ input = component.uri.takeIf { it != Uri.EMPTY },
+ isLoading = component.isImageLoading,
+ size = component.uri.fileSize(LocalContext.current) ?: 0L
+ )
+ },
+ onGoBack = onBack,
+ topAppBarPersistentActions = {
+ if (component.uri == Uri.EMPTY) {
+ TopAppBarEmoji()
+ }
+ ZoomButton(
+ onClick = { showZoomSheet = true },
+ visible = component.uri != Uri.EMPTY
+ )
+ },
+ actions = {
+ var editSheetData by remember {
+ mutableStateOf(listOf())
+ }
+ ShareButton(
+ enabled = component.uri != Uri.EMPTY,
+ onShare = {
+ component.shareBitmap(showConfetti)
+ },
+ onCopy = { manager ->
+ component.cacheCurrentImage { uri ->
+ manager.setClip(uri.asClip(context))
+ showConfetti()
+ }
+ },
+ onEdit = {
+ component.cacheCurrentImage { uri ->
+ editSheetData = listOf(uri)
+ }
+ }
+ )
+ ProcessImagesPreferenceSheet(
+ uris = editSheetData,
+ visible = editSheetData.isNotEmpty(),
+ onDismiss = { editSheetData = emptyList() },
+ onNavigate = component.onNavigate
+ )
+ },
+ imagePreview = {
+ Box(
+ contentAlignment = Alignment.Center
+ ) {
+ var aspectRatio by remember {
+ mutableFloatStateOf(1f)
+ }
+ Picture(
+ model = component.uri,
+ modifier = Modifier
+ .container(MaterialTheme.shapes.medium)
+ .aspectRatio(aspectRatio),
+ onSuccess = {
+ aspectRatio = it.result.image.toBitmap().safeAspectRatio
+ },
+ shape = MaterialTheme.shapes.medium,
+ contentScale = ContentScale.FillBounds
+ )
+ if (component.isImageLoading) LoadingIndicator()
+ }
+ },
+ controls = {
+ var showEditExifDialog by rememberSaveable { mutableStateOf(false) }
+
+ PreferenceItem(
+ onClick = {
+ showEditExifDialog = true
+ },
+ modifier = Modifier.fillMaxWidth(),
+ title = stringResource(R.string.edit_exif),
+ subtitle = stringResource(R.string.edit_exif_tag),
+ shape = RoundedCornerShape(24.dp),
+ enabled = component.imageFormat.canWriteExif,
+ onDisabledClick = {
+ essentials.showToast(
+ context.getString(R.string.image_exif_warning, component.imageFormat.title)
+ )
+ },
+ startIcon = Icons.Rounded.Exif,
+ endIcon = Icons.Rounded.MiniEdit
+ )
+ Spacer(Modifier.height(8.dp))
+ FormatExifWarning(component.imageFormat)
+
+ EditExifSheet(
+ visible = showEditExifDialog,
+ onDismiss = {
+ showEditExifDialog = false
+ },
+ exif = component.exif,
+ onClearExif = component::clearExif,
+ onUpdateTag = component::updateExifByTag,
+ onRemoveTag = component::removeExifTag
+ )
+ },
+ buttons = {
+ var showFolderSelectionDialog by rememberSaveable {
+ mutableStateOf(false)
+ }
+ var showOneTimeImagePickingDialog by rememberSaveable {
+ mutableStateOf(false)
+ }
+ BottomButtonsBlock(
+ targetState = (component.uri == Uri.EMPTY) to isPortrait,
+ onSecondaryButtonClick = pickImage,
+ onSecondaryButtonLongClick = {
+ showOneTimeImagePickingDialog = true
+ },
+ onPrimaryButtonClick = {
+ saveBitmap(null)
+ },
+ onPrimaryButtonLongClick = {
+ showFolderSelectionDialog = true
+ },
+ actions = {
+ if (isPortrait) it()
+ }
+ )
+ OneTimeSaveLocationSelectionDialog(
+ visible = showFolderSelectionDialog,
+ onDismiss = { showFolderSelectionDialog = false },
+ onSaveRequest = saveBitmap
+ )
+ OneTimeImagePickingDialog(
+ onDismiss = { showOneTimeImagePickingDialog = false },
+ picker = Picker.Single,
+ imagePicker = imagePicker,
+ visible = showOneTimeImagePickingDialog
+ )
+ },
+ canShowScreenData = component.uri != Uri.EMPTY,
+ noDataControls = {
+ if (!component.isImageLoading) {
+ ImageNotPickedWidget(onPickImage = pickImage)
+ }
+ },
+ forceImagePreviewToMax = showOriginal,
+ isPortrait = isPortrait
+ )
+
+ ExitWithoutSavingDialog(
+ onExit = component.onGoBack,
+ onDismiss = { showExitDialog = false },
+ visible = showExitDialog
+ )
+
+ LoadingDialog(
+ visible = component.isSaving,
+ onCancelLoading = component::cancelSaving
+ )
+}
\ No newline at end of file
diff --git a/feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/screenLogic/EditExifComponent.kt b/feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/screenLogic/EditExifComponent.kt
new file mode 100644
index 0000000000..234fcb71f7
--- /dev/null
+++ b/feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/screenLogic/EditExifComponent.kt
@@ -0,0 +1,207 @@
+/*
+ * ImageToolbox is an image editor for android
+ * Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * You should have received a copy of the Apache License
+ * along with this program. If not, see .
+ */
+
+package ru.tech.imageresizershrinker.feature.edit_exif.presentation.screenLogic
+
+import android.graphics.Bitmap
+import android.net.Uri
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.core.net.toUri
+import androidx.exifinterface.media.ExifInterface
+import com.arkivanov.decompose.ComponentContext
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.Job
+import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
+import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
+import ru.tech.imageresizershrinker.core.domain.image.ShareProvider
+import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormat
+import ru.tech.imageresizershrinker.core.domain.image.model.MetadataTag
+import ru.tech.imageresizershrinker.core.domain.saving.FileController
+import ru.tech.imageresizershrinker.core.domain.saving.FilenameCreator
+import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget
+import ru.tech.imageresizershrinker.core.domain.saving.model.SaveResult
+import ru.tech.imageresizershrinker.core.domain.utils.smartJob
+import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
+import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
+import ru.tech.imageresizershrinker.core.ui.utils.state.update
+
+
+class EditExifComponent @AssistedInject internal constructor(
+ @Assisted componentContext: ComponentContext,
+ @Assisted val initialUri: Uri?,
+ @Assisted val onGoBack: () -> Unit,
+ @Assisted val onNavigate: (Screen) -> Unit,
+ private val fileController: FileController,
+ private val imageGetter: ImageGetter,
+ private val shareProvider: ShareProvider,
+ private val filenameCreator: FilenameCreator,
+ dispatchersHolder: DispatchersHolder
+) : BaseComponent(dispatchersHolder, componentContext) {
+
+ init {
+ debounce {
+ initialUri?.let(::setUri)
+ }
+ }
+
+ private val _exif: MutableState = mutableStateOf(null)
+ val exif by _exif
+
+ private val _imageFormat: MutableState = mutableStateOf(ImageFormat.Default)
+ val imageFormat by _imageFormat
+
+ private val _uri: MutableState = mutableStateOf(Uri.EMPTY)
+ val uri: Uri by _uri
+
+ private val _isSaving: MutableState = mutableStateOf(false)
+ val isSaving by _isSaving
+
+ private var savingJob: Job? by smartJob {
+ _isSaving.update { false }
+ }
+
+ fun saveBitmap(
+ oneTimeSaveLocationUri: String?,
+ onComplete: (result: SaveResult) -> Unit,
+ ) {
+ savingJob = componentScope.launch(defaultDispatcher) {
+ _isSaving.update { true }
+ runCatching {
+ imageGetter.getImage(uri.toString())
+ }.getOrNull()?.let {
+ val result = fileController.save(
+ ImageSaveTarget(
+ imageInfo = it.imageInfo,
+ originalUri = uri.toString(),
+ sequenceNumber = null,
+ metadata = exif,
+ data = ByteArray(0),
+ readFromUriInsteadOfData = true
+ ),
+ keepOriginalMetadata = false,
+ oneTimeSaveLocationUri = oneTimeSaveLocationUri
+ )
+
+ onComplete(result.onSuccess(::registerSave))
+ }
+ _isSaving.update { false }
+ }
+ }
+
+ fun setUri(uri: Uri) {
+ _uri.update { uri }
+ componentScope.launch {
+ imageGetter.getImage(uri.toString())?.let {
+ _exif.value = it.metadata
+ _imageFormat.value = it.imageInfo.imageFormat
+ }
+ }
+ }
+
+ fun shareBitmap(onComplete: () -> Unit) {
+ cacheCurrentImage {
+ componentScope.launch {
+ shareProvider.shareUris(listOf(it.toString()))
+ onComplete()
+ }
+ }
+ }
+
+ fun cacheCurrentImage(onComplete: (Uri) -> Unit) {
+ savingJob = componentScope.launch {
+ _isSaving.update { true }
+ imageGetter.getImage(
+ uri.toString()
+ )?.let {
+ shareProvider.cacheData(
+ writeData = { w ->
+ w.writeBytes(
+ fileController.readBytes(uri.toString())
+ )
+ },
+ filename = filenameCreator.constructImageFilename(
+ saveTarget = ImageSaveTarget(
+ imageInfo = it.imageInfo.copy(originalUri = uri.toString()),
+ originalUri = uri.toString(),
+ metadata = exif,
+ sequenceNumber = null,
+ data = ByteArray(0)
+ )
+ )
+ )?.let { uri ->
+ fileController.writeMetadata(
+ imageUri = uri,
+ metadata = exif
+ )
+ onComplete(uri.toUri())
+ }
+ }
+ _isSaving.update { false }
+ }
+ }
+
+ fun clearExif() {
+ val tempExif = _exif.value
+ MetadataTag.entries.forEach {
+ tempExif?.setAttribute(it.key, null)
+ }
+ _exif.update {
+ tempExif
+ }
+ registerChanges()
+ }
+
+ private fun updateExif(exifInterface: ExifInterface?) {
+ _exif.update { exifInterface }
+ registerChanges()
+ }
+
+ fun removeExifTag(tag: MetadataTag) {
+ val exifInterface = _exif.value
+ exifInterface?.setAttribute(tag.key, null)
+ updateExif(exifInterface)
+ }
+
+ fun updateExifByTag(
+ tag: MetadataTag,
+ value: String,
+ ) {
+ val exifInterface = _exif.value
+ exifInterface?.setAttribute(tag.key, value)
+ updateExif(exifInterface)
+ }
+
+ fun cancelSaving() {
+ savingJob?.cancel()
+ savingJob = null
+ _isSaving.update { false }
+ }
+
+ @AssistedFactory
+ fun interface Factory {
+ operator fun invoke(
+ componentContext: ComponentContext,
+ initialUri: Uri?,
+ onGoBack: () -> Unit,
+ onNavigate: (Screen) -> Unit,
+ ): EditExifComponent
+ }
+}
diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/components/FiltersContentSheets.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/components/FiltersContentSheets.kt
index 68417c5b36..b86a0df8c1 100644
--- a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/components/FiltersContentSheets.kt
+++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/components/FiltersContentSheets.kt
@@ -18,7 +18,9 @@
package ru.tech.imageresizershrinker.feature.filters.presentation.components
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import ru.tech.imageresizershrinker.core.filters.presentation.widget.FilterReorderSheet
import ru.tech.imageresizershrinker.core.filters.presentation.widget.addFilters.AddFiltersSheet
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
@@ -36,15 +38,12 @@ internal fun FiltersContentSheets(
val isPortrait by isPortraitOrientationAsState()
if (component.filterType is Screen.Filter.Type.Basic) {
+ val transformations by remember(component.basicFilterState, component.imageInfo) {
+ derivedStateOf(component::getFiltersTransformation)
+ }
+
PickImageFromUrisSheet(
- transformations = listOf(
- component.imageInfoTransformationFactory(
- imageInfo = component.imageInfo,
- transformations = component.basicFilterState.filters.map(
- component.filterProvider::filterToTransformation
- )
- )
- ),
+ transformations = transformations,
visible = component.isPickImageFromUrisSheetVisible,
onDismiss = component::hidePickImageFromUrisSheet,
uris = component.basicFilterState.uris,
diff --git a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/screenLogic/FiltersComponent.kt b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/screenLogic/FiltersComponent.kt
index 56682171e3..554e78c4d8 100644
--- a/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/screenLogic/FiltersComponent.kt
+++ b/feature/filters/src/main/java/ru/tech/imageresizershrinker/feature/filters/presentation/screenLogic/FiltersComponent.kt
@@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
+import coil3.transform.Transformation
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import dagger.assisted.Assisted
@@ -73,8 +74,8 @@ class FiltersComponent @AssistedInject internal constructor(
private val filterMaskApplier: FilterMaskApplier,
private val imageGetter: ImageGetter,
private val imageScaler: ImageScaler,
- val filterProvider: FilterProvider,
- val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
+ private val filterProvider: FilterProvider,
+ private val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
private val shareProvider: ShareProvider,
dispatchersHolder: DispatchersHolder,
addFiltersSheetComponentFactory: AddFiltersSheetComponent.Factory,
@@ -107,6 +108,15 @@ class FiltersComponent @AssistedInject internal constructor(
)
)
+ fun getFiltersTransformation(): List = listOf(
+ imageInfoTransformationFactory(
+ imageInfo = imageInfo,
+ transformations = basicFilterState.filters.map(
+ filterProvider::filterToTransformation
+ )
+ )
+ )
+
private val _isPickImageFromUrisSheetVisible = mutableStateOf(false)
val isPickImageFromUrisSheetVisible by _isPickImageFromUrisSheetVisible
diff --git a/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/FormatConversionContent.kt b/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/FormatConversionContent.kt
index 46574e9e22..8ec1d6594a 100644
--- a/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/FormatConversionContent.kt
+++ b/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/FormatConversionContent.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -37,7 +38,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ru.tech.imageresizershrinker.core.data.utils.fileSize
-import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
@@ -288,13 +288,12 @@ fun FormatConversionContent(
isPortrait = isPortrait
)
+ val transformations by remember(component.imageInfo) {
+ derivedStateOf(component::getConversionTransformation)
+ }
+
PickImageFromUrisSheet(
- transformations = listOf(
- component.imageInfoTransformationFactory(
- imageInfo = component.imageInfo,
- preset = Preset.Original
- )
- ),
+ transformations = transformations,
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false
diff --git a/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/screenLogic/FormatConversionComponent.kt b/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/screenLogic/FormatConversionComponent.kt
index 757eea55d5..dbd1bb2f6d 100644
--- a/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/screenLogic/FormatConversionComponent.kt
+++ b/feature/format-conversion/src/main/java/ru/tech/imageresizershrinker/feature/format_conversion/presentation/screenLogic/FormatConversionComponent.kt
@@ -66,7 +66,7 @@ class FormatConversionComponent @AssistedInject internal constructor(
private val imageGetter: ImageGetter,
private val imageScaler: ImageScaler,
private val shareProvider: ShareProvider,
- val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
+ private val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
@@ -450,6 +450,13 @@ class FormatConversionComponent @AssistedInject internal constructor(
if (uris?.size == 1) imageInfo.imageFormat
else null
+ fun getConversionTransformation() = listOf(
+ imageInfoTransformationFactory(
+ imageInfo = imageInfo,
+ preset = Preset.Original
+ )
+ )
+
@AssistedFactory
fun interface Factory {
diff --git a/feature/gradient-maker/src/main/java/ru/tech/imageresizershrinker/feature/gradient_maker/presentation/GradientMakerContent.kt b/feature/gradient-maker/src/main/java/ru/tech/imageresizershrinker/feature/gradient_maker/presentation/GradientMakerContent.kt
index 7402908db1..2d04aafbf6 100644
--- a/feature/gradient-maker/src/main/java/ru/tech/imageresizershrinker/feature/gradient_maker/presentation/GradientMakerContent.kt
+++ b/feature/gradient-maker/src/main/java/ru/tech/imageresizershrinker/feature/gradient_maker/presentation/GradientMakerContent.kt
@@ -445,14 +445,16 @@ fun GradientMakerContent(
).value
)
+ val transformations by remember(component.brush) {
+ derivedStateOf {
+ listOf(
+ component.getGradientTransformation()
+ )
+ }
+ }
+
PickImageFromUrisSheet(
- transformations = remember(component.brush) {
- derivedStateOf {
- listOf(
- component.getGradientTransformation()
- )
- }
- }.value,
+ transformations = transformations,
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false
diff --git a/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeContent.kt b/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeContent.kt
index 6d83282a02..264df487ac 100644
--- a/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeContent.kt
+++ b/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeContent.kt
@@ -31,7 +31,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import ru.tech.imageresizershrinker.core.domain.image.model.ImageInfo
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
@@ -293,11 +292,6 @@ fun LimitsResizeContent(
)
PickImageFromUrisSheet(
- transformations = listOf(
- component.imageInfoTransformationFactory(
- imageInfo = ImageInfo()
- )
- ),
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false
diff --git a/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/screenLogic/LimitsResizeComponent.kt b/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/screenLogic/LimitsResizeComponent.kt
index 374505acaa..f86821e29e 100644
--- a/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/screenLogic/LimitsResizeComponent.kt
+++ b/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/screenLogic/LimitsResizeComponent.kt
@@ -45,7 +45,6 @@ import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget
import ru.tech.imageresizershrinker.core.domain.saving.model.SaveResult
import ru.tech.imageresizershrinker.core.domain.saving.model.onSuccess
import ru.tech.imageresizershrinker.core.domain.utils.smartJob
-import ru.tech.imageresizershrinker.core.ui.transformation.ImageInfoTransformation
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.core.ui.utils.state.update
@@ -62,7 +61,6 @@ class LimitsResizeComponent @AssistedInject internal constructor(
private val imageGetter: ImageGetter,
private val imageScaler: LimitsImageScaler,
private val shareProvider: ShareProvider,
- val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
diff --git a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/FilteredScreenListFor.kt b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/FilteredScreenListFor.kt
index bf06ee2196..bf6bc7c73c 100644
--- a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/FilteredScreenListFor.kt
+++ b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/FilteredScreenListFor.kt
@@ -55,7 +55,7 @@ internal fun filteredScreenListFor(
) {
derivedStateOf {
if (settingsState.groupOptionsByTypes && (screenSearchKeyword.isEmpty() && !showScreenSearch)) {
- Screen.typedEntries[selectedNavigationItem].first
+ Screen.typedEntries[selectedNavigationItem].entries
} else if (!settingsState.groupOptionsByTypes && (screenSearchKeyword.isEmpty() && !showScreenSearch)) {
if (selectedNavigationItem == 0) {
screenList.filter {
diff --git a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationBar.kt b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationBar.kt
index 81e5011daa..d424437112 100644
--- a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationBar.kt
+++ b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationBar.kt
@@ -53,7 +53,7 @@ internal fun MainNavigationBar(
.calculateBottomPadding()
),
) {
- Screen.typedEntries.forEachIndexed { index, (_, data) ->
+ Screen.typedEntries.forEachIndexed { index, group ->
val selected = index == selectedIndex
val haptics = LocalHapticFeedback.current
NavigationBarItem(
@@ -73,14 +73,14 @@ internal fun MainNavigationBar(
}
) { selected ->
Icon(
- imageVector = if (selected) data.second else data.third,
- contentDescription = null
+ imageVector = group.icon(selected),
+ contentDescription = stringResource(group.title)
)
}
},
label = {
Text(
- text = stringResource(data.first),
+ text = stringResource(group.title),
modifier = Modifier.marquee()
)
}
diff --git a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationRail.kt b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationRail.kt
index 399a8ade02..920b3b34ad 100644
--- a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationRail.kt
+++ b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/MainNavigationRail.kt
@@ -102,7 +102,7 @@ internal fun MainNavigationRail(
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(8.dp))
- Screen.typedEntries.forEachIndexed { index, (_, data) ->
+ Screen.typedEntries.forEachIndexed { index, group ->
val selected = index == selectedIndex
val haptics = LocalHapticFeedback.current
NavigationRailItem(
@@ -122,20 +122,20 @@ internal fun MainNavigationRail(
}
) { selected ->
Icon(
- imageVector = if (selected) data.second else data.third,
- contentDescription = stringResource(data.first)
+ imageVector = group.icon(selected),
+ contentDescription = stringResource(group.title)
)
}
},
label = {
- Text(stringResource(data.first))
+ Text(stringResource(group.title))
}
)
}
Spacer(Modifier.height(8.dp))
}
}
- Box(
+ Spacer(
Modifier
.fillMaxHeight()
.width(settingsState.borderWidth)
diff --git a/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertContent.kt b/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertContent.kt
index ed8e5d28a4..33b280a40b 100644
--- a/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertContent.kt
+++ b/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertContent.kt
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.rounded.History
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -392,13 +393,12 @@ fun ResizeAndConvertContent(
onReset = component::resetValues
)
+ val transformations by remember(component.imageInfo, component.presetSelected) {
+ derivedStateOf(component::getTransformations)
+ }
+
PickImageFromUrisSheet(
- transformations = listOf(
- component.imageInfoTransformationFactory(
- imageInfo = component.imageInfo,
- preset = component.presetSelected
- )
- ),
+ transformations = transformations,
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false
diff --git a/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/screenLogic/ResizeAndConvertComponent.kt b/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/screenLogic/ResizeAndConvertComponent.kt
index 9086ec9616..9805e93cf6 100644
--- a/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/screenLogic/ResizeAndConvertComponent.kt
+++ b/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/screenLogic/ResizeAndConvertComponent.kt
@@ -70,7 +70,7 @@ class ResizeAndConvertComponent @AssistedInject internal constructor(
private val imageGetter: ImageGetter,
private val imageScaler: ImageScaler,
private val shareProvider: ShareProvider,
- val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
+ private val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
settingsProvider: SettingsProvider,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
@@ -623,6 +623,12 @@ class ResizeAndConvertComponent @AssistedInject internal constructor(
if (uris?.size == 1) imageInfo.imageFormat
else null
+ fun getTransformations() = listOf(
+ imageInfoTransformationFactory(
+ imageInfo = imageInfo,
+ preset = presetSelected
+ )
+ )
@AssistedFactory
fun interface Factory {
diff --git a/feature/root/build.gradle.kts b/feature/root/build.gradle.kts
index 287f6509af..0920e3ee10 100644
--- a/feature/root/build.gradle.kts
+++ b/feature/root/build.gradle.kts
@@ -67,4 +67,5 @@ dependencies {
implementation(projects.feature.base64Tools)
implementation(projects.feature.checksumTools)
implementation(projects.feature.meshGradients)
+ implementation(projects.feature.editExif)
}
\ No newline at end of file
diff --git a/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/ChildProvider.kt b/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/ChildProvider.kt
index ae973ddb78..f5ce64c2f3 100644
--- a/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/ChildProvider.kt
+++ b/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/ChildProvider.kt
@@ -31,6 +31,7 @@ import ru.tech.imageresizershrinker.feature.delete_exif.presentation.screenLogic
import ru.tech.imageresizershrinker.feature.document_scanner.presentation.screenLogic.DocumentScannerComponent
import ru.tech.imageresizershrinker.feature.draw.presentation.screenLogic.DrawComponent
import ru.tech.imageresizershrinker.feature.easter_egg.presentation.screenLogic.EasterEggComponent
+import ru.tech.imageresizershrinker.feature.edit_exif.presentation.screenLogic.EditExifComponent
import ru.tech.imageresizershrinker.feature.erase_background.presentation.screenLogic.EraseBackgroundComponent
import ru.tech.imageresizershrinker.feature.filters.presentation.screenLogic.FiltersComponent
import ru.tech.imageresizershrinker.feature.format_conversion.presentation.screenLogic.FormatConversionComponent
@@ -107,7 +108,8 @@ internal class ChildProvider @Inject constructor(
private val markupLayersComponentFactory: MarkupLayersComponent.Factory,
private val base64ToolsComponentFactory: Base64ToolsComponent.Factory,
private val checksumToolsComponentFactory: ChecksumToolsComponent.Factory,
- private val meshGradientsComponentFactory: MeshGradientsComponent.Factory
+ private val meshGradientsComponentFactory: MeshGradientsComponent.Factory,
+ private val editExifComponentFactory: EditExifComponent.Factory
) {
fun RootComponent.createChild(
config: Screen,
@@ -475,5 +477,14 @@ internal class ChildProvider @Inject constructor(
onNavigate = ::navigateTo
)
)
+
+ is Screen.EditExif -> EditExif(
+ editExifComponentFactory(
+ componentContext = componentContext,
+ initialUri = config.uri,
+ onGoBack = ::navigateBack,
+ onNavigate = ::navigateTo
+ )
+ )
}
}
\ No newline at end of file
diff --git a/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/NavigationChild.kt b/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/NavigationChild.kt
index 173ed17801..4cdf0ffd69 100644
--- a/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/NavigationChild.kt
+++ b/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/navigation/NavigationChild.kt
@@ -42,6 +42,8 @@ import ru.tech.imageresizershrinker.feature.draw.presentation.DrawContent
import ru.tech.imageresizershrinker.feature.draw.presentation.screenLogic.DrawComponent
import ru.tech.imageresizershrinker.feature.easter_egg.presentation.EasterEggContent
import ru.tech.imageresizershrinker.feature.easter_egg.presentation.screenLogic.EasterEggComponent
+import ru.tech.imageresizershrinker.feature.edit_exif.presentation.EditExifContent
+import ru.tech.imageresizershrinker.feature.edit_exif.presentation.screenLogic.EditExifComponent
import ru.tech.imageresizershrinker.feature.erase_background.presentation.EraseBackgroundContent
import ru.tech.imageresizershrinker.feature.erase_background.presentation.screenLogic.EraseBackgroundComponent
import ru.tech.imageresizershrinker.feature.filters.presentation.FiltersContent
@@ -320,4 +322,9 @@ internal sealed class NavigationChild {
override fun Content() = MeshGradientsContent(component)
}
+ class EditExif(val component: EditExifComponent) : NavigationChild() {
+ @Composable
+ override fun Content() = EditExifContent(component)
+ }
+
}
\ No newline at end of file
diff --git a/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/WeightResizeContent.kt b/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/WeightResizeContent.kt
index 1fb560443a..3ef0ed0915 100644
--- a/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/WeightResizeContent.kt
+++ b/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/WeightResizeContent.kt
@@ -43,7 +43,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormat
import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormatGroup
-import ru.tech.imageresizershrinker.core.domain.image.model.ImageInfo
import ru.tech.imageresizershrinker.core.domain.image.model.Preset
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
@@ -346,11 +345,6 @@ fun WeightResizeContent(
)
PickImageFromUrisSheet(
- transformations = listOf(
- component.imageInfoTransformationFactory(
- imageInfo = ImageInfo()
- )
- ),
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false
diff --git a/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/screenLogic/WeightResizeComponent.kt b/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/screenLogic/WeightResizeComponent.kt
index b73281370b..b53b262985 100644
--- a/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/screenLogic/WeightResizeComponent.kt
+++ b/feature/weight-resize/src/main/java/ru/tech/imageresizershrinker/feature/weight_resize/presentation/screenLogic/WeightResizeComponent.kt
@@ -46,7 +46,6 @@ import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget
import ru.tech.imageresizershrinker.core.domain.saving.model.SaveResult
import ru.tech.imageresizershrinker.core.domain.saving.model.onSuccess
import ru.tech.imageresizershrinker.core.domain.utils.smartJob
-import ru.tech.imageresizershrinker.core.ui.transformation.ImageInfoTransformation
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.core.ui.utils.state.update
@@ -64,7 +63,6 @@ class WeightResizeComponent @AssistedInject internal constructor(
private val imageCompressor: ImageCompressor,
private val imageScaler: WeightImageScaler,
private val shareProvider: ShareProvider,
- val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 31e6ce5f24..2e4d08e5d2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -107,6 +107,7 @@ include(":feature:markup-layers")
include(":feature:base64-tools")
include(":feature:checksum-tools")
include(":feature:mesh-gradients")
+include(":feature:edit-exif")
include(":feature:root")