mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-05-17 05:26:02 +08:00
Added ability to edit EXIF without recompression in separate tool by #1606
This commit is contained in:
@ -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()
|
||||
}
|
@ -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>
|
@ -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
|
||||
)
|
||||
)
|
@ -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<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
|
@ -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> {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
1
feature/edit-exif/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
25
feature/edit-exif/build.gradle.kts
Normal file
25
feature/edit-exif/build.gradle.kts
Normal 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"
|
4
feature/edit-exif/src/main/AndroidManifest.xml
Normal file
4
feature/edit-exif/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -67,4 +67,5 @@ dependencies {
|
||||
implementation(projects.feature.base64Tools)
|
||||
implementation(projects.feature.checksumTools)
|
||||
implementation(projects.feature.meshGradients)
|
||||
implementation(projects.feature.editExif)
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
Reference in New Issue
Block a user