From d3de80b989e401b01e65d5d538fe359bb49df6cf Mon Sep 17 00:00:00 2001 From: T8RIN Date: Mon, 20 Jan 2025 00:55:31 +0300 Subject: [PATCH] Added ability to edit EXIF without recompression in separate tool by #1606 --- .../core/resources/icons/ExifEdit.kt | 137 +++++++++ .../resources/src/main/res/values/strings.xml | 3 + .../core/ui/utils/helper/ClipboardUtils.kt | 20 +- .../core/ui/utils/navigation/Screen.kt | 142 +++++---- .../core/ui/utils/navigation/ScreenUtils.kt | 3 + .../core/ui/widget/image/Picture.kt | 4 +- .../widget/sheets/PickImageFromUrisSheet.kt | 2 +- .../core/ui/widget/utils/ScreenList.kt | 1 + .../presentation/DeleteExifContent.kt | 6 - .../screenLogic/DeleteExifComponent.kt | 2 - feature/edit-exif/.gitignore | 1 + feature/edit-exif/build.gradle.kts | 25 ++ .../edit-exif/src/main/AndroidManifest.xml | 4 + .../edit_exif/presentation/EditExifContent.kt | 281 ++++++++++++++++++ .../screenLogic/EditExifComponent.kt | 207 +++++++++++++ .../components/FiltersContentSheets.kt | 15 +- .../screenLogic/FiltersComponent.kt | 14 +- .../presentation/FormatConversionContent.kt | 13 +- .../screenLogic/FormatConversionComponent.kt | 9 +- .../presentation/GradientMakerContent.kt | 16 +- .../presentation/LimitsResizeContent.kt | 6 - .../screenLogic/LimitsResizeComponent.kt | 2 - .../components/FilteredScreenListFor.kt | 2 +- .../components/MainNavigationBar.kt | 8 +- .../components/MainNavigationRail.kt | 10 +- .../presentation/ResizeAndConvertContent.kt | 12 +- .../screenLogic/ResizeAndConvertComponent.kt | 8 +- feature/root/build.gradle.kts | 1 + .../components/navigation/ChildProvider.kt | 13 +- .../components/navigation/NavigationChild.kt | 7 + .../presentation/WeightResizeContent.kt | 6 - .../screenLogic/WeightResizeComponent.kt | 2 - settings.gradle.kts | 1 + 33 files changed, 848 insertions(+), 135 deletions(-) create mode 100644 core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ExifEdit.kt create mode 100644 feature/edit-exif/.gitignore create mode 100644 feature/edit-exif/build.gradle.kts create mode 100644 feature/edit-exif/src/main/AndroidManifest.xml create mode 100644 feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/EditExifContent.kt create mode 100644 feature/edit-exif/src/main/java/ru/tech/imageresizershrinker/feature/edit_exif/presentation/screenLogic/EditExifComponent.kt 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")