Start working on base64 encode/decode to image by #1545

This commit is contained in:
T8RIN
2024-12-24 18:23:21 +03:00
parent b1ffc87e69
commit f5cb4d0848
25 changed files with 1256 additions and 67 deletions

View File

@ -27,7 +27,7 @@ import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import okio.Buffer
import java.util.regex.Pattern
import ru.tech.imageresizershrinker.core.domain.utils.isBase64
internal class Base64Fetcher(
private val options: Options,
@ -56,20 +56,12 @@ internal class Base64Fetcher(
imageLoader: ImageLoader,
): Fetcher? {
val stripped = data.toString().substringAfter(",")
return if (data.toString().startsWith("data:image") || isBase64(stripped)) {
return if (isBase64(stripped)) {
Base64Fetcher(
options = options,
base64 = stripped
)
} else null
}
companion object {
private fun isBase64(data: String) = BASE64_PATTERN.matcher(data).matches()
private val BASE64_PATTERN = Pattern.compile(
"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
)
}
}
}

View File

@ -17,7 +17,17 @@
package ru.tech.imageresizershrinker.core.domain.utils
import java.util.regex.Pattern
inline fun <reified T> T?.notNullAnd(
predicate: (T) -> Boolean
): Boolean = if (this != null) predicate(this)
else false
else false
fun isBase64(data: String) = data.trim { it.isWhitespace() }
.isNotEmpty()
.and(BASE64_PATTERN.matcher(data).matches() || data.startsWith("data:image"))
private val BASE64_PATTERN = Pattern.compile(
"^(?=(.{4})*\$)[A-Za-z0-9+/]*={0,2}\$"
)

View File

