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.document_scanner"
|
||||
|
||||
dependencies {
|
||||
implementation(projects.feature.pdfTools)
|
||||
}
|
||||
@@ -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>
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* 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.document_scanner.presentation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.t8rin.imagetoolbox.core.domain.model.MimeType
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.Flavor
|
||||
import com.t8rin.imagetoolbox.core.resources.Icons
|
||||
import com.t8rin.imagetoolbox.core.resources.R
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.AddPhotoAlt
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.DocumentScanner
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Pdf
|
||||
import com.t8rin.imagetoolbox.core.resources.icons.Share
|
||||
import com.t8rin.imagetoolbox.core.ui.theme.takeColorFromScheme
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.Picker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberDocumentScanner
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberFileCreator
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.content_pickers.rememberImagePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ScanResult
|
||||
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.ShareButton
|
||||
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.EnhancedButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedFloatingActionButton
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.AutoFilePicker
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.FileNotPickedWidget
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.ImagePager
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.UrisPreview
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.image.urisPreview
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.InfoContainer
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.other.TopAppBarEmoji
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
|
||||
import com.t8rin.imagetoolbox.core.ui.widget.text.marquee
|
||||
import com.t8rin.imagetoolbox.feature.document_scanner.presentation.screenLogic.DocumentScannerComponent
|
||||
|
||||
|
||||
@Composable
|
||||
fun DocumentScannerContent(
|
||||
component: DocumentScannerComponent
|
||||
) {
|
||||
var showExitDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val savePdfLauncher = rememberFileCreator(
|
||||
mimeType = MimeType.Pdf,
|
||||
onSuccess = component::savePdfTo
|
||||
)
|
||||
|
||||
val documentScanner = rememberDocumentScanner {
|
||||
if (Flavor.isFoss()) {
|
||||
component.addScanResult(it)
|
||||
} else {
|
||||
component.parseScanResult(it)
|
||||
}
|
||||
}
|
||||
|
||||
val additionalDocumentScanner = rememberDocumentScanner {
|
||||
component.addScanResult(it)
|
||||
}
|
||||
|
||||
AutoFilePicker(
|
||||
onAutoPick = documentScanner::scan,
|
||||
isPickedAlready = false
|
||||
)
|
||||
|
||||
val isPortrait by isPortraitOrientationAsState()
|
||||
|
||||
val saveBitmaps: (oneTimeSaveLocationUri: String?) -> Unit = {
|
||||
component.saveBitmaps(
|
||||
oneTimeSaveLocationUri = it
|
||||
)
|
||||
}
|
||||
|
||||
val addImagesPicker = rememberImagePicker { uris: List<Uri> ->
|
||||
component.addScanResult(
|
||||
ScanResult(
|
||||
imageUris = uris
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
var selectedUriForPreview by remember {
|
||||
mutableStateOf<Uri?>(null)
|
||||
}
|
||||
|
||||
val previewBlock = @Composable {
|
||||
UrisPreview(
|
||||
modifier = Modifier.urisPreview(isPortrait = isPortrait),
|
||||
uris = component.uris,
|
||||
isPortrait = true,
|
||||
onRemoveUri = component::removeImageUri,
|
||||
onAddUris = additionalDocumentScanner::scan,
|
||||
isAddUrisVisible = !Flavor.isFoss(),
|
||||
onClickUri = { uri ->
|
||||
selectedUriForPreview = uri
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AdaptiveLayoutScreen(
|
||||
shouldDisableBackHandler = false,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.document_scanner),
|
||||
modifier = Modifier.marquee()
|
||||
)
|
||||
},
|
||||
topAppBarPersistentActions = {
|
||||
TopAppBarEmoji()
|
||||
},
|
||||
onGoBack = {
|
||||
showExitDialog = true
|
||||
},
|
||||
actions = {},
|
||||
imagePreview = {
|
||||
if (!isPortrait) previewBlock()
|
||||
},
|
||||
showImagePreviewAsStickyHeader = false,
|
||||
addHorizontalCutoutPaddingIfNoPreview = false,
|
||||
noDataControls = {
|
||||
FileNotPickedWidget(
|
||||
onPickFile = documentScanner::scan,
|
||||
text = stringResource(R.string.click_to_start_scanning)
|
||||
)
|
||||
},
|
||||
controls = {
|
||||
if (isPortrait) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
previewBlock()
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.container(
|
||||
resultPadding = 0.dp,
|
||||
shape = ShapeDefaults.large,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
EnhancedButton(
|
||||
onClick = component::sharePdf,
|
||||
containerColor = MaterialTheme.colorScheme.secondary,
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 8.dp,
|
||||
start = 12.dp,
|
||||
end = 20.dp
|
||||
),
|
||||
shape = ShapeDefaults.top,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Share,
|
||||
contentDescription = stringResource(R.string.share_as_pdf)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
AutoSizeText(
|
||||
text = stringResource(id = R.string.share_as_pdf),
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
EnhancedButton(
|
||||
onClick = {
|
||||
savePdfLauncher.make(component.generatePdfFilename())
|
||||
},
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 8.dp,
|
||||
start = 16.dp,
|
||||
end = 20.dp
|
||||
),
|
||||
shape = ShapeDefaults.bottom,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Pdf,
|
||||
contentDescription = stringResource(R.string.save_as_pdf)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
AutoSizeText(
|
||||
text = stringResource(id = R.string.save_as_pdf),
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
InfoContainer(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = stringResource(R.string.options_below_is_for_images)
|
||||
)
|
||||
if (component.imageFormat.canChangeCompressionValue) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
QualitySelector(
|
||||
imageFormat = component.imageFormat,
|
||||
quality = component.quality,
|
||||
onQualityChange = component::setQuality
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
ImageFormatSelector(
|
||||
value = component.imageFormat,
|
||||
onValueChange = component::setImageFormat,
|
||||
quality = component.quality,
|
||||
)
|
||||
},
|
||||
buttons = {
|
||||
var showFolderSelectionDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showOneTimeImagePickingDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
BottomButtonsBlock(
|
||||
isNoData = component.uris.isEmpty(),
|
||||
onSecondaryButtonClick = documentScanner::scan,
|
||||
secondaryButtonIcon = Icons.Rounded.DocumentScanner,
|
||||
secondaryButtonText = stringResource(R.string.start_scanning),
|
||||
onPrimaryButtonClick = {
|
||||
saveBitmaps(null)
|
||||
},
|
||||
onPrimaryButtonLongClick = {
|
||||
showFolderSelectionDialog = true
|
||||
},
|
||||
actions = {
|
||||
ShareButton(
|
||||
enabled = component.uris.isNotEmpty(),
|
||||
onShare = component::shareBitmaps
|
||||
)
|
||||
},
|
||||
showMiddleFabInRow = true,
|
||||
middleFab = if (Flavor.isFoss()) {
|
||||
{
|
||||
EnhancedFloatingActionButton(
|
||||
onClick = addImagesPicker::pickImage,
|
||||
onLongClick = {
|
||||
showOneTimeImagePickingDialog = true
|
||||
},
|
||||
containerColor = takeColorFromScheme {
|
||||
if (component.uris.isEmpty()) tertiaryContainer
|
||||
else secondaryContainer
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.AddPhotoAlt,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
)
|
||||
OneTimeSaveLocationSelectionDialog(
|
||||
visible = showFolderSelectionDialog,
|
||||
onDismiss = { showFolderSelectionDialog = false },
|
||||
onSaveRequest = saveBitmaps,
|
||||
formatForFilenameSelection = component.getFormatForFilenameSelection()
|
||||
)
|
||||
OneTimeImagePickingDialog(
|
||||
onDismiss = { showOneTimeImagePickingDialog = false },
|
||||
picker = Picker.Multiple,
|
||||
imagePicker = addImagesPicker,
|
||||
visible = showOneTimeImagePickingDialog
|
||||
)
|
||||
},
|
||||
canShowScreenData = component.uris.isNotEmpty()
|
||||
)
|
||||
|
||||
ImagePager(
|
||||
visible = selectedUriForPreview != null,
|
||||
selectedUri = selectedUriForPreview,
|
||||
uris = component.uris,
|
||||
onNavigate = {
|
||||
selectedUriForPreview = null
|
||||
component.onNavigate(it)
|
||||
},
|
||||
onUriSelected = { selectedUriForPreview = it },
|
||||
onShare = component::shareUri,
|
||||
onDismiss = { selectedUriForPreview = null }
|
||||
)
|
||||
|
||||
ExitWithoutSavingDialog(
|
||||
onExit = component.onGoBack,
|
||||
onDismiss = { showExitDialog = false },
|
||||
visible = showExitDialog
|
||||
)
|
||||
|
||||
LoadingDialog(
|
||||
visible = component.isSaving,
|
||||
done = component.done,
|
||||
left = component.left,
|
||||
onCancelLoading = component::cancelSaving
|
||||
)
|
||||
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 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.document_scanner.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.ImageShareProvider
|
||||
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.Quality
|
||||
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.runSuspendCatching
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.smartJob
|
||||
import com.t8rin.imagetoolbox.core.domain.utils.timestamp
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppToastHost
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.helper.ScanResult
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.navigation.Screen
|
||||
import com.t8rin.imagetoolbox.core.ui.utils.state.update
|
||||
import com.t8rin.imagetoolbox.feature.pdf_tools.domain.PdfManager
|
||||
import com.t8rin.imagetoolbox.feature.pdf_tools.domain.model.PdfCreationParams
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.random.Random
|
||||
|
||||
class DocumentScannerComponent @AssistedInject internal constructor(
|
||||
@Assisted componentContext: ComponentContext,
|
||||
@Assisted val onGoBack: () -> Unit,
|
||||
@Assisted val onNavigate: (Screen) -> Unit,
|
||||
private val shareProvider: ImageShareProvider<Bitmap>,
|
||||
private val imageCompressor: ImageCompressor<Bitmap>,
|
||||
private val imageGetter: ImageGetter<Bitmap>,
|
||||
private val fileController: FileController,
|
||||
private val pdfManager: PdfManager,
|
||||
dispatchersHolder: DispatchersHolder
|
||||
) : BaseComponent(dispatchersHolder, componentContext) {
|
||||
|
||||
private val _uris = mutableStateOf<List<Uri>>(emptyList())
|
||||
val uris by _uris
|
||||
|
||||
private val _imageFormat = mutableStateOf(ImageFormat.Default)
|
||||
val imageFormat by _imageFormat
|
||||
|
||||
private val _quality = mutableStateOf<Quality>(Quality.Base())
|
||||
val quality by _quality
|
||||
|
||||
private val _pdfUris = mutableStateOf<List<Uri>>(emptyList())
|
||||
|
||||
private suspend fun getPdfUri(): Uri? =
|
||||
if (_pdfUris.value.size > 1 || _pdfUris.value.isEmpty()) {
|
||||
createPdfUri()
|
||||
} else _pdfUris.value.firstOrNull()
|
||||
|
||||
private val _isSaving: MutableState<Boolean> = mutableStateOf(false)
|
||||
val isSaving by _isSaving
|
||||
|
||||
private val _done: MutableState<Int> = mutableIntStateOf(0)
|
||||
val done by _done
|
||||
|
||||
private val _left: MutableState<Int> = mutableIntStateOf(-1)
|
||||
val left by _left
|
||||
|
||||
fun parseScanResult(scanResult: ScanResult) {
|
||||
if (scanResult.imageUris.isNotEmpty()) {
|
||||
_uris.update { scanResult.imageUris }
|
||||
}
|
||||
if (scanResult.pdfUri != null) {
|
||||
_pdfUris.update { listOfNotNull(scanResult.pdfUri) }
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
val imageInfo = ImageInfo(
|
||||
width = bitmap.width,
|
||||
height = bitmap.height,
|
||||
imageFormat = imageFormat,
|
||||
quality = quality,
|
||||
originalUri = uri.toString()
|
||||
)
|
||||
results.add(
|
||||
fileController.save(
|
||||
saveTarget = ImageSaveTarget(
|
||||
imageInfo = imageInfo,
|
||||
metadata = null,
|
||||
originalUri = uri.toString(),
|
||||
sequenceNumber = _done.value + 1,
|
||||
data = imageCompressor.compressAndTransform(
|
||||
image = bitmap,
|
||||
imageInfo = imageInfo
|
||||
)
|
||||
),
|
||||
keepOriginalMetadata = true,
|
||||
oneTimeSaveLocationUri = oneTimeSaveLocationUri
|
||||
)
|
||||
)
|
||||
} ?: results.add(
|
||||
SaveResult.Error.Exception(Throwable())
|
||||
)
|
||||
|
||||
_done.value += 1
|
||||
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = left
|
||||
)
|
||||
}
|
||||
parseSaveResults(results.onSuccess(::registerSave))
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun savePdfTo(
|
||||
uri: Uri
|
||||
) {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
getPdfUri()?.let { pdfUri ->
|
||||
fileController.transferBytes(
|
||||
fromUri = pdfUri.toString(),
|
||||
toUri = uri.toString(),
|
||||
).also(::parseFileSaveResult).onSuccess(::registerSave)
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createPdfUri(): Uri? {
|
||||
_done.update { 0 }
|
||||
_left.update { 0 }
|
||||
return runSuspendCatching {
|
||||
pdfManager.createPdf(
|
||||
imageUris = uris.map { it.toString() },
|
||||
params = PdfCreationParams()
|
||||
)
|
||||
}.getOrNull()?.toUri()
|
||||
}
|
||||
|
||||
fun generatePdfFilename(): String {
|
||||
val timeStamp = "${timestamp()}_${Random(Random.nextInt()).hashCode().toString().take(4)}"
|
||||
return "PDF_$timeStamp.pdf"
|
||||
}
|
||||
|
||||
fun sharePdf() {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
getPdfUri()?.let { pdfUri ->
|
||||
_done.update { 0 }
|
||||
_left.update { 0 }
|
||||
|
||||
runSuspendCatching {
|
||||
shareProvider.shareUri(
|
||||
uri = pdfUri.toString(),
|
||||
onComplete = AppToastHost::showConfetti
|
||||
)
|
||||
}.onFailure {
|
||||
val bytes = fileController.readBytes(pdfUri.toString())
|
||||
shareProvider.shareByteArray(
|
||||
byteArray = bytes,
|
||||
filename = generatePdfFilename(),
|
||||
onComplete = AppToastHost::showConfetti
|
||||
)
|
||||
}
|
||||
}
|
||||
_isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSaving() {
|
||||
savingJob?.cancel()
|
||||
savingJob = null
|
||||
_isSaving.value = false
|
||||
}
|
||||
|
||||
fun removeImageUri(uri: Uri) {
|
||||
_uris.update { it - uri }
|
||||
_pdfUris.update { emptyList() }
|
||||
}
|
||||
|
||||
fun addScanResult(scanResult: ScanResult) {
|
||||
_uris.update { (it + scanResult.imageUris).distinct() }
|
||||
_pdfUris.update { (it + scanResult.pdfUri).distinct().filterNotNull() }
|
||||
}
|
||||
|
||||
fun shareBitmaps() {
|
||||
savingJob = trackProgress {
|
||||
_isSaving.value = true
|
||||
shareProvider.shareImages(
|
||||
uris = uris.map { it.toString() },
|
||||
imageLoader = { uri ->
|
||||
imageGetter.getImage(uri)?.image?.let { bmp ->
|
||||
bmp to ImageInfo(
|
||||
width = bmp.width,
|
||||
height = bmp.height,
|
||||
imageFormat = imageFormat,
|
||||
quality = quality,
|
||||
originalUri = uri
|
||||
)
|
||||
}
|
||||
},
|
||||
onProgressChange = {
|
||||
if (it == -1) {
|
||||
AppToastHost.showConfetti()
|
||||
_isSaving.value = false
|
||||
_done.value = 0
|
||||
} else {
|
||||
_done.value = it
|
||||
}
|
||||
updateProgress(
|
||||
done = done,
|
||||
total = left
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setImageFormat(imageFormat: ImageFormat) {
|
||||
_imageFormat.update { imageFormat }
|
||||
}
|
||||
|
||||
fun setQuality(quality: Quality) {
|
||||
_quality.update { quality }
|
||||
}
|
||||
|
||||
fun getFormatForFilenameSelection(): ImageFormat = imageFormat
|
||||
|
||||
fun shareUri(uri: Uri) {
|
||||
componentScope.launch {
|
||||
shareProvider.shareUri(
|
||||
uri = uri.toString(),
|
||||
onComplete = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
operator fun invoke(
|
||||
componentContext: ComponentContext,
|
||||
onGoBack: () -> Unit,
|
||||
onNavigate: (Screen) -> Unit,
|
||||
): DocumentScannerComponent
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user