chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.image.toolbox.library)
|
||||
alias(libs.plugins.image.toolbox.feature)
|
||||
alias(libs.plugins.image.toolbox.hilt)
|
||||
alias(libs.plugins.image.toolbox.compose)
|
||||
}
|
||||
|
||||
android.namespace = "com.t8rin.imagetoolbox.feature.format_conversion"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.feature.compare)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ 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>.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.format_conversion.presentation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.Picker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberImagePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.Clipboard
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.isPortraitOrientationAsState
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.AdaptiveLayoutScreen
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.BottomButtonsBlock
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.CompareButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.ShareButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.buttons.ZoomButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.SaveExifWidget
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.UndoRedoButtons
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.ImageFormatSelector
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.controls.selection.QualitySelector
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.ExitWithoutSavingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.LoadingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.OneTimeImagePickingDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.dialogs.OneTimeSaveLocationSelectionDialog
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.enhancedHorizontalScroll
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.AutoFilePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.ImageContainer
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.ImageCounter
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.ImageNotPickedWidget
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.detectSwipes
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.fadingEdges
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.TopAppBarEmoji
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.sheets.PickImageFromUrisSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.sheets.ProcessImagesPreferenceSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.sheets.ZoomModalSheet
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.TopAppBarTitle
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.utils.AutoContentBasedColors
|
||||
import com.t8rin.imagetoolbox.core.utils.fileSize
|
||||
import com.t8rin.imagetoolbox.feature.compare.presentation.components.CompareSheet
|
||||
import com.t8rin.imagetoolbox.feature.format_conversion.presentation.screenLogic.FormatConversionComponent
|
||||
|
||||
@Composable
|
||||
fun FormatConversionContent(
|
||||
component: FormatConversionComponent
|
||||
) {
|
||||
AutoContentBasedColors(component.bitmap)
|
||||
|
||||
val imagePicker = rememberImagePicker { uris: List<Uri> ->
|
||||
component.setUris(
|
||||
uris = uris
|
||||
)
|
||||
}
|
||||
|
||||
val pickImage = imagePicker::pickImage
|
||||
|
||||
AutoFilePicker(
|
||||
onAutoPick = pickImage,
|
||||
isPickedAlready = !component.initialUris.isNullOrEmpty()
|
||||
)
|
||||
|
||||
val saveBitmaps: (oneTimeSaveLocationUri: String?) -> Unit = {
|
||||
component.saveBitmaps(
|
||||
oneTimeSaveLocationUri = it
|
||||
)
|
||||
}
|
||||
|
||||
var showPickImageFromUrisSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
var showExitDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onBack = {
|
||||
if (component.haveChanges) showExitDialog = true
|
||||
else component.onGoBack()
|
||||
}
|
||||
|
||||
var showZoomSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var showCompareSheet by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
CompareSheet(
|
||||
data = component.bitmap to component.previewBitmap,
|
||||
visible = showCompareSheet,
|
||||
onDismiss = {
|
||||
showCompareSheet = false
|
||||
}
|
||||
)
|
||||
|
||||
ZoomModalSheet(
|
||||
data = component.previewBitmap,
|
||||
visible = showZoomSheet,
|
||||
onDismiss = {
|
||||
showZoomSheet = false
|
||||
}
|
||||
)
|
||||
|
||||
AdaptiveLayoutScreen(
|
||||
shouldDisableBackHandler = !component.haveChanges,
|
||||
title = {
|
||||
TopAppBarTitle(
|
||||
title = stringResource(R.string.format_conversion),
|
||||
input = component.bitmap,
|
||||
isLoading = component.isImageLoading,
|
||||
size = component.imageInfo.sizeInBytes.toLong(),
|
||||
originalSize = component.selectedUri?.fileSize()
|
||||
)
|
||||
},
|
||||
onGoBack = onBack,
|
||||
actions = {
|
||||
val state = rememberScrollState()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fadingEdges(state)
|
||||
.enhancedHorizontalScroll(state),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (!isPortrait) {
|
||||
UndoRedoButtons(
|
||||
canUndo = component.canUndo,
|
||||
canRedo = component.canRedo,
|
||||
onUndo = component::undo,
|
||||
onRedo = component::redo,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
)
|
||||
}
|
||||
var editSheetData by remember {
|
||||
mutableStateOf(listOf<Uri>())
|
||||
}
|
||||
ShareButton(
|
||||
enabled = component.bitmap != null,
|
||||
onShare = component::shareBitmaps,
|
||||
onCopy = {
|
||||
component.cacheCurrentImage(Clipboard::copy)
|
||||
},
|
||||
onEdit = {
|
||||
component.cacheImages {
|
||||
editSheetData = it
|
||||
}
|
||||
}
|
||||
)
|
||||
ProcessImagesPreferenceSheet(
|
||||
uris = editSheetData,
|
||||
visible = editSheetData.isNotEmpty(),
|
||||
onDismiss = {
|
||||
editSheetData = emptyList()
|
||||
},
|
||||
onNavigate = component.onNavigate
|
||||
)
|
||||
if (isPortrait) {
|
||||
UndoRedoButtons(
|
||||
canUndo = component.canUndo,
|
||||
canRedo = component.canRedo,
|
||||
onUndo = component::undo,
|
||||
onRedo = component::redo,
|
||||
modifier = Modifier.padding(2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
imagePreview = {
|
||||
ImageContainer(
|
||||
modifier = Modifier
|
||||
.detectSwipes(
|
||||
onSwipeRight = component::selectLeftUri,
|
||||
onSwipeLeft = component::selectRightUri
|
||||
),
|
||||
imageInside = isPortrait,
|
||||
showOriginal = false,
|
||||
previewBitmap = component.previewBitmap,
|
||||
originalBitmap = component.bitmap,
|
||||
isLoading = component.isImageLoading,
|
||||
shouldShowPreview = component.shouldShowPreview
|
||||
)
|
||||
},
|
||||
controls = {
|
||||
val imageInfo = component.imageInfo
|
||||
ImageCounter(
|
||||
imageCount = component.uris?.size?.takeIf { it > 1 },
|
||||
onRepick = {
|
||||
showPickImageFromUrisSheet = true
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SaveExifWidget(
|
||||
imageFormat = component.imageInfo.imageFormat,
|
||||
checked = component.keepExif,
|
||||
onCheckedChange = component::setKeepExif
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
ImageFormatSelector(
|
||||
value = imageInfo.imageFormat,
|
||||
onValueChange = component::setImageFormat,
|
||||
quality = imageInfo.quality
|
||||
)
|
||||
if (imageInfo.imageFormat.canChangeCompressionValue) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
QualitySelector(
|
||||
imageFormat = imageInfo.imageFormat,
|
||||
quality = imageInfo.quality,
|
||||
onQualityChange = component::setQuality
|
||||
)
|
||||
},
|
||||
buttons = { actions ->
|
||||
var showFolderSelectionDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showOneTimeImagePickingDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
BottomButtonsBlock(
|
||||
isNoData = component.uris.isNullOrEmpty(),
|
||||
onSecondaryButtonClick = pickImage,
|
||||
onPrimaryButtonClick = {
|
||||
saveBitmaps(null)
|
||||
},
|
||||
onPrimaryButtonLongClick = {
|
||||
showFolderSelectionDialog = true
|
||||
},
|
||||
actions = {
|
||||
if (isPortrait) actions()
|
||||
},
|
||||
onSecondaryButtonLongClick = {
|
||||
showOneTimeImagePickingDialog = true
|
||||
}
|
||||
)
|
||||
OneTimeSaveLocationSelectionDialog(
|
||||
visible = showFolderSelectionDialog,
|
||||
onDismiss = { showFolderSelectionDialog = false },
|
||||
onSaveRequest = saveBitmaps,
|
||||
formatForFilenameSelection = component.getFormatForFilenameSelection()
|
||||
)
|
||||
OneTimeImagePickingDialog(
|
||||
onDismiss = { showOneTimeImagePickingDialog = false },
|
||||
picker = Picker.Multiple,
|
||||
imagePicker = imagePicker,
|
||||
visible = showOneTimeImagePickingDialog
|
||||
)
|
||||
},
|
||||
topAppBarPersistentActions = {
|
||||
if (component.bitmap == null) TopAppBarEmoji()
|
||||
CompareButton(
|
||||
onClick = { showCompareSheet = true },
|
||||
visible = component.previewBitmap != null
|
||||
&& component.bitmap != null
|
||||
&& component.shouldShowPreview
|
||||
)
|
||||
ZoomButton(
|
||||
onClick = { showZoomSheet = true },
|
||||
visible = component.previewBitmap != null && component.shouldShowPreview
|
||||
)
|
||||
},
|
||||
canShowScreenData = component.bitmap != null,
|
||||
forceImagePreviewToMax = false,
|
||||
noDataControls = {
|
||||
if (!component.isImageLoading) {
|
||||
ImageNotPickedWidget(onPickImage = pickImage)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val transformations by remember(component.imageInfo) {
|
||||
derivedStateOf(component::getConversionTransformation)
|
||||
}
|
||||
|
||||
PickImageFromUrisSheet(
|
||||
transformations = transformations,
|
||||
visible = showPickImageFromUrisSheet,
|
||||
onDismiss = {
|
||||
showPickImageFromUrisSheet = false
|
||||
},
|
||||
uris = component.uris,
|
||||
selectedUri = component.selectedUri,
|
||||
onUriPicked = component::updateSelectedUri,
|
||||
onUriRemoved = component::updateUrisSilently,
|
||||
columns = if (isPortrait) 2 else 4,
|
||||
)
|
||||
|
||||
ExitWithoutSavingDialog(
|
||||
onExit = component.onGoBack,
|
||||
onDismiss = { showExitDialog = false },
|
||||
visible = showExitDialog
|
||||
)
|
||||
|
||||
LoadingDialog(
|
||||
visible = component.isSaving,
|
||||
done = component.done,
|
||||
left = component.uris?.size ?: 1,
|
||||
onCancelLoading = component::cancelSaving
|
||||
)
|
||||
}
|
||||
+552
@@ -0,0 +1,552 @@
|
||||
/*
|
||||
* ImageToolbox is an image editor for android
|
||||
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* You should have received a copy of the Apache License
|
||||
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
*/
|
||||
|
||||
package com.t8rin.imagetoolbox.feature.format_conversion.presentation.screenLogic
|
||||
|
||||
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 com.arkivanov.decompose.ComponentContext
|
||||
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImagePreviewCreator
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageShareProvider
|
||||
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageData
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.Preset
|
||||
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
|
||||
import com.t8rin.imagetoolbox.core.domain.model.ColorModel
|
||||
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.FileController
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.model.ImageSaveTarget
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.model.SaveResult
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.model.onSuccess
|
||||
import com.t8rin.imagetoolbox.core.domain.saving.updateProgress
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.ListUtils.leftFrom
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.ListUtils.rightFrom
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.smartJob
|
||||
import com.t8rin.imagetoolbox.core.settings.domain.SettingsManager
|
||||
import com.t8rin.imagetoolbox.core.ui.transformation.ImageInfoTransformation
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.BaseHistoryComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppToastHost
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.navigation.Screen
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.state.update
|
||||
import com.t8rin.imagetoolbox.feature.format_conversion.presentation.screenLogic.FormatConversionComponent.HistorySnapshot
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
class FormatConversionComponent @AssistedInject internal constructor(
|
||||
@Assisted componentContext: ComponentContext,
|
||||
@Assisted val initialUris: List<Uri>?,
|
||||
@Assisted val onGoBack: () -> Unit,
|
||||
@Assisted val onNavigate: (Screen) -> Unit,
|
||||
private val fileController: FileController,
|
||||
private val imageTransformer: ImageTransformer<Bitmap>,
|
||||
private val imagePreviewCreator: ImagePreviewCreator<Bitmap>,
|
||||
private val imageCompressor: ImageCompressor<Bitmap>,
|
||||
private val imageGetter: ImageGetter<Bitmap>,
|
||||
private val imageScaler: ImageScaler<Bitmap>,
|
||||
private val shareProvider: ImageShareProvider<Bitmap>,
|
||||
private val imageInfoTransformationFactory: ImageInfoTransformation.Factory,
|
||||
private val settingsManager: SettingsManager,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : BaseHistoryComponent<HistorySnapshot>(
|
||||
dispatchersHolder = dispatchersHolder,
|
||||
componentContext = componentContext
|
||||
) {
|
||||
|
||||
init {
|
||||
debounce {
|
||||
initialUris?.let(::setUris)
|
||||
}
|
||||
}
|
||||
|
||||
private val _originalSize: MutableState<IntegerSize?> = mutableStateOf(null)
|
||||
|
||||
private val _uris = mutableStateOf<List<Uri>?>(null)
|
||||
val uris by _uris
|
||||
|
||||
private val _bitmap: MutableState<Bitmap?> = mutableStateOf(null)
|
||||
val bitmap: Bitmap? by _bitmap
|
||||
|
||||
private val _keepExif = mutableStateOf(false)
|
||||
val keepExif by _keepExif
|
||||
|
||||
private val _imageInfo: MutableState<ImageInfo> = mutableStateOf(ImageInfo())
|
||||
val imageInfo: ImageInfo by _imageInfo
|
||||
|
||||
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
|
||||
val isSaving: Boolean by _isSaving
|
||||
|
||||
private val _shouldShowPreview: MutableState<Boolean> = mutableStateOf(true)
|
||||
val shouldShowPreview by _shouldShowPreview
|
||||
|
||||
private val _previewBitmap: MutableState<Bitmap?> = mutableStateOf(null)
|
||||
val previewBitmap: Bitmap? by _previewBitmap
|
||||
|
||||
private val _done: MutableState<Int> = mutableIntStateOf(0)
|
||||
val done by _done
|
||||
|
||||
private val _selectedUri: MutableState<Uri?> = mutableStateOf(null)
|
||||
val selectedUri by _selectedUri
|
||||
|
||||
private var job: Job? by smartJob {
|
||||
_isImageLoading.update { false }
|
||||
}
|
||||
|
||||
fun setUris(
|
||||
uris: List<Uri>?
|
||||
) {
|
||||
clearHistory()
|
||||
registerChangesCleared()
|
||||
_uris.value = null
|
||||
_uris.value = uris
|
||||
_selectedUri.value = uris?.firstOrNull()
|
||||
uris?.firstOrNull()?.let { uri ->
|
||||
_imageInfo.update {
|
||||
it.copy(originalUri = uri.toString())
|
||||
}
|
||||
imageGetter.getImageAsync(
|
||||
uri = uri.toString(),
|
||||
originalSize = false,
|
||||
onGetImage = ::setImageData,
|
||||
onFailure = AppToastHost::showFailureToast
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUrisSilently(
|
||||
removedUri: Uri
|
||||
) {
|
||||
componentScope.launch {
|
||||
_uris.value = uris
|
||||
if (_selectedUri.value == removedUri) {
|
||||
val index = uris?.indexOf(removedUri) ?: -1
|
||||
if (index == 0) {
|
||||
uris?.getOrNull(1)?.let {
|
||||
updateSelectedUri(it)
|
||||
}
|
||||
} else {
|
||||
uris?.getOrNull(index - 1)?.let {
|
||||
updateSelectedUri(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
val u = _uris.value?.toMutableList()?.apply {
|
||||
remove(removedUri)
|
||||
}
|
||||
_uris.value = u
|
||||
|
||||
registerChanges()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkBitmapAndUpdate(
|
||||
clearPreview: Boolean = true
|
||||
) {
|
||||
_bitmap.value?.let { bmp ->
|
||||
val preview = updatePreview(bmp)
|
||||
if (clearPreview) {
|
||||
_previewBitmap.value = null
|
||||
}
|
||||
_shouldShowPreview.value = imagePreviewCreator.canShow(preview)
|
||||
if (shouldShowPreview) _previewBitmap.value = preview
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updatePreview(
|
||||
bitmap: Bitmap
|
||||
): Bitmap? = imagePreviewCreator.createPreview(
|
||||
image = bitmap,
|
||||
imageInfo = imageInfo,
|
||||
onGetByteCount = { size ->
|
||||
_imageInfo.update { it.copy(sizeInBytes = size) }
|
||||
}
|
||||
)
|
||||
|
||||
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<Bitmap>) {
|
||||
job = componentScope.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()
|
||||
resetHistory()
|
||||
registerChangesCleared()
|
||||
_isImageLoading.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
fun setQuality(quality: Quality) {
|
||||
val coercedQuality = quality.coerceIn(imageInfo.imageFormat)
|
||||
if (_imageInfo.value.quality != coercedQuality) {
|
||||
beginPendingHistoryTransaction()
|
||||
_imageInfo.value = _imageInfo.value.copy(quality = coercedQuality)
|
||||
debouncedImageCalculation {
|
||||
checkBitmapAndUpdate()
|
||||
}
|
||||
registerChanges()
|
||||
schedulePendingHistoryCommit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setImageFormat(imageFormat: ImageFormat) {
|
||||
if (_imageInfo.value.imageFormat != imageFormat) {
|
||||
if (pendingHistoryMode != PendingHistoryMode.FormatChange) {
|
||||
finalizePendingHistoryTransaction()
|
||||
}
|
||||
beginPendingHistoryTransaction(
|
||||
mode = PendingHistoryMode.FormatChange,
|
||||
commitDelayMillis = formatHistoryTransactionDebounce
|
||||
)
|
||||
_imageInfo.value = _imageInfo.value.copy(
|
||||
imageFormat = imageFormat,
|
||||
quality = imageInfo.quality.coerceIn(imageFormat)
|
||||
)
|
||||
debouncedImageCalculation {
|
||||
checkBitmapAndUpdate()
|
||||
}
|
||||
registerChanges()
|
||||
schedulePendingHistoryCommit()
|
||||
}
|
||||
}
|
||||
|
||||
fun setKeepExif(boolean: Boolean) {
|
||||
if (_keepExif.value == boolean) return
|
||||
finalizePendingHistoryTransaction()
|
||||
val beforeSnapshot = currentHistorySnapshot()
|
||||
_keepExif.value = boolean
|
||||
commitHistoryFrom(beforeSnapshot)
|
||||
}
|
||||
|
||||
private var savingJob: Job? by smartJob {
|
||||
_isSaving.update { false }
|
||||
}
|
||||
|
||||
fun saveBitmaps(
|
||||
oneTimeSaveLocationUri: String?
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
val results = mutableListOf<SaveResult>()
|
||||
_done.value = 0
|
||||
uris?.forEach { uri ->
|
||||
runSuspendCatching {
|
||||
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
|
||||
),
|
||||
canSkipIfLarger = true
|
||||
),
|
||||
keepOriginalMetadata = keepExif,
|
||||
oneTimeSaveLocationUri = oneTimeSaveLocationUri
|
||||
)
|
||||
)
|
||||
}
|
||||
} ?: results.add(
|
||||
SaveResult.Error.Exception(Throwable())
|
||||
)
|
||||
|
||||
_done.value += 1
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = uris.orEmpty().size
|
||||
)
|
||||
}
|
||||
parseSaveResults(results.onSuccess(::registerSave))
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelectedUri(
|
||||
uri: Uri
|
||||
) {
|
||||
_selectedUri.value = uri
|
||||
componentScope.launch {
|
||||
runSuspendCatching {
|
||||
_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 }
|
||||
AppToastHost.showFailureToast(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shareBitmaps() {
|
||||
savingJob = trackProgress {
|
||||
_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) {
|
||||
AppToastHost.showConfetti()
|
||||
_isSaving.value = false
|
||||
_done.value = 0
|
||||
} else {
|
||||
_done.value = it
|
||||
}
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = uris.orEmpty().size
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSaving() {
|
||||
savingJob?.cancel()
|
||||
savingJob = null
|
||||
_isSaving.value = false
|
||||
}
|
||||
|
||||
fun cacheCurrentImage(onComplete: (Uri) -> Unit) {
|
||||
savingJob = trackProgress {
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
||||
fun cacheImages(
|
||||
onComplete: (List<Uri>) -> Unit
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
_done.value = 0
|
||||
val list = mutableListOf<Uri>()
|
||||
uris?.forEach {
|
||||
imageGetter.getImage(it.toString())?.image?.let { bmp ->
|
||||
bmp to imageInfo.copy(
|
||||
originalUri = it.toString()
|
||||
).let { info ->
|
||||
imageTransformer.applyPresetBy(
|
||||
image = bitmap,
|
||||
preset = Preset.Original,
|
||||
currentInfo = info
|
||||
)
|
||||
}
|
||||
}?.let { (image, imageInfo) ->
|
||||
shareProvider.cacheImage(
|
||||
image = image,
|
||||
imageInfo = imageInfo.copy(originalUri = it.toString())
|
||||
)?.let { uri ->
|
||||
list.add(uri.toUri())
|
||||
}
|
||||
}
|
||||
_done.value += 1
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = uris.orEmpty().size
|
||||
)
|
||||
}
|
||||
onComplete(list)
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun selectLeftUri() {
|
||||
uris
|
||||
?.indexOf(selectedUri ?: Uri.EMPTY)
|
||||
?.takeIf { it >= 0 }
|
||||
?.let {
|
||||
uris?.leftFrom(it)
|
||||
}
|
||||
?.let(::updateSelectedUri)
|
||||
}
|
||||
|
||||
fun selectRightUri() {
|
||||
uris
|
||||
?.indexOf(selectedUri ?: Uri.EMPTY)
|
||||
?.takeIf { it >= 0 }
|
||||
?.let {
|
||||
uris?.rightFrom(it)
|
||||
}
|
||||
?.let(::updateSelectedUri)
|
||||
}
|
||||
|
||||
fun getFormatForFilenameSelection(): ImageFormat? =
|
||||
if (uris?.size == 1) imageInfo.imageFormat
|
||||
else null
|
||||
|
||||
fun getConversionTransformation() = listOf(
|
||||
imageInfoTransformationFactory(
|
||||
imageInfo = imageInfo,
|
||||
preset = Preset.Original
|
||||
)
|
||||
)
|
||||
|
||||
override fun currentHistorySnapshot(): HistorySnapshot = HistorySnapshot(
|
||||
imageInfo = imageInfo.asHistoryImageInfo(),
|
||||
keepExif = keepExif,
|
||||
backgroundColorForNoAlphaFormats = settingsManager
|
||||
.settingsState
|
||||
.value
|
||||
.backgroundForNoAlphaImageFormats
|
||||
)
|
||||
|
||||
override fun applyHistorySnapshot(snapshot: HistorySnapshot) {
|
||||
_imageInfo.update {
|
||||
it.copy(
|
||||
imageFormat = snapshot.imageInfo.imageFormat,
|
||||
quality = snapshot.imageInfo.quality
|
||||
)
|
||||
}
|
||||
_keepExif.value = snapshot.keepExif
|
||||
restoreBackgroundColorForNoAlphaFormats(
|
||||
settingsManager = settingsManager,
|
||||
backgroundColorForNoAlphaFormats = snapshot.backgroundColorForNoAlphaFormats
|
||||
)
|
||||
debouncedImageCalculation {
|
||||
bitmap?.let {
|
||||
checkBitmapAndUpdate(clearPreview = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasSameUndoState(
|
||||
first: HistorySnapshot,
|
||||
second: HistorySnapshot
|
||||
): Boolean = first.normalized() == second.normalized()
|
||||
|
||||
private fun ImageInfo.asHistoryImageInfo(): ImageInfo = ImageInfo(
|
||||
imageFormat = imageFormat,
|
||||
quality = quality
|
||||
)
|
||||
|
||||
private fun HistorySnapshot.normalized(): HistorySnapshot = copy(
|
||||
imageInfo = imageInfo.asHistoryImageInfo()
|
||||
)
|
||||
|
||||
data class HistorySnapshot(
|
||||
val imageInfo: ImageInfo = ImageInfo(),
|
||||
val keepExif: Boolean = false,
|
||||
val backgroundColorForNoAlphaFormats: ColorModel = ColorModel(-0x1000000)
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
operator fun invoke(
|
||||
componentContext: ComponentContext,
|
||||
initialUris: List<Uri>?,
|
||||
onGoBack: () -> Unit,
|
||||
onNavigate: (Screen) -> Unit,
|
||||
): FormatConversionComponent
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user