@ -0,0 +1,245 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.core.resources.icons
import androidx.compose.material.icons.Icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val Icons.Rounded.Base64: ImageVector by lazy {
ImageVector.Builder(
name = "Base64Rounded",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(6.566f, 16.554f)
horizontalLineToRelative(0.909f)
verticalLineToRelative(0.909f)
horizontalLineToRelative(-0.909f)
close()
}
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(14.526f, 8.969f)
lineToRelative(1.756f, 0f)
lineToRelative(0.307f, -1.756f)
lineToRelative(-1.756f, 0f)
close()
}
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(18.307f, 3f)
horizontalLineToRelative(-5.259f)
horizontalLineTo(5.694f)
horizontalLineTo(5.693f)
curveTo(4.206f, 3f, 3f, 4.206f, 3f, 5.693f)
verticalLineToRelative(12.613f)
curveToRelative(0f, 1.302f, 0.923f, 2.388f, 2.151f, 2.639f)
curveToRelative(0.105f, 0.021f, 0.215f, 0.024f, 0.324f, 0.033f)
curveTo(5.548f, 20.984f, 5.618f, 21f, 5.693f, 21f)
horizontalLineToRelative(0f)
horizontalLineToRelative(8.294f)
horizontalLineToRelative(4.319f)
curveTo(19.794f, 21f, 21f, 19.794f, 21f, 18.307f)
verticalLineToRelative(-5.259f)
curveToRelative(0f, 0f, 0f, 0f, 0f, 0f)
verticalLineToRelative(-7.354f)
curveTo(21f, 4.206f, 19.794f, 3f, 18.307f, 3f)
close()
moveTo(8.383f, 14.737f)
horizontalLineTo(6.566f)
verticalLineToRelative(0.909f)
horizontalLineToRelative(0.909f)
curveToRelative(0.502f, 0f, 0.908f, 0.407f, 0.908f, 0.909f)
verticalLineToRelative(0.909f)
curveToRelative(0f, 0.502f, -0.407f, 0.908f, -0.908f, 0.908f)
horizontalLineTo(6.566f)
curveToRelative(-0.502f, 0f, -0.909f, -0.407f, -0.909f, -0.908f)
verticalLineToRelative(-2.726f)
curveToRelative(0f, -0.502f, 0.407f, -0.908f, 0.909f, -0.908f)
horizontalLineToRelative(1.817f)
verticalLineTo(14.737f)
close()
moveTo(11.942f, 18.371f)
horizontalLineToRelative(-0.909f)
verticalLineToRelative(-1.817f)
horizontalLineTo(9.217f)
verticalLineToRelative(-2.725f)
horizontalLineToRelative(0.909f)
verticalLineToRelative(1.817f)
horizontalLineToRelative(0.908f)
verticalLineToRelative(-1.817f)
horizontalLineToRelative(0.909f)
verticalLineTo(18.371f)
close()
moveTo(18.344f, 7.213f)
horizontalLineToRelative(-0.878f)
lineTo(17.159f, 8.969f)
horizontalLineToRelative(0.878f)
lineToRelative(-0.158f, 0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(-0.154f, 0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(0.154f, -0.878f)
horizontalLineToRelative(-1.756f)
lineToRelative(-0.154f, 0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(0.154f, -0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(0.158f, -0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(0.307f, -1.756f)
horizontalLineToRelative(-0.878f)
lineToRelative(0.154f, -0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(0.158f, -0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(-0.158f, 0.878f)
horizontalLineToRelative(1.756f)
lineToRelative(0.158f, -0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(-0.158f, 0.878f)
horizontalLineToRelative(0.878f)
lineTo(18.344f, 7.213f)
close()
}
}.build()
}
val Icons.Outlined.Base64: ImageVector by lazy {
ImageVector.Builder(
name = "Base64Outlined",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(6.566f, 13.829f)
curveToRelative(-0.502f, 0f, -0.908f, 0.407f, -0.908f, 0.908f)
verticalLineToRelative(2.725f)
curveToRelative(0f, 0.502f, 0.407f, 0.908f, 0.908f, 0.908f)
horizontalLineToRelative(0.908f)
curveToRelative(0.502f, 0f, 0.908f, -0.407f, 0.908f, -0.908f)
verticalLineToRelative(-0.908f)
curveToRelative(0f, -0.502f, -0.407f, -0.908f, -0.908f, -0.908f)
horizontalLineToRelative(-0.908f)
verticalLineToRelative(-0.908f)
horizontalLineToRelative(1.817f)
verticalLineToRelative(-0.908f)
horizontalLineTo(6.566f)
moveTo(6.566f, 16.554f)
horizontalLineToRelative(0.908f)
verticalLineToRelative(0.908f)
horizontalLineToRelative(-0.908f)
verticalLineTo(16.554f)
close()
}
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(9.217f, 13.829f)
verticalLineToRelative(2.725f)
horizontalLineToRelative(1.817f)
verticalLineToRelative(1.817f)
horizontalLineToRelative(0.908f)
verticalLineToRelative(-4.542f)
horizontalLineToRelative(-0.908f)
verticalLineToRelative(1.817f)
horizontalLineToRelative(-0.908f)
verticalLineToRelative(-1.817f)
horizontalLineTo(9.217f)
close()
}
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(18.344f, 7.213f)
lineToRelative(0.154f, -0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(0.158f, -0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(-0.158f, 0.878f)
horizontalLineToRelative(-1.756f)
lineToRelative(0.158f, -0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(-0.158f, 0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(-0.154f, 0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(-0.307f, 1.756f)
horizontalLineToRelative(-0.878f)
lineTo(12.612f, 9.847f)
horizontalLineToRelative(0.878f)
lineToRelative(-0.154f, 0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(0.154f, -0.878f)
horizontalLineToRelative(1.756f)
lineToRelative(-0.154f, 0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(0.154f, -0.878f)
horizontalLineToRelative(0.878f)
lineToRelative(0.158f, -0.878f)
horizontalLineToRelative(-0.878f)
lineToRelative(0.307f, -1.756f)
horizontalLineTo(18.344f)
close()
moveTo(16.281f, 8.969f)
horizontalLineToRelative(-1.756f)
lineToRelative(0.307f, -1.756f)
horizontalLineToRelative(1.756f)
lineTo(16.281f, 8.969f)
close()
}
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(21f, 5.693f)
curveTo(21f, 4.206f, 19.794f, 3f, 18.307f, 3f)
horizontalLineToRelative(-5.259f)
horizontalLineTo(5.694f)
horizontalLineTo(5.693f)
curveTo(4.206f, 3f, 3f, 4.206f, 3f, 5.693f)
verticalLineToRelative(12.613f)
curveToRelative(0f, 1.302f, 0.923f, 2.388f, 2.151f, 2.639f)
curveToRelative(0.105f, 0.021f, 0.215f, 0.024f, 0.324f, 0.033f)
curveTo(5.548f, 20.984f, 5.618f, 21f, 5.693f, 21f)
horizontalLineToRelative(0f)
horizontalLineToRelative(8.294f)
horizontalLineToRelative(4.319f)
curveTo(19.794f, 21f, 21f, 19.794f, 21f, 18.307f)
verticalLineToRelative(-5.259f)
curveToRelative(0f, 0f, 0f, 0f, 0f, 0f)
verticalLineTo(5.693f)
close()
moveTo(19f, 17.715f)
curveTo(19f, 18.424f, 18.424f, 19f, 17.715f, 19f)
horizontalLineToRelative(-3.737f)
horizontalLineTo(6.286f)
curveToRelative(-0.71f, 0f, -1.285f, -0.576f, -1.285f, -1.285f)
verticalLineTo(6.285f)
curveTo(5f, 5.576f, 5.576f, 5f, 6.286f, 5f)
horizontalLineToRelative(6.778f)
horizontalLineToRelative(4.651f)
curveToRelative(0.71f, 0f, 1.285f, 0.576f, 1.285f, 1.285f)
verticalLineToRelative(3.737f)
curveToRelative(-0f, 0f, -0f, 0f, -0f, 0f)
verticalLineTo(17.715f)
close()
}
}.build()
}

View File

@ -1489,7 +1489,7 @@
<string name="material_you_slider_sub">Modern Material You slider, futuristic, fresh, acessible</string>
<string name="apply">Apply</string>
<string name="center_align_dialog_buttons">Center Dialog Buttons</string>
<string name="center_align_dialog_buttons_sub">Buttons of dialogs will be postioned at center instead of left side if possible</string>
<string name="center_align_dialog_buttons_sub">Buttons of dialogs will be positioned at center instead of left side if possible</string>
<string name="open_source_licenses">Open Source Licenses</string>
<string name="open_source_licenses_sub">View licenses of open source libraries used in this app</string>
<string name="area">Area</string>
@ -1510,4 +1510,15 @@
<string name="clear_selection">Clear Selection</string>
<string name="settings_group_visibility_hidden">Setting group \"%s\" will be collapsed by default</string>
<string name="settings_group_visibility_visible">Setting group \"%s\" will be expanded by default</string>
<string name="base_64_conversion">Base64 conversion</string>
<string name="base_64_conversion_sub">Decode base64 string to image, or encode image to base64 format</string>
<string name="base_64">Base64</string>
<string name="not_a_valid_base_64">Pasted string is not a valid Base64 string</string>
<string name="copy_not_a_valid_base_64">Cannot copy empty or invalid Base64 string</string>
<string name="paste_base_64">Paste Base64</string>
<string name="copy_base_64">Copy Base64</string>
<string name="base_64_tips">Load image to copy or save Base64 string, if you have any, you can paste it above to obtain image</string>
<string name="save_base_64">Save Base64</string>
<string name="share_base_64">Share Base64</string>
<string name="options">Options</string>
</resources>

View File

@ -20,7 +20,7 @@ package ru.tech.imageresizershrinker.core.ui.utils.helper
import androidx.compose.foundation.Indication
import androidx.compose.material.LocalContentColor
import androidx.compose.material.RippleDefaults
import androidx.compose.material.ripple
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.Dp

View File

@ -55,6 +55,7 @@ import kotlinx.serialization.encoding.Encoder
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.resources.icons.Apng
import ru.tech.imageresizershrinker.core.resources.icons.ApngBox
import ru.tech.imageresizershrinker.core.resources.icons.Base64
import ru.tech.imageresizershrinker.core.resources.icons.CropSmall
import ru.tech.imageresizershrinker.core.resources.icons.Draw
import ru.tech.imageresizershrinker.core.resources.icons.Encrypted
@ -133,6 +134,7 @@ sealed class Screen(
is CollageMaker -> "Collage_Maker"
is LibrariesInfo -> "Libraries_Info"
is MarkupLayers -> "Markup_Layers"
is Base64Conversion -> "Base64_Conversion"
}
val icon: ImageVector?
@ -177,6 +179,7 @@ sealed class Screen(
NoiseGeneration -> Icons.Outlined.Grain
is CollageMaker -> Icons.Outlined.AutoAwesomeMosaic
is MarkupLayers -> Icons.Outlined.Stack
is Base64Conversion -> Icons.Outlined.Base64
}
@Serializable
@ -804,6 +807,15 @@ sealed class Screen(
subtitle = R.string.markup_layers_sub
)
@Serializable
data class Base64Conversion(
val uri: KUri? = null
) : Screen(
id = 35,
title = R.string.base_64_conversion,
subtitle = R.string.base_64_conversion_sub
)
companion object {
val typedEntries by lazy {
listOf(
@ -854,6 +866,7 @@ sealed class Screen(
DocumentScanner,
ScanQrCode(),
ColorTools,
Base64Conversion(),
GifTools(),
JxlTools(),
ApngTools(),
@ -872,7 +885,7 @@ sealed class Screen(
typedEntries.flatMap { it.first }.sortedBy { it.id }
}
const val FEATURES_COUNT = 55
const val FEATURES_COUNT = 56
}
}

View File

@ -25,13 +25,21 @@ import android.os.Build
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.accessibility.AccessibilityManager
import androidx.compose.foundation.Indication
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.semantics.Role
import androidx.core.content.getSystemService
private fun View.vibrate() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -127,3 +135,42 @@ fun rememberCustomHapticFeedback(hapticsStrength: Int): HapticFeedback {
return haptics
}
fun Modifier.hapticsClickable(
interactionSource: MutableInteractionSource?,
indication: Indication?,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier = this.composed {
val haptics = LocalHapticFeedback.current
Modifier.clickable(
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
onClickLabel = onClickLabel,
role = role,
onClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
onClick()
}
)
}
fun Modifier.hapticsClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier = this.composed {
hapticsClickable(
interactionSource = null,
indication = LocalIndication.current,
enabled = enabled,
onClickLabel = onClickLabel,
role = role,
onClick = onClick
)
}

View File

@ -273,7 +273,7 @@ fun RoundedTextFieldColors(
isError: Boolean,
containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
focusedIndicatorColor: Color = MaterialTheme.colorScheme.primary,
unfocusedIndicatorColor: Color = MaterialTheme.colorScheme.surfaceVariant.inverse({ 0.2f })
unfocusedIndicatorColor: Color = MaterialTheme.colorScheme.surfaceVariant.inverse({ 0.4f })
): TextFieldColors =
MaterialTheme.colorScheme.run {
val containerColorNew = if (isError) {

View File

@ -22,9 +22,32 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.platform.LocalContext
import coil3.request.ImageRequest
import coil3.toBitmap
import com.t8rin.dynamic.theme.LocalDynamicThemeState
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
import ru.tech.imageresizershrinker.core.settings.presentation.provider.rememberAppColorTuple
import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalImageLoader
@Composable
fun <T : Any> AutoContentBasedColors(
model: T?,
allowChangeColor: Boolean = true
) {
val context = LocalContext.current
val imageLoader = LocalImageLoader.current
AutoContentBasedColors(
model = model,
selector = {
imageLoader.execute(
ImageRequest.Builder(context).data(model).build()
).image?.toBitmap()
},
allowChangeColor = allowChangeColor
)
}
@Composable
fun <T : Any> AutoContentBasedColors(

1
feature/base64-conversion/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,25 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.feature)
alias(libs.plugins.image.toolbox.hilt)
alias(libs.plugins.image.toolbox.compose)
}
android.namespace = "ru.tech.imageresizershrinker.feature.base64_conversion"

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -0,0 +1,71 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.base64_conversion.data
import android.graphics.Bitmap
import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import kotlinx.coroutines.withContext
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageCompressor
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
import ru.tech.imageresizershrinker.core.domain.image.ShareProvider
import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormat
import ru.tech.imageresizershrinker.core.domain.image.model.Quality
import ru.tech.imageresizershrinker.core.domain.saving.FileController
import ru.tech.imageresizershrinker.feature.base64_conversion.domain.Base64Converter
import javax.inject.Inject
internal class AndroidBase64Converter @Inject constructor(
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
private val fileController: FileController,
private val shareProvider: ShareProvider<Bitmap>,
private val imageCompressor: ImageCompressor<Bitmap>,
dispatchersHolder: DispatchersHolder
) : Base64Converter, DispatchersHolder by dispatchersHolder {
override suspend fun decode(
base64: String
): String? = withContext(ioDispatcher) {
val decoded = runCatching {
Base64.decode(base64, Base64.DEFAULT or Base64.NO_WRAP)
}.getOrNull() ?: return@withContext null
imageGetter.getImage(decoded)?.let { bitmap ->
shareProvider.cacheData(
writeData = {
it.writeBytes(
imageCompressor.compress(
image = bitmap,
imageFormat = ImageFormat.Png.Lossless,
quality = Quality.Base()
)
)
},
filename = "Base64_decoded_${System.currentTimeMillis()}.png"
)
}
}
override suspend fun encode(
uri: String
): String = withContext(ioDispatcher) {
Base64.encodeToString(fileController.readBytes(uri), Base64.DEFAULT or Base64.NO_WRAP)
}
}

View File

@ -0,0 +1,38 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.base64_conversion.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.tech.imageresizershrinker.feature.base64_conversion.data.AndroidBase64Converter
import ru.tech.imageresizershrinker.feature.base64_conversion.domain.Base64Converter
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface Base64ConversionModule {
@Binds
@Singleton
fun provideConverter(
impl: AndroidBase64Converter
): Base64Converter
}

View File

@ -0,0 +1,34 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.base64_conversion.domain
interface Base64Converter {
/**
* @param base64 - string to decode to uri
* @return uri
**/
suspend fun decode(base64: String): String?
/**
* @param uri - uri to decode
* @return base64
**/
suspend fun encode(uri: String): String
}

View File

@ -0,0 +1,435 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.base64_conversion.presentation
import android.net.Uri
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material.icons.rounded.CopyAll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
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 coil3.toBitmap
import ru.tech.imageresizershrinker.core.domain.utils.isBase64
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.resources.icons.Base64
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
import ru.tech.imageresizershrinker.core.ui.utils.helper.ImageUtils.safeAspectRatio
import ru.tech.imageresizershrinker.core.ui.utils.helper.asClip
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalComponentActivity
import ru.tech.imageresizershrinker.core.ui.utils.provider.rememberLocalEssentials
import ru.tech.imageresizershrinker.core.ui.widget.AdaptiveLayoutScreen
import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.ImageFormatSelector
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.QualitySelector
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.LoadingDialog
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeImagePickingDialog
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
import ru.tech.imageresizershrinker.core.ui.widget.haptics.hapticsClickable
import ru.tech.imageresizershrinker.core.ui.widget.image.Picture
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
import ru.tech.imageresizershrinker.core.ui.widget.other.TopAppBarEmoji
import ru.tech.imageresizershrinker.core.ui.widget.sheets.ProcessImagesPreferenceSheet
import ru.tech.imageresizershrinker.core.ui.widget.text.AutoSizeText
import ru.tech.imageresizershrinker.core.ui.widget.text.TopAppBarTitle
import ru.tech.imageresizershrinker.core.ui.widget.utils.AutoContentBasedColors
import ru.tech.imageresizershrinker.feature.base64_conversion.presentation.components.Base64ConversionComponent
@Composable
fun Base64ConversionContent(
component: Base64ConversionComponent
) {
AutoContentBasedColors(component.uri)
val isPortrait by isPortraitOrientationAsState()
val context = LocalComponentActivity.current
val essentials = rememberLocalEssentials()
val showConfetti: () -> Unit = essentials::showConfetti
val imagePicker = rememberImagePicker(onSuccess = component::setUri)
val pickImage = imagePicker::pickImage
val saveBitmap: (oneTimeSaveLocationUri: String?) -> Unit = {
component.saveBitmap(
oneTimeSaveLocationUri = it,
onComplete = essentials::parseSaveResult
)
}
AdaptiveLayoutScreen(
shouldDisableBackHandler = true,
title = {
TopAppBarTitle(
title = stringResource(R.string.base_64_conversion),
input = component.uri,
isLoading = component.isImageLoading,
size = null
)
},
onGoBack = component.onGoBack,
topAppBarPersistentActions = {
if (component.uri == null) {
TopAppBarEmoji()
}
},
actions = {
var editSheetData by remember {
mutableStateOf(listOf<Uri>())
}
ShareButton(
enabled = component.uri != null,
onShare = {
component.shareBitmap(showConfetti)
},
onCopy = { manager ->
component.cacheCurrentImage { uri ->
manager.setClip(uri.asClip(context))
showConfetti()
}
},
onEdit = {
component.cacheCurrentImage { uri ->
editSheetData = listOf(uri)
}
}
)
ProcessImagesPreferenceSheet(
uris = editSheetData,
visible = editSheetData.isNotEmpty(),
onDismiss = { editSheetData = emptyList() },
onNavigate = component.onNavigate
)
},
imagePreview = {
Box(
modifier = Modifier
.container()
.padding(4.dp)
.animateContentSize(),
contentAlignment = Alignment.Center
) {
var aspectRatio by remember {
mutableFloatStateOf(1f)
}
Picture(
model = component.base64String,
contentScale = ContentScale.FillBounds,
modifier = Modifier.aspectRatio(aspectRatio),
onSuccess = {
aspectRatio = it.result.image.toBitmap().safeAspectRatio
},
shape = MaterialTheme.shapes.medium,
isLoadingFromDifferentPlace = component.isImageLoading
)
}
},
controls = {
Spacer(Modifier.height(8.dp))
Base64ConversionTiles(component)
Spacer(Modifier.height(8.dp))
if (component.uri != null) {
if (component.imageFormat.canChangeCompressionValue) {
Spacer(Modifier.height(8.dp))
}
QualitySelector(
imageFormat = component.imageFormat,
quality = component.quality,
onQualityChange = component::setQuality
)
Spacer(Modifier.height(8.dp))
ImageFormatSelector(
value = component.imageFormat,
onValueChange = component::setImageFormat
)
} else {
Row(
modifier = Modifier
.padding(8.dp)
.container(
color = MaterialTheme.colorScheme.secondaryContainer.copy(0.2f),
resultPadding = 0.dp,
shape = RoundedCornerShape(16.dp)
)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.5f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.base_64_tips),
fontSize = 12.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold,
lineHeight = 14.sp,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.5f)
)
}
}
},
buttons = {
var showFolderSelectionDialog by rememberSaveable {
mutableStateOf(false)
}
var showOneTimeImagePickingDialog by rememberSaveable {
mutableStateOf(false)
}
BottomButtonsBlock(
targetState = (component.uri == null) to isPortrait,
onSecondaryButtonClick = pickImage,
onSecondaryButtonLongClick = {
showOneTimeImagePickingDialog = true
},
onPrimaryButtonClick = {
saveBitmap(null)
},
onPrimaryButtonLongClick = {
showFolderSelectionDialog = true
},
actions = {
if (isPortrait) it()
}
)
OneTimeSaveLocationSelectionDialog(
visible = showFolderSelectionDialog,
onDismiss = { showFolderSelectionDialog = false },
onSaveRequest = saveBitmap,
formatForFilenameSelection = component.getFormatForFilenameSelection()
)
OneTimeImagePickingDialog(
onDismiss = { showOneTimeImagePickingDialog = false },
picker = Picker.Single,
imagePicker = imagePicker,
visible = showOneTimeImagePickingDialog
)
},
canShowScreenData = true,
isPortrait = isPortrait,
)
LoadingDialog(
visible = component.isSaving,
onCancelLoading = component::cancelSaving
)
}
@Composable
internal fun Base64ConversionTiles(component: Base64ConversionComponent) {
val essentials = rememberLocalEssentials()
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
val pasteTile: @Composable RowScope.(shape: Shape) -> Unit = { shape ->
Tile(
onClick = {
val text = clipboardManager.getText()?.text?.substringAfter(",") ?: ""
if (isBase64(text)) {
component.setBase64(text)
} else {
essentials.showToast(
message = context.getString(R.string.not_a_valid_base_64),
icon = Icons.Rounded.Base64
)
}
},
shape = shape,
icon = Icons.Rounded.ContentPaste,
textRes = R.string.paste_base_64
)
}
AnimatedContent(component.uri == null) { isNoUri ->
if (isNoUri) {
Row {
pasteTile(CircleShape)
}
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.container(
shape = RoundedCornerShape(20.dp),
resultPadding = 8.dp
)
) {
Text(
text = stringResource(R.string.options),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(8.dp)
)
Spacer(Modifier.height(8.dp))
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
pasteTile(
RoundedCornerShape(
topStart = 16.dp,
topEnd = 4.dp,
bottomStart = 4.dp,
bottomEnd = 4.dp
)
)
Tile(
onClick = {
val text = buildAnnotatedString {
append(component.base64String)
}
if (isBase64(component.base64String)) {
clipboardManager.setText(text)
} else {
essentials.showToast(
message = context.getString(R.string.copy_not_a_valid_base_64),
icon = Icons.Rounded.Base64
)
}
},
shape = RoundedCornerShape(
topStart = 4.dp,
topEnd = 16.dp,
bottomStart = 4.dp,
bottomEnd = 4.dp
),
icon = Icons.Rounded.CopyAll,
textRes = R.string.copy_base_64
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Tile(
onClick = {
},
shape = RoundedCornerShape(
topStart = 4.dp,
topEnd = 4.dp,
bottomStart = 16.dp,
bottomEnd = 4.dp
),
icon = Icons.Outlined.Share,
textRes = R.string.share_base_64
)
Tile(
onClick = {
},
shape = RoundedCornerShape(
topStart = 4.dp,
topEnd = 4.dp,
bottomStart = 4.dp,
bottomEnd = 16.dp
),
icon = Icons.Outlined.Save,
textRes = R.string.save_base_64
)
}
}
}
}
}
}
@Composable
private fun RowScope.Tile(
onClick: () -> Unit,
shape: Shape,
icon: ImageVector,
textRes: Int
) {
Row(
modifier = Modifier
.weight(1f)
.height(56.dp)
.container(
color = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f),
shape = shape,
resultPadding = 0.dp
)
.hapticsClickable(onClick = onClick)
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.CenterHorizontally
)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
AutoSizeText(
text = stringResource(id = textRes),
color = MaterialTheme.colorScheme.onSecondaryContainer,
textAlign = TextAlign.Center
)
}
}

View File

@ -0,0 +1,225 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.base64_conversion.presentation.components
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import com.arkivanov.decompose.ComponentContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Job
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.domain.image.ImageCompressor
import ru.tech.imageresizershrinker.core.domain.image.ImageGetter
import ru.tech.imageresizershrinker.core.domain.image.ShareProvider
import ru.tech.imageresizershrinker.core.domain.image.model.ImageFormat
import ru.tech.imageresizershrinker.core.domain.image.model.ImageInfo
import ru.tech.imageresizershrinker.core.domain.image.model.Quality
import ru.tech.imageresizershrinker.core.domain.saving.FileController
import ru.tech.imageresizershrinker.core.domain.saving.model.ImageSaveTarget
import ru.tech.imageresizershrinker.core.domain.saving.model.SaveResult
import ru.tech.imageresizershrinker.core.domain.utils.smartJob
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.core.ui.utils.state.update
import ru.tech.imageresizershrinker.feature.base64_conversion.domain.Base64Converter
import kotlin.toString
class Base64ConversionComponent @AssistedInject internal constructor(
@Assisted componentContext: ComponentContext,
@Assisted initialUri: Uri?,
@Assisted val onGoBack: () -> Unit,
@Assisted val onNavigate: (Screen) -> Unit,
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
private val shareProvider: ShareProvider<Bitmap>,
private val fileController: FileController,
private val converter: Base64Converter,
private val imageCompressor: ImageCompressor<Bitmap>,
dispatchersHolder: DispatchersHolder,
) : BaseComponent(dispatchersHolder, componentContext) {
private val _imageFormat = mutableStateOf(ImageFormat.Default)
val imageFormat by _imageFormat
private val _quality = mutableStateOf<Quality>(Quality.Base())
val quality by _quality
private val _uri = mutableStateOf<Uri?>(null)
val uri by _uri
private val _base64String = mutableStateOf("")
val base64String by _base64String
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
val isSaving by _isSaving
private var savingJob: Job? by smartJob {
_isSaving.update { false }
}
init {
debounce {
initialUri?.let(::setUri)
}
}
fun setUri(uri: Uri) {
_uri.value = uri
updateBase64()
}
private fun updateBase64() {
debouncedImageCalculation {
uri?.let { imageGetter.getImage(it) }?.let { image ->
shareProvider.cacheImage(
image = image,
imageInfo = ImageInfo(
width = image.width,
height = image.height,
imageFormat = imageFormat,
quality = quality,
originalUri = uri.toString()
)
)?.let {
_base64String.value = converter.encode(it)
}
}
}
}
fun setBase64(base64: String) {
_base64String.value = base64
debouncedImageCalculation {
_uri.value = converter.decode(base64)?.toUri()
}
}
fun setImageFormat(imageFormat: ImageFormat) {
_imageFormat.update { imageFormat }
updateBase64()
}
fun setQuality(quality: Quality) {
_quality.update { quality }
updateBase64()
}
fun getFormatForFilenameSelection(): ImageFormat = imageFormat
fun shareBitmap(onComplete: () -> Unit) {
savingJob = componentScope.launch {
_isSaving.update { true }
uri?.let { imageGetter.getImage(it) }?.let { image ->
shareProvider.shareImage(
image = image,
imageInfo = ImageInfo(
width = image.width,
height = image.height,
imageFormat = imageFormat,
quality = quality,
originalUri = uri.toString()
),
onComplete = onComplete
)
}
_isSaving.update { false }
}
}
fun cacheCurrentImage(onComplete: (Uri) -> Unit) {
savingJob = componentScope.launch {
_isSaving.update { true }
uri?.let { imageGetter.getImage(it) }?.let { image ->
shareProvider.cacheImage(
image = image,
imageInfo = ImageInfo(
width = image.width,
height = image.height,
imageFormat = imageFormat,
quality = quality,
originalUri = uri.toString()
)
)?.let { uri ->
onComplete(uri.toUri())
}
}
_isSaving.update { false }
}
}
fun cancelSaving() {
savingJob?.cancel()
savingJob = null
_isSaving.update { false }
}
fun saveBitmap(
oneTimeSaveLocationUri: String?,
onComplete: (result: SaveResult) -> Unit,
) {
savingJob = componentScope.launch(defaultDispatcher) {
_isSaving.update { true }
uri?.let { imageGetter.getImage(it) }?.let { image ->
val imageInfo = ImageInfo(
width = image.width,
height = image.height,
imageFormat = imageFormat,
quality = quality,
originalUri = uri.toString()
)
onComplete(
fileController.save(
saveTarget = ImageSaveTarget(
imageInfo = imageInfo,
metadata = null,
originalUri = uri.toString(),
sequenceNumber = null,
data = imageCompressor.compressAndTransform(
image = image,
imageInfo = imageInfo.copy(
originalUri = uri.toString()
)
)
),
keepOriginalMetadata = true,
oneTimeSaveLocationUri = oneTimeSaveLocationUri
).onSuccess(::registerSave)
)
}
_isSaving.update { false }
}
}
@AssistedFactory
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
initialUri: Uri?,
onGoBack: () -> Unit,
onNavigate: (Screen) -> Unit,
): Base64ConversionComponent
}
}

View File

@ -85,7 +85,6 @@ import ru.tech.imageresizershrinker.core.ui.theme.toColor
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.Picker
import ru.tech.imageresizershrinker.core.ui.utils.content_pickers.rememberImagePicker
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalComponentActivity
import ru.tech.imageresizershrinker.core.ui.utils.provider.rememberLocalEssentials
import ru.tech.imageresizershrinker.core.ui.widget.AdaptiveBottomScaffoldLayoutScreen
import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
@ -114,7 +113,7 @@ import ru.tech.imageresizershrinker.feature.markup_layers.presentation.screenLog
fun MarkupLayersContent(
component: MarkupLayersComponent
) {
LocalComponentActivity.current
AutoContentBasedColors(component.bitmap)
val themeState = LocalDynamicThemeState.current

View File

@ -64,4 +64,5 @@ dependencies {
implementation(projects.feature.colllageMaker)
implementation(projects.feature.librariesInfo)
implementation(projects.feature.markupLayers)
implementation(projects.feature.base64Conversion)
}

View File

@ -24,7 +24,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.height
@ -48,6 +47,7 @@ import ru.tech.imageresizershrinker.core.settings.domain.model.FastSettingsSide
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
import ru.tech.imageresizershrinker.core.ui.theme.blend
import ru.tech.imageresizershrinker.core.ui.theme.takeColorFromScheme
import ru.tech.imageresizershrinker.core.ui.widget.haptics.hapticsClickable
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
@Composable
@ -91,7 +91,7 @@ internal fun BoxScope.SettingsOpenButton(
else 24.dp
).value
)
.clickable(
.hapticsClickable(
enabled = canExpandSettings,
indication = null,
interactionSource = null

View File

@ -22,6 +22,7 @@ import ru.tech.imageresizershrinker.colllage_maker.presentation.screenLogic.Coll
import ru.tech.imageresizershrinker.color_tools.presentation.screenLogic.ColorToolsComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
import ru.tech.imageresizershrinker.feature.apng_tools.presentation.screenLogic.ApngToolsComponent
import ru.tech.imageresizershrinker.feature.base64_conversion.presentation.components.Base64ConversionComponent
import ru.tech.imageresizershrinker.feature.cipher.presentation.screenLogic.CipherComponent
import ru.tech.imageresizershrinker.feature.compare.presentation.screenLogic.CompareComponent
import ru.tech.imageresizershrinker.feature.crop.presentation.screenLogic.CropComponent
@ -48,6 +49,7 @@ import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.screenLogic.P
import ru.tech.imageresizershrinker.feature.pick_color.presentation.screenLogic.PickColorFromImageComponent
import ru.tech.imageresizershrinker.feature.recognize.text.presentation.screenLogic.RecognizeTextComponent
import ru.tech.imageresizershrinker.feature.resize_convert.presentation.screenLogic.ResizeAndConvertComponent
import ru.tech.imageresizershrinker.feature.root.presentation.components.navigation.NavigationChild.*
import ru.tech.imageresizershrinker.feature.root.presentation.screenLogic.RootComponent
import ru.tech.imageresizershrinker.feature.scan_qr_code.presentation.screenLogic.ScanQrCodeComponent
import ru.tech.imageresizershrinker.feature.settings.presentation.screenLogic.SettingsComponent
@ -100,27 +102,28 @@ internal class ChildProvider @Inject constructor(
private val colorToolsComponentFactory: ColorToolsComponent.Factory,
private val librariesInfoComponentFactory: LibrariesInfoComponent.Factory,
private val mainComponentFactory: MainComponent.Factory,
private val markupLayersComponentFactory: MarkupLayersComponent.Factory
private val markupLayersComponentFactory: MarkupLayersComponent.Factory,
private val base64ConversionComponentFactory: Base64ConversionComponent.Factory
) {
fun RootComponent.createChild(
config: Screen,
componentContext: ComponentContext
): NavigationChild = when (config) {
Screen.ColorTools -> NavigationChild.ColorTools(
Screen.ColorTools -> ColorTools(
colorToolsComponentFactory(
componentContext = componentContext,
onGoBack = ::navigateBack
)
)
Screen.EasterEgg -> NavigationChild.EasterEgg(
Screen.EasterEgg -> EasterEgg(
easterEggComponentFactory(
componentContext = componentContext,
onGoBack = ::navigateBack
)
)
Screen.Main -> NavigationChild.Main(
Screen.Main -> Main(
mainComponentFactory(
componentContext = componentContext,
onTryGetUpdate = ::tryGetUpdate,
@ -130,7 +133,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ApngTools -> NavigationChild.ApngTools(
is Screen.ApngTools -> ApngTools(
apngToolsComponentFactory(
componentContext = componentContext,
initialType = config.type,
@ -139,7 +142,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Cipher -> NavigationChild.Cipher(
is Screen.Cipher -> Cipher(
cipherComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -147,7 +150,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.CollageMaker -> NavigationChild.CollageMaker(
is Screen.CollageMaker -> CollageMaker(
collageMakerComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -156,7 +159,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Compare -> NavigationChild.Compare(
is Screen.Compare -> Compare(
compareComponentFactory(
componentContext = componentContext,
initialComparableUris = config.uris
@ -166,7 +169,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Crop -> NavigationChild.Crop(
is Screen.Crop -> Crop(
cropComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -175,7 +178,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.DeleteExif -> NavigationChild.DeleteExif(
is Screen.DeleteExif -> DeleteExif(
deleteExifComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -184,7 +187,7 @@ internal class ChildProvider @Inject constructor(
)
)
Screen.DocumentScanner -> NavigationChild.DocumentScanner(
Screen.DocumentScanner -> DocumentScanner(
documentScannerComponentFactory(
componentContext = componentContext,
onGoBack = ::navigateBack,
@ -192,7 +195,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Draw -> NavigationChild.Draw(
is Screen.Draw -> Draw(
drawComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -201,7 +204,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.EraseBackground -> NavigationChild.EraseBackground(
is Screen.EraseBackground -> EraseBackground(
eraseBackgroundComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -210,7 +213,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Filter -> NavigationChild.Filter(
is Screen.Filter -> Filter(
filtersComponentFactory(
componentContext = componentContext,
initialType = config.type,
@ -219,7 +222,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.FormatConversion -> NavigationChild.FormatConversion(
is Screen.FormatConversion -> FormatConversion(
formatConversionComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -228,7 +231,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.GeneratePalette -> NavigationChild.GeneratePalette(
is Screen.GeneratePalette -> GeneratePalette(
generatePaletteComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -236,7 +239,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.GifTools -> NavigationChild.GifTools(
is Screen.GifTools -> GifTools(
gifToolsComponentFactory(
componentContext = componentContext,
initialType = config.type,
@ -244,7 +247,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.GradientMaker -> NavigationChild.GradientMaker(
is Screen.GradientMaker -> GradientMaker(
gradientMakerComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -253,7 +256,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ImagePreview -> NavigationChild.ImagePreview(
is Screen.ImagePreview -> ImagePreview(
imagePreviewComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -262,7 +265,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ImageSplitting -> NavigationChild.ImageSplitting(
is Screen.ImageSplitting -> ImageSplitting(
imageSplittingComponentFactory(
componentContext = componentContext,
initialUris = config.uri,
@ -271,7 +274,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ImageStacking -> NavigationChild.ImageStacking(
is Screen.ImageStacking -> ImageStacking(
imageStackingComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -280,7 +283,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ImageStitching -> NavigationChild.ImageStitching(
is Screen.ImageStitching -> ImageStitching(
imageStitchingComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -289,7 +292,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.JxlTools -> NavigationChild.JxlTools(
is Screen.JxlTools -> JxlTools(
jxlToolsComponentFactory(
componentContext = componentContext,
initialType = config.type,
@ -297,7 +300,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.LimitResize -> NavigationChild.LimitResize(
is Screen.LimitResize -> LimitResize(
limitResizeComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -306,7 +309,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.LoadNetImage -> NavigationChild.LoadNetImage(
is Screen.LoadNetImage -> LoadNetImage(
loadNetImageComponentFactory(
componentContext = componentContext,
initialUrl = config.url,
@ -316,7 +319,7 @@ internal class ChildProvider @Inject constructor(
)
Screen.NoiseGeneration -> NavigationChild.NoiseGeneration(
Screen.NoiseGeneration -> NoiseGeneration(
noiseGenerationComponentFactory(
componentContext = componentContext,
onGoBack = ::navigateBack,
@ -324,7 +327,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.PdfTools -> NavigationChild.PdfTools(
is Screen.PdfTools -> PdfTools(
pdfToolsComponentFactory(
componentContext = componentContext,
initialType = config.type,
@ -332,7 +335,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.PickColorFromImage -> NavigationChild.PickColorFromImage(
is Screen.PickColorFromImage -> PickColorFromImage(
pickColorFromImageComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -340,7 +343,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.RecognizeText -> NavigationChild.RecognizeText(
is Screen.RecognizeText -> RecognizeText(
recognizeTextComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -348,7 +351,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ResizeAndConvert -> NavigationChild.ResizeAndConvert(
is Screen.ResizeAndConvert -> ResizeAndConvert(
resizeAndConvertComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -357,7 +360,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.ScanQrCode -> NavigationChild.ScanQrCode(
is Screen.ScanQrCode -> ScanQrCode(
scanQrCodeComponentFactory(
componentContext = componentContext,
initialQrCodeContent = config.qrCodeContent,
@ -365,7 +368,7 @@ internal class ChildProvider @Inject constructor(
)
)
Screen.Settings -> NavigationChild.Settings(
Screen.Settings -> Settings(
settingsComponentFactory(
componentContext = componentContext,
onTryGetUpdate = ::tryGetUpdate,
@ -375,7 +378,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.SingleEdit -> NavigationChild.SingleEdit(
is Screen.SingleEdit -> SingleEdit(
singleEditComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -384,7 +387,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.SvgMaker -> NavigationChild.SvgMaker(
is Screen.SvgMaker -> SvgMaker(
svgMakerComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -392,7 +395,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Watermarking -> NavigationChild.Watermarking(
is Screen.Watermarking -> Watermarking(
watermarkingComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -401,7 +404,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.WebpTools -> NavigationChild.WebpTools(
is Screen.WebpTools -> WebpTools(
webpToolsComponentFactory(
componentContext = componentContext,
initialType = config.type,
@ -410,7 +413,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.WeightResize -> NavigationChild.WeightResize(
is Screen.WeightResize -> WeightResize(
weightResizeComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -419,7 +422,7 @@ internal class ChildProvider @Inject constructor(
)
)
is Screen.Zip -> NavigationChild.Zip(
is Screen.Zip -> Zip(
zipComponentFactory(
componentContext = componentContext,
initialUris = config.uris,
@ -427,14 +430,14 @@ internal class ChildProvider @Inject constructor(
)
)
Screen.LibrariesInfo -> NavigationChild.LibrariesInfo(
Screen.LibrariesInfo -> LibrariesInfo(
librariesInfoComponentFactory(
componentContext = componentContext,
onGoBack = ::navigateBack
)
)
is Screen.MarkupLayers -> NavigationChild.MarkupLayers(
is Screen.MarkupLayers -> MarkupLayers(
markupLayersComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
@ -442,5 +445,14 @@ internal class ChildProvider @Inject constructor(
onNavigate = ::navigateTo
)
)
is Screen.Base64Conversion -> Base64Conversion(
base64ConversionComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
onGoBack = ::navigateBack,
onNavigate = ::navigateTo
)
)
}
}

View File

@ -24,6 +24,8 @@ import ru.tech.imageresizershrinker.color_tools.presentation.ColorToolsContent
import ru.tech.imageresizershrinker.color_tools.presentation.screenLogic.ColorToolsComponent
import ru.tech.imageresizershrinker.feature.apng_tools.presentation.ApngToolsContent
import ru.tech.imageresizershrinker.feature.apng_tools.presentation.screenLogic.ApngToolsComponent
import ru.tech.imageresizershrinker.feature.base64_conversion.presentation.Base64ConversionContent
import ru.tech.imageresizershrinker.feature.base64_conversion.presentation.components.Base64ConversionComponent
import ru.tech.imageresizershrinker.feature.cipher.presentation.CipherContent
import ru.tech.imageresizershrinker.feature.cipher.presentation.screenLogic.CipherComponent
import ru.tech.imageresizershrinker.feature.compare.presentation.CompareContent
@ -299,4 +301,9 @@ internal sealed class NavigationChild {
override fun Content() = MarkupLayersContent(component)
}
class Base64Conversion(val component: Base64ConversionComponent) : NavigationChild() {
@Composable
override fun Content() = Base64ConversionContent(component)
}
}

View File

@ -34,7 +34,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.t8rin.logger.makeLog
import kotlinx.coroutines.launch
import ru.tech.imageresizershrinker.core.resources.R
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
@ -63,7 +62,7 @@ fun SettingGroupItem(
val settingsState = LocalSettingsState.current
val initialState =
settingsState.settingGroupsInitialVisibility[groupKey].makeLog("COCK") ?: initialState
settingsState.settingGroupsInitialVisibility[groupKey] ?: initialState
val simpleSettingsInteractor = LocalSimpleSettingsInteractor.current
val interactionSource = remember {

View File

@ -17,12 +17,6 @@
@file:Suppress("UnstableApiUsage")
include(":feature:markup-layers")
include(":feature:libraries-info")
pluginManagement {
repositories {
includeBuild("build-logic")
@ -108,6 +102,9 @@ include(":feature:color-tools")
include(":feature:webp-tools")
include(":feature:noise-generation")
include(":feature:colllage-maker")
include(":feature:libraries-info")
include(":feature:markup-layers")
include(":feature:base64-conversion")
include(":feature:root")