Added ability to edit EXIF without recompression in separate tool by #1606

This commit is contained in:
T8RIN
2025-01-20 00:55:31 +03:00
parent 808532018a
commit d3de80b989
33 changed files with 848 additions and 135 deletions

View File

@ -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()
}

View File

@ -1560,4 +1560,7 @@
<string name="pages_selection">Pages Selection</string>
<string name="tool_exit_confirmation">Tool Exit Confirmation</string>
<string name="tool_exit_confirmation_sub">If you have unsaved changes while using particular tools and try to close it, then confirm dialog will be shown</string>
<string name="edit_exif_screen">Edit EXIF</string>
<string name="edit_exif_screen_sub">Change metadata of single image without recompression</string>
<string name="edit_exif_tag">Tap to edit available tags</string>
</resources>

View File

@ -112,9 +112,13 @@ fun rememberClipboardText(): State<String> {
return clip
}
fun ClipboardManager?.clipList(): List<Uri> = this?.primaryClip?.clipList() ?: emptyList()
fun ClipboardManager?.clipList(): List<Uri> = 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<Uri>.toClipData(): ClipData? {
fun List<Uri>.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<Uri>.toClipData(): ClipData? {
}
fun Uri.asClip(
context: Context
context: Context,
label: String = "Image"
): ClipEntry = ClipEntry(
ClipData.newUri(
context.contentResolver,
"IMAGE",
label,
this
)
)

View File

@ -710,23 +710,35 @@ 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(
ScreenGroup(
entries = listOf(
SingleEdit(),
ResizeAndConvert(),
FormatConversion(),
Crop(),
WeightResize(),
LimitResize(),
EditExif(),
DeleteExif(),
) to Triple(
R.string.edit,
Icons.Rounded.MiniEditLarge,
Icons.Outlined.MiniEditLarge
),
listOf(
title = R.string.edit,
selectedIcon = Icons.Rounded.MiniEditLarge,
baseIcon = Icons.Outlined.MiniEditLarge
),
ScreenGroup(
entries = listOf(
Filter(),
Draw(),
EraseBackground(),
@ -738,12 +750,13 @@ sealed class Screen(
Watermarking(),
GradientMaker(),
NoiseGeneration,
) to Triple(
R.string.create,
Icons.Filled.AutoAwesome,
Icons.Outlined.AutoAwesome
),
listOf(
title = R.string.create,
selectedIcon = Icons.Filled.AutoAwesome,
baseIcon = Icons.Outlined.AutoAwesome
),
ScreenGroup(
entries = listOf(
PickColorFromImage(),
RecognizeText(),
Compare(),
@ -752,12 +765,13 @@ sealed class Screen(
SvgMaker(),
GeneratePalette(),
LoadNetImage(),
) to Triple(
R.string.image,
Icons.Filled.FilterHdr,
Icons.Outlined.FilterHdr
),
listOf(
title = R.string.image,
selectedIcon = Icons.Filled.FilterHdr,
baseIcon = Icons.Outlined.FilterHdr
),
ScreenGroup(
entries = listOf(
PdfTools(),
DocumentScanner,
ScanQrCode(),
@ -769,19 +783,29 @@ sealed class Screen(
JxlTools(),
ApngTools(),
WebpTools()
) to Triple(
R.string.tools,
Icons.Rounded.Toolbox,
Icons.Outlined.Toolbox
),
title = R.string.tools,
selectedIcon = Icons.Rounded.Toolbox,
baseIcon = Icons.Outlined.Toolbox
)
)
}
val entries by lazy {
typedEntries.flatMap { it.first }.sortedBy { it.id }
}
const val FEATURES_COUNT = 60
val entries by lazy {
typedEntries.flatMap { it.entries }.sortedBy { it.id }
}
const val FEATURES_COUNT = 61
}
}
data class ScreenGroup(
val entries: List<Screen>,
@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

View File

@ -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<Uri> {

View File

@ -68,7 +68,7 @@ import ru.tech.imageresizershrinker.core.ui.widget.modifier.transparencyChecker
fun Picture(
model: Any?,
modifier: Modifier = Modifier,
transformations: List<Transformation> = emptyList(),
transformations: List<Transformation>? = 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()
}

View File

@ -68,7 +68,7 @@ import ru.tech.imageresizershrinker.core.ui.widget.text.TitleItem
fun PickImageFromUrisSheet(
visible: Boolean,
onDismiss: () -> Unit,
transformations: List<Transformation>,
transformations: List<Transformation>? = null,
uris: List<Uri>?,
selectedUri: Uri?,
onUriRemoved: (Uri) -> Unit,

View File

@ -141,6 +141,7 @@ internal fun List<Uri>.screenList(
),
Screen.SvgMaker(uris),
Screen.Zip(uris),
Screen.EditExif(uris.firstOrNull()),
Screen.DeleteExif(uris),
Screen.LimitResize(uris)
).let {

View File

@ -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

View File

@ -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<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
private val filenameCreator: FilenameCreator,
val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {

1
feature/edit-exif/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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"

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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<Uri>())
}
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
)
}

View File

@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
*/
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<Bitmap, ExifInterface>,
private val shareProvider: ShareProvider<Bitmap>,
private val filenameCreator: FilenameCreator,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
init {
debounce {
initialUri?.let(::setUri)
}
}
private val _exif: MutableState<ExifInterface?> = mutableStateOf(null)
val exif by _exif
private val _imageFormat: MutableState<ImageFormat> = mutableStateOf(ImageFormat.Default)
val imageFormat by _imageFormat
private val _uri: MutableState<Uri> = mutableStateOf(Uri.EMPTY)
val uri: Uri by _uri
private val _isSaving: MutableState<Boolean> = 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
}
}

View File

@ -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,

View File

@ -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<Bitmap, Path, Color>,
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
private val imageScaler: ImageScaler<Bitmap>,
val filterProvider: FilterProvider<Bitmap>,
val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
private val filterProvider: FilterProvider<Bitmap>,
private val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
private val shareProvider: ShareProvider<Bitmap>,
dispatchersHolder: DispatchersHolder,
addFiltersSheetComponentFactory: AddFiltersSheetComponent.Factory,
@ -107,6 +108,15 @@ class FiltersComponent @AssistedInject internal constructor(
)
)
fun getFiltersTransformation(): List<Transformation> = listOf(
imageInfoTransformationFactory(
imageInfo = imageInfo,
transformations = basicFilterState.filters.map(
filterProvider::filterToTransformation
)
)
)
private val _isPickImageFromUrisSheetVisible = mutableStateOf(false)
val isPickImageFromUrisSheetVisible by _isPickImageFromUrisSheetVisible

View File

@ -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

View File

@ -66,7 +66,7 @@ class FormatConversionComponent @AssistedInject internal constructor(
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
private val imageScaler: ImageScaler<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
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 {

View File

@ -445,14 +445,16 @@ fun GradientMakerContent(
).value
)
PickImageFromUrisSheet(
transformations = remember(component.brush) {
val transformations by remember(component.brush) {
derivedStateOf {
listOf(
component.getGradientTransformation()
)
}
}.value,
}
PickImageFromUrisSheet(
transformations = transformations,
visible = showPickImageFromUrisSheet,
onDismiss = {
showPickImageFromUrisSheet = false

View File

@ -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

View File

@ -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<Bitmap, ExifInterface>,
private val imageScaler: LimitsImageScaler<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {

View File

@ -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 {

View File

@ -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()
)
}

View File

@ -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)

View File

@ -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

View File

@ -70,7 +70,7 @@ class ResizeAndConvertComponent @AssistedInject internal constructor(
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
private val imageScaler: ImageScaler<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
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 {

View File

@ -67,4 +67,5 @@ dependencies {
implementation(projects.feature.base64Tools)
implementation(projects.feature.checksumTools)
implementation(projects.feature.meshGradients)
implementation(projects.feature.editExif)
}

View File

@ -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
)
)
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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<Bitmap>,
private val imageScaler: WeightImageScaler<Bitmap>,
private val shareProvider: ShareProvider<Bitmap>,
val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {

View File

@ -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")