From 9109905e635ec572c64e41c9c2f6dba73540cd0b Mon Sep 17 00:00:00 2001 From: T8RIN Date: Mon, 10 Nov 2025 03:23:35 +0300 Subject: [PATCH] Work on Palette Tools --- .../imagetoolbox/core/data/utils/UriUtils.kt | 3 +- .../core/domain/utils/ListUtils.kt | 5 + .../resources/src/main/res/values/strings.xml | 5 + .../core/ui/utils/navigation/ScreenUtils.kt | 2 +- .../presentation/PaletteToolsContent.kt | 74 ++-- .../components/EditPaletteControls.kt | 315 ++++++++++++++++++ .../components/PaletteColorNameField.kt | 110 ++++++ .../components/PaletteToolsScreenControls.kt | 22 +- .../presentation/components/PaletteType.kt | 3 +- .../components/model/NamedColor.kt | 60 ++++ .../components/model/PaletteFormatHelper.kt | 46 +++ .../screenLogic/PaletteToolsComponent.kt | 71 +++- 12 files changed, 677 insertions(+), 39 deletions(-) create mode 100644 feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/EditPaletteControls.kt create mode 100644 feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteColorNameField.kt create mode 100644 feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/NamedColor.kt create mode 100644 feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/PaletteFormatHelper.kt diff --git a/core/data/src/main/java/com/t8rin/imagetoolbox/core/data/utils/UriUtils.kt b/core/data/src/main/java/com/t8rin/imagetoolbox/core/data/utils/UriUtils.kt index 9683fda13..a30c44979 100644 --- a/core/data/src/main/java/com/t8rin/imagetoolbox/core/data/utils/UriUtils.kt +++ b/core/data/src/main/java/com/t8rin/imagetoolbox/core/data/utils/UriUtils.kt @@ -30,6 +30,7 @@ import com.t8rin.imagetoolbox.core.domain.model.ImageModel import com.t8rin.imagetoolbox.core.domain.saving.io.Writeable import com.t8rin.imagetoolbox.core.domain.utils.FileMode import com.t8rin.imagetoolbox.core.resources.R +import com.t8rin.imagetoolbox.core.utils.appContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filterIsInstance @@ -190,7 +191,7 @@ internal fun Uri.tryRequireOriginal(context: Context): Uri { } fun Uri.getFilename( - context: Context + context: Context = appContext ): String? = DocumentFile.fromSingleUri(context, this)?.name fun String.decodeEscaped(): String = runCatching { diff --git a/core/domain/src/main/kotlin/com/t8rin/imagetoolbox/core/domain/utils/ListUtils.kt b/core/domain/src/main/kotlin/com/t8rin/imagetoolbox/core/domain/utils/ListUtils.kt index 5489249c1..3c5aa61be 100644 --- a/core/domain/src/main/kotlin/com/t8rin/imagetoolbox/core/domain/utils/ListUtils.kt +++ b/core/domain/src/main/kotlin/com/t8rin/imagetoolbox/core/domain/utils/ListUtils.kt @@ -54,6 +54,11 @@ object ListUtils { else this + item } + inline fun Iterable.replaceAt(index: Int, transform: (T) -> T): List = + toMutableList().apply { + this[index] = transform(this[index]) + } + fun Set.toggle(item: T): Set = run { if (item in this) this - item else this + item diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index bec07da7f..be82dd25f 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -1851,4 +1851,9 @@ Spray particles will be square shaped instead of circles Palette Tools Generate basic/material you palette from image, or import/export across different palette formats + Edit Palette + Export/import palette across various formats + Color name + Palette name + Palette Format diff --git a/core/ui/src/main/kotlin/com/t8rin/imagetoolbox/core/ui/utils/navigation/ScreenUtils.kt b/core/ui/src/main/kotlin/com/t8rin/imagetoolbox/core/ui/utils/navigation/ScreenUtils.kt index 547f8f44b..2aa5048df 100644 --- a/core/ui/src/main/kotlin/com/t8rin/imagetoolbox/core/ui/utils/navigation/ScreenUtils.kt +++ b/core/ui/src/main/kotlin/com/t8rin/imagetoolbox/core/ui/utils/navigation/ScreenUtils.kt @@ -401,5 +401,5 @@ private object ScreenConstantsImpl : ScreenConstants { typedEntries.flatMap { it.entries }.sortedBy { it.id } } - override val FEATURES_COUNT = 72 + override val FEATURES_COUNT = 73 } \ No newline at end of file diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/PaletteToolsContent.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/PaletteToolsContent.kt index 91b60060f..6e9120528 100644 --- a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/PaletteToolsContent.kt +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/PaletteToolsContent.kt @@ -33,6 +33,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.InsertDriveFile +import androidx.compose.material.icons.rounded.FileOpen import androidx.compose.material.icons.rounded.Palette import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -47,10 +49,12 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.t8rin.imagetoolbox.core.resources.R +import com.t8rin.imagetoolbox.core.resources.icons.AddPhotoAlt import com.t8rin.imagetoolbox.core.resources.icons.Eyedropper import com.t8rin.imagetoolbox.core.resources.icons.PaletteSwatch import com.t8rin.imagetoolbox.core.resources.icons.Theme import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.Picker +import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberFilePicker import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberImagePicker import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState import com.t8rin.imagetoolbox.core.ui.widget.AdaptiveLayoutScreen @@ -111,9 +115,15 @@ fun PaletteToolsContent( component.setUri(uri) } + val paletteFormatPicker = rememberFilePicker { uri: Uri -> + component.setPaletteType(PaletteType.Edit) + component.setUri(uri) + } + val pickImage = when (paletteType) { PaletteType.MaterialYou -> materialYouImageLauncher::pickImage PaletteType.Default -> paletteImageLauncher::pickImage + PaletteType.Edit -> paletteFormatPicker::pickFile null -> imagePicker::pickImage } @@ -164,16 +174,30 @@ fun PaletteToolsContent( } ) } + val preference3 = @Composable { + PreferenceItem( + title = stringResource(R.string.edit_palette), + subtitle = stringResource(R.string.edit_palette_sub), + startIcon = Icons.AutoMirrored.Outlined.InsertDriveFile, + modifier = Modifier.fillMaxWidth(), + onClick = { + component.setPaletteType(PaletteType.Edit) + showPreferencePicker = false + } + ) + } if (isPortrait) { Column { preference1() Spacer(modifier = Modifier.height(8.dp)) preference2() + Spacer(modifier = Modifier.height(8.dp)) + preference3() } } else { val direction = LocalLayoutDirection.current - Row( - modifier = Modifier.padding( + Column( + Modifier.padding( WindowInsets.displayCutout.asPaddingValues() .let { PaddingValues( @@ -183,9 +207,17 @@ fun PaletteToolsContent( } ) ) { - preference1.withModifier(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.width(8.dp)) - preference2.withModifier(modifier = Modifier.weight(1f)) + Row { + 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 { + preference3() + Spacer(modifier = Modifier.width(8.dp)) + Spacer(Modifier.weight(1f)) + } } } } @@ -197,6 +229,7 @@ fun PaletteToolsContent( title = when (paletteType) { PaletteType.MaterialYou -> stringResource(R.string.material_you) PaletteType.Default -> stringResource(R.string.generate_palette) + PaletteType.Edit -> stringResource(R.string.edit_palette) null -> stringResource(R.string.palette_tools) }, input = component.bitmap, @@ -216,7 +249,7 @@ fun PaletteToolsContent( onClick = { showZoomSheet = true }, visible = component.bitmap != null, ) - if (component.uri != null) { + if (component.bitmap != null) { EnhancedIconButton( onClick = { showColorPickerSheet = true @@ -240,12 +273,9 @@ fun PaletteToolsContent( showImagePreviewAsStickyHeader = paletteType == PaletteType.Default, placeImagePreview = paletteType == PaletteType.Default, controls = { - component.bitmap?.let { bitmap -> - PaletteToolsScreenControls( - bitmap = bitmap, - paletteType = paletteType - ) - } + PaletteToolsScreenControls( + component = component + ) }, buttons = { actions -> var showOneTimeImagePickingDialog by rememberSaveable { @@ -253,17 +283,25 @@ fun PaletteToolsContent( } BottomButtonsBlock( - isNoData = paletteType == null || component.bitmap == null, + isNoData = if (paletteType == PaletteType.Edit) { + !component.palette.isNotEmpty() + } else { + paletteType == null || component.bitmap == null + }, onSecondaryButtonClick = pickImage, isPrimaryButtonVisible = false, + secondaryButtonIcon = if (paletteType == PaletteType.Edit) Icons.Rounded.FileOpen else Icons.Rounded.AddPhotoAlt, + secondaryButtonText = stringResource( + if (paletteType == PaletteType.Edit) R.string.pick_file else R.string.pick_image_alt + ), onPrimaryButtonClick = {}, - showNullDataButtonAsContainer = true, + showNullDataButtonAsContainer = paletteType != PaletteType.Edit, actions = { if (isPortrait) actions() }, onSecondaryButtonLongClick = { showOneTimeImagePickingDialog = true - } + }.takeIf { paletteType != PaletteType.Edit } ) OneTimeImagePickingDialog( @@ -278,10 +316,8 @@ fun PaletteToolsContent( else 20.dp ).value, insetsForNoData = WindowInsets(0), - noDataControls = { - preferences() - }, - canShowScreenData = paletteType != null && component.bitmap != null + noDataControls = { preferences() }, + canShowScreenData = paletteType != null && (paletteType == PaletteType.Edit || component.bitmap != null) ) var colorPickerValue by rememberSaveable(stateSaver = ColorSaver) { diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/EditPaletteControls.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/EditPaletteControls.kt new file mode 100644 index 000000000..9551e14c0 --- /dev/null +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/EditPaletteControls.kt @@ -0,0 +1,315 @@ +/* + * 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 . + */ + +package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.t8rin.imagetoolbox.core.domain.utils.ListUtils.replaceAt +import com.t8rin.imagetoolbox.core.resources.R +import com.t8rin.imagetoolbox.core.resources.icons.Swatch +import com.t8rin.imagetoolbox.core.ui.utils.helper.toHex +import com.t8rin.imagetoolbox.core.ui.widget.color_picker.ColorPickerSheet +import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedChip +import com.t8rin.imagetoolbox.core.ui.widget.enhanced.hapticsClickable +import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults +import com.t8rin.imagetoolbox.core.ui.widget.modifier.animateContentSizeNoClip +import com.t8rin.imagetoolbox.core.ui.widget.modifier.container +import com.t8rin.imagetoolbox.core.ui.widget.modifier.shapeByInteraction +import com.t8rin.imagetoolbox.core.ui.widget.text.RoundedTextField +import com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model.NamedPalette +import com.t8rin.palette.PaletteFormat + + +@Composable +internal fun EditPaletteControls( + paletteFormat: PaletteFormat?, + onPaletteFormatChange: (PaletteFormat) -> Unit, + palette: NamedPalette, + onPaletteChange: (NamedPalette) -> Unit +) { + Spacer(modifier = Modifier.height(16.dp)) + + RoundedTextField( + value = palette.name, + onValueChange = { + onPaletteChange( + palette.copy( + name = it + ) + ) + }, + modifier = Modifier + .container( + shape = ShapeDefaults.top, + resultPadding = 8.dp + ), + label = { Text(stringResource(R.string.palette_name)) }, + startIcon = { + Icon( + imageVector = Icons.Rounded.Swatch, + contentDescription = null + ) + } + ) + Spacer(modifier = Modifier.height(4.dp)) + PaletteFormatSelector( + shape = ShapeDefaults.bottom, + value = paletteFormat ?: PaletteFormat.JSON, + onValueChange = onPaletteFormatChange + ) + Spacer(modifier = Modifier.height(12.dp)) + AnimatedVisibility( + visible = palette.colors.isNotEmpty(), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.container(resultPadding = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + palette.colors.forEachIndexed { index, data -> + val baseShape = ShapeDefaults.byIndex( + index = index, + size = palette.colors.size + ) + val interactionSource = remember { MutableInteractionSource() } + val shape = shapeByInteraction( + shape = baseShape, + pressedShape = ShapeDefaults.pressed, + interactionSource = interactionSource + ) + var showColorPicker by remember { + mutableStateOf(false) + } + + ColorPickerSheet( + visible = showColorPicker, + onDismiss = { showColorPicker = false }, + color = data.color, + onColorSelected = { + onPaletteChange( + palette.copy( + colors = palette.colors.replaceAt(index) { item -> + item.copy( + color = it.copy(1f) + ) + } + ) + ) + }, + allowAlpha = false + ) + + Row( + modifier = Modifier + .container( + shape = shape, + color = MaterialTheme.colorScheme.surface, + resultPadding = 0.dp + ) + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(40.dp) + .container( + shape = CircleShape, + color = data.color + ) + ) + + PaletteColorNameField( + value = data.name, + onValueChange = { + onPaletteChange( + palette.copy( + colors = palette.colors.replaceAt(index) { item -> + item.copy( + name = it + ) + } + ) + ) + }, + modifier = Modifier + .weight(1f) + .heightIn(min = 40.dp) + ) + + val containerColor = + MaterialTheme.colorScheme.secondaryContainer + val interactionSource = + remember { MutableInteractionSource() } + + Box( + modifier = Modifier + .container( + shape = shapeByInteraction( + shape = CircleShape, + pressedShape = ShapeDefaults.pressed, + interactionSource = interactionSource + ), + color = containerColor, + resultPadding = 0.dp, + ) + .hapticsClickable( + interactionSource = interactionSource, + indication = LocalIndication.current + ) { + showColorPicker = true + }, + contentAlignment = Alignment.Center + ) { + Text( + text = "#FFFFFF", + fontSize = 15.sp, + modifier = Modifier + .padding( + vertical = 8.dp, + horizontal = 16.dp + ) + .alpha(0f) + ) + + Text( + text = remember(data.color) { + data.color.toHex().uppercase() + }, + color = MaterialTheme.colorScheme.contentColorFor( + containerColor + ), + fontSize = 15.sp, + modifier = Modifier + .padding( + vertical = 8.dp, + horizontal = 16.dp + ) + ) + } + } + } + } + } + Spacer(modifier = Modifier.height(16.dp)) +} + +@Composable +internal fun PaletteFormatSelector( + modifier: Modifier = Modifier, + shape: Shape = ShapeDefaults.extraLarge, + backgroundColor: Color = Color.Unspecified, + entries: List = PaletteFormat.formatsWithDecodeAndEncode, + value: PaletteFormat, + onValueChange: (PaletteFormat) -> Unit +) { + Column( + modifier = modifier + .container( + shape = shape, + color = backgroundColor + ) + .animateContentSizeNoClip(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Spacer(Modifier.height(4.dp)) + Text( + text = stringResource(R.string.palette_format), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + + AnimatedContent( + targetState = entries, + modifier = Modifier.fillMaxWidth() + ) { items -> + FlowRow( + verticalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.CenterVertically + ), + horizontalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.CenterHorizontally + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .container( + shape = ShapeDefaults.default, + color = MaterialTheme.colorScheme.surface + ) + .padding(horizontal = 8.dp, vertical = 12.dp) + ) { + items.forEach { + EnhancedChip( + onClick = { + onValueChange(it) + }, + selected = value == it, + label = { + Text(text = it.name.uppercase().replace("_", " ")) + }, + selectedColor = MaterialTheme.colorScheme.tertiary, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp) + ) + } + } + } + Spacer(Modifier.height(4.dp)) + } +} diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteColorNameField.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteColorNameField.kt new file mode 100644 index 000000000..75b6bd35e --- /dev/null +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteColorNameField.kt @@ -0,0 +1,110 @@ +/* + * 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 . + */ + +package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.t8rin.imagetoolbox.core.resources.R +import com.t8rin.imagetoolbox.core.ui.widget.text.RoundedTextFieldColors + + +@Composable +internal fun PaletteColorNameField( + value: String, + onValueChange: (String) -> Unit, + shape: Shape = RoundedCornerShape(20.dp), + colors: TextFieldColors = RoundedTextFieldColors(false), + modifier: Modifier = Modifier +) { + var isFocused by remember { + mutableStateOf(false) + } + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = LocalTextStyle.current.copy( + fontSize = 17.sp, + fontWeight = FontWeight.Bold, + color = colors.textColor( + enabled = true, + isError = false, + focused = true + ) + ), + modifier = modifier + .background( + color = colors.containerColor( + enabled = true, + isError = false, + focused = isFocused + ), + shape = shape + ) + .border( + width = animateDpAsState( + if (isFocused) 2.dp else 1.dp + ).value, + color = if (isFocused) { + colors.focusedIndicatorColor + } else { + colors.unfocusedIndicatorColor + }, + shape = shape + ) + .onFocusChanged { isFocused = it.isFocused }, + maxLines = 3, + cursorBrush = SolidColor(colors.focusedIndicatorColor) + ) { inner -> + Box( + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = 8.dp + ) + ) { + inner() + if (value.isEmpty()) { + Text( + text = stringResource(id = R.string.color_name), + color = MaterialTheme.colorScheme.outline + ) + } + } + } +} \ No newline at end of file diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteToolsScreenControls.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteToolsScreenControls.kt index 14e6b3ec6..f99f25203 100644 --- a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteToolsScreenControls.kt +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteToolsScreenControls.kt @@ -17,18 +17,19 @@ package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components -import android.graphics.Bitmap import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import com.t8rin.imagetoolbox.feature.palette_tools.presentation.screenLogic.PaletteToolsComponent @Composable internal fun PaletteToolsScreenControls( - bitmap: Bitmap, - paletteType: PaletteType? + component: PaletteToolsComponent ) { - if (paletteType == null) return + val paletteType = component.paletteType ?: return + + val bitmap = component.bitmap AnimatedContent( targetState = paletteType @@ -37,8 +38,17 @@ internal fun PaletteToolsScreenControls( horizontalAlignment = Alignment.CenterHorizontally ) { when (type) { - PaletteType.Default -> DefaultPaletteControls(bitmap) - PaletteType.MaterialYou -> MaterialYouPaletteControls(bitmap) + PaletteType.Default -> bitmap?.let { DefaultPaletteControls(bitmap) } + PaletteType.MaterialYou -> bitmap?.let { MaterialYouPaletteControls(bitmap) } + + PaletteType.Edit -> { + EditPaletteControls( + paletteFormat = component.paletteFormat, + onPaletteFormatChange = component::updatePaletteFormat, + palette = component.palette, + onPaletteChange = component::updatePalette + ) + } } } } diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteType.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteType.kt index 270aea3ff..e2d2907ff 100644 --- a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteType.kt +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/PaletteType.kt @@ -19,5 +19,6 @@ package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components enum class PaletteType { Default, - MaterialYou + MaterialYou, + Edit } \ No newline at end of file diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/NamedColor.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/NamedColor.kt new file mode 100644 index 000000000..0d50ab28d --- /dev/null +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/NamedColor.kt @@ -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 . + */ + +package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model + +import androidx.compose.ui.graphics.Color +import com.t8rin.palette.Palette +import com.t8rin.palette.PaletteColor + +data class NamedColor( + val color: Color, + val name: String +) + +data class NamedColorGroup( + val name: String, + val colors: List +) + +data class NamedPalette( + val name: String = "", + val colors: List = emptyList(), + val groups: List = emptyList() +) { + fun isNotEmpty() = name.isNotBlank() || colors.isNotEmpty() || groups.isNotEmpty() +} + +fun Palette.toNamed(): NamedPalette? { + if (name.isEmpty() && colors.isEmpty() && groups.isEmpty()) return null + + return NamedPalette( + name = name, + colors = colors.map { it.toNamed() }.filter { it.color.alpha > 0f }.distinct(), + groups = groups.map { group -> + NamedColorGroup( + name = group.name, + colors = group.colors.map { it.toNamed() }.filter { it.color.alpha > 0f } + ) + }.distinct() + ) +} + +fun PaletteColor.toNamed(): NamedColor = NamedColor( + color = toComposeColor(), + name = name +) \ No newline at end of file diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/PaletteFormatHelper.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/PaletteFormatHelper.kt new file mode 100644 index 000000000..bb527072d --- /dev/null +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/components/model/PaletteFormatHelper.kt @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model + +import com.t8rin.palette.PaletteFormat + +object PaletteFormatHelper { + val entries: Set = + PaletteFormat.formatsWithDecodeAndEncode.toSet().minus( + setOf( + PaletteFormat.CSV, + PaletteFormat.HEX_RGBA + ) + ).plus( + setOf( + PaletteFormat.HEX_RGBA, + PaletteFormat.CSV + ) + ) + + fun entriesFor(filename: String): Set = buildSet { + val format = entries.firstOrNull { format -> + format.fileExtension.isNotEmpty() && format.fileExtension.any(filename::endsWith) + } + + format?.let { + add(format) + addAll(entries - format) + } ?: addAll(entries) + } +} \ No newline at end of file diff --git a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/screenLogic/PaletteToolsComponent.kt b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/screenLogic/PaletteToolsComponent.kt index 9e3d0afb2..f96b7b785 100644 --- a/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/screenLogic/PaletteToolsComponent.kt +++ b/feature/palette-tools/src/main/java/com/t8rin/imagetoolbox/feature/palette_tools/presentation/screenLogic/PaletteToolsComponent.kt @@ -23,12 +23,21 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import com.arkivanov.decompose.ComponentContext +import com.t8rin.imagetoolbox.core.data.utils.getFilename import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder import com.t8rin.imagetoolbox.core.domain.image.ImageGetter import com.t8rin.imagetoolbox.core.domain.image.ImageScaler +import com.t8rin.imagetoolbox.core.domain.saving.FileController import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent import com.t8rin.imagetoolbox.core.ui.utils.state.update import com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.PaletteType +import com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model.NamedPalette +import com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model.PaletteFormatHelper +import com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model.toNamed +import com.t8rin.palette.PaletteFormat +import com.t8rin.palette.decode +import com.t8rin.palette.getCoder +import com.t8rin.palette.use import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -39,6 +48,7 @@ class PaletteToolsComponent @AssistedInject internal constructor( @Assisted val onGoBack: () -> Unit, private val imageScaler: ImageScaler, private val imageGetter: ImageGetter, + private val fileController: FileController, dispatchersHolder: DispatchersHolder ) : BaseComponent(dispatchersHolder, componentContext) { @@ -51,6 +61,12 @@ class PaletteToolsComponent @AssistedInject internal constructor( private val _paletteType: MutableState = mutableStateOf(null) val paletteType by _paletteType + private val _paletteFormat: MutableState = mutableStateOf(null) + val paletteFormat by _paletteFormat + + private val _palette: MutableState = mutableStateOf(NamedPalette()) + val palette by _palette + private val _bitmap: MutableState = mutableStateOf(null) val bitmap: Bitmap? by _bitmap @@ -61,26 +77,59 @@ class PaletteToolsComponent @AssistedInject internal constructor( _uri.value = uri if (uri == null) { _paletteType.update { null } + _paletteFormat.update { null } + _palette.update { NamedPalette() } _bitmap.value = null return } - imageGetter.getImageAsync( - uri = uri.toString(), - originalSize = false, - onGetImage = { - componentScope.launch { - _isImageLoading.value = true - _bitmap.value = imageScaler.scaleUntilCanShow(it.image) - _isImageLoading.value = false + componentScope.launch { + _isImageLoading.value = true + + _bitmap.value = imageScaler.scaleUntilCanShow( + imageGetter.getImage( + data = uri.toString(), + originalSize = false + ) + ) + + if (bitmap == null) { + val data = fileController.readBytes(uri.toString()) + val entries = PaletteFormatHelper.entriesFor(uri.getFilename() ?: uri.toString()) + + for (format in entries) { + format.getCoder().use { decode(data) }.onSuccess { palette -> + palette.toNamed()?.let { named -> + _palette.update { named } + updatePaletteFormat(format) + break + } + } } - }, - onFailure = {} - ) + + if (palette.isNotEmpty()) { + + } + } + + _isImageLoading.value = false + } + } + + fun updatePalette(palette: NamedPalette) { + _palette.update { palette } + } + + fun updatePaletteFormat(format: PaletteFormat) { + _paletteFormat.update { format } } fun setPaletteType(type: PaletteType) { _paletteType.update { type } + if (type != PaletteType.Edit) { + _palette.update { NamedPalette() } + _paletteFormat.update { null } + } } @AssistedFactory