mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-12-28 13:22:30 +00:00
Work on Palette Tools
This commit is contained in:
parent
548a49b263
commit
9109905e63
@ -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 {
|
||||
|
||||
@ -54,6 +54,11 @@ object ListUtils {
|
||||
else this + item
|
||||
}
|
||||
|
||||
inline fun <T> Iterable<T>.replaceAt(index: Int, transform: (T) -> T): List<T> =
|
||||
toMutableList().apply {
|
||||
this[index] = transform(this[index])
|
||||
}
|
||||
|
||||
fun <T> Set<T>.toggle(item: T): Set<T> = run {
|
||||
if (item in this) this - item
|
||||
else this + item
|
||||
|
||||
@ -1851,4 +1851,9 @@
|
||||
<string name="square_particles_sub">Spray particles will be square shaped instead of circles</string>
|
||||
<string name="palette_tools">Palette Tools</string>
|
||||
<string name="palette_tools_sub">Generate basic/material you palette from image, or import/export across different palette formats</string>
|
||||
<string name="edit_palette">Edit Palette</string>
|
||||
<string name="edit_palette_sub">Export/import palette across various formats</string>
|
||||
<string name="color_name">Color name</string>
|
||||
<string name="palette_name">Palette name</string>
|
||||
<string name="palette_format">Palette Format</string>
|
||||
</resources>
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
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> = 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))
|
||||
}
|
||||
}
|
||||
@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,5 +19,6 @@ package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components
|
||||
|
||||
enum class PaletteType {
|
||||
Default,
|
||||
MaterialYou
|
||||
MaterialYou,
|
||||
Edit
|
||||
}
|
||||
@ -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 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<NamedColor>
|
||||
)
|
||||
|
||||
data class NamedPalette(
|
||||
val name: String = "",
|
||||
val colors: List<NamedColor> = emptyList(),
|
||||
val groups: List<NamedColorGroup> = 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
|
||||
)
|
||||
@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components.model
|
||||
|
||||
import com.t8rin.palette.PaletteFormat
|
||||
|
||||
object PaletteFormatHelper {
|
||||
val entries: Set<PaletteFormat> =
|
||||
PaletteFormat.formatsWithDecodeAndEncode.toSet().minus(
|
||||
setOf(
|
||||
PaletteFormat.CSV,
|
||||
PaletteFormat.HEX_RGBA
|
||||
)
|
||||
).plus(
|
||||
setOf(
|
||||
PaletteFormat.HEX_RGBA,
|
||||
PaletteFormat.CSV
|
||||
)
|
||||
)
|
||||
|
||||
fun entriesFor(filename: String): Set<PaletteFormat> = buildSet {
|
||||
val format = entries.firstOrNull { format ->
|
||||
format.fileExtension.isNotEmpty() && format.fileExtension.any(filename::endsWith)
|
||||
}
|
||||
|
||||
format?.let {
|
||||
add(format)
|
||||
addAll(entries - format)
|
||||
} ?: addAll(entries)
|
||||
}
|
||||
}
|
||||
@ -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<Bitmap>,
|
||||
private val imageGetter: ImageGetter<Bitmap>,
|
||||
private val fileController: FileController,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : BaseComponent(dispatchersHolder, componentContext) {
|
||||
|
||||
@ -51,6 +61,12 @@ class PaletteToolsComponent @AssistedInject internal constructor(
|
||||
private val _paletteType: MutableState<PaletteType?> = mutableStateOf(null)
|
||||
val paletteType by _paletteType
|
||||
|
||||
private val _paletteFormat: MutableState<PaletteFormat?> = mutableStateOf(null)
|
||||
val paletteFormat by _paletteFormat
|
||||
|
||||
private val _palette: MutableState<NamedPalette> = mutableStateOf(NamedPalette())
|
||||
val palette by _palette
|
||||
|
||||
private val _bitmap: MutableState<Bitmap?> = 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user