Commit bfd003e0 by Aleksandr Tamakov

1

parent 4025e1da
...@@ -12,10 +12,17 @@ import com.isidroid.rendering.model.RenderSettingsV2 ...@@ -12,10 +12,17 @@ import com.isidroid.rendering.model.RenderSettingsV2
import com.isidroid.spot.model.PrintProfile import com.isidroid.spot.model.PrintProfile
import com.isidroid.rendering.repository.RenderRepository import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.repository.ActiveSpotRepository import com.isidroid.spot.repository.ActiveSpotRepository
import com.isidroid.utils.getMimeType
import com.isidroid.utils.isMimeTypeImage
import com.isidroid.utils.isMimeTypePdf
import com.isidroid.utils.saveAsBitmapToFile
import com.isidroid.utils.savePdfAsBitmapFiles
import com.isidroid.utils.saveToFile import com.isidroid.utils.saveToFile
import com.isidroid.utils.toBitmap import com.isidroid.utils.toBitmap
import com.isidroid.utils.transformToBitmapFiles
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
...@@ -30,23 +37,30 @@ class RenderUseCase @Inject constructor( ...@@ -30,23 +37,30 @@ class RenderUseCase @Inject constructor(
fun loadSpot() = flow { fun loadSpot() = flow {
val richSpot = spotRepository.getDefaultSpot() val richSpot = spotRepository.getDefaultSpot()
if (richSpot?.printProfiles.isNullOrEmpty()) throw SpotHasNoPrintProfilesException(context.getString(R.string.error_spot_has_no_printing_profiles)) if (richSpot?.printProfiles.isNullOrEmpty()) throw SpotHasNoPrintProfilesException(
context.getString(
R.string.error_spot_has_no_printing_profiles
)
)
emit(richSpot!!) emit(richSpot!!)
} }
fun render(uri: Uri?, containerSize: IntSize?, printProfile: PrintProfile?, renderSettings: RenderSettingsV2?) = flow { fun render(
uri ?: throw IllegalStateException("Uri is null") sourceFile: File?,
containerSize: IntSize?,
printProfile: PrintProfile?,
renderSettings: RenderSettingsV2?
) = flow {
sourceFile ?: throw IllegalStateException("Uri is null")
containerSize ?: throw IllegalStateException("Container size is null") containerSize ?: throw IllegalStateException("Container size is null")
printProfile ?: throw IllegalStateException("Paper is null") printProfile ?: throw IllegalStateException("Profile is null")
renderSettings ?: throw IllegalStateException("renderSettings is null") renderSettings ?: throw IllegalStateException("renderSettings is null")
emit(FlowResult.Loading) emit(FlowResult.Loading)
val bitmap = uri.toBitmap(context) ?: throw IllegalStateException("Can't create bitmap from uri $uri")
val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg") val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg")
bitmap.saveToFile(file) sourceFile.copyTo(file, overwrite = true)
bitmap.recycle()
val updatedSettings = renderSettings.copy(filePath = file.absolutePath) val updatedSettings = renderSettings.copy(filePath = file.absolutePath)
...@@ -71,16 +85,28 @@ class RenderUseCase @Inject constructor( ...@@ -71,16 +85,28 @@ class RenderUseCase @Inject constructor(
marginLeft = printProfile.marginLeft, marginLeft = printProfile.marginLeft,
dpix = printProfile.dpix, dpix = printProfile.dpix,
dpiy = printProfile.dpiy, dpiy = printProfile.dpiy,
renderSettingsV2 = updatedSettings.copy(canvasWidth = previewContainer.width, canvasHeight = previewContainer.height), renderSettingsV2 = updatedSettings.copy(
canvasWidth = previewContainer.width,
canvasHeight = previewContainer.height
),
picture = picture, picture = picture,
) )
result.saveToFile(file) result.saveToFile(file)
val renderResult = RenderResult(width = previewContainer.width, height = previewContainer.height, filePath = file.absolutePath) val renderResult = RenderResult(
width = previewContainer.width,
height = previewContainer.height,
filePath = file.absolutePath
)
emit(FlowResult.Success(renderResult)) emit(FlowResult.Success(renderResult))
} }
fun prepare(uris: List<Uri>) = flow {
emit(FlowResult.Loading)
val files = uris.transformToBitmapFiles(context)
emit(FlowResult.Success(files))
}
} }
...@@ -61,7 +61,7 @@ fun ContentScreen( ...@@ -61,7 +61,7 @@ fun ContentScreen(
} }
LaunchedEffect(state.value.galleryHash) { if (state.value.galleryHash != null) launcher.launch("image/*") } LaunchedEffect(state.value.galleryHash) { if (state.value.galleryHash != null) launcher.launch("image/*") }
LaunchedEffect(state.value.documentHash) { if (state.value.documentHash != null) launcher.launch("document/pdf") } LaunchedEffect(state.value.documentHash) { if (state.value.documentHash != null) launcher.launch("application/pdf") }
LaunchedEffect(SIDE_EFFECTS_KEY) { LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect -> effectFlow?.collect { effect ->
when (effect) { when (effect) {
......
...@@ -26,10 +26,11 @@ class RenderContract { ...@@ -26,10 +26,11 @@ class RenderContract {
data class UpdateRenderSettings( data class UpdateRenderSettings(
@PrintSize val size: Int? = null, @PrintSize val size: Int? = null,
@PrintOrientation val orientation: Int? = null, @PrintOrientation val orientation: Int? = null,
val increaseCopy: Boolean? = null val increaseCopy: Boolean? = null,
val page: Int = -1
) : Event ) : Event
data class ChangePrintProfile(val profileId: String?) : Event data class ChangePrintProfile(val profileId: String?, val page: Int? = null) : Event
} }
sealed interface Effect : ViewSideEffect { sealed interface Effect : ViewSideEffect {
...@@ -41,8 +42,8 @@ class RenderContract { ...@@ -41,8 +42,8 @@ class RenderContract {
@Stable @Stable
data class State( data class State(
val rendering: Boolean = false,
val renderSettings: RenderSettingsV2 = RenderSettingsV2(), val renderSettings: RenderSettingsV2 = RenderSettingsV2(),
val uris: List<Uri> = emptyList(),
val printProfile: PrintProfile? = null, val printProfile: PrintProfile? = null,
val renderSeed: Int = 0, val renderSeed: Int = 0,
@PrintSize val openPrintSize: Int? = null, @PrintSize val openPrintSize: Int? = null,
...@@ -50,6 +51,7 @@ class RenderContract { ...@@ -50,6 +51,7 @@ class RenderContract {
val openCopies: Int? = null, val openCopies: Int? = null,
var richSpot: RichSpot? = null, var richSpot: RichSpot? = null,
val spotHasNoPrintProfiles: Boolean = false, val spotHasNoPrintProfiles: Boolean = false,
val printProfilesSelector: List<PrintProfile>? = null val printProfilesSelector: List<PrintProfile>? = null,
val pageCount: Int = 0
) : ViewState ) : ViewState
} }
\ No newline at end of file
...@@ -17,8 +17,11 @@ import androidx.compose.material3.Surface ...@@ -17,8 +17,11 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Devices
...@@ -58,9 +61,10 @@ fun RenderPreviewScreen( ...@@ -58,9 +61,10 @@ fun RenderPreviewScreen(
val copies = state.value.openCopies val copies = state.value.openCopies
val spotHasNoPrintProfiles = state.value.spotHasNoPrintProfiles val spotHasNoPrintProfiles = state.value.spotHasNoPrintProfiles
val printProfileSelector = state.value.printProfilesSelector val printProfileSelector = state.value.printProfilesSelector
val bottomColor = MaterialTheme.colorScheme.secondaryContainer val bottomColor = MaterialTheme.colorScheme.secondaryContainer
var pagerPage by remember { mutableIntStateOf(-1) }
LaunchedEffect(SIDE_EFFECTS_KEY) { LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect -> effectFlow?.collect { effect ->
when (effect) { when (effect) {
...@@ -73,6 +77,8 @@ fun RenderPreviewScreen( ...@@ -73,6 +77,8 @@ fun RenderPreviewScreen(
onEventSent(RenderContract.Event.ToBack) onEventSent(RenderContract.Event.ToBack)
} }
if (state.value.rendering) return
when { when {
spotHasNoPrintProfiles -> { spotHasNoPrintProfiles -> {
SpotHasNotPrintProfilesComponent( SpotHasNotPrintProfilesComponent(
...@@ -86,22 +92,22 @@ fun RenderPreviewScreen( ...@@ -86,22 +92,22 @@ fun RenderPreviewScreen(
printProfileSelector != null -> PrintProfileListSelectorComponent( printProfileSelector != null -> PrintProfileListSelectorComponent(
list = printProfileSelector, list = printProfileSelector,
current = state.value.printProfile, current = state.value.printProfile,
onChange = { onEventSent(RenderContract.Event.ChangePrintProfile(it)) } onChange = { onEventSent(RenderContract.Event.ChangePrintProfile(it, page = pagerPage)) }
) )
openPrintSize != null -> PrintSizeModalComponent( openPrintSize != null -> PrintSizeModalComponent(
currentPrintSize = openPrintSize, currentPrintSize = openPrintSize,
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(size = it)) } onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(size = it, page = pagerPage)) }
) )
openOrientation != null -> PrintOrientationModalComponent( openOrientation != null -> PrintOrientationModalComponent(
currentOrientation = openOrientation, currentOrientation = openOrientation,
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(orientation = it)) } onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(orientation = it, page = pagerPage)) }
) )
copies != null -> PrintCopiesModalComponent( copies != null -> PrintCopiesModalComponent(
copies = copies, copies = copies,
onChange = { increaseCopy -> onEventSent(RenderContract.Event.UpdateRenderSettings(increaseCopy = increaseCopy)) }, onChange = { increaseCopy -> onEventSent(RenderContract.Event.UpdateRenderSettings(increaseCopy = increaseCopy, page = pagerPage)) },
) )
} }
...@@ -136,7 +142,10 @@ fun RenderPreviewScreen( ...@@ -136,7 +142,10 @@ fun RenderPreviewScreen(
linkTo(top = parent.top, bottom = paperInfo.top) linkTo(top = parent.top, bottom = paperInfo.top)
height = Dimension.fillToConstraints height = Dimension.fillToConstraints
}, },
onPageOpened = { page, containerSize -> onEventSent(RenderContract.Event.OnPageOpened(page, containerSize)) } onPageOpened = { page, containerSize ->
pagerPage = page
onEventSent(RenderContract.Event.OnPageOpened(page, containerSize))
}
) )
PaperInfoComponent( PaperInfoComponent(
......
...@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.filterNotNull ...@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random import kotlin.random.Random
...@@ -32,6 +33,7 @@ class RenderViewModel @Inject constructor( ...@@ -32,6 +33,7 @@ class RenderViewModel @Inject constructor(
private val useCase: RenderUseCase, private val useCase: RenderUseCase,
savedState: SavedStateHandle savedState: SavedStateHandle
) : BaseViewModel<RenderContract.Event, RenderContract.State, RenderContract.Effect>() { ) : BaseViewModel<RenderContract.Event, RenderContract.State, RenderContract.Effect>() {
private var _sourceList: List<File>? = null
private var _containerSize: IntSize? = null private var _containerSize: IntSize? = null
private var _richSpot: RichSpot? = null private var _richSpot: RichSpot? = null
private val _renderResults = SparseArray<RenderResult>() private val _renderResults = SparseArray<RenderResult>()
...@@ -42,12 +44,12 @@ class RenderViewModel @Inject constructor( ...@@ -42,12 +44,12 @@ class RenderViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
savedState.getStateFlow<String?>(Argument.URI, null) savedState.getStateFlow<String?>(Argument.URI, null)
.filterNotNull() .filterNotNull()
.collect { uris -> load(uris) } .collect { uris -> prepareSourceFileList(uris) }
} }
} }
override val isDebug: Boolean = isDebug() override val isDebug: Boolean = isDebug()
override fun setInitialState(): RenderContract.State = RenderContract.State() override fun setInitialState(): RenderContract.State = RenderContract.State(rendering = true)
override suspend fun handleEvents(event: RenderContract.Event) { override suspend fun handleEvents(event: RenderContract.Event) {
when (event) { when (event) {
RenderContract.Event.Click -> RenderContract.State() RenderContract.Event.Click -> RenderContract.State()
...@@ -60,27 +62,27 @@ class RenderViewModel @Inject constructor( ...@@ -60,27 +62,27 @@ class RenderViewModel @Inject constructor(
is RenderContract.Event.OnPageOpened -> onPageOpened(event.page, event.containerSize) is RenderContract.Event.OnPageOpened -> onPageOpened(event.page, event.containerSize)
is RenderContract.Event.UpdateRenderSettings -> updateRenderSettings(size = event.size, orientation = event.orientation, increaseCopy = event.increaseCopy) is RenderContract.Event.UpdateRenderSettings -> updateRenderSettings(
is RenderContract.Event.ChangePrintProfile -> changePrintProfile(event.profileId) size = event.size,
orientation = event.orientation,
increaseCopy = event.increaseCopy,
page = event.page
)
is RenderContract.Event.ChangePrintProfile -> changePrintProfile(event.profileId, event.page)
} }
} }
// events // events
private suspend fun load(uris: String) { private suspend fun prepareSourceFileList(uris: String) {
useCase.loadSpot() useCase.prepare(uris.split(",").map { it.trim().toUri() })
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.catchTimber { setState { copy(spotHasNoPrintProfiles = true) } } .catchTimber { }
.collect { richSpot -> .collect { res ->
_richSpot = richSpot when (res) {
val printProfile = richSpot.printProfiles.first() FlowResult.Loading -> setState { copy(rendering = true) }
setState { is FlowResult.Success -> onSourceFileListReady(res.result)
copy(
richSpot = richSpot,
uris = uris.split(",").map { it.trim().toUri() },
renderSettings = renderPreviewDefaultSettings(greyscale = printProfile.grayscale),
printProfile = printProfile
)
} }
} }
} }
...@@ -98,7 +100,12 @@ class RenderViewModel @Inject constructor( ...@@ -98,7 +100,12 @@ class RenderViewModel @Inject constructor(
} }
val state = viewState.value val state = viewState.value
useCase.render(state.uris.getOrNull(page), size, state.printProfile, state.renderSettings) useCase.render(
sourceFile = _sourceList?.get(page),
containerSize = size,
printProfile = state.printProfile,
renderSettings = state.renderSettings
)
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.catchTimber { Timber.e(it) } .catchTimber { Timber.e(it) }
.collect { flowResult -> .collect { flowResult ->
...@@ -109,7 +116,13 @@ class RenderViewModel @Inject constructor( ...@@ -109,7 +116,13 @@ class RenderViewModel @Inject constructor(
} }
} }
private suspend fun updateRenderSettings(@PrintSize size: Int?, @PrintOrientation orientation: Int?, increaseCopy: Boolean?, forceRefresh: Boolean = false) { private suspend fun updateRenderSettings(
@PrintSize size: Int?,
@PrintOrientation orientation: Int?,
increaseCopy: Boolean?,
forceRefresh: Boolean = false,
page: Int? = null
) {
val refreshRender = (increaseCopy == null && viewState.value.openCopies == null) || forceRefresh val refreshRender = (increaseCopy == null && viewState.value.openCopies == null) || forceRefresh
updateRenderResults(refreshRender) updateRenderResults(refreshRender)
...@@ -135,7 +148,7 @@ class RenderViewModel @Inject constructor( ...@@ -135,7 +148,7 @@ class RenderViewModel @Inject constructor(
if (refreshRender) { if (refreshRender) {
onPageOpened(0) onPageOpened(page ?: 0)
} }
} }
...@@ -149,15 +162,37 @@ class RenderViewModel @Inject constructor( ...@@ -149,15 +162,37 @@ class RenderViewModel @Inject constructor(
setEffect { RenderContract.Effect.Navigation.ToBack } setEffect { RenderContract.Effect.Navigation.ToBack }
} }
private suspend fun changePrintProfile(profileId: String?) { private suspend fun changePrintProfile(profileId: String?, page: Int?) {
val printProfile = profileId?.let { _richSpot?.printProfiles?.find { it.id == profileId } } ?: viewState.value.printProfile val printProfile = profileId?.let { _richSpot?.printProfiles?.find { it.id == profileId } } ?: viewState.value.printProfile
setState { copy(printProfilesSelector = null, printProfile = printProfile) } setState { copy(printProfilesSelector = null, printProfile = printProfile) }
if (profileId != null) if (profileId != null)
updateRenderSettings(increaseCopy = null, orientation = null, size = null) updateRenderSettings(increaseCopy = null, orientation = null, size = null, page = page)
} }
// callbacks // callbacks
private suspend fun onSourceFileListReady(list: List<File>) {
_sourceList = list
useCase.loadSpot()
.flowOn(Dispatchers.IO)
.catchTimber { setState { copy(spotHasNoPrintProfiles = true) } }
.collect { richSpot ->
_richSpot = richSpot
val printProfile = richSpot.printProfiles.first()
setState {
copy(
richSpot = richSpot,
renderSettings = renderPreviewDefaultSettings(greyscale = printProfile.grayscale),
printProfile = printProfile,
pageCount = list.size,
rendering = false
)
}
}
}
private suspend fun onRenderPage(result: RenderResult, page: Int) { private suspend fun onRenderPage(result: RenderResult, page: Int) {
_renderResults[page] = result _renderResults[page] = result
_renderResultsFlow.emit(_renderResults) _renderResultsFlow.emit(_renderResults)
......
...@@ -67,7 +67,7 @@ private fun PagerContent( ...@@ -67,7 +67,7 @@ private fun PagerContent(
modifier: Modifier, modifier: Modifier,
onPageOpened: (Int) -> Unit, onPageOpened: (Int) -> Unit,
) { ) {
val pageCount = state.value.uris.size val pageCount = state.value.pageCount
val renderResults = renderResultsFlow?.collectAsState(initial = null) val renderResults = renderResultsFlow?.collectAsState(initial = null)
var currentPage by remember { mutableIntStateOf(1) } var currentPage by remember { mutableIntStateOf(1) }
......
package com.isidroid.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import timber.log.Timber
import java.io.File
fun Uri.savePdfAsBitmapFiles(context: Context): List<File>? {
val descriptor = context.contentResolver.openFileDescriptor(this, "r") ?: return null
val renderer = PdfRenderer(descriptor)
val result = (0 until renderer.pageCount).mapNotNull { pageNumber ->
val page = renderer.openPage(pageNumber)
try {
val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)
bitmap.saveToTempFile()
} catch (t: Throwable) {
Timber.e(t)
null
} finally {
page.close()
}
}
descriptor.close()
renderer.close()
return result
}
\ No newline at end of file
package com.isidroid.utils package com.isidroid.utils
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.UUID
fun Uri.getDate(context: Context): Date { fun Uri.getDate(context: Context): Date {
val cursor = context.contentResolver.query(this, null, null, null, null) val cursor = context.contentResolver.query(this, null, null, null, null)
...@@ -43,3 +47,55 @@ fun Uri.getDate(context: Context): Date { ...@@ -43,3 +47,55 @@ fun Uri.getDate(context: Context): Date {
cursor?.close() cursor?.close()
return date return date
} }
fun Uri.getMimeType(context: Context): String? {
var mimeType: String?
mimeType = context.contentResolver.getType(this)
if (mimeType == null) {
val extension = fileExtension
if (extension != null) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
}
return mimeType
}
private val Uri.fileExtension: String?
get() {
val path = path ?: return null
val extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(path)).toString())
return extension.ifEmpty { null }
}
val String?.isMimeTypePdf get() = this == "application/pdf"
val String?.isMimeTypeImage get() = this?.startsWith("image/") == true
fun Uri.saveAsBitmapToFile(context: Context): File? {
val bitmap = toBitmap(context) ?: return null
return bitmap.saveToTempFile()
}
internal fun Bitmap.saveToTempFile(): File {
val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg")
saveToFile(file)
recycle()
return file
}
fun List<Uri>.transformToBitmapFiles(context: Context): List<File> {
val uris = this
val result = mutableListOf<File>()
uris.forEach { uri ->
val mime = uri.getMimeType(context)
val isImage = mime.isMimeTypeImage
val isPdf = mime.isMimeTypePdf
when {
isImage -> uri.saveAsBitmapToFile(context)?.also { result.add(it) }
isPdf -> uri.savePdfAsBitmapFiles(context)?.also { result.addAll(it) }
}
}
return result
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment