mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-08-06 15:49:35 +08:00
Code refactor
This commit is contained in:
@ -44,7 +44,7 @@ import ru.tech.imageresizershrinker.core.ui.widget.modifier.shapeByInteraction
|
||||
|
||||
@Composable
|
||||
fun ShowOriginalButton(
|
||||
canShow: Boolean,
|
||||
canShow: Boolean = true,
|
||||
onStateChange: (Boolean) -> Unit
|
||||
) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
|
@ -18,91 +18,38 @@
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.material.icons.rounded.Build
|
||||
import androidx.compose.material.icons.rounded.DensitySmall
|
||||
import androidx.compose.material.icons.rounded.GridOn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
|
||||
import ru.tech.imageresizershrinker.core.resources.R
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.ImageOverlay
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.MeshDownload
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.MeshGradient
|
||||
import ru.tech.imageresizershrinker.core.ui.theme.blend
|
||||
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.asClip
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
|
||||
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.CompareButton
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShowOriginalButton
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.controls.SaveExifWidget
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.AlphaSelector
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.ImageFormatSelector
|
||||
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.enhanced.EnhancedSliderItem
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.image.ImageCounter
|
||||
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.modifier.detectSwipes
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.withModifier
|
||||
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.PickImageFromUrisSheet
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.sheets.ProcessImagesPreferenceSheet
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.text.TitleItem
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.text.TopAppBarTitle
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.utils.AutoContentBasedColors
|
||||
import ru.tech.imageresizershrinker.feature.compare.presentation.components.CompareSheet
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.ColorStopSelection
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientPreview
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientPropertiesSelector
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientSizeSelector
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientTypeSelector
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.MeshGradientEditor
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.TileModeSelector
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.generateMesh
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.rememberGradientState
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientMakerAppColorSchemeHandler
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientMakerBottomButtons
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientMakerCompareButton
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientMakerControls
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientMakerImagePreview
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components.GradientMakerNoDataControls
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun GradientMakerContent(
|
||||
@ -113,23 +60,9 @@ fun GradientMakerContent(
|
||||
val essentials = rememberLocalEssentials()
|
||||
val showConfetti: () -> Unit = essentials::showConfetti
|
||||
|
||||
var allowPickingImage by rememberSaveable(component.initialUris) {
|
||||
mutableStateOf(
|
||||
if (component.initialUris.isNullOrEmpty()) null
|
||||
else true
|
||||
)
|
||||
}
|
||||
val allowPickingImage = component.allowPickingImage
|
||||
|
||||
AutoContentBasedColors(
|
||||
model = Triple(component.brush, component.meshPoints, component.selectedUri),
|
||||
selector = { (_, uri) ->
|
||||
component.createGradientBitmap(
|
||||
data = uri,
|
||||
integerSize = IntegerSize(1000, 1000)
|
||||
)
|
||||
},
|
||||
allowChangeColor = allowPickingImage != null
|
||||
)
|
||||
GradientMakerAppColorSchemeHandler(component)
|
||||
|
||||
LaunchedEffect(allowPickingImage) {
|
||||
if (allowPickingImage != true && !component.isMeshGradient) {
|
||||
@ -139,29 +72,7 @@ fun GradientMakerContent(
|
||||
|
||||
var showExitDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
LaunchedEffect(component.colorStops) {
|
||||
if (component.colorStops.isEmpty()) {
|
||||
colorScheme.apply {
|
||||
component.addColorStop(
|
||||
pair = 0f to primary.blend(primaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
component.addColorStop(
|
||||
pair = 0.5f to secondary.blend(secondaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
component.addColorStop(
|
||||
pair = 1f to tertiary.blend(tertiaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val imagePicker = rememberImagePicker { uris: List<Uri> ->
|
||||
allowPickingImage = true
|
||||
component.setUris(
|
||||
uris = uris,
|
||||
onFailure = essentials::showFailureToast
|
||||
@ -173,11 +84,6 @@ fun GradientMakerContent(
|
||||
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
var showPickImageFromUrisSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var showCompareSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showOriginal by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
AdaptiveLayoutScreen(
|
||||
shouldDisableBackHandler = !component.haveChanges,
|
||||
isPortrait = isPortrait,
|
||||
@ -209,10 +115,7 @@ fun GradientMakerContent(
|
||||
actions = {
|
||||
if (component.uris.isNotEmpty()) {
|
||||
ShowOriginalButton(
|
||||
canShow = true,
|
||||
onStateChange = {
|
||||
showOriginal = it
|
||||
}
|
||||
onStateChange = component::setShowOriginal
|
||||
)
|
||||
}
|
||||
var editSheetData by remember {
|
||||
@ -248,406 +151,36 @@ fun GradientMakerContent(
|
||||
if (allowPickingImage == null) {
|
||||
TopAppBarEmoji()
|
||||
}
|
||||
CompareButton(
|
||||
onClick = { showCompareSheet = true },
|
||||
visible = component.brush != null && allowPickingImage == true && component.selectedUri != Uri.EMPTY
|
||||
)
|
||||
|
||||
GradientMakerCompareButton(component)
|
||||
},
|
||||
imagePreview = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.detectSwipes(
|
||||
onSwipeRight = component::selectLeftUri,
|
||||
onSwipeLeft = component::selectRightUri
|
||||
)
|
||||
.container()
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (component.isMeshGradient) {
|
||||
GradientPreview(
|
||||
meshGradientState = component.meshGradientState,
|
||||
gradientAlpha = if (showOriginal) 0f else component.gradientAlpha,
|
||||
allowPickingImage = allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
} else {
|
||||
GradientPreview(
|
||||
brush = component.brush,
|
||||
gradientAlpha = if (showOriginal) 0f else component.gradientAlpha,
|
||||
allowPickingImage = allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
onSizeChanged = component::setPreviewSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
}
|
||||
}
|
||||
GradientMakerImagePreview(component)
|
||||
},
|
||||
controls = {
|
||||
ImageCounter(
|
||||
imageCount = component.uris.size.takeIf { it > 1 },
|
||||
onRepick = {
|
||||
showPickImageFromUrisSheet = true
|
||||
}
|
||||
)
|
||||
AnimatedContent(
|
||||
allowPickingImage == false
|
||||
) { canChangeSize ->
|
||||
if (canChangeSize) {
|
||||
GradientSizeSelector(
|
||||
value = component.gradientSize,
|
||||
onWidthChange = component::updateWidth,
|
||||
onHeightChange = component::updateHeight
|
||||
)
|
||||
} else {
|
||||
AlphaSelector(
|
||||
value = component.gradientAlpha,
|
||||
onValueChange = component::updateGradientAlpha,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (component.isMeshGradient) {
|
||||
Column(
|
||||
modifier = Modifier.container(
|
||||
resultPadding = 0.dp
|
||||
)
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
TitleItem(
|
||||
text = stringResource(R.string.points_customization),
|
||||
icon = Icons.Rounded.Build,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp
|
||||
)
|
||||
)
|
||||
MeshGradientEditor(
|
||||
state = component.meshGradientState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnhancedSliderItem(
|
||||
value = component.meshGradientState.gridSize,
|
||||
title = stringResource(R.string.grid_size),
|
||||
icon = Icons.Rounded.GridOn,
|
||||
valueRange = 2f..6f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = { value ->
|
||||
if (value.roundToInt() != component.meshGradientState.gridSize) {
|
||||
val size = value.roundToInt()
|
||||
component.setResolution(lerp(1f, 16f, 2f / size))
|
||||
component.meshGradientState.points.apply {
|
||||
clear()
|
||||
addAll(generateMesh(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnhancedSliderItem(
|
||||
value = component.meshResolutionX,
|
||||
title = stringResource(R.string.resolution),
|
||||
icon = Icons.Rounded.DensitySmall,
|
||||
valueRange = 1f..64f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = component::setResolution
|
||||
)
|
||||
} else {
|
||||
GradientTypeSelector(
|
||||
value = component.gradientType,
|
||||
onValueChange = component::setGradientType
|
||||
) {
|
||||
GradientPropertiesSelector(
|
||||
gradientType = component.gradientType,
|
||||
linearAngle = component.angle,
|
||||
onLinearAngleChange = component::updateLinearAngle,
|
||||
centerFriction = component.centerFriction,
|
||||
radiusFriction = component.radiusFriction,
|
||||
onRadialDimensionsChange = component::setRadialProperties
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ColorStopSelection(
|
||||
colorStops = component.colorStops,
|
||||
onRemoveClick = component::removeColorStop,
|
||||
onValueChange = component::updateColorStop,
|
||||
onAddColorStop = component::addColorStop
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TileModeSelector(
|
||||
value = component.tileMode,
|
||||
onValueChange = component::setTileMode
|
||||
)
|
||||
}
|
||||
if (allowPickingImage == true) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SaveExifWidget(
|
||||
checked = component.keepExif,
|
||||
imageFormat = component.imageFormat,
|
||||
onCheckedChange = component::toggleKeepExif
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ImageFormatSelector(
|
||||
value = component.imageFormat,
|
||||
forceEnabled = allowPickingImage == false,
|
||||
onValueChange = component::setImageFormat
|
||||
)
|
||||
GradientMakerControls(component)
|
||||
},
|
||||
insetsForNoData = WindowInsets(0),
|
||||
noDataControls = {
|
||||
val preference1 = @Composable {
|
||||
val screen = remember {
|
||||
Screen.GradientMaker()
|
||||
}
|
||||
PreferenceItem(
|
||||
title = stringResource(screen.title),
|
||||
subtitle = stringResource(screen.subtitle),
|
||||
startIcon = screen.icon,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(false)
|
||||
allowPickingImage = false
|
||||
}
|
||||
GradientMakerNoDataControls(
|
||||
component = component,
|
||||
onPickImage = pickImage
|
||||
)
|
||||
}
|
||||
val preference2 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.gradient_maker_type_image),
|
||||
subtitle = stringResource(R.string.gradient_maker_type_image_sub),
|
||||
startIcon = Icons.Outlined.Collections,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(false)
|
||||
pickImage()
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference3 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.mesh_gradients),
|
||||
subtitle = stringResource(R.string.mesh_gradients_sub),
|
||||
startIcon = Icons.Outlined.MeshGradient,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(true)
|
||||
allowPickingImage = false
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference4 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.gradient_maker_type_image_mesh),
|
||||
subtitle = stringResource(R.string.gradient_maker_type_image_mesh_sub),
|
||||
startIcon = Icons.Outlined.ImageOverlay,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(true)
|
||||
pickImage()
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference5 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.collection_mesh_gradients),
|
||||
subtitle = stringResource(R.string.collection_mesh_gradients_sub),
|
||||
startIcon = Icons.Outlined.MeshDownload,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.onNavigate(Screen.MeshGradients)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isPortrait) {
|
||||
Column {
|
||||
preference1()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference2()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference3()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference4()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference5()
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)
|
||||
)
|
||||
) {
|
||||
preference1.withModifier(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
preference2.withModifier(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)
|
||||
)
|
||||
) {
|
||||
preference3.withModifier(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
preference4.withModifier(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference5.withModifier(modifier = Modifier.fillMaxWidth(0.5f))
|
||||
}
|
||||
}
|
||||
},
|
||||
buttons = { actions ->
|
||||
val saveBitmap: (oneTimeSaveLocationUri: String?) -> Unit = {
|
||||
if (component.brush != null) {
|
||||
component.saveBitmaps(
|
||||
oneTimeSaveLocationUri = it,
|
||||
onStandaloneGradientSaveResult = essentials::parseSaveResult,
|
||||
onResult = essentials::parseSaveResults
|
||||
)
|
||||
}
|
||||
}
|
||||
var showFolderSelectionDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showOneTimeImagePickingDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
BottomButtonsBlock(
|
||||
targetState = (allowPickingImage == null) to isPortrait,
|
||||
onSecondaryButtonClick = pickImage,
|
||||
isSecondaryButtonVisible = allowPickingImage == true,
|
||||
isPrimaryButtonVisible = component.brush != null,
|
||||
showNullDataButtonAsContainer = true,
|
||||
onPrimaryButtonClick = {
|
||||
saveBitmap(null)
|
||||
},
|
||||
onPrimaryButtonLongClick = {
|
||||
showFolderSelectionDialog = true
|
||||
},
|
||||
actions = {
|
||||
if (isPortrait) actions()
|
||||
},
|
||||
onSecondaryButtonLongClick = {
|
||||
showOneTimeImagePickingDialog = true
|
||||
}
|
||||
)
|
||||
OneTimeSaveLocationSelectionDialog(
|
||||
visible = showFolderSelectionDialog,
|
||||
onDismiss = { showFolderSelectionDialog = false },
|
||||
onSaveRequest = saveBitmap,
|
||||
formatForFilenameSelection = component.getFormatForFilenameSelection()
|
||||
)
|
||||
OneTimeImagePickingDialog(
|
||||
onDismiss = { showOneTimeImagePickingDialog = false },
|
||||
picker = Picker.Multiple,
|
||||
imagePicker = imagePicker,
|
||||
visible = showOneTimeImagePickingDialog
|
||||
GradientMakerBottomButtons(
|
||||
component = component,
|
||||
actions = actions,
|
||||
imagePicker = imagePicker
|
||||
)
|
||||
},
|
||||
forceImagePreviewToMax = showOriginal,
|
||||
forceImagePreviewToMax = component.showOriginal,
|
||||
contentPadding = animateDpAsState(
|
||||
if (allowPickingImage == null) 12.dp
|
||||
else 20.dp
|
||||
).value
|
||||
)
|
||||
|
||||
val transformations by remember(
|
||||
component.brush,
|
||||
component.isMeshGradient,
|
||||
component.meshPoints,
|
||||
component.meshResolutionX,
|
||||
component.meshResolutionY,
|
||||
component.gradientAlpha
|
||||
) {
|
||||
derivedStateOf {
|
||||
listOf(
|
||||
component.getGradientTransformation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PickImageFromUrisSheet(
|
||||
transformations = transformations,
|
||||
visible = showPickImageFromUrisSheet,
|
||||
onDismiss = {
|
||||
showPickImageFromUrisSheet = false
|
||||
},
|
||||
uris = component.uris,
|
||||
selectedUri = component.selectedUri,
|
||||
onUriPicked = { uri ->
|
||||
component.updateSelectedUri(
|
||||
uri = uri,
|
||||
onFailure = essentials::showFailureToast
|
||||
)
|
||||
},
|
||||
onUriRemoved = { uri ->
|
||||
component.updateUrisSilently(removedUri = uri)
|
||||
},
|
||||
columns = if (isPortrait) 2 else 4,
|
||||
)
|
||||
|
||||
CompareSheet(
|
||||
beforeContent = {
|
||||
Picture(
|
||||
model = component.selectedUri,
|
||||
modifier = Modifier.aspectRatio(
|
||||
component.imageAspectRatio
|
||||
)
|
||||
)
|
||||
},
|
||||
afterContent = {
|
||||
if (component.isMeshGradient) {
|
||||
GradientPreview(
|
||||
meshGradientState = component.meshGradientState,
|
||||
gradientAlpha = component.gradientAlpha,
|
||||
allowPickingImage = allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
} else {
|
||||
val gradientState = rememberGradientState()
|
||||
LaunchedEffect(component.brush) {
|
||||
gradientState.gradientType = component.gradientType
|
||||
gradientState.linearGradientAngle = component.angle
|
||||
gradientState.centerFriction = component.centerFriction
|
||||
gradientState.radiusFriction = component.radiusFriction
|
||||
gradientState.colorStops.apply {
|
||||
clear()
|
||||
addAll(component.colorStops)
|
||||
}
|
||||
gradientState.tileMode = component.tileMode
|
||||
}
|
||||
GradientPreview(
|
||||
brush = gradientState.brush,
|
||||
gradientAlpha = component.gradientAlpha,
|
||||
allowPickingImage = allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
onSizeChanged = {
|
||||
gradientState.size = it
|
||||
},
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
}
|
||||
},
|
||||
visible = showCompareSheet,
|
||||
onDismiss = {
|
||||
showCompareSheet = false
|
||||
}
|
||||
)
|
||||
|
||||
LoadingDialog(
|
||||
visible = component.isSaving || component.isImageLoading,
|
||||
done = component.done,
|
||||
@ -659,7 +192,6 @@ fun GradientMakerContent(
|
||||
ExitWithoutSavingDialog(
|
||||
onExit = {
|
||||
if (allowPickingImage != null) {
|
||||
allowPickingImage = null
|
||||
component.resetState()
|
||||
} else {
|
||||
component.onGoBack()
|
||||
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
|
||||
import ru.tech.imageresizershrinker.core.ui.theme.blend
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.utils.AutoContentBasedColors
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerAppColorSchemeHandler(component: GradientMakerComponent) {
|
||||
AutoContentBasedColors(
|
||||
model = Triple(component.brush, component.meshPoints, component.selectedUri),
|
||||
selector = { (_, uri) ->
|
||||
component.createGradientBitmap(
|
||||
data = uri,
|
||||
integerSize = IntegerSize(1000, 1000)
|
||||
)
|
||||
},
|
||||
allowChangeColor = component.allowPickingImage != null
|
||||
)
|
||||
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
LaunchedEffect(component.colorStops) {
|
||||
if (component.colorStops.isEmpty()) {
|
||||
colorScheme.apply {
|
||||
component.addColorStop(
|
||||
pair = 0f to primary.blend(primaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
component.addColorStop(
|
||||
pair = 0.5f to secondary.blend(secondaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
component.addColorStop(
|
||||
pair = 1f to tertiary.blend(tertiaryContainer, 0.5f),
|
||||
isInitial = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.ImagePicker
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.provider.rememberLocalEssentials
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeImagePickingDialog
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerBottomButtons(
|
||||
component: GradientMakerComponent,
|
||||
actions: @Composable RowScope.() -> Unit,
|
||||
imagePicker: ImagePicker
|
||||
) {
|
||||
val essentials = rememberLocalEssentials()
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
val saveBitmap: (oneTimeSaveLocationUri: String?) -> Unit = {
|
||||
if (component.brush != null) {
|
||||
component.saveBitmaps(
|
||||
oneTimeSaveLocationUri = it,
|
||||
onStandaloneGradientSaveResult = essentials::parseSaveResult,
|
||||
onResult = essentials::parseSaveResults
|
||||
)
|
||||
}
|
||||
}
|
||||
var showFolderSelectionDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showOneTimeImagePickingDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
BottomButtonsBlock(
|
||||
targetState = (component.allowPickingImage == null) to isPortrait,
|
||||
onSecondaryButtonClick = imagePicker::pickImage,
|
||||
isSecondaryButtonVisible = component.allowPickingImage == true,
|
||||
isPrimaryButtonVisible = component.brush != null,
|
||||
showNullDataButtonAsContainer = true,
|
||||
onPrimaryButtonClick = {
|
||||
saveBitmap(null)
|
||||
},
|
||||
onPrimaryButtonLongClick = {
|
||||
showFolderSelectionDialog = true
|
||||
},
|
||||
actions = {
|
||||
if (isPortrait) actions()
|
||||
},
|
||||
onSecondaryButtonLongClick = {
|
||||
showOneTimeImagePickingDialog = true
|
||||
}
|
||||
)
|
||||
OneTimeSaveLocationSelectionDialog(
|
||||
visible = showFolderSelectionDialog,
|
||||
onDismiss = { showFolderSelectionDialog = false },
|
||||
onSaveRequest = saveBitmap,
|
||||
formatForFilenameSelection = component.getFormatForFilenameSelection()
|
||||
)
|
||||
OneTimeImagePickingDialog(
|
||||
onDismiss = { showOneTimeImagePickingDialog = false },
|
||||
picker = Picker.Multiple,
|
||||
imagePicker = imagePicker,
|
||||
visible = showOneTimeImagePickingDialog
|
||||
)
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.buttons.CompareButton
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.image.Picture
|
||||
import ru.tech.imageresizershrinker.feature.compare.presentation.components.CompareSheet
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerCompareButton(component: GradientMakerComponent) {
|
||||
var showCompareSheet by rememberSaveable { mutableStateOf(false) }
|
||||
CompareButton(
|
||||
onClick = { showCompareSheet = true },
|
||||
visible = component.brush != null && component.allowPickingImage == true && component.selectedUri != Uri.EMPTY
|
||||
)
|
||||
|
||||
CompareSheet(
|
||||
beforeContent = {
|
||||
Picture(
|
||||
model = component.selectedUri,
|
||||
modifier = Modifier.aspectRatio(
|
||||
component.imageAspectRatio
|
||||
)
|
||||
)
|
||||
},
|
||||
afterContent = {
|
||||
if (component.isMeshGradient) {
|
||||
MeshGradientPreview(
|
||||
meshGradientState = component.meshGradientState,
|
||||
gradientAlpha = component.gradientAlpha,
|
||||
allowPickingImage = component.allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
} else {
|
||||
val gradientState = rememberGradientState()
|
||||
LaunchedEffect(component.brush) {
|
||||
gradientState.gradientType = component.gradientType
|
||||
gradientState.linearGradientAngle = component.angle
|
||||
gradientState.centerFriction = component.centerFriction
|
||||
gradientState.radiusFriction = component.radiusFriction
|
||||
gradientState.colorStops.apply {
|
||||
clear()
|
||||
addAll(component.colorStops)
|
||||
}
|
||||
gradientState.tileMode = component.tileMode
|
||||
}
|
||||
GradientPreview(
|
||||
brush = gradientState.brush,
|
||||
gradientAlpha = component.gradientAlpha,
|
||||
allowPickingImage = component.allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
onSizeChanged = {
|
||||
gradientState.size = it
|
||||
},
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
}
|
||||
},
|
||||
visible = showCompareSheet,
|
||||
onDismiss = {
|
||||
showCompareSheet = false
|
||||
}
|
||||
)
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Build
|
||||
import androidx.compose.material.icons.rounded.DensitySmall
|
||||
import androidx.compose.material.icons.rounded.GridOn
|
||||
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
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import ru.tech.imageresizershrinker.core.resources.R
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.provider.rememberLocalEssentials
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.controls.SaveExifWidget
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.AlphaSelector
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.ImageFormatSelector
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedSliderItem
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.image.ImageCounter
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.sheets.PickImageFromUrisSheet
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.text.TitleItem
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerControls(component: GradientMakerComponent) {
|
||||
var showPickImageFromUrisSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ImageCounter(
|
||||
imageCount = component.uris.size.takeIf { it > 1 },
|
||||
onRepick = {
|
||||
showPickImageFromUrisSheet = true
|
||||
}
|
||||
)
|
||||
AnimatedContent(
|
||||
component.allowPickingImage == false
|
||||
) { canChangeSize ->
|
||||
if (canChangeSize) {
|
||||
GradientSizeSelector(
|
||||
value = component.gradientSize,
|
||||
onWidthChange = component::updateWidth,
|
||||
onHeightChange = component::updateHeight
|
||||
)
|
||||
} else {
|
||||
AlphaSelector(
|
||||
value = component.gradientAlpha,
|
||||
onValueChange = component::updateGradientAlpha,
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (component.isMeshGradient) {
|
||||
Column(
|
||||
modifier = Modifier.container(
|
||||
resultPadding = 0.dp
|
||||
)
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
TitleItem(
|
||||
text = stringResource(R.string.points_customization),
|
||||
icon = Icons.Rounded.Build,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp
|
||||
)
|
||||
)
|
||||
MeshGradientEditor(
|
||||
state = component.meshGradientState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnhancedSliderItem(
|
||||
value = component.meshGradientState.gridSize,
|
||||
title = stringResource(R.string.grid_size),
|
||||
icon = Icons.Rounded.GridOn,
|
||||
valueRange = 2f..6f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = { value ->
|
||||
if (value.roundToInt() != component.meshGradientState.gridSize) {
|
||||
val size = value.roundToInt()
|
||||
component.setResolution(lerp(1f, 16f, 2f / size))
|
||||
component.meshGradientState.points.apply {
|
||||
clear()
|
||||
addAll(generateMesh(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EnhancedSliderItem(
|
||||
value = component.meshResolutionX,
|
||||
title = stringResource(R.string.resolution),
|
||||
icon = Icons.Rounded.DensitySmall,
|
||||
valueRange = 1f..64f,
|
||||
internalStateTransformation = { it.roundToInt() },
|
||||
onValueChange = component::setResolution
|
||||
)
|
||||
} else {
|
||||
GradientTypeSelector(
|
||||
value = component.gradientType,
|
||||
onValueChange = component::setGradientType
|
||||
) {
|
||||
GradientPropertiesSelector(
|
||||
gradientType = component.gradientType,
|
||||
linearAngle = component.angle,
|
||||
onLinearAngleChange = component::updateLinearAngle,
|
||||
centerFriction = component.centerFriction,
|
||||
radiusFriction = component.radiusFriction,
|
||||
onRadialDimensionsChange = component::setRadialProperties
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ColorStopSelection(
|
||||
colorStops = component.colorStops,
|
||||
onRemoveClick = component::removeColorStop,
|
||||
onValueChange = component::updateColorStop,
|
||||
onAddColorStop = component::addColorStop
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TileModeSelector(
|
||||
value = component.tileMode,
|
||||
onValueChange = component::setTileMode
|
||||
)
|
||||
}
|
||||
if (component.allowPickingImage == true) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SaveExifWidget(
|
||||
checked = component.keepExif,
|
||||
imageFormat = component.imageFormat,
|
||||
onCheckedChange = component::toggleKeepExif
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ImageFormatSelector(
|
||||
value = component.imageFormat,
|
||||
forceEnabled = component.allowPickingImage == false,
|
||||
onValueChange = component::setImageFormat
|
||||
)
|
||||
}
|
||||
|
||||
val transformations by remember(
|
||||
component.brush,
|
||||
component.isMeshGradient,
|
||||
component.meshPoints,
|
||||
component.meshResolutionX,
|
||||
component.meshResolutionY,
|
||||
component.gradientAlpha
|
||||
) {
|
||||
derivedStateOf {
|
||||
listOf(
|
||||
component.getGradientTransformation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
val essentials = rememberLocalEssentials()
|
||||
|
||||
PickImageFromUrisSheet(
|
||||
transformations = transformations,
|
||||
visible = showPickImageFromUrisSheet,
|
||||
onDismiss = {
|
||||
showPickImageFromUrisSheet = false
|
||||
},
|
||||
uris = component.uris,
|
||||
selectedUri = component.selectedUri,
|
||||
onUriPicked = { uri ->
|
||||
component.updateSelectedUri(
|
||||
uri = uri,
|
||||
onFailure = essentials::showFailureToast
|
||||
)
|
||||
},
|
||||
onUriRemoved = { uri ->
|
||||
component.updateUrisSilently(removedUri = uri)
|
||||
},
|
||||
columns = if (isPortrait) 2 else 4,
|
||||
)
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.detectSwipes
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerImagePreview(component: GradientMakerComponent) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.detectSwipes(
|
||||
onSwipeRight = component::selectLeftUri,
|
||||
onSwipeLeft = component::selectRightUri
|
||||
)
|
||||
.container()
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (component.isMeshGradient) {
|
||||
MeshGradientPreview(
|
||||
meshGradientState = component.meshGradientState,
|
||||
gradientAlpha = if (component.showOriginal) 0f else component.gradientAlpha,
|
||||
allowPickingImage = component.allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
} else {
|
||||
GradientPreview(
|
||||
brush = component.brush,
|
||||
gradientAlpha = if (component.showOriginal) 0f else component.gradientAlpha,
|
||||
allowPickingImage = component.allowPickingImage,
|
||||
gradientSize = component.gradientSize,
|
||||
onSizeChanged = component::setPreviewSize,
|
||||
selectedUri = component.selectedUri,
|
||||
imageAspectRatio = component.imageAspectRatio
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.tech.imageresizershrinker.core.resources.R
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.ImageOverlay
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.MeshDownload
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.MeshGradient
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.withModifier
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.preferences.PreferenceItem
|
||||
import ru.tech.imageresizershrinker.feature.gradient_maker.presentation.screenLogic.GradientMakerComponent
|
||||
|
||||
@Composable
|
||||
internal fun GradientMakerNoDataControls(
|
||||
component: GradientMakerComponent,
|
||||
onPickImage: () -> Unit
|
||||
) {
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
val preference1 = @Composable {
|
||||
val screen = remember {
|
||||
Screen.GradientMaker()
|
||||
}
|
||||
PreferenceItem(
|
||||
title = stringResource(screen.title),
|
||||
subtitle = stringResource(screen.subtitle),
|
||||
startIcon = screen.icon,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(
|
||||
isMeshGradient = false,
|
||||
allowPickingImage = false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference2 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.gradient_maker_type_image),
|
||||
subtitle = stringResource(R.string.gradient_maker_type_image_sub),
|
||||
startIcon = Icons.Outlined.Collections,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(
|
||||
isMeshGradient = false,
|
||||
allowPickingImage = true
|
||||
)
|
||||
onPickImage()
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference3 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.mesh_gradients),
|
||||
subtitle = stringResource(R.string.mesh_gradients_sub),
|
||||
startIcon = Icons.Outlined.MeshGradient,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(
|
||||
isMeshGradient = true,
|
||||
allowPickingImage = false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference4 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.gradient_maker_type_image_mesh),
|
||||
subtitle = stringResource(R.string.gradient_maker_type_image_mesh_sub),
|
||||
startIcon = Icons.Outlined.ImageOverlay,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.setIsMeshGradient(
|
||||
isMeshGradient = true,
|
||||
allowPickingImage = true
|
||||
)
|
||||
onPickImage()
|
||||
}
|
||||
)
|
||||
}
|
||||
val preference5 = @Composable {
|
||||
PreferenceItem(
|
||||
title = stringResource(R.string.collection_mesh_gradients),
|
||||
subtitle = stringResource(R.string.collection_mesh_gradients_sub),
|
||||
startIcon = Icons.Outlined.MeshDownload,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
component.onNavigate(Screen.MeshGradients)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isPortrait) {
|
||||
Column {
|
||||
preference1()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference2()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference3()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference4()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference5()
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)
|
||||
)
|
||||
) {
|
||||
preference1.withModifier(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
preference2.withModifier(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)
|
||||
)
|
||||
) {
|
||||
preference3.withModifier(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
preference4.withModifier(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
preference5.withModifier(modifier = Modifier.fillMaxWidth(0.5f))
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -43,11 +42,10 @@ import com.smarttoolfactory.colordetector.util.ColorUtil.roundToTwoDigits
|
||||
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
|
||||
import ru.tech.imageresizershrinker.core.resources.icons.BrokenImageAlt
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.image.Picture
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.meshGradient
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.transparencyChecker
|
||||
|
||||
@Composable
|
||||
fun GradientPreview(
|
||||
internal fun GradientPreview(
|
||||
brush: ShaderBrush?,
|
||||
gradientAlpha: Float,
|
||||
allowPickingImage: Boolean?,
|
||||
@ -116,53 +114,3 @@ fun GradientPreview(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GradientPreview(
|
||||
meshGradientState: UiMeshGradientState,
|
||||
gradientAlpha: Float,
|
||||
allowPickingImage: Boolean?,
|
||||
gradientSize: IntegerSize,
|
||||
imageAspectRatio: Float,
|
||||
selectedUri: Uri
|
||||
) {
|
||||
val alpha by animateFloatAsState(gradientAlpha)
|
||||
AnimatedContent(
|
||||
targetState = if (allowPickingImage == true) {
|
||||
imageAspectRatio
|
||||
} else {
|
||||
gradientSize
|
||||
.aspectRatio
|
||||
.roundToTwoDigits()
|
||||
.coerceIn(0.01f..100f)
|
||||
}
|
||||
) { aspectRatio ->
|
||||
Box {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.aspectRatio(aspectRatio)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.then(
|
||||
if (allowPickingImage != true) {
|
||||
Modifier.transparencyChecker()
|
||||
} else Modifier
|
||||
)
|
||||
.meshGradient(
|
||||
points = meshGradientState.points,
|
||||
resolutionX = meshGradientState.resolutionX,
|
||||
resolutionY = meshGradientState.resolutionY,
|
||||
alpha = alpha
|
||||
)
|
||||
.zIndex(2f)
|
||||
)
|
||||
if (allowPickingImage == true) {
|
||||
Picture(
|
||||
model = selectedUri,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
size = 1500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
package ru.tech.imageresizershrinker.feature.gradient_maker.presentation.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.smarttoolfactory.colordetector.util.ColorUtil.roundToTwoDigits
|
||||
import ru.tech.imageresizershrinker.core.domain.model.IntegerSize
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.image.Picture
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.meshGradient
|
||||
import ru.tech.imageresizershrinker.core.ui.widget.modifier.transparencyChecker
|
||||
|
||||
@Composable
|
||||
internal fun MeshGradientPreview(
|
||||
meshGradientState: UiMeshGradientState,
|
||||
gradientAlpha: Float,
|
||||
allowPickingImage: Boolean?,
|
||||
gradientSize: IntegerSize,
|
||||
imageAspectRatio: Float,
|
||||
selectedUri: Uri
|
||||
) {
|
||||
val alpha by animateFloatAsState(gradientAlpha)
|
||||
AnimatedContent(
|
||||
targetState = if (allowPickingImage == true) {
|
||||
imageAspectRatio
|
||||
} else {
|
||||
gradientSize
|
||||
.aspectRatio
|
||||
.roundToTwoDigits()
|
||||
.coerceIn(0.01f..100f)
|
||||
}
|
||||
) { aspectRatio ->
|
||||
Box {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.aspectRatio(aspectRatio)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.then(
|
||||
if (allowPickingImage != true) {
|
||||
Modifier.transparencyChecker()
|
||||
} else Modifier
|
||||
)
|
||||
.meshGradient(
|
||||
points = meshGradientState.points,
|
||||
resolutionX = meshGradientState.resolutionX,
|
||||
resolutionY = meshGradientState.resolutionY,
|
||||
alpha = alpha
|
||||
)
|
||||
.zIndex(2f)
|
||||
)
|
||||
if (allowPickingImage == true) {
|
||||
Picture(
|
||||
model = selectedUri,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
size = 1500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -90,6 +90,15 @@ class GradientMakerComponent @AssistedInject internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val _allowPickingImage: MutableState<Boolean?> = mutableStateOf(
|
||||
if (initialUris.isNullOrEmpty()) null
|
||||
else true
|
||||
)
|
||||
val allowPickingImage by _allowPickingImage
|
||||
|
||||
private val _showOriginal: MutableState<Boolean> = mutableStateOf(false)
|
||||
val showOriginal by _showOriginal
|
||||
|
||||
private var _gradientState = UiGradientState()
|
||||
private val gradientState: UiGradientState get() = _gradientState
|
||||
|
||||
@ -371,8 +380,12 @@ class GradientMakerComponent @AssistedInject internal constructor(
|
||||
registerChanges()
|
||||
}
|
||||
|
||||
fun setIsMeshGradient(value: Boolean) {
|
||||
_isMeshGradient.update { value }
|
||||
fun setIsMeshGradient(
|
||||
isMeshGradient: Boolean,
|
||||
allowPickingImage: Boolean
|
||||
) {
|
||||
_isMeshGradient.update { isMeshGradient }
|
||||
_allowPickingImage.update { allowPickingImage }
|
||||
}
|
||||
|
||||
fun addColorStop(
|
||||
@ -437,6 +450,7 @@ class GradientMakerComponent @AssistedInject internal constructor(
|
||||
_gradientState = UiGradientState()
|
||||
_meshGradientState = UiMeshGradientState()
|
||||
_isMeshGradient.update { false }
|
||||
_allowPickingImage.update { null }
|
||||
registerChangesCleared()
|
||||
}
|
||||
}
|
||||
@ -463,6 +477,7 @@ class GradientMakerComponent @AssistedInject internal constructor(
|
||||
uris: List<Uri>,
|
||||
onFailure: (Throwable) -> Unit = {}
|
||||
) {
|
||||
_allowPickingImage.update { true }
|
||||
_uris.update { uris }
|
||||
uris.firstOrNull()?.let { updateSelectedUri(it, onFailure) }
|
||||
}
|
||||
@ -584,6 +599,10 @@ class GradientMakerComponent @AssistedInject internal constructor(
|
||||
fun getFormatForFilenameSelection(): ImageFormat? = if (uris.size < 2) imageFormat
|
||||
else null
|
||||
|
||||
fun setShowOriginal(value: Boolean) {
|
||||
_showOriginal.update { value }
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
operator fun invoke(
|
||||
|
@ -93,7 +93,6 @@ fun ToneCurvesEditOption(
|
||||
)
|
||||
}
|
||||
ShowOriginalButton(
|
||||
canShow = true,
|
||||
onStateChange = {
|
||||
showOriginal = it
|
||||
}
|
||||
|
@ -183,7 +183,6 @@ fun WatermarkingContent(
|
||||
}
|
||||
if (component.internalBitmap != null) {
|
||||
ShowOriginalButton(
|
||||
canShow = true,
|
||||
onStateChange = {
|
||||
showOriginal = it
|
||||
}
|
||||
|
Reference in New Issue
Block a user