diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageTransformer.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageTransformer.kt index 83e4e668a3..766d0225e7 100644 --- a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageTransformer.kt +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageTransformer.kt @@ -119,7 +119,7 @@ internal class AndroidImageTransformer @Inject constructor( ) } - is Preset.Numeric -> currentInfo.copy( + is Preset.Percentage -> currentInfo.copy( quality = when (val quality = currentInfo.quality) { is Quality.Base -> quality.copy(qualityValue = preset.value) is Quality.Jxl -> quality.copy(qualityValue = preset.value) diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/model/Preset.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/model/Preset.kt index 8a6080bc1d..4ac134cda6 100644 --- a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/model/Preset.kt +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/model/Preset.kt @@ -23,20 +23,24 @@ sealed class Preset { data object None : Preset() - class Numeric(val value: Int) : Preset() + data class Percentage(val value: Int) : Preset() fun isTelegram(): Boolean = this is Telegram - fun value(): Int? = (this as? Numeric)?.value + fun value(): Int? = (this as? Percentage)?.value fun isEmpty(): Boolean = this is None companion object { + val Original by lazy { + Percentage(100) + } + fun createListFromInts(presets: String?): List { return ((presets?.split("*")?.map { it.toInt() } ?: List(6) { 100 - it * 10 })).toSortedSet().reversed().toList() - .map { Numeric(it) } + .map { Percentage(it) } } } } \ No newline at end of file diff --git a/core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ImageConvert.kt b/core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ImageConvert.kt new file mode 100644 index 0000000000..454cf80dee --- /dev/null +++ b/core/resources/src/main/java/ru/tech/imageresizershrinker/core/resources/icons/ImageConvert.kt @@ -0,0 +1,108 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.core.resources.icons + +import androidx.compose.material.icons.Icons +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val Icons.Outlined.ImageConvert: ImageVector by lazy { + ImageVector.Builder( + name = "ImageConvert", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(9.0629f, 19.3864f) + curveTo(7.4781f, 18.6591f, 6.1905f, 17.5606f, 5.2f, 16.0909f) + curveToRelative(-0.9905f, -1.4697f, -1.4857f, -3.1288f, -1.4857f, -4.9773f) + curveToRelative(0.0f, -0.3939f, 0.019f, -0.7803f, 0.0571f, -1.1591f) + curveToRelative(0.0381f, -0.3788f, 0.1029f, -0.75f, 0.1943f, -1.1136f) + lineTo(2.9143f, 9.4545f) + lineTo(2.0f, 7.8864f) + lineToRelative(4.3657f, -2.5f) + lineToRelative(2.5143f, 4.3182f) + lineToRelative(-1.6f, 0.9091f) + lineTo(6.0457f, 8.4773f) + curveTo(5.8781f, 8.8864f, 5.7524f, 9.3106f, 5.6686f, 9.75f) + curveToRelative(-0.0838f, 0.4394f, -0.1257f, 0.8939f, -0.1257f, 1.3636f) + curveToRelative(0.0f, 1.4697f, 0.4038f, 2.8068f, 1.2114f, 4.0114f) + reflectiveCurveToRelative(1.8819f, 2.0947f, 3.2229f, 2.6705f) + lineTo(9.0629f, 19.3864f) + close() + moveTo(16.0571f, 8.3636f) + verticalLineTo(6.5455f) + horizontalLineToRelative(2.4914f) + curveToRelative(-0.701f, -0.8636f, -1.5467f, -1.5341f, -2.5371f, -2.0114f) + reflectiveCurveToRelative(-2.0419f, -0.7159f, -3.1543f, -0.7159f) + curveToRelative(-0.8381f, 0.0f, -1.6305f, 0.1288f, -2.3771f, 0.3864f) + curveToRelative(-0.7467f, 0.2576f, -1.4324f, 0.6212f, -2.0571f, 1.0909f) + lineToRelative(-0.9143f, -1.5909f) + curveTo(8.2705f, 3.1742f, 9.101f, 2.7576f, 10.0f, 2.4545f) + reflectiveCurveTo(11.8514f, 2.0f, 12.8571f, 2.0f) + curveToRelative(1.2038f, 0.0f, 2.3543f, 0.2235f, 3.4514f, 0.6705f) + reflectiveCurveToRelative(2.08f, 1.0947f, 2.9486f, 1.9432f) + verticalLineTo(3.3636f) + horizontalLineToRelative(1.8286f) + verticalLineToRelative(5.0f) + horizontalLineTo(16.0571f) + close() + moveTo(15.4629f, 22.0f) + lineToRelative(-4.3657f, -2.5f) + lineToRelative(2.5143f, -4.3182f) + lineTo(15.1886f, 16.0909f) + lineToRelative(-1.3029f, 2.2273f) + curveToRelative(1.7981f, -0.2576f, 3.2952f, -1.0682f, 4.4914f, -2.4318f) + curveToRelative(1.1962f, -1.3636f, 1.7943f, -2.9621f, 1.7943f, -4.7955f) + curveToRelative(0.0f, -0.1667f, -0.0038f, -0.322f, -0.0114f, -0.4659f) + reflectiveCurveToRelative(-0.0267f, -0.2917f, -0.0571f, -0.4432f) + horizontalLineToRelative(1.8514f) + curveToRelative(0.0152f, 0.1515f, 0.0267f, 0.2992f, 0.0343f, 0.4432f) + reflectiveCurveTo(22.0f, 10.9242f, 22.0f, 11.0909f) + curveToRelative(0.0f, 2.0455f, -0.6133f, 3.875f, -1.84f, 5.4886f) + reflectiveCurveToRelative(-2.8229f, 2.7008f, -4.7886f, 3.2614f) + lineToRelative(1.0057f, 0.5909f) + lineTo(15.4629f, 22.0f) + close() + } + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(8.7447f, 13.0043f) + lineToRelative(8.0f, 0.0f) + lineToRelative(-2.625f, -3.5f) + lineToRelative(-1.875f, 2.5f) + lineToRelative(-1.375f, -1.825f) + close() + } + }.build() +} \ No newline at end of file diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index 88d107d7b8..63de7af1e3 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -912,4 +912,6 @@ Legacy LSTM network + Convert + Convert image batches to given format \ No newline at end of file diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/transformation/ImageInfoTransformation.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/transformation/ImageInfoTransformation.kt index 85804a6826..8bdf16abf0 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/transformation/ImageInfoTransformation.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/transformation/ImageInfoTransformation.kt @@ -37,8 +37,8 @@ import coil.transform.Transformation as CoilTransformation class ImageInfoTransformation @AssistedInject constructor( @Assisted private val imageInfo: ImageInfo, - @Assisted private val preset: Preset = Preset.Numeric(100), - @Assisted private val transformations: List> = emptyList(), + @Assisted private val preset: Preset, + @Assisted private val transformations: List>, private val imageTransformer: ImageTransformer, private val imageScaler: ImageScaler, private val imagePreviewCreator: ImagePreviewCreator @@ -111,7 +111,7 @@ class ImageInfoTransformation @AssistedInject constructor( interface Factory { operator fun invoke( imageInfo: ImageInfo, - preset: Preset = Preset.Numeric(100), + preset: Preset = Preset.Original, transformations: List> = emptyList(), ): ImageInfoTransformation } diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ContextUtils.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ContextUtils.kt index 3b949c3b8c..3ba0028f4a 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ContextUtils.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/helper/ContextUtils.kt @@ -31,10 +31,12 @@ import android.net.Uri import android.os.Build import android.widget.Toast import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.ui.graphics.vector.ImageVector import androidx.core.app.ActivityCompat +import androidx.core.os.LocaleListCompat import androidx.documentfile.provider.DocumentFile import ru.tech.imageresizershrinker.core.resources.R import ru.tech.imageresizershrinker.core.ui.utils.helper.IntentUtils.parcelable @@ -296,8 +298,15 @@ object ContextUtils { return !getSystemProperty("ro.miui.ui.version.name").isNullOrBlank() } + fun isRedMagic(): Boolean { + val osName = System.getProperty("os.name")?.lowercase() ?: "" + return listOf("redmagic", "magic", "red").all { + it !in osName + } + } + private fun getSystemProperty(name: String): String? { - return kotlin.runCatching { + return runCatching { val p = Runtime.getRuntime().exec("getprop $name") BufferedReader(InputStreamReader(p.inputStream), 1024).use { return@runCatching it.readLine() @@ -318,4 +327,41 @@ object ContextUtils { } } + fun Context.getLanguages(): Map { + val languages = mutableListOf("" to getString(R.string.system)).apply { + addAll( + LocaleConfigCompat(this@getLanguages) + .supportedLocales!!.toList() + .map { + it.toLanguageTag() to it.getDisplayName(it) + .replaceFirstChar(Char::uppercase) + } + ) + } + + return languages.let { tags -> + listOf(tags.first()) + tags.drop(1).sortedBy { it.second } + }.toMap() + } + + fun Context.getCurrentLocaleString(): String { + val locales = AppCompatDelegate.getApplicationLocales() + if (locales == LocaleListCompat.getEmptyLocaleList()) { + return getString(R.string.system) + } + return getDisplayName(locales.toLanguageTags()) + } + + private fun getDisplayName(lang: String?): String { + if (lang == null) { + return "" + } + + val locale = when (lang) { + "" -> LocaleListCompat.getAdjustedDefault()[0] + else -> Locale.forLanguageTag(lang) + } + return locale!!.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) } + } + } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt index d2a7b31344..6e2830fd0a 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/navigation/Screen.kt @@ -49,6 +49,7 @@ import ru.tech.imageresizershrinker.core.resources.icons.CropSmall import ru.tech.imageresizershrinker.core.resources.icons.Encrypted import ru.tech.imageresizershrinker.core.resources.icons.Exif import ru.tech.imageresizershrinker.core.resources.icons.ImageCombine +import ru.tech.imageresizershrinker.core.resources.icons.ImageConvert import ru.tech.imageresizershrinker.core.resources.icons.ImageDownload import ru.tech.imageresizershrinker.core.resources.icons.ImageEdit import ru.tech.imageresizershrinker.core.resources.icons.ImageLimit @@ -70,6 +71,7 @@ sealed class Screen( @StringRes val subtitle: Int ) : Parcelable { + @Suppress("unused") val simpleName: String? get() = when (this) { is ApngTools -> "APNG_Tools" @@ -100,10 +102,15 @@ sealed class Screen( is Watermarking -> "Watermarking" is Zip -> "Zip" is Svg -> "Svg" + is Convert -> "Convert" } val icon: ImageVector? get() = when (this) { + EasterEgg, + Main, + Settings -> null + is SingleEdit -> Icons.Outlined.ImageEdit is ApngTools -> Icons.Rounded.ApngBox is Cipher -> Icons.Outlined.Encrypted @@ -129,9 +136,7 @@ sealed class Screen( is Watermarking -> Icons.AutoMirrored.Outlined.BrandingWatermark is Zip -> Icons.Outlined.FolderZip is Svg -> Icons.Outlined.Svg - EasterEgg, - Main, - Settings -> null + is Convert -> Icons.Outlined.ImageConvert } data object Settings : Screen( @@ -577,12 +582,21 @@ sealed class Screen( subtitle = R.string.images_to_svg_sub ) + data class Convert( + val uris: List? = null + ) : Screen( + id = 25, + title = R.string.convert, + subtitle = R.string.convert_sub + ) + companion object { val typedEntries by lazy { listOf( listOf( SingleEdit(), ResizeAndConvert(), + Convert(), Crop(), ResizeByBytes(), LimitResize(), @@ -635,6 +649,6 @@ sealed class Screen( typedEntries.flatMap { it.first }.sortedBy { it.id } } - const val FEATURES_COUNT = 38 + const val FEATURES_COUNT = 39 } } \ No newline at end of file diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/state/Update.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/state/Update.kt index 867ac1580f..2fbfc2d08e 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/state/Update.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/utils/state/Update.kt @@ -19,7 +19,7 @@ package ru.tech.imageresizershrinker.core.ui.utils.state import androidx.compose.runtime.MutableState -fun MutableState.update( +inline fun MutableState.update( transform: (T) -> T ): MutableState = apply { this.value = transform(this.value) diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/controls/PresetSelector.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/controls/PresetSelector.kt index e53ae0b4e1..63bc0bf3a1 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/controls/PresetSelector.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/controls/PresetSelector.kt @@ -177,7 +177,7 @@ fun PresetSelector( val selected = value.value() == it EnhancedChip( selected = selected, - onClick = { onValueChange(Preset.Numeric(it)) }, + onClick = { onValueChange(Preset.Percentage(it)) }, selectedColor = MaterialTheme.colorScheme.primary, shape = MaterialTheme.shapes.medium ) { diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/other/LoadingDialog.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/other/LoadingDialog.kt index 0eb5f35286..0330f9ce00 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/other/LoadingDialog.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/other/LoadingDialog.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoadingDialog( - canCancel: Boolean = true, - onCancelLoading: () -> Unit = {} + onCancelLoading: () -> Unit = {}, + canCancel: Boolean = true ) { var showWantDismissDialog by remember(canCancel) { mutableStateOf(false) } BasicAlertDialog(onDismissRequest = { showWantDismissDialog = canCancel }) { @@ -67,8 +67,8 @@ fun LoadingDialog( fun LoadingDialog( done: Int, left: Int, + onCancelLoading: () -> Unit, canCancel: Boolean = true, - onCancelLoading: () -> Unit ) { var showWantDismissDialog by remember(canCancel) { mutableStateOf(false) } BasicAlertDialog(onDismissRequest = { showWantDismissDialog = canCancel }) { diff --git a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt index 038febe5fb..6f7c589002 100644 --- a/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt +++ b/core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/utils/ScreenList.kt @@ -90,6 +90,7 @@ internal fun List.screenList( listOf( Screen.SingleEdit(uris.firstOrNull()), Screen.ResizeAndConvert(uris), + Screen.Convert(uris), Screen.ResizeByBytes(uris), Screen.Crop(uris.firstOrNull()), Screen.Filter( @@ -150,6 +151,7 @@ internal fun List.screenList( mutableListOf( Screen.ResizeAndConvert(uris), Screen.ResizeByBytes(uris), + Screen.Convert(uris), Screen.Filter( type = Screen.Filter.Type.Basic(uris) ) @@ -226,7 +228,7 @@ internal fun List.screenList( extraImageType == "pdf" -> pdfAvailableScreens extraImageType == "gif" -> gifAvailableScreens extraImageType == "file" -> filesAvailableScreens - uris.size <= 1 -> singleImageScreens + uris.size < 2 -> singleImageScreens else -> multipleImagesScreens } } diff --git a/feature/bytes-resize/src/main/java/ru/tech/imageresizershrinker/feature/bytes_resize/presentation/BytesResizeScreen.kt b/feature/bytes-resize/src/main/java/ru/tech/imageresizershrinker/feature/bytes_resize/presentation/BytesResizeScreen.kt index 0bad04506f..5c193ca01b 100644 --- a/feature/bytes-resize/src/main/java/ru/tech/imageresizershrinker/feature/bytes_resize/presentation/BytesResizeScreen.kt +++ b/feature/bytes-resize/src/main/java/ru/tech/imageresizershrinker/feature/bytes_resize/presentation/BytesResizeScreen.kt @@ -264,7 +264,7 @@ fun BytesResizeScreen( } else { PresetSelector( value = viewModel.presetSelected.let { - Preset.Numeric(it) + Preset.Percentage(it) }, includeTelegramOption = false, onValueChange = viewModel::selectPreset @@ -334,9 +334,11 @@ fun BytesResizeScreen( ) if (viewModel.isSaving) { - LoadingDialog(viewModel.done, viewModel.uris?.size ?: 1) { - viewModel.cancelSaving() - } + LoadingDialog( + done = viewModel.done, + left = viewModel.uris?.size ?: 1, + onCancelLoading = viewModel::cancelSaving + ) } PickImageFromUrisSheet( diff --git a/feature/cipher/src/main/java/ru/tech/imageresizershrinker/feature/cipher/presentation/FileCipherScreen.kt b/feature/cipher/src/main/java/ru/tech/imageresizershrinker/feature/cipher/presentation/FileCipherScreen.kt index 4037e3473b..daeed91083 100644 --- a/feature/cipher/src/main/java/ru/tech/imageresizershrinker/feature/cipher/presentation/FileCipherScreen.kt +++ b/feature/cipher/src/main/java/ru/tech/imageresizershrinker/feature/cipher/presentation/FileCipherScreen.kt @@ -756,9 +756,7 @@ fun FileCipherScreen( } } - if (viewModel.isSaving) LoadingDialog { - viewModel.cancelSaving() - } + if (viewModel.isSaving) LoadingDialog(viewModel::cancelSaving) } } diff --git a/feature/compare/src/main/java/ru/tech/imageresizershrinker/feature/compare/presentation/CompareScreen.kt b/feature/compare/src/main/java/ru/tech/imageresizershrinker/feature/compare/presentation/CompareScreen.kt index 1b37ff78f1..c806bacb9f 100644 --- a/feature/compare/src/main/java/ru/tech/imageresizershrinker/feature/compare/presentation/CompareScreen.kt +++ b/feature/compare/src/main/java/ru/tech/imageresizershrinker/feature/compare/presentation/CompareScreen.kt @@ -277,7 +277,9 @@ fun CompareScreen( previewBitmap = previewBitmap ) - if (viewModel.isImageLoading) LoadingDialog { viewModel.cancelSaving() } + if (viewModel.isImageLoading) { + LoadingDialog(viewModel::cancelSaving) + } BackHandler(onBack = onGoBack) } \ No newline at end of file diff --git a/feature/convert/.gitignore b/feature/convert/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature/convert/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/convert/build.gradle.kts b/feature/convert/build.gradle.kts new file mode 100644 index 0000000000..28258b7171 --- /dev/null +++ b/feature/convert/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * 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 . + */ + +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.convert" + +dependencies { + implementation(projects.feature.compare) +} \ No newline at end of file diff --git a/feature/convert/src/main/AndroidManifest.xml b/feature/convert/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0862d94c4b --- /dev/null +++ b/feature/convert/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/feature/convert/src/main/java/ru/tech/imageresizershrinker/feature/convert/presentation/ConvertScreen.kt b/feature/convert/src/main/java/ru/tech/imageresizershrinker/feature/convert/presentation/ConvertScreen.kt new file mode 100644 index 0000000000..a1a5b6d7ce --- /dev/null +++ b/feature/convert/src/main/java/ru/tech/imageresizershrinker/feature/convert/presentation/ConvertScreen.kt @@ -0,0 +1,324 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.convert.presentation + +import android.content.res.Configuration +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.t8rin.dynamic.theme.LocalDynamicThemeState +import dev.olshevski.navigation.reimagined.hilt.hiltViewModel +import kotlinx.coroutines.launch +import ru.tech.imageresizershrinker.core.domain.image.model.Preset +import ru.tech.imageresizershrinker.core.resources.R +import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState +import ru.tech.imageresizershrinker.core.ui.utils.confetti.LocalConfettiHostState +import ru.tech.imageresizershrinker.core.ui.utils.helper.ImageUtils.haveChanges +import ru.tech.imageresizershrinker.core.ui.utils.helper.Picker +import ru.tech.imageresizershrinker.core.ui.utils.helper.asClip +import ru.tech.imageresizershrinker.core.ui.utils.helper.failedToSaveImages +import ru.tech.imageresizershrinker.core.ui.utils.helper.localImagePickerMode +import ru.tech.imageresizershrinker.core.ui.utils.helper.rememberImagePicker +import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalWindowSizeClass +import ru.tech.imageresizershrinker.core.ui.widget.AdaptiveLayoutScreen +import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock +import ru.tech.imageresizershrinker.core.ui.widget.buttons.CompareButton +import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton +import ru.tech.imageresizershrinker.core.ui.widget.buttons.ZoomButton +import ru.tech.imageresizershrinker.core.ui.widget.controls.ImageFormatSelector +import ru.tech.imageresizershrinker.core.ui.widget.controls.QualitySelector +import ru.tech.imageresizershrinker.core.ui.widget.controls.SaveExifWidget +import ru.tech.imageresizershrinker.core.ui.widget.dialogs.ExitWithoutSavingDialog +import ru.tech.imageresizershrinker.core.ui.widget.image.AutoFilePicker +import ru.tech.imageresizershrinker.core.ui.widget.image.ImageContainer +import ru.tech.imageresizershrinker.core.ui.widget.image.ImageCounter +import ru.tech.imageresizershrinker.core.ui.widget.image.ImageNotPickedWidget +import ru.tech.imageresizershrinker.core.ui.widget.other.LoadingDialog +import ru.tech.imageresizershrinker.core.ui.widget.other.LocalToastHostState +import ru.tech.imageresizershrinker.core.ui.widget.other.TopAppBarEmoji +import ru.tech.imageresizershrinker.core.ui.widget.other.showError +import ru.tech.imageresizershrinker.core.ui.widget.sheets.PickImageFromUrisSheet +import ru.tech.imageresizershrinker.core.ui.widget.sheets.ZoomModalSheet +import ru.tech.imageresizershrinker.core.ui.widget.text.TopAppBarTitle +import ru.tech.imageresizershrinker.feature.compare.presentation.components.CompareSheet +import ru.tech.imageresizershrinker.feature.convert.presentation.viewModel.ConvertViewModel + +@Composable +fun ConvertScreen( + uriState: List?, + onGoBack: () -> Unit, + viewModel: ConvertViewModel = hiltViewModel() +) { + val context = LocalContext.current as ComponentActivity + val toastHostState = LocalToastHostState.current + val themeState = LocalDynamicThemeState.current + + val settingsState = LocalSettingsState.current + val allowChangeColor = settingsState.allowChangeColorByImage + + val scope = rememberCoroutineScope() + val confettiHostState = LocalConfettiHostState.current + val showConfetti: () -> Unit = { + scope.launch { + confettiHostState.showConfetti() + } + } + + LaunchedEffect(uriState) { + uriState?.takeIf { it.isNotEmpty() }?.let { + viewModel.updateUris(it) { + scope.launch { + toastHostState.showError(context, it) + } + } + } + } + LaunchedEffect(viewModel.bitmap) { + viewModel.bitmap?.let { + if (allowChangeColor) { + themeState.updateColorByImage(it) + } + } + } + + val pickImageLauncher = + rememberImagePicker( + mode = localImagePickerMode(Picker.Multiple) + ) { list -> + list.takeIf { it.isNotEmpty() }?.let { + viewModel.updateUris(list) { + scope.launch { + toastHostState.showError(context, it) + } + } + } + } + + val pickImage = pickImageLauncher::pickImage + + AutoFilePicker( + onAutoPick = pickImage, + isPickedAlready = !uriState.isNullOrEmpty() + ) + + val saveBitmaps: () -> Unit = { + viewModel.saveBitmaps { results, savingPath -> + context.failedToSaveImages( + scope = scope, + results = results, + toastHostState = toastHostState, + savingPathString = savingPath, + isOverwritten = settingsState.overwriteFiles, + showConfetti = showConfetti + ) + } + } + + val showPickImageFromUrisSheet = rememberSaveable { mutableStateOf(false) } + + val isPortrait = + LocalConfiguration.current.orientation != Configuration.ORIENTATION_LANDSCAPE || LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact + + var showExitDialog by rememberSaveable { mutableStateOf(false) } + + val onBack = { + if (viewModel.imageInfo.haveChanges(viewModel.bitmap)) showExitDialog = true + else onGoBack() + } + + val showZoomSheet = rememberSaveable { mutableStateOf(false) } + + val showCompareSheet = rememberSaveable { mutableStateOf(false) } + + CompareSheet( + data = viewModel.bitmap to viewModel.previewBitmap, + visible = showCompareSheet + ) + + ZoomModalSheet( + data = viewModel.previewBitmap, + visible = showZoomSheet + ) + + AdaptiveLayoutScreen( + title = { + TopAppBarTitle( + title = stringResource(R.string.convert), + input = viewModel.bitmap, + isLoading = viewModel.isImageLoading, + size = viewModel.imageInfo.sizeInBytes.toLong() + ) + }, + onGoBack = onBack, + actions = { + ShareButton( + enabled = viewModel.bitmap != null, + onShare = { + viewModel.shareBitmaps(showConfetti) + }, + onCopy = { manager -> + viewModel.cacheCurrentImage { uri -> + manager.setClip(uri.asClip(context)) + showConfetti() + } + } + ) + }, + imagePreview = { + ImageContainer( + imageInside = isPortrait, + showOriginal = false, + previewBitmap = viewModel.previewBitmap, + originalBitmap = viewModel.bitmap, + isLoading = viewModel.isImageLoading, + shouldShowPreview = viewModel.shouldShowPreview + ) + }, + controls = { + val imageInfo = viewModel.imageInfo + ImageCounter( + imageCount = viewModel.uris?.size?.takeIf { it > 1 }, + onRepick = { + showPickImageFromUrisSheet.value = true + } + ) + Spacer(Modifier.size(8.dp)) + AnimatedVisibility( + visible = viewModel.uris?.size != 1, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + SaveExifWidget( + imageFormat = viewModel.imageInfo.imageFormat, + checked = viewModel.keepExif, + onCheckedChange = viewModel::setKeepExif + ) + Spacer(Modifier.size(8.dp)) + } + } + if (imageInfo.imageFormat.canChangeCompressionValue) { + Spacer(Modifier.height(8.dp)) + } + QualitySelector( + imageFormat = imageInfo.imageFormat, + enabled = viewModel.bitmap != null, + quality = imageInfo.quality, + onQualityChange = viewModel::setQuality + ) + Spacer(Modifier.height(8.dp)) + ImageFormatSelector( + value = imageInfo.imageFormat, + onValueChange = viewModel::setImageFormat + ) + }, + buttons = { actions -> + BottomButtonsBlock( + targetState = (viewModel.uris.isNullOrEmpty()) to isPortrait, + onSecondaryButtonClick = pickImage, + onPrimaryButtonClick = saveBitmaps, + actions = { + if (isPortrait) actions() + } + ) + }, + topAppBarPersistentActions = { + if (viewModel.bitmap == null) TopAppBarEmoji() + CompareButton( + onClick = { showCompareSheet.value = true }, + visible = viewModel.previewBitmap != null + && viewModel.bitmap != null + && viewModel.shouldShowPreview + ) + ZoomButton( + onClick = { showZoomSheet.value = true }, + visible = viewModel.previewBitmap != null && viewModel.shouldShowPreview + ) + }, + canShowScreenData = viewModel.bitmap != null, + forceImagePreviewToMax = false, + noDataControls = { + if (!viewModel.isImageLoading) { + ImageNotPickedWidget(onPickImage = pickImage) + } + }, + isPortrait = isPortrait + ) + + PickImageFromUrisSheet( + transformations = listOf( + viewModel.imageInfoTransformationFactory( + imageInfo = viewModel.imageInfo, + preset = Preset.Original + ) + ), + visible = showPickImageFromUrisSheet, + uris = viewModel.uris, + selectedUri = viewModel.selectedUri, + onUriPicked = { uri -> + viewModel.setBitmap(uri = uri) { + scope.launch { + toastHostState.showError(context, it) + } + } + }, + onUriRemoved = { uri -> + viewModel.updateUrisSilently(removedUri = uri) { + scope.launch { + toastHostState.showError(context, it) + } + } + }, + columns = if (isPortrait) 2 else 4, + ) + + ExitWithoutSavingDialog( + onExit = onGoBack, + onDismiss = { showExitDialog = false }, + visible = showExitDialog + ) + + if (viewModel.isSaving) { + LoadingDialog( + done = viewModel.done, + left = viewModel.uris?.size ?: 1, + onCancelLoading = viewModel::cancelSaving + ) + } +} \ No newline at end of file diff --git a/feature/convert/src/main/java/ru/tech/imageresizershrinker/feature/convert/presentation/viewModel/ConvertViewModel.kt b/feature/convert/src/main/java/ru/tech/imageresizershrinker/feature/convert/presentation/viewModel/ConvertViewModel.kt new file mode 100644 index 0000000000..f62f584aa7 --- /dev/null +++ b/feature/convert/src/main/java/ru/tech/imageresizershrinker/feature/convert/presentation/viewModel/ConvertViewModel.kt @@ -0,0 +1,374 @@ +/* + * 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 . + */ + +package ru.tech.imageresizershrinker.feature.convert.presentation.viewModel + +import android.graphics.Bitmap +import android.net.Uri +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +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.ImagePreviewCreator +import ru.tech.imageresizershrinker.core.domain.image.ImageScaler +import ru.tech.imageresizershrinker.core.domain.image.ImageTransformer +import ru.tech.imageresizershrinker.core.domain.image.ShareProvider +import ru.tech.imageresizershrinker.core.domain.image.model.ImageData +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.Preset +import ru.tech.imageresizershrinker.core.domain.image.model.Quality +import ru.tech.imageresizershrinker.core.domain.model.IntegerSize +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.transformation.ImageInfoTransformation +import ru.tech.imageresizershrinker.core.ui.utils.BaseViewModel +import ru.tech.imageresizershrinker.core.ui.utils.state.update +import javax.inject.Inject + +@HiltViewModel +class ConvertViewModel @Inject constructor( + private val fileController: FileController, + private val imageTransformer: ImageTransformer, + private val imagePreviewCreator: ImagePreviewCreator, + private val imageCompressor: ImageCompressor, + private val imageGetter: ImageGetter, + private val imageScaler: ImageScaler, + private val shareProvider: ShareProvider, + val imageInfoTransformationFactory: ImageInfoTransformation.Factory, + dispatchersHolder: DispatchersHolder +) : BaseViewModel(dispatchersHolder) { + + private val _originalSize: MutableState = mutableStateOf(null) + + private val _uris = mutableStateOf?>(null) + val uris by _uris + + private val _bitmap: MutableState = mutableStateOf(null) + val bitmap: Bitmap? by _bitmap + + private val _keepExif = mutableStateOf(false) + val keepExif by _keepExif + + private val _imageInfo: MutableState = mutableStateOf(ImageInfo()) + val imageInfo: ImageInfo by _imageInfo + + private val _isSaving: MutableState = mutableStateOf(false) + val isSaving: Boolean by _isSaving + + private val _shouldShowPreview: MutableState = mutableStateOf(true) + val shouldShowPreview by _shouldShowPreview + + private val _previewBitmap: MutableState = mutableStateOf(null) + val previewBitmap: Bitmap? by _previewBitmap + + private val _done: MutableState = mutableIntStateOf(0) + val done by _done + + private val _selectedUri: MutableState = mutableStateOf(null) + val selectedUri by _selectedUri + + private var job: Job? by smartJob { + _isImageLoading.update { false } + } + + fun updateUris( + uris: List?, + onError: (Throwable) -> Unit + ) { + _uris.value = null + _uris.value = uris + _selectedUri.value = uris?.firstOrNull() + uris?.firstOrNull()?.let { uri -> + viewModelScope.launch { + _imageInfo.update { + it.copy(originalUri = uri.toString()) + } + imageGetter.getImageAsync( + uri = uri.toString(), + originalSize = true, + onGetImage = ::setImageData, + onError = onError + ) + } + } + } + + fun updateUrisSilently( + removedUri: Uri, + onError: (Throwable) -> Unit + ) { + viewModelScope.launch(defaultDispatcher) { + _uris.value = uris + if (_selectedUri.value == removedUri) { + val index = uris?.indexOf(removedUri) ?: -1 + if (index == 0) { + uris?.getOrNull(1)?.let { + setBitmap(it, onError) + } + } else { + uris?.getOrNull(index - 1)?.let { + setBitmap(it, onError) + } + } + } + val u = _uris.value?.toMutableList()?.apply { + remove(removedUri) + } + _uris.value = u + } + } + + private suspend fun checkBitmapAndUpdate() { + _bitmap.value?.let { bmp -> + val preview = updatePreview(bmp) + _previewBitmap.value = null + _shouldShowPreview.value = imagePreviewCreator.canShow(preview) + if (shouldShowPreview) _previewBitmap.value = preview + } + } + + private suspend fun updatePreview( + bitmap: Bitmap + ): Bitmap? = withContext(defaultDispatcher) { + return@withContext imageInfo.run { + imagePreviewCreator.createPreview( + image = bitmap, + imageInfo = this, + onGetByteCount = { + _imageInfo.value = _imageInfo.value.copy(sizeInBytes = it) + } + ) + } + } + + private fun resetValues() { + _imageInfo.value = ImageInfo( + width = _originalSize.value?.width ?: 0, + height = _originalSize.value?.height ?: 0, + imageFormat = imageInfo.imageFormat, + originalUri = selectedUri?.toString() + ) + debouncedImageCalculation { + checkBitmapAndUpdate() + } + } + + private fun setImageData(imageData: ImageData) { + job = viewModelScope.launch { + _isImageLoading.update { true } + val bitmap = imageData.image + val size = bitmap.width to bitmap.height + _originalSize.update { + size.run { IntegerSize(width = first, height = second) } + } + _bitmap.update { + imageScaler.scaleUntilCanShow(bitmap) + } + resetValues() + _imageInfo.update { + imageData.imageInfo.copy( + width = size.first, + height = size.second + ) + } + checkBitmapAndUpdate() + _isImageLoading.update { false } + } + } + + fun setQuality(quality: Quality) { + if (_imageInfo.value.quality != quality) { + _imageInfo.value = _imageInfo.value.copy(quality = quality) + debouncedImageCalculation { + checkBitmapAndUpdate() + } + } + } + + fun setImageFormat(imageFormat: ImageFormat) { + if (_imageInfo.value.imageFormat != imageFormat) { + _imageInfo.value = _imageInfo.value.copy(imageFormat = imageFormat) + debouncedImageCalculation { + checkBitmapAndUpdate() + } + } + } + + fun setKeepExif(boolean: Boolean) { + _keepExif.value = boolean + } + + private var savingJob: Job? by smartJob { + _isSaving.update { false } + } + + fun saveBitmaps( + onComplete: (List, path: String) -> Unit + ) { + savingJob = viewModelScope.launch(defaultDispatcher) { + _isSaving.value = true + val results = mutableListOf() + _done.value = 0 + uris?.forEach { uri -> + runCatching { + imageGetter.getImage(uri.toString())?.image + }.getOrNull()?.let { bitmap -> + imageInfo.copy( + originalUri = uri.toString() + ).let { + imageTransformer.applyPresetBy( + image = bitmap, + preset = Preset.Original, + currentInfo = it + ) + }.let { imageInfo -> + results.add( + fileController.save( + saveTarget = ImageSaveTarget( + imageInfo = imageInfo, + metadata = null, + originalUri = uri.toString(), + sequenceNumber = _done.value + 1, + data = imageCompressor.compressAndTransform( + image = bitmap, + imageInfo = imageInfo + ) + ), + keepOriginalMetadata = keepExif + ) + ) + } + } ?: results.add( + SaveResult.Error.Exception(Throwable()) + ) + + _done.value += 1 + } + onComplete(results, fileController.savingPath) + _isSaving.value = false + } + } + + fun setBitmap( + uri: Uri, + onError: (Throwable) -> Unit + ) { + _selectedUri.value = uri + viewModelScope.launch(defaultDispatcher) { + runCatching { + _isImageLoading.update { true } + val bitmap = imageGetter.getImage( + uri = uri.toString(), + originalSize = true + )?.image + val size = bitmap?.let { it.width to it.height } + _originalSize.value = size?.run { IntegerSize(width = first, height = second) } + _bitmap.value = imageScaler.scaleUntilCanShow(bitmap) + _imageInfo.value = _imageInfo.value.copy( + width = size?.first ?: 0, + height = size?.second ?: 0, + originalUri = uri.toString() + ) + _imageInfo.value = imageTransformer.applyPresetBy( + image = _bitmap.value, + preset = Preset.Original, + currentInfo = _imageInfo.value + ) + checkBitmapAndUpdate() + _isImageLoading.update { false } + }.onFailure { + _isImageLoading.update { false } + onError(it) + } + } + } + + fun shareBitmaps(onComplete: () -> Unit) { + savingJob = viewModelScope.launch { + _isSaving.value = true + shareProvider.shareImages( + uris = uris?.map { it.toString() } ?: emptyList(), + imageLoader = { uri -> + imageGetter.getImage(uri)?.image?.let { bmp -> + bmp to imageInfo.copy( + originalUri = uri + ).let { + imageTransformer.applyPresetBy( + image = bitmap, + preset = Preset.Original, + currentInfo = it + ) + } + } + }, + onProgressChange = { + if (it == -1) { + onComplete() + _isSaving.value = false + _done.value = 0 + } else { + _done.value = it + } + } + ) + } + } + + fun cancelSaving() { + savingJob?.cancel() + savingJob = null + _isSaving.value = false + } + + fun cacheCurrentImage(onComplete: (Uri) -> Unit) { + savingJob = viewModelScope.launch { + _isSaving.value = true + imageGetter.getImage(selectedUri.toString())?.image?.let { bmp -> + bmp to imageInfo.copy( + originalUri = selectedUri.toString() + ).let { + imageTransformer.applyPresetBy( + image = bitmap, + preset = Preset.Original, + currentInfo = it + ) + } + }?.let { (image, imageInfo) -> + shareProvider.cacheImage( + image = image, + imageInfo = imageInfo.copy(originalUri = selectedUri.toString()) + )?.let { uri -> + onComplete(uri.toUri()) + } + } + _isSaving.value = false + } + } + +} \ No newline at end of file diff --git a/feature/crop/src/main/java/ru/tech/imageresizershrinker/feature/crop/presentation/CropScreen.kt b/feature/crop/src/main/java/ru/tech/imageresizershrinker/feature/crop/presentation/CropScreen.kt index 921b50b696..06862cfaf5 100644 --- a/feature/crop/src/main/java/ru/tech/imageresizershrinker/feature/crop/presentation/CropScreen.kt +++ b/feature/crop/src/main/java/ru/tech/imageresizershrinker/feature/crop/presentation/CropScreen.kt @@ -580,7 +580,10 @@ fun CropScreen( } if (viewModel.isSaving || viewModel.isImageLoading) { - LoadingDialog(canCancel = viewModel.isSaving) { viewModel.cancelSaving() } + LoadingDialog( + onCancelLoading = viewModel::cancelSaving, + canCancel = viewModel.isSaving + ) } ExitWithoutSavingDialog( diff --git a/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifScreen.kt b/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifScreen.kt index 56eafd7da1..fa971ddd62 100644 --- a/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifScreen.kt +++ b/feature/delete-exif/src/main/java/ru/tech/imageresizershrinker/feature/delete_exif/presentation/DeleteExifScreen.kt @@ -263,9 +263,11 @@ fun DeleteExifScreen( ) if (viewModel.isSaving) { - LoadingDialog(viewModel.done, viewModel.uris?.size ?: 1) { - viewModel.cancelSaving() - } + LoadingDialog( + done = viewModel.done, + left = viewModel.uris?.size ?: 1, + onCancelLoading = viewModel::cancelSaving + ) } PickImageFromUrisSheet( diff --git a/feature/draw/src/main/java/ru/tech/imageresizershrinker/feature/draw/presentation/DrawScreen.kt b/feature/draw/src/main/java/ru/tech/imageresizershrinker/feature/draw/presentation/DrawScreen.kt index 82948f8ffe..589a861633 100644 --- a/feature/draw/src/main/java/ru/tech/imageresizershrinker/feature/draw/presentation/DrawScreen.kt +++ b/feature/draw/src/main/java/ru/tech/imageresizershrinker/feature/draw/presentation/DrawScreen.kt @@ -963,9 +963,10 @@ fun DrawScreen( } if (viewModel.isSaving || viewModel.isImageLoading) { - LoadingDialog(viewModel.isSaving) { - viewModel.cancelSaving() - } + LoadingDialog( + onCancelLoading = viewModel::cancelSaving, + canCancel = viewModel.isSaving + ) } var colorPickerColor by rememberSaveable { mutableStateOf(Color.Black) } diff --git a/feature/erase-background/src/main/java/ru/tech/imageresizershrinker/feature/erase_background/presentation/EraseBackgroundScreen.kt b/feature/erase-background/src/main/java/ru/tech/imageresizershrinker/feature/erase_background/presentation/EraseBackgroundScreen.kt index 86e81cfb0c..5641fa0a2f 100644 --- a/feature/erase-background/src/main/java/ru/tech/imageresizershrinker/feature/erase_background/presentation/EraseBackgroundScreen.kt +++ b/feature/erase-background/src/main/java/ru/tech/imageresizershrinker/feature/erase_background/presentation/EraseBackgroundScreen.kt @@ -749,9 +749,7 @@ fun EraseBackgroundScreen( if (viewModel.isSaving || viewModel.isImageLoading || viewModel.isErasingBG) { - LoadingDialog(viewModel.isSaving) { - viewModel.cancelSaving() - } + LoadingDialog(viewModel::cancelSaving, viewModel.isSaving) } ExitWithoutSavingDialog( diff --git a/feature/image-stitch/src/main/java/ru/tech/imageresizershrinker/feature/image_stitch/presentation/ImageStitchingScreen.kt b/feature/image-stitch/src/main/java/ru/tech/imageresizershrinker/feature/image_stitch/presentation/ImageStitchingScreen.kt index e223ce85ee..c02c2c17db 100644 --- a/feature/image-stitch/src/main/java/ru/tech/imageresizershrinker/feature/image_stitch/presentation/ImageStitchingScreen.kt +++ b/feature/image-stitch/src/main/java/ru/tech/imageresizershrinker/feature/image_stitch/presentation/ImageStitchingScreen.kt @@ -304,9 +304,7 @@ fun ImageStitchingScreen( ) if (viewModel.isSaving) { - LoadingDialog { - viewModel.cancelSaving() - } + LoadingDialog(viewModel::cancelSaving) } ExitWithoutSavingDialog( diff --git a/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeScreen.kt b/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeScreen.kt index 9aa210ca03..a657af0694 100644 --- a/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeScreen.kt +++ b/feature/limits-resize/src/main/java/ru/tech/imageresizershrinker/feature/limits_resize/presentation/LimitsResizeScreen.kt @@ -281,10 +281,10 @@ fun LimitsResizeScreen( if (viewModel.isSaving) { LoadingDialog( - done = viewModel.done, left = viewModel.uris?.size ?: 1 - ) { - viewModel.cancelSaving() - } + done = viewModel.done, + left = viewModel.uris?.size ?: 1, + onCancelLoading = viewModel::cancelSaving + ) } PickImageFromUrisSheet( diff --git a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenPreferenceSelection.kt b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenPreferenceSelection.kt index 4849b8515c..e23caa980b 100644 --- a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenPreferenceSelection.kt +++ b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/components/ScreenPreferenceSelection.kt @@ -201,10 +201,12 @@ internal fun RowScope.ScreenPreferenceSelection( .using(SizeTransform(false)) } ) { icon -> - Icon( - imageVector = icon!!, - contentDescription = null - ) + icon?.let { + Icon( + imageVector = icon, + contentDescription = null + ) + } } }, interactionSource = interactionSource diff --git a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/data/AndroidPdfManager.kt b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/data/AndroidPdfManager.kt index 1573ed3304..5aae92644a 100644 --- a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/data/AndroidPdfManager.kt +++ b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/data/AndroidPdfManager.kt @@ -61,7 +61,7 @@ internal class AndroidPdfManager @Inject constructor( imageUris: List, onProgressChange: suspend (Int) -> Unit, scaleSmallImagesToLarge: Boolean, - preset: Preset.Numeric + preset: Preset.Percentage ): ByteArray = withContext(encodingDispatcher) { val pdfDocument = PdfDocument() @@ -111,7 +111,7 @@ internal class AndroidPdfManager @Inject constructor( override fun convertPdfToImages( pdfUri: String, pages: List?, - preset: Preset.Numeric, + preset: Preset.Percentage, onGetPagesCount: suspend (Int) -> Unit, onProgressChange: suspend (Int, Bitmap) -> Unit, onComplete: suspend () -> Unit diff --git a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/domain/PdfManager.kt b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/domain/PdfManager.kt index 7fa34e995e..641a1f1307 100644 --- a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/domain/PdfManager.kt +++ b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/domain/PdfManager.kt @@ -31,13 +31,13 @@ interface PdfManager { imageUris: List, onProgressChange: suspend (Int) -> Unit, scaleSmallImagesToLarge: Boolean, - preset: Preset.Numeric + preset: Preset.Percentage ): ByteArray fun convertPdfToImages( pdfUri: String, pages: List?, - preset: Preset.Numeric, + preset: Preset.Percentage, onGetPagesCount: suspend (Int) -> Unit, onProgressChange: suspend (Int, I) -> Unit, onComplete: suspend () -> Unit = {} diff --git a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/PdfToolsScreen.kt b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/PdfToolsScreen.kt index d94d95a836..05042a856f 100644 --- a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/PdfToolsScreen.kt +++ b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/PdfToolsScreen.kt @@ -490,7 +490,7 @@ fun PdfToolsScreen( value = viewModel.presetSelected, includeTelegramOption = false, onValueChange = { - if (it is Preset.Numeric) { + if (it is Preset.Percentage) { viewModel.selectPreset(it) } }, @@ -517,7 +517,7 @@ fun PdfToolsScreen( value = viewModel.presetSelected, includeTelegramOption = false, onValueChange = { - if (it is Preset.Numeric) { + if (it is Preset.Percentage) { viewModel.selectPreset(it) } }, @@ -947,13 +947,13 @@ fun PdfToolsScreen( if (viewModel.isSaving) { if (viewModel.left != 0) { - LoadingDialog(viewModel.done, viewModel.left) { - viewModel.cancelSaving() - } + LoadingDialog( + done = viewModel.done, + left = viewModel.left, + onCancelLoading = viewModel::cancelSaving + ) } else { - LoadingDialog { - viewModel.cancelSaving() - } + LoadingDialog(viewModel::cancelSaving) } } } diff --git a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt index 4242964aff..5025ec7153 100644 --- a/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt +++ b/feature/pdf-tools/src/main/java/ru/tech/imageresizershrinker/feature/pdf_tools/presentation/viewModel/PdfToolsViewModel.kt @@ -80,7 +80,8 @@ class PdfToolsViewModel @Inject constructor( private val _isSaving: MutableState = mutableStateOf(false) val isSaving by _isSaving - private val _presetSelected: MutableState = mutableStateOf(Preset.Numeric(100)) + private val _presetSelected: MutableState = + mutableStateOf(Preset.Percentage(100)) val presetSelected by _presetSelected private val _scaleSmallImagesToLarge: MutableState = mutableStateOf(false) @@ -182,7 +183,7 @@ class PdfToolsViewModel @Inject constructor( _pdfPreviewUri.update { null } _imagesToPdfState.update { null } _pdfToImageState.update { null } - _presetSelected.update { Preset.Numeric(100) } + _presetSelected.update { Preset.Original } _showOOMWarning.value = false _imageInfo.value = ImageInfo() resetCalculatedData() @@ -392,7 +393,7 @@ class PdfToolsViewModel @Inject constructor( } } - fun selectPreset(preset: Preset.Numeric) { + fun selectPreset(preset: Preset.Percentage) { _presetSelected.update { preset } preset.value()?.takeIf { it <= 100f }?.let { quality -> _imageInfo.update { diff --git a/feature/pick-color/src/main/java/ru/tech/imageresizershrinker/feature/pick_color/presentation/PickColorFromImageScreen.kt b/feature/pick-color/src/main/java/ru/tech/imageresizershrinker/feature/pick_color/presentation/PickColorFromImageScreen.kt index 00be79dd6d..6ae699a5c4 100644 --- a/feature/pick-color/src/main/java/ru/tech/imageresizershrinker/feature/pick_color/presentation/PickColorFromImageScreen.kt +++ b/feature/pick-color/src/main/java/ru/tech/imageresizershrinker/feature/pick_color/presentation/PickColorFromImageScreen.kt @@ -651,7 +651,7 @@ fun PickColorFromImageScreen( } } - if (viewModel.isImageLoading) LoadingDialog(false) {} + if (viewModel.isImageLoading) LoadingDialog(canCancel = false) BackHandler { onGoBack() diff --git a/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertScreen.kt b/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertScreen.kt index c648ef4159..594ab58665 100644 --- a/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertScreen.kt +++ b/feature/resize-convert/src/main/java/ru/tech/imageresizershrinker/feature/resize_convert/presentation/ResizeAndConvertScreen.kt @@ -432,9 +432,8 @@ fun ResizeAndConvertScreen( if (viewModel.isSaving) { LoadingDialog( done = viewModel.done, - left = viewModel.uris?.size ?: 1 - ) { - viewModel.cancelSaving() - } + left = viewModel.uris?.size ?: 1, + onCancelLoading = viewModel::cancelSaving + ) } } \ No newline at end of file diff --git a/feature/root/build.gradle.kts b/feature/root/build.gradle.kts index db52bc9f46..d79142b76f 100644 --- a/feature/root/build.gradle.kts +++ b/feature/root/build.gradle.kts @@ -53,4 +53,5 @@ dependencies { implementation(projects.feature.settings) implementation(projects.feature.easterEgg) implementation(projects.feature.svg) + implementation(projects.feature.convert) } \ No newline at end of file diff --git a/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/ScreenSelector.kt b/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/ScreenSelector.kt index c0d686e66d..a052d3680e 100644 --- a/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/ScreenSelector.kt +++ b/feature/root/src/main/java/ru/tech/imageresizershrinker/feature/root/presentation/components/ScreenSelector.kt @@ -33,6 +33,7 @@ import ru.tech.imageresizershrinker.feature.apng_tools.presentation.ApngToolsScr import ru.tech.imageresizershrinker.feature.bytes_resize.presentation.BytesResizeScreen import ru.tech.imageresizershrinker.feature.cipher.presentation.FileCipherScreen import ru.tech.imageresizershrinker.feature.compare.presentation.CompareScreen +import ru.tech.imageresizershrinker.feature.convert.presentation.ConvertScreen import ru.tech.imageresizershrinker.feature.crop.presentation.CropScreen import ru.tech.imageresizershrinker.feature.delete_exif.presentation.DeleteExifScreen import ru.tech.imageresizershrinker.feature.draw.presentation.DrawScreen @@ -285,6 +286,13 @@ internal fun ScreenSelector( onGoBack = onGoBack ) } + + is Screen.Convert -> { + ConvertScreen( + uriState = screen.uris, + onGoBack = onGoBack + ) + } } } diff --git a/feature/settings/src/main/java/ru/tech/imageresizershrinker/feature/settings/presentation/components/ChangeLanguageSettingItem.kt b/feature/settings/src/main/java/ru/tech/imageresizershrinker/feature/settings/presentation/components/ChangeLanguageSettingItem.kt index adac5983c9..d34e96f784 100644 --- a/feature/settings/src/main/java/ru/tech/imageresizershrinker/feature/settings/presentation/components/ChangeLanguageSettingItem.kt +++ b/feature/settings/src/main/java/ru/tech/imageresizershrinker/feature/settings/presentation/components/ChangeLanguageSettingItem.kt @@ -17,7 +17,6 @@ package ru.tech.imageresizershrinker.feature.settings.presentation.components -import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build @@ -53,8 +52,8 @@ import androidx.core.os.LocaleListCompat import ru.tech.imageresizershrinker.core.resources.R import ru.tech.imageresizershrinker.core.resources.icons.MiniEdit import ru.tech.imageresizershrinker.core.ui.utils.helper.ContextUtils -import ru.tech.imageresizershrinker.core.ui.utils.helper.LocaleConfigCompat -import ru.tech.imageresizershrinker.core.ui.utils.helper.toList +import ru.tech.imageresizershrinker.core.ui.utils.helper.ContextUtils.getCurrentLocaleString +import ru.tech.imageresizershrinker.core.ui.utils.helper.ContextUtils.getLanguages import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedButton import ru.tech.imageresizershrinker.core.ui.widget.modifier.ContainerShapeDefaults import ru.tech.imageresizershrinker.core.ui.widget.preferences.PreferenceItem @@ -62,7 +61,6 @@ import ru.tech.imageresizershrinker.core.ui.widget.preferences.PreferenceItemOve import ru.tech.imageresizershrinker.core.ui.widget.sheets.SimpleSheet import ru.tech.imageresizershrinker.core.ui.widget.text.AutoSizeText import ru.tech.imageresizershrinker.core.ui.widget.text.TitleItem -import java.util.Locale @Composable fun ChangeLanguageSettingItem( @@ -70,29 +68,31 @@ fun ChangeLanguageSettingItem( shape: Shape = RoundedCornerShape(16.dp) ) { val context = LocalContext.current - val showDialog = rememberSaveable { mutableStateOf(false) } + val showEmbeddedLanguagePicker = rememberSaveable { mutableStateOf(false) } Column(Modifier.animateContentSize()) { PreferenceItem( shape = shape, modifier = modifier.padding(bottom = 1.dp), title = stringResource(R.string.language), - subtitle = context.getCurrentLocaleString(), + subtitle = remember { + context.getCurrentLocaleString() + }, startIcon = Icons.Outlined.Language, endIcon = Icons.Rounded.MiniEdit, onClick = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !ContextUtils.isMiUi()) { - kotlin.runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !ContextUtils.isMiUi() && !ContextUtils.isRedMagic()) { + runCatching { context.startActivity( Intent( Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:${context.packageName}") ) ) - }.getOrNull().let { - if (it == null) showDialog.value = true + }.onFailure { + showEmbeddedLanguagePicker.value = true } } else { - showDialog.value = true + showEmbeddedLanguagePicker.value = true } } ) @@ -102,7 +102,9 @@ fun ChangeLanguageSettingItem( entries = remember { context.getLanguages() }, - selected = context.getCurrentLocaleString(), + selected = remember { + context.getCurrentLocaleString() + }, onSelect = { val locale = if (it == "") { LocaleListCompat.getEmptyLocaleList() @@ -111,7 +113,7 @@ fun ChangeLanguageSettingItem( } AppCompatDelegate.setApplicationLocales(locale) }, - visible = showDialog + visible = showEmbeddedLanguagePicker ) } @@ -188,40 +190,4 @@ private fun PickLanguageSheet( }, visible = visible ) -} - -private fun Context.getLanguages(): Map { - val languages = mutableListOf("" to getString(R.string.system)).apply { - addAll( - LocaleConfigCompat(this@getLanguages) - .supportedLocales!!.toList() - .map { - it.toLanguageTag() to it.getDisplayName(it).replaceFirstChar(Char::uppercase) - } - ) - } - - return languages.let { tags -> - listOf(tags.first()) + tags.drop(1).sortedBy { it.second } - }.toMap() -} - -private fun Context.getCurrentLocaleString(): String { - val locales = AppCompatDelegate.getApplicationLocales() - if (locales == LocaleListCompat.getEmptyLocaleList()) { - return getString(R.string.system) - } - return getDisplayName(locales.toLanguageTags()) -} - -private fun getDisplayName(lang: String?): String { - if (lang == null) { - return "" - } - - val locale = when (lang) { - "" -> LocaleListCompat.getAdjustedDefault()[0] - else -> Locale.forLanguageTag(lang) - } - return locale!!.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) } -} +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b541ae1f0e..1e3671970f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -87,6 +87,7 @@ include(":feature:quick-tiles") include(":feature:settings") include(":feature:easter-egg") include(":feature:svg") +include(":feature:convert") include(":feature:root")