mirror of
https://github.com/T8RIN/ImageToolbox.git
synced 2025-08-06 07:39:47 +08:00
Added Collage Maker by #881
This commit is contained in:
@ -121,11 +121,8 @@ internal class AndroidFileController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val originalUri = saveTarget.originalUri.toUri()
|
val originalUri = saveTarget.originalUri.toUri()
|
||||||
val hasOriginalUri = runCatching {
|
|
||||||
context.contentResolver.openFileDescriptor(originalUri, "r")
|
|
||||||
}.isSuccess
|
|
||||||
|
|
||||||
if (settingsState.overwriteFiles && hasOriginalUri) {
|
if (settingsState.overwriteFiles) {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (originalUri == Uri.EMPTY) throw IllegalStateException()
|
if (originalUri == Uri.EMPTY) throw IllegalStateException()
|
||||||
|
|
||||||
@ -258,7 +255,8 @@ internal class AndroidFileController @Inject constructor(
|
|||||||
|
|
||||||
return@withContext SaveResult.Success(
|
return@withContext SaveResult.Success(
|
||||||
message = if (savingPath.isNotEmpty()) {
|
message = if (savingPath.isNotEmpty()) {
|
||||||
val isFile = documentFile?.isDirectory != true
|
val isFile =
|
||||||
|
(documentFile?.isDirectory != true && oneTimeSaveLocationUri != null)
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
context.getString(R.string.saved_to_custom)
|
context.getString(R.string.saved_to_custom)
|
||||||
} else if (filename.isNotEmpty()) {
|
} else if (filename.isNotEmpty()) {
|
||||||
|
@ -1406,4 +1406,8 @@
|
|||||||
<string name="custom_filename">Custom Filename</string>
|
<string name="custom_filename">Custom Filename</string>
|
||||||
<string name="custom_filename_sub">Select location and filename which are will be used to save current image</string>
|
<string name="custom_filename_sub">Select location and filename which are will be used to save current image</string>
|
||||||
<string name="saved_to_custom">Saved to folder with custom name</string>
|
<string name="saved_to_custom">Saved to folder with custom name</string>
|
||||||
|
<string name="collage_maker">Collage Maker</string>
|
||||||
|
<string name="collage_maker_sub">Make various collages from 2..10 images</string>
|
||||||
|
<string name="collage_type">Collage Type</string>
|
||||||
|
<string name="pick_images_collage">Pick 2..10 images</string>
|
||||||
</resources>
|
</resources>
|
@ -25,6 +25,7 @@ import androidx.compose.material.icons.automirrored.outlined.BrandingWatermark
|
|||||||
import androidx.compose.material.icons.filled.AutoAwesome
|
import androidx.compose.material.icons.filled.AutoAwesome
|
||||||
import androidx.compose.material.icons.filled.FilterHdr
|
import androidx.compose.material.icons.filled.FilterHdr
|
||||||
import androidx.compose.material.icons.outlined.AutoAwesome
|
import androidx.compose.material.icons.outlined.AutoAwesome
|
||||||
|
import androidx.compose.material.icons.outlined.AutoAwesomeMosaic
|
||||||
import androidx.compose.material.icons.outlined.AutoFixHigh
|
import androidx.compose.material.icons.outlined.AutoFixHigh
|
||||||
import androidx.compose.material.icons.outlined.Collections
|
import androidx.compose.material.icons.outlined.Collections
|
||||||
import androidx.compose.material.icons.outlined.ColorLens
|
import androidx.compose.material.icons.outlined.ColorLens
|
||||||
@ -118,6 +119,7 @@ sealed class Screen(
|
|||||||
is ColorTools -> "Color_Tools"
|
is ColorTools -> "Color_Tools"
|
||||||
is WebpTools -> "WEBP_Tools"
|
is WebpTools -> "WEBP_Tools"
|
||||||
is NoiseGeneration -> "Noise_Generation"
|
is NoiseGeneration -> "Noise_Generation"
|
||||||
|
is CollageMaker -> "Collage_Maker"
|
||||||
}
|
}
|
||||||
|
|
||||||
val icon: ImageVector?
|
val icon: ImageVector?
|
||||||
@ -159,6 +161,7 @@ sealed class Screen(
|
|||||||
ColorTools -> Icons.Outlined.ColorLens
|
ColorTools -> Icons.Outlined.ColorLens
|
||||||
is WebpTools -> Icons.Rounded.WebpBox
|
is WebpTools -> Icons.Rounded.WebpBox
|
||||||
NoiseGeneration -> Icons.Outlined.Grain
|
NoiseGeneration -> Icons.Outlined.Grain
|
||||||
|
is CollageMaker -> Icons.Outlined.AutoAwesomeMosaic
|
||||||
}
|
}
|
||||||
|
|
||||||
data object Settings : Screen(
|
data object Settings : Screen(
|
||||||
@ -707,6 +710,14 @@ sealed class Screen(
|
|||||||
subtitle = R.string.noise_generation_sub
|
subtitle = R.string.noise_generation_sub
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CollageMaker(
|
||||||
|
val uris: List<Uri>? = null
|
||||||
|
) : Screen(
|
||||||
|
id = 33,
|
||||||
|
title = R.string.collage_maker,
|
||||||
|
subtitle = R.string.collage_maker_sub
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val typedEntries by lazy {
|
val typedEntries by lazy {
|
||||||
listOf(
|
listOf(
|
||||||
@ -726,6 +737,7 @@ sealed class Screen(
|
|||||||
Filter(),
|
Filter(),
|
||||||
Draw(),
|
Draw(),
|
||||||
EraseBackground(),
|
EraseBackground(),
|
||||||
|
CollageMaker(),
|
||||||
ImageStitching(),
|
ImageStitching(),
|
||||||
ImageStacking(),
|
ImageStacking(),
|
||||||
ImageSplitting(),
|
ImageSplitting(),
|
||||||
@ -773,6 +785,6 @@ sealed class Screen(
|
|||||||
typedEntries.flatMap { it.first }.sortedBy { it.id }
|
typedEntries.flatMap { it.first }.sortedBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
const val FEATURES_COUNT = 52
|
const val FEATURES_COUNT = 53
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -40,6 +40,18 @@ fun DrawLockScreenOrientation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LockScreenOrientation() {
|
||||||
|
val activity = LocalContext.current as Activity
|
||||||
|
DisposableEffect(activity) {
|
||||||
|
val originalOrientation = activity.requestedOrientation
|
||||||
|
activity.lockOrientation()
|
||||||
|
onDispose {
|
||||||
|
activity.requestedOrientation = originalOrientation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Activity.lockOrientation() {
|
private fun Activity.lockOrientation() {
|
||||||
val display = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
val display = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
display
|
display
|
||||||
|
@ -174,6 +174,9 @@ internal fun List<Uri>.screenList(
|
|||||||
add(Screen.ImageStitching(uris))
|
add(Screen.ImageStitching(uris))
|
||||||
add(Screen.PdfTools(Screen.PdfTools.Type.ImagesToPdf(uris)))
|
add(Screen.PdfTools(Screen.PdfTools.Type.ImagesToPdf(uris)))
|
||||||
if (uris.size == 2) add(Screen.Compare(uris))
|
if (uris.size == 2) add(Screen.Compare(uris))
|
||||||
|
if (uris.size in 2..10) {
|
||||||
|
add(Screen.CollageMaker(uris))
|
||||||
|
}
|
||||||
add(Screen.GradientMaker(uris))
|
add(Screen.GradientMaker(uris))
|
||||||
add(Screen.Watermarking(uris))
|
add(Screen.Watermarking(uris))
|
||||||
add(
|
add(
|
||||||
|
1
feature/colllage-maker/.gitignore
vendored
Normal file
1
feature/colllage-maker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
12
feature/colllage-maker/build.gradle.kts
Normal file
12
feature/colllage-maker/build.gradle.kts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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.collage_maker"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.toolbox.collages)
|
||||||
|
}
|
4
feature/colllage-maker/src/main/AndroidManifest.xml
Normal file
4
feature/colllage-maker/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest>
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,544 @@
|
|||||||
|
/*
|
||||||
|
* 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.colllage_maker.presentation
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.AutoAwesomeMosaic
|
||||||
|
import androidx.compose.material.icons.rounded.FormatLineSpacing
|
||||||
|
import androidx.compose.material.icons.rounded.RoundedCorner
|
||||||
|
import androidx.compose.material.icons.rounded.Tune
|
||||||
|
import androidx.compose.material3.BottomSheetScaffold
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
|
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.t8rin.collages.Collage
|
||||||
|
import com.t8rin.collages.CollageTypeSelection
|
||||||
|
import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.tech.imageresizershrinker.colllage_maker.presentation.viewModel.CollageMakerViewModel
|
||||||
|
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.Picker
|
||||||
|
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.helper.localImagePickerMode
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.parseSaveResult
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.rememberImagePicker
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.utils.provider.ProvideContainerDefaults
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.buttons.BottomButtonsBlock
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedIconButton
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.buttons.ShareButton
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.controls.EnhancedSliderItem
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.controls.selection.BackgroundColorSelector
|
||||||
|
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.ExitWithoutSavingDialog
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.image.AutoFilePicker
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.image.ImageNotPickedWidget
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.modifier.fadingEdges
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.modifier.transparencyChecker
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.other.EnhancedTopAppBar
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.other.EnhancedTopAppBarType
|
||||||
|
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.LockScreenOrientation
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.sheets.ProcessImagesPreferenceSheet
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.sheets.SimpleSheetDefaults
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.widget.text.TopAppBarTitle
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollageMakerContent(
|
||||||
|
uriState: List<Uri>?,
|
||||||
|
onGoBack: () -> Unit,
|
||||||
|
onNavigate: (Screen) -> Unit,
|
||||||
|
viewModel: CollageMakerViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
LockScreenOrientation()
|
||||||
|
val context = LocalContext.current as ComponentActivity
|
||||||
|
val toastHostState = LocalToastHostState.current
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val confettiHostState = LocalConfettiHostState.current
|
||||||
|
val showConfetti: () -> Unit = {
|
||||||
|
scope.launch {
|
||||||
|
confettiHostState.showConfetti()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uriState) {
|
||||||
|
uriState?.takeIf { it.isNotEmpty() }?.let {
|
||||||
|
if (it.size in 2..10) {
|
||||||
|
viewModel.updateUris(it)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
toastHostState.showToast(
|
||||||
|
message = context.getString(R.string.pick_images_collage),
|
||||||
|
icon = Icons.Outlined.AutoAwesomeMosaic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pickImageLauncher = rememberImagePicker(
|
||||||
|
mode = localImagePickerMode(Picker.Multiple)
|
||||||
|
) { list ->
|
||||||
|
list.takeIf { it.isNotEmpty() }?.let {
|
||||||
|
if (list.size in 2..10) {
|
||||||
|
viewModel.updateUris(list)
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
toastHostState.showToast(
|
||||||
|
message = context.getString(R.string.pick_images_collage),
|
||||||
|
icon = Icons.Outlined.AutoAwesomeMosaic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pickImage = pickImageLauncher::pickImage
|
||||||
|
|
||||||
|
AutoFilePicker(
|
||||||
|
onAutoPick = pickImage,
|
||||||
|
isPickedAlready = !uriState.isNullOrEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
|
val saveBitmaps: (oneTimeSaveLocationUri: String?) -> Unit = {
|
||||||
|
viewModel.saveBitmap(it) { saveResult ->
|
||||||
|
context.parseSaveResult(
|
||||||
|
scope = scope,
|
||||||
|
saveResult = saveResult,
|
||||||
|
toastHostState = toastHostState,
|
||||||
|
onSuccess = showConfetti
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isPortrait by isPortraitOrientationAsState()
|
||||||
|
|
||||||
|
var showExitDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val onBack = {
|
||||||
|
if (viewModel.haveChanges) showExitDialog = true
|
||||||
|
else onGoBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||||
|
bottomSheetState = rememberStandardBottomSheetState(
|
||||||
|
confirmValueChange = {
|
||||||
|
when (it) {
|
||||||
|
SheetValue.Hidden -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val focus = LocalFocusManager.current
|
||||||
|
|
||||||
|
LaunchedEffect(scaffoldState.bottomSheetState.currentValue) {
|
||||||
|
if (scaffoldState.bottomSheetState.currentValue != SheetValue.Expanded) {
|
||||||
|
focus.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val collagePreview: @Composable () -> Unit = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.container(
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
resultPadding = 0.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Collage(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.transparencyChecker(),
|
||||||
|
images = viewModel.uris ?: emptyList(),
|
||||||
|
collageType = viewModel.collageType,
|
||||||
|
collageCreationTrigger = viewModel.collageCreationTrigger,
|
||||||
|
onCollageCreated = viewModel::updateCollageBitmap,
|
||||||
|
backgroundColor = viewModel.backgroundColor,
|
||||||
|
spacing = viewModel.spacing,
|
||||||
|
cornerRadius = viewModel.cornerRadius
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val controls: @Composable () -> Unit = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.container(
|
||||||
|
resultPadding = 0.dp,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
text = stringResource(R.string.collage_type),
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
val state = rememberLazyListState()
|
||||||
|
CollageTypeSelection(
|
||||||
|
state = state,
|
||||||
|
imagesCount = viewModel.uris?.size ?: 0,
|
||||||
|
value = viewModel.collageType,
|
||||||
|
onValueChange = viewModel::setCollageType,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(100.dp)
|
||||||
|
.fadingEdges(state),
|
||||||
|
contentPadding = PaddingValues(10.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
itemModifierFactory = { isSelected ->
|
||||||
|
Modifier
|
||||||
|
.container(
|
||||||
|
resultPadding = 0.dp,
|
||||||
|
color = animateColorAsState(
|
||||||
|
targetValue = if (isSelected) {
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
} else MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||||
|
).value,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
BackgroundColorSelector(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.container(
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
resultPadding = 0.dp
|
||||||
|
),
|
||||||
|
value = viewModel.backgroundColor,
|
||||||
|
onValueChange = viewModel::setBackgroundColor
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
EnhancedSliderItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = viewModel.spacing,
|
||||||
|
title = stringResource(R.string.spacing),
|
||||||
|
valueRange = 0f..50f,
|
||||||
|
onValueChange = viewModel::setSpacing,
|
||||||
|
sliderModifier = Modifier
|
||||||
|
.padding(
|
||||||
|
top = 14.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
),
|
||||||
|
icon = Icons.Rounded.FormatLineSpacing,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
EnhancedSliderItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = viewModel.cornerRadius,
|
||||||
|
title = stringResource(R.string.corners),
|
||||||
|
valueRange = 0f..50f,
|
||||||
|
onValueChange = viewModel::setCornerRadius,
|
||||||
|
sliderModifier = Modifier
|
||||||
|
.padding(
|
||||||
|
top = 14.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp,
|
||||||
|
bottom = 10.dp
|
||||||
|
),
|
||||||
|
icon = Icons.Rounded.RoundedCorner,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
QualitySelector(
|
||||||
|
imageFormat = viewModel.imageFormat,
|
||||||
|
quality = viewModel.quality,
|
||||||
|
onQualityChange = viewModel::setQuality
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
ImageFormatSelector(
|
||||||
|
value = viewModel.imageFormat,
|
||||||
|
onValueChange = viewModel::setImageFormat,
|
||||||
|
forceEnabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val actions: @Composable RowScope.() -> Unit = {
|
||||||
|
var editSheetData by remember {
|
||||||
|
mutableStateOf(listOf<Uri>())
|
||||||
|
}
|
||||||
|
EnhancedIconButton(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
enableAutoShadowAndBorder = false,
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) {
|
||||||
|
scaffoldState.bottomSheetState.partialExpand()
|
||||||
|
} else {
|
||||||
|
scaffoldState.bottomSheetState.expand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Tune,
|
||||||
|
contentDescription = stringResource(R.string.properties)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ShareButton(
|
||||||
|
onShare = {
|
||||||
|
viewModel.performSharing(showConfetti)
|
||||||
|
},
|
||||||
|
onCopy = { manager ->
|
||||||
|
viewModel.cacheImage { uri ->
|
||||||
|
manager.setClip(uri.asClip(context))
|
||||||
|
showConfetti()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEdit = {
|
||||||
|
viewModel.cacheImage {
|
||||||
|
editSheetData = listOf(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ProcessImagesPreferenceSheet(
|
||||||
|
uris = editSheetData,
|
||||||
|
visible = editSheetData.isNotEmpty(),
|
||||||
|
onDismiss = {
|
||||||
|
if (!it) {
|
||||||
|
editSheetData = emptyList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigate = { screen ->
|
||||||
|
scope.launch {
|
||||||
|
editSheetData = emptyList()
|
||||||
|
delay(200)
|
||||||
|
onNavigate(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val buttons: @Composable () -> Unit = {
|
||||||
|
var showFolderSelectionDialog by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
BottomButtonsBlock(
|
||||||
|
targetState = (viewModel.uris.isNullOrEmpty()) to isPortrait,
|
||||||
|
onSecondaryButtonClick = pickImage,
|
||||||
|
onPrimaryButtonClick = {
|
||||||
|
saveBitmaps(null)
|
||||||
|
},
|
||||||
|
onPrimaryButtonLongClick = {
|
||||||
|
showFolderSelectionDialog = true
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (isPortrait) actions()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (showFolderSelectionDialog) {
|
||||||
|
OneTimeSaveLocationSelectionDialog(
|
||||||
|
onDismiss = { showFolderSelectionDialog = false },
|
||||||
|
onSaveRequest = saveBitmaps,
|
||||||
|
formatForFilenameSelection = viewModel.getFormatForFilenameSelection()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val noDataControls: @Composable () -> Unit = {
|
||||||
|
if (!viewModel.isImageLoading) {
|
||||||
|
ImageNotPickedWidget(onPickImage = pickImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val topAppBar: @Composable () -> Unit = {
|
||||||
|
EnhancedTopAppBar(
|
||||||
|
title = {
|
||||||
|
TopAppBarTitle(
|
||||||
|
title = stringResource(R.string.collage_maker),
|
||||||
|
input = viewModel.uris,
|
||||||
|
isLoading = viewModel.isImageLoading,
|
||||||
|
size = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
EnhancedIconButton(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = LocalContentColor.current,
|
||||||
|
enableAutoShadowAndBorder = false,
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.exit)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type = if (viewModel.uris.isNullOrEmpty()) EnhancedTopAppBarType.Large
|
||||||
|
else EnhancedTopAppBarType.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedContent(viewModel.uris.isNullOrEmpty()) { noData ->
|
||||||
|
if (noData) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
topAppBar()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
noDataControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val settingsState = LocalSettingsState.current
|
||||||
|
if (isPortrait) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.align(settingsState.fabAlignment)
|
||||||
|
) {
|
||||||
|
buttons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BottomSheetScaffold(
|
||||||
|
sheetContent = {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight(0.6f)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures { focus.clearFocus() }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
buttons()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp)
|
||||||
|
.navigationBarsPadding()
|
||||||
|
) {
|
||||||
|
ProvideContainerDefaults(
|
||||||
|
color = SimpleSheetDefaults.contentContainerColor
|
||||||
|
) {
|
||||||
|
controls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sheetPeekHeight = 80.dp + WindowInsets.navigationBars.asPaddingValues()
|
||||||
|
.calculateBottomPadding(),
|
||||||
|
sheetDragHandle = null,
|
||||||
|
sheetShape = RectangleShape,
|
||||||
|
scaffoldState = scaffoldState
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(it)) {
|
||||||
|
topAppBar()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(20.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
collagePreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = onBack)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ExitWithoutSavingDialog(
|
||||||
|
onExit = onGoBack,
|
||||||
|
onDismiss = { showExitDialog = false },
|
||||||
|
visible = showExitDialog
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewModel.isSaving || viewModel.isImageLoading) {
|
||||||
|
LoadingDialog(
|
||||||
|
onCancelLoading = viewModel::cancelSaving
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* 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.colllage_maker.presentation.viewModel
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.t8rin.collages.CollageType
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
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.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.utils.BaseViewModel
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.utils.state.update
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class CollageMakerViewModel @Inject constructor(
|
||||||
|
private val fileController: FileController,
|
||||||
|
private val imageCompressor: ImageCompressor<Bitmap>,
|
||||||
|
private val shareProvider: ShareProvider<Bitmap>,
|
||||||
|
private val imageGetter: ImageGetter<Bitmap, ExifInterface>,
|
||||||
|
dispatchersHolder: DispatchersHolder
|
||||||
|
) : BaseViewModel(dispatchersHolder) {
|
||||||
|
|
||||||
|
private val _spacing = mutableFloatStateOf(0f)
|
||||||
|
val spacing: Float by _spacing
|
||||||
|
|
||||||
|
private val _cornerRadius = mutableFloatStateOf(0f)
|
||||||
|
val cornerRadius: Float by _cornerRadius
|
||||||
|
|
||||||
|
private val _backgroundColor = mutableStateOf(Color.Black)
|
||||||
|
val backgroundColor: Color by _backgroundColor
|
||||||
|
|
||||||
|
private val _collageCreationTrigger = mutableStateOf(false)
|
||||||
|
val collageCreationTrigger by _collageCreationTrigger
|
||||||
|
|
||||||
|
private val _collageType: MutableState<CollageType> = mutableStateOf(CollageType.Empty)
|
||||||
|
val collageType by _collageType
|
||||||
|
|
||||||
|
private val _collageBitmap = mutableStateOf<Bitmap?>(null)
|
||||||
|
private val collageBitmap by _collageBitmap
|
||||||
|
|
||||||
|
private val _uris = mutableStateOf<List<Uri>?>(null)
|
||||||
|
val uris by _uris
|
||||||
|
|
||||||
|
private val _imageFormat: MutableState<ImageFormat> = mutableStateOf(ImageFormat.Default)
|
||||||
|
val imageFormat: ImageFormat by _imageFormat
|
||||||
|
|
||||||
|
private val _quality: MutableState<Quality> = mutableStateOf(Quality.Base())
|
||||||
|
val quality: Quality by _quality
|
||||||
|
|
||||||
|
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
|
||||||
|
val isSaving: Boolean by _isSaving
|
||||||
|
|
||||||
|
private var requestedOperation: () -> Unit = {}
|
||||||
|
|
||||||
|
fun setCollageType(collageType: CollageType) {
|
||||||
|
_collageType.update { collageType }
|
||||||
|
registerChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCollageBitmap(bitmap: Bitmap) {
|
||||||
|
_collageCreationTrigger.update { false }
|
||||||
|
_collageBitmap.update { bitmap }
|
||||||
|
requestedOperation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateUris(uris: List<Uri>?) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isImageLoading.update { true }
|
||||||
|
_uris.update {
|
||||||
|
uris?.mapNotNull {
|
||||||
|
val image =
|
||||||
|
imageGetter.getImage(it, IntegerSize(2000, 2000)) ?: return@mapNotNull null
|
||||||
|
|
||||||
|
shareProvider.cacheImage(
|
||||||
|
image = image,
|
||||||
|
imageInfo = ImageInfo(
|
||||||
|
width = image.width,
|
||||||
|
height = image.height,
|
||||||
|
quality = Quality.Base(100),
|
||||||
|
imageFormat = ImageFormat.Png.Lossless
|
||||||
|
)
|
||||||
|
)?.toUri()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_isImageLoading.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setQuality(quality: Quality) {
|
||||||
|
_quality.update { quality }
|
||||||
|
registerChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setImageFormat(imageFormat: ImageFormat) {
|
||||||
|
_imageFormat.update { imageFormat }
|
||||||
|
registerChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var savingJob: Job? by smartJob {
|
||||||
|
_isSaving.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveBitmap(
|
||||||
|
oneTimeSaveLocationUri: String?,
|
||||||
|
onComplete: (SaveResult) -> Unit
|
||||||
|
) {
|
||||||
|
_isSaving.update { true }
|
||||||
|
_collageCreationTrigger.update { true }
|
||||||
|
requestedOperation = {
|
||||||
|
savingJob = viewModelScope.launch(defaultDispatcher) {
|
||||||
|
collageBitmap?.let { image ->
|
||||||
|
_isSaving.update { true }
|
||||||
|
val imageInfo = ImageInfo(
|
||||||
|
width = image.width,
|
||||||
|
height = image.height,
|
||||||
|
quality = quality,
|
||||||
|
imageFormat = imageFormat
|
||||||
|
)
|
||||||
|
val result = fileController.save(
|
||||||
|
saveTarget = ImageSaveTarget<ExifInterface>(
|
||||||
|
imageInfo = imageInfo,
|
||||||
|
originalUri = "",
|
||||||
|
sequenceNumber = null,
|
||||||
|
data = imageCompressor.compress(
|
||||||
|
image = image,
|
||||||
|
imageFormat = imageFormat,
|
||||||
|
quality = quality
|
||||||
|
)
|
||||||
|
),
|
||||||
|
keepOriginalMetadata = false,
|
||||||
|
oneTimeSaveLocationUri = oneTimeSaveLocationUri
|
||||||
|
)
|
||||||
|
|
||||||
|
onComplete(result.onSuccess(::registerSave))
|
||||||
|
_isSaving.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSharing(
|
||||||
|
onComplete: () -> Unit
|
||||||
|
) {
|
||||||
|
_isSaving.update { true }
|
||||||
|
_collageCreationTrigger.update { true }
|
||||||
|
requestedOperation = {
|
||||||
|
collageBitmap?.let { image ->
|
||||||
|
savingJob = viewModelScope.launch {
|
||||||
|
_isSaving.update { true }
|
||||||
|
shareProvider.cacheImage(
|
||||||
|
image = image,
|
||||||
|
imageInfo = ImageInfo(
|
||||||
|
width = image.width,
|
||||||
|
height = image.height,
|
||||||
|
quality = quality,
|
||||||
|
imageFormat = imageFormat
|
||||||
|
)
|
||||||
|
)?.let { uri ->
|
||||||
|
shareProvider.shareUri(
|
||||||
|
uri = uri,
|
||||||
|
onComplete = onComplete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_isSaving.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cacheImage(
|
||||||
|
onComplete: (Uri) -> Unit
|
||||||
|
) {
|
||||||
|
_isSaving.update { true }
|
||||||
|
_collageCreationTrigger.update { true }
|
||||||
|
requestedOperation = {
|
||||||
|
collageBitmap?.let { image ->
|
||||||
|
savingJob = viewModelScope.launch {
|
||||||
|
_isSaving.update { true }
|
||||||
|
shareProvider.cacheImage(
|
||||||
|
image = image,
|
||||||
|
imageInfo = ImageInfo(
|
||||||
|
width = image.width,
|
||||||
|
height = image.height,
|
||||||
|
quality = quality,
|
||||||
|
imageFormat = imageFormat
|
||||||
|
)
|
||||||
|
)?.let { uri ->
|
||||||
|
onComplete(uri.toUri())
|
||||||
|
}
|
||||||
|
_isSaving.update { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelSaving() {
|
||||||
|
savingJob?.cancel()
|
||||||
|
savingJob = null
|
||||||
|
_isSaving.update { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBackgroundColor(color: Color) {
|
||||||
|
_backgroundColor.update { color }
|
||||||
|
registerChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSpacing(value: Float) {
|
||||||
|
_spacing.update { value }
|
||||||
|
registerChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCornerRadius(value: Float) {
|
||||||
|
_cornerRadius.update { value }
|
||||||
|
registerChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFormatForFilenameSelection(): ImageFormat = imageFormat
|
||||||
|
|
||||||
|
}
|
@ -21,7 +21,6 @@ package ru.tech.imageresizershrinker.feature.draw.presentation
|
|||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@ -87,7 +86,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@ -117,9 +115,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import com.t8rin.dynamic.theme.LocalDynamicThemeState
|
import com.t8rin.dynamic.theme.LocalDynamicThemeState
|
||||||
import com.t8rin.dynamic.theme.observeAsState
|
|
||||||
import com.t8rin.dynamic.theme.rememberAppColorTuple
|
import com.t8rin.dynamic.theme.rememberAppColorTuple
|
||||||
import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
|
import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -136,11 +132,11 @@ import ru.tech.imageresizershrinker.core.ui.utils.confetti.LocalConfettiHostStat
|
|||||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.ImageUtils.restrict
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.ImageUtils.restrict
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.Picker
|
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.asClip
|
||||||
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.isPortraitOrientationAsState
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.localImagePickerMode
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.localImagePickerMode
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.parseSaveResult
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.parseSaveResult
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.helper.rememberImagePicker
|
import ru.tech.imageresizershrinker.core.ui.utils.helper.rememberImagePicker
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
|
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.provider.LocalWindowSizeClass
|
|
||||||
import ru.tech.imageresizershrinker.core.ui.utils.provider.ProvideContainerDefaults
|
import ru.tech.imageresizershrinker.core.ui.utils.provider.ProvideContainerDefaults
|
||||||
import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedButton
|
import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedButton
|
||||||
import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedFloatingActionButton
|
import ru.tech.imageresizershrinker.core.ui.widget.buttons.EnhancedFloatingActionButton
|
||||||
@ -273,17 +269,7 @@ fun DrawContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val sizeClass = LocalWindowSizeClass.current.widthSizeClass
|
val portrait by isPortraitOrientationAsState()
|
||||||
val portrait =
|
|
||||||
remember(
|
|
||||||
LocalLifecycleOwner.current.lifecycle.observeAsState().value,
|
|
||||||
sizeClass,
|
|
||||||
configuration
|
|
||||||
) {
|
|
||||||
derivedStateOf {
|
|
||||||
configuration.orientation != Configuration.ORIENTATION_LANDSCAPE || sizeClass == WindowWidthSizeClass.Compact
|
|
||||||
}
|
|
||||||
}.value
|
|
||||||
|
|
||||||
var showPickColorSheet by rememberSaveable { mutableStateOf(false) }
|
var showPickColorSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
@ -100,16 +100,7 @@ fun NoiseGenerationContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AdaptiveLayoutScreen(
|
val shareButton: @Composable () -> Unit = {
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.noise_generation),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.marquee()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onGoBack = onGoBack,
|
|
||||||
actions = {
|
|
||||||
var editSheetData by remember {
|
var editSheetData by remember {
|
||||||
mutableStateOf(listOf<Uri>())
|
mutableStateOf(listOf<Uri>())
|
||||||
}
|
}
|
||||||
@ -145,7 +136,18 @@ fun NoiseGenerationContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AdaptiveLayoutScreen(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.noise_generation),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.marquee()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
onGoBack = onGoBack,
|
||||||
|
actions = {},
|
||||||
topAppBarPersistentActions = {
|
topAppBarPersistentActions = {
|
||||||
TopAppBarEmoji()
|
TopAppBarEmoji()
|
||||||
},
|
},
|
||||||
@ -183,7 +185,8 @@ fun NoiseGenerationContent(
|
|||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
ImageFormatSelector(
|
ImageFormatSelector(
|
||||||
value = viewModel.imageFormat,
|
value = viewModel.imageFormat,
|
||||||
onValueChange = viewModel::setImageFormat
|
onValueChange = viewModel::setImageFormat,
|
||||||
|
forceEnabled = true
|
||||||
)
|
)
|
||||||
QualitySelector(
|
QualitySelector(
|
||||||
quality = viewModel.quality,
|
quality = viewModel.quality,
|
||||||
@ -206,7 +209,9 @@ fun NoiseGenerationContent(
|
|||||||
onPrimaryButtonLongClick = {
|
onPrimaryButtonLongClick = {
|
||||||
showFolderSelectionDialog = true
|
showFolderSelectionDialog = true
|
||||||
},
|
},
|
||||||
actions = it
|
actions = {
|
||||||
|
shareButton()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
if (showFolderSelectionDialog) {
|
if (showFolderSelectionDialog) {
|
||||||
OneTimeSaveLocationSelectionDialog(
|
OneTimeSaveLocationSelectionDialog(
|
||||||
|
@ -106,7 +106,7 @@ class NoiseGenerationViewModel @Inject constructor(
|
|||||||
saveTarget = ImageSaveTarget(
|
saveTarget = ImageSaveTarget(
|
||||||
imageInfo = imageInfo,
|
imageInfo = imageInfo,
|
||||||
metadata = null,
|
metadata = null,
|
||||||
originalUri = "Noise",
|
originalUri = "",
|
||||||
sequenceNumber = null,
|
sequenceNumber = null,
|
||||||
data = imageCompressor.compress(
|
data = imageCompressor.compress(
|
||||||
image = bitmap,
|
image = bitmap,
|
||||||
|
@ -61,4 +61,5 @@ dependencies {
|
|||||||
implementation(projects.feature.colorTools)
|
implementation(projects.feature.colorTools)
|
||||||
implementation(projects.feature.webpTools)
|
implementation(projects.feature.webpTools)
|
||||||
implementation(projects.feature.noiseGeneration)
|
implementation(projects.feature.noiseGeneration)
|
||||||
|
implementation(projects.feature.colllageMaker)
|
||||||
}
|
}
|
@ -29,6 +29,7 @@ import dev.olshevski.navigation.reimagined.navigate
|
|||||||
import dev.olshevski.navigation.reimagined.popUpTo
|
import dev.olshevski.navigation.reimagined.popUpTo
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.tech.imageresizershrinker.colllage_maker.presentation.CollageMakerContent
|
||||||
import ru.tech.imageresizershrinker.color_tools.presentation.ColorToolsContent
|
import ru.tech.imageresizershrinker.color_tools.presentation.ColorToolsContent
|
||||||
import ru.tech.imageresizershrinker.core.domain.utils.Lambda
|
import ru.tech.imageresizershrinker.core.domain.utils.Lambda
|
||||||
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
|
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
|
||||||
@ -408,6 +409,14 @@ internal fun ScreenSelector(
|
|||||||
onNavigate = onNavigate
|
onNavigate = onNavigate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Screen.CollageMaker -> {
|
||||||
|
CollageMakerContent(
|
||||||
|
uriState = screen.uris,
|
||||||
|
onGoBack = onGoBack,
|
||||||
|
onNavigate = onNavigate
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScreenBasedMaxBrightnessEnforcement(navController.currentDestination())
|
ScreenBasedMaxBrightnessEnforcement(navController.currentDestination())
|
||||||
|
@ -3,13 +3,13 @@ androidMinSdk = "21"
|
|||||||
androidTargetSdk = "34"
|
androidTargetSdk = "34"
|
||||||
androidCompileSdk = "34"
|
androidCompileSdk = "34"
|
||||||
|
|
||||||
versionName = "3.0.1-alpha01"
|
versionName = "3.1.0-alpha01"
|
||||||
versionCode = "153"
|
versionCode = "153"
|
||||||
|
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
compose-compiler = "1.5.15"
|
compose-compiler = "1.5.15"
|
||||||
|
|
||||||
imageToolboxLibs = "2.5.5"
|
imageToolboxLibs = "2.6.0"
|
||||||
trickle = "1.1.2"
|
trickle = "1.1.2"
|
||||||
|
|
||||||
avifCoder = "1.8.0"
|
avifCoder = "1.8.0"
|
||||||
@ -109,6 +109,7 @@ toolbox-awebp = { module = "com.github.T8RIN.ImageToolboxLibs:awebp", version.re
|
|||||||
toolbox-psd = { module = "com.github.T8RIN.ImageToolboxLibs:psd", version.ref = "imageToolboxLibs" }
|
toolbox-psd = { module = "com.github.T8RIN.ImageToolboxLibs:psd", version.ref = "imageToolboxLibs" }
|
||||||
toolbox-djvuCoder = { module = "com.github.T8RIN.ImageToolboxLibs:djvu-coder", version.ref = "imageToolboxLibs" }
|
toolbox-djvuCoder = { module = "com.github.T8RIN.ImageToolboxLibs:djvu-coder", version.ref = "imageToolboxLibs" }
|
||||||
toolbox-fastNoise = { module = "com.github.T8RIN.ImageToolboxLibs:fast-noise", version.ref = "imageToolboxLibs" }
|
toolbox-fastNoise = { module = "com.github.T8RIN.ImageToolboxLibs:fast-noise", version.ref = "imageToolboxLibs" }
|
||||||
|
toolbox-collages = { module = "com.github.T8RIN.ImageToolboxLibs:collages", version.ref = "imageToolboxLibs" }
|
||||||
|
|
||||||
|
|
||||||
aire = { module = "com.github.awxkee:aire", version.ref = "aire" }
|
aire = { module = "com.github.awxkee:aire", version.ref = "aire" }
|
||||||
|
@ -101,6 +101,7 @@ include(":feature:image-splitting")
|
|||||||
include(":feature:color-tools")
|
include(":feature:color-tools")
|
||||||
include(":feature:webp-tools")
|
include(":feature:webp-tools")
|
||||||
include(":feature:noise-generation")
|
include(":feature:noise-generation")
|
||||||
|
include(":feature:colllage-maker")
|
||||||
|
|
||||||
include(":feature:root")
|
include(":feature:root")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user