Added Collage Maker by #881

This commit is contained in:
T8RIN
2024-09-17 02:58:29 +03:00
parent 45e256c60e
commit 8bad4c4f74
17 changed files with 911 additions and 64 deletions

View File

@ -121,11 +121,8 @@ internal class AndroidFileController @Inject constructor(
}
val originalUri = saveTarget.originalUri.toUri()
val hasOriginalUri = runCatching {
context.contentResolver.openFileDescriptor(originalUri, "r")
}.isSuccess
if (settingsState.overwriteFiles && hasOriginalUri) {
if (settingsState.overwriteFiles) {
runCatching {
if (originalUri == Uri.EMPTY) throw IllegalStateException()
@ -258,7 +255,8 @@ internal class AndroidFileController @Inject constructor(
return@withContext SaveResult.Success(
message = if (savingPath.isNotEmpty()) {
val isFile = documentFile?.isDirectory != true
val isFile =
(documentFile?.isDirectory != true && oneTimeSaveLocationUri != null)
if (isFile) {
context.getString(R.string.saved_to_custom)
} else if (filename.isNotEmpty()) {

View File

@ -1406,4 +1406,8 @@
<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="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>

View File

@ -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.FilterHdr
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.Collections
import androidx.compose.material.icons.outlined.ColorLens
@ -118,6 +119,7 @@ sealed class Screen(
is ColorTools -> "Color_Tools"
is WebpTools -> "WEBP_Tools"
is NoiseGeneration -> "Noise_Generation"
is CollageMaker -> "Collage_Maker"
}
val icon: ImageVector?
@ -159,6 +161,7 @@ sealed class Screen(
ColorTools -> Icons.Outlined.ColorLens
is WebpTools -> Icons.Rounded.WebpBox
NoiseGeneration -> Icons.Outlined.Grain
is CollageMaker -> Icons.Outlined.AutoAwesomeMosaic
}
data object Settings : Screen(
@ -707,6 +710,14 @@ sealed class Screen(
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 {
val typedEntries by lazy {
listOf(
@ -726,6 +737,7 @@ sealed class Screen(
Filter(),
Draw(),
EraseBackground(),
CollageMaker(),
ImageStitching(),
ImageStacking(),
ImageSplitting(),
@ -773,6 +785,6 @@ sealed class Screen(
typedEntries.flatMap { it.first }.sortedBy { it.id }
}
const val FEATURES_COUNT = 52
const val FEATURES_COUNT = 53
}
}

View File

@ -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() {
val display = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
display

View File

@ -174,6 +174,9 @@ internal fun List<Uri>.screenList(
add(Screen.ImageStitching(uris))
add(Screen.PdfTools(Screen.PdfTools.Type.ImagesToPdf(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.Watermarking(uris))
add(

1
feature/colllage-maker/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,6 @@ package ru.tech.imageresizershrinker.feature.draw.presentation
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.activity.ComponentActivity
@ -87,7 +86,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.t8rin.dynamic.theme.LocalDynamicThemeState
import com.t8rin.dynamic.theme.observeAsState
import com.t8rin.dynamic.theme.rememberAppColorTuple
import dev.olshevski.navigation.reimagined.hilt.hiltViewModel
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.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.LocalWindowSizeClass
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.EnhancedFloatingActionButton
@ -273,17 +269,7 @@ fun DrawContent(
}
val configuration = LocalConfiguration.current
val sizeClass = LocalWindowSizeClass.current.widthSizeClass
val portrait =
remember(
LocalLifecycleOwner.current.lifecycle.observeAsState().value,
sizeClass,
configuration
) {
derivedStateOf {
configuration.orientation != Configuration.ORIENTATION_LANDSCAPE || sizeClass == WindowWidthSizeClass.Compact
}
}.value
val portrait by isPortraitOrientationAsState()
var showPickColorSheet by rememberSaveable { mutableStateOf(false) }

View File

@ -100,6 +100,44 @@ fun NoiseGenerationContent(
}
}
val shareButton: @Composable () -> Unit = {
var editSheetData by remember {
mutableStateOf(listOf<Uri>())
}
ShareButton(
onShare = {
viewModel.shareNoise(showConfetti)
},
onCopy = { manager ->
viewModel.cacheCurrentNoise { uri ->
manager.setClip(uri.asClip(context))
showConfetti()
}
},
onEdit = {
viewModel.cacheCurrentNoise {
editSheetData = listOf(it)
}
}
)
ProcessImagesPreferenceSheet(
uris = editSheetData,
visible = editSheetData.isNotEmpty(),
onDismiss = {
if (!it) {
editSheetData = emptyList()
}
},
onNavigate = { screen ->
scope.launch {
editSheetData = emptyList()
delay(200)
onNavigate(screen)
}
}
)
}
AdaptiveLayoutScreen(
title = {
Text(
@ -109,43 +147,7 @@ fun NoiseGenerationContent(
)
},
onGoBack = onGoBack,
actions = {
var editSheetData by remember {
mutableStateOf(listOf<Uri>())
}
ShareButton(
onShare = {
viewModel.shareNoise(showConfetti)
},
onCopy = { manager ->
viewModel.cacheCurrentNoise { uri ->
manager.setClip(uri.asClip(context))
showConfetti()
}
},
onEdit = {
viewModel.cacheCurrentNoise {
editSheetData = listOf(it)
}
}
)
ProcessImagesPreferenceSheet(
uris = editSheetData,
visible = editSheetData.isNotEmpty(),
onDismiss = {
if (!it) {
editSheetData = emptyList()
}
},
onNavigate = { screen ->
scope.launch {
editSheetData = emptyList()
delay(200)
onNavigate(screen)
}
}
)
},
actions = {},
topAppBarPersistentActions = {
TopAppBarEmoji()
},
@ -183,7 +185,8 @@ fun NoiseGenerationContent(
Spacer(Modifier.height(4.dp))
ImageFormatSelector(
value = viewModel.imageFormat,
onValueChange = viewModel::setImageFormat
onValueChange = viewModel::setImageFormat,
forceEnabled = true
)
QualitySelector(
quality = viewModel.quality,
@ -206,7 +209,9 @@ fun NoiseGenerationContent(
onPrimaryButtonLongClick = {
showFolderSelectionDialog = true
},
actions = it
actions = {
shareButton()
}
)
if (showFolderSelectionDialog) {
OneTimeSaveLocationSelectionDialog(

View File

@ -106,7 +106,7 @@ class NoiseGenerationViewModel @Inject constructor(
saveTarget = ImageSaveTarget(
imageInfo = imageInfo,
metadata = null,
originalUri = "Noise",
originalUri = "",
sequenceNumber = null,
data = imageCompressor.compress(
image = bitmap,

View File

@ -61,4 +61,5 @@ dependencies {
implementation(projects.feature.colorTools)
implementation(projects.feature.webpTools)
implementation(projects.feature.noiseGeneration)
implementation(projects.feature.colllageMaker)
}

View File

@ -29,6 +29,7 @@ import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.popUpTo
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ru.tech.imageresizershrinker.colllage_maker.presentation.CollageMakerContent
import ru.tech.imageresizershrinker.color_tools.presentation.ColorToolsContent
import ru.tech.imageresizershrinker.core.domain.utils.Lambda
import ru.tech.imageresizershrinker.core.settings.presentation.provider.LocalSettingsState
@ -408,6 +409,14 @@ internal fun ScreenSelector(
onNavigate = onNavigate
)
}
is Screen.CollageMaker -> {
CollageMakerContent(
uriState = screen.uris,
onGoBack = onGoBack,
onNavigate = onNavigate
)
}
}
}
ScreenBasedMaxBrightnessEnforcement(navController.currentDestination())

View File

@ -3,13 +3,13 @@ androidMinSdk = "21"
androidTargetSdk = "34"
androidCompileSdk = "34"
versionName = "3.0.1-alpha01"
versionName = "3.1.0-alpha01"
versionCode = "153"
jvmTarget = "17"
compose-compiler = "1.5.15"
imageToolboxLibs = "2.5.5"
imageToolboxLibs = "2.6.0"
trickle = "1.1.2"
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-djvuCoder = { module = "com.github.T8RIN.ImageToolboxLibs:djvu-coder", 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" }

View File

@ -101,6 +101,7 @@ include(":feature:image-splitting")
include(":feature:color-tools")
include(":feature:webp-tools")
include(":feature:noise-generation")
include(":feature:colllage-maker")
include(":feature:root")