Work on Palette Tools

This commit is contained in:
T8RIN 2025-11-10 03:23:35 +03:00
parent 548a49b263
commit 9109905e63
12 changed files with 677 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,5 +19,6 @@ package com.t8rin.imagetoolbox.feature.palette_tools.presentation.components
enum class PaletteType {
Default,
MaterialYou
MaterialYou,
Edit
}

View File

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

View File

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

View File

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