Commit bfd003e0 by Aleksandr Tamakov

1

parent 4025e1da
......@@ -12,10 +12,17 @@ import com.isidroid.rendering.model.RenderSettingsV2
import com.isidroid.spot.model.PrintProfile
import com.isidroid.rendering.repository.RenderRepository
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.toBitmap
import com.isidroid.utils.transformToBitmapFiles
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.io.File
import java.util.UUID
import javax.inject.Inject
......@@ -30,23 +37,30 @@ class RenderUseCase @Inject constructor(
fun loadSpot() = flow {
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!!)
}
fun render(uri: Uri?, containerSize: IntSize?, printProfile: PrintProfile?, renderSettings: RenderSettingsV2?) = flow {
uri ?: throw IllegalStateException("Uri is null")
fun render(
sourceFile: File?,
containerSize: IntSize?,
printProfile: PrintProfile?,
renderSettings: RenderSettingsV2?
) = flow {
sourceFile ?: throw IllegalStateException("Uri 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")
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")
bitmap.saveToFile(file)
bitmap.recycle()
sourceFile.copyTo(file, overwrite = true)
val updatedSettings = renderSettings.copy(filePath = file.absolutePath)
......@@ -71,16 +85,28 @@ class RenderUseCase @Inject constructor(
marginLeft = printProfile.marginLeft,
dpix = printProfile.dpix,
dpiy = printProfile.dpiy,
renderSettingsV2 = updatedSettings.copy(canvasWidth = previewContainer.width, canvasHeight = previewContainer.height),
renderSettingsV2 = updatedSettings.copy(
canvasWidth = previewContainer.width,
canvasHeight = previewContainer.height
),
picture = picture,
)
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))
}
fun prepare(uris: List<Uri>) = flow {
emit(FlowResult.Loading)
val files = uris.transformToBitmapFiles(context)
emit(FlowResult.Success(files))
}
}
......@@ -61,7 +61,7 @@ fun ContentScreen(
}
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) {
effectFlow?.collect { effect ->
when (effect) {
......
......@@ -26,10 +26,11 @@ class RenderContract {
data class UpdateRenderSettings(
@PrintSize val size: Int? = null,
@PrintOrientation val orientation: Int? = null,
val increaseCopy: Boolean? = null
val increaseCopy: Boolean? = null,
val page: Int = -1
) : Event
data class ChangePrintProfile(val profileId: String?) : Event
data class ChangePrintProfile(val profileId: String?, val page: Int? = null) : Event
}
sealed interface Effect : ViewSideEffect {
......@@ -41,8 +42,8 @@ class RenderContract {
@Stable
data class State(
val rendering: Boolean = false,
val renderSettings: RenderSettingsV2 = RenderSettingsV2(),
val uris: List<Uri> = emptyList(),
val printProfile: PrintProfile? = null,
val renderSeed: Int = 0,
@PrintSize val openPrintSize: Int? = null,
......@@ -50,6 +51,7 @@ class RenderContract {
val openCopies: Int? = null,
var richSpot: RichSpot? = null,
val spotHasNoPrintProfiles: Boolean = false,
val printProfilesSelector: List<PrintProfile>? = null
val printProfilesSelector: List<PrintProfile>? = null,
val pageCount: Int = 0
) : ViewState
}
\ No newline at end of file
......@@ -17,8 +17,11 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
......@@ -58,9 +61,10 @@ fun RenderPreviewScreen(
val copies = state.value.openCopies
val spotHasNoPrintProfiles = state.value.spotHasNoPrintProfiles
val printProfileSelector = state.value.printProfilesSelector
val bottomColor = MaterialTheme.colorScheme.secondaryContainer
var pagerPage by remember { mutableIntStateOf(-1) }
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
......@@ -73,6 +77,8 @@ fun RenderPreviewScreen(
onEventSent(RenderContract.Event.ToBack)
}
if (state.value.rendering) return
when {
spotHasNoPrintProfiles -> {
SpotHasNotPrintProfilesComponent(
......@@ -86,22 +92,22 @@ fun RenderPreviewScreen(
printProfileSelector != null -> PrintProfileListSelectorComponent(
list = printProfileSelector,
current = state.value.printProfile,
onChange = { onEventSent(RenderContract.Event.ChangePrintProfile(it)) }
onChange = { onEventSent(RenderContract.Event.ChangePrintProfile(it, page = pagerPage)) }
)
openPrintSize != null -> PrintSizeModalComponent(
currentPrintSize = openPrintSize,
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(size = it)) }
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(size = it, page = pagerPage)) }
)
openOrientation != null -> PrintOrientationModalComponent(
currentOrientation = openOrientation,
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(orientation = it)) }
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(orientation = it, page = pagerPage)) }
)
copies != null -> PrintCopiesModalComponent(
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(
linkTo(top = parent.top, bottom = paperInfo.top)
height = Dimension.fillToConstraints
},
onPageOpened = { page, containerSize -> onEventSent(RenderContract.Event.OnPageOpened(page, containerSize)) }
onPageOpened = { page, containerSize ->
pagerPage = page
onEventSent(RenderContract.Event.OnPageOpened(page, containerSize))
}
)
PaperInfoComponent(
......
......@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import javax.inject.Inject
import kotlin.random.Random
......@@ -32,6 +33,7 @@ class RenderViewModel @Inject constructor(
private val useCase: RenderUseCase,
savedState: SavedStateHandle
) : BaseViewModel<RenderContract.Event, RenderContract.State, RenderContract.Effect>() {
private var _sourceList: List<File>? = null
private var _containerSize: IntSize? = null
private var _richSpot: RichSpot? = null
private val _renderResults = SparseArray<RenderResult>()
......@@ -42,12 +44,12 @@ class RenderViewModel @Inject constructor(
viewModelScope.launch {
savedState.getStateFlow<String?>(Argument.URI, null)
.filterNotNull()
.collect { uris -> load(uris) }
.collect { uris -> prepareSourceFileList(uris) }
}
}
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) {
when (event) {
RenderContract.Event.Click -> RenderContract.State()
......@@ -60,27 +62,27 @@ class RenderViewModel @Inject constructor(
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.ChangePrintProfile -> changePrintProfile(event.profileId)
is RenderContract.Event.UpdateRenderSettings -> updateRenderSettings(
size = event.size,
orientation = event.orientation,
increaseCopy = event.increaseCopy,
page = event.page
)
is RenderContract.Event.ChangePrintProfile -> changePrintProfile(event.profileId, event.page)
}
}
// events
private suspend fun load(uris: String) {
useCase.loadSpot()
private suspend fun prepareSourceFileList(uris: String) {
useCase.prepare(uris.split(",").map { it.trim().toUri() })
.flowOn(Dispatchers.IO)
.catchTimber { setState { copy(spotHasNoPrintProfiles = true) } }
.collect { richSpot ->
_richSpot = richSpot
val printProfile = richSpot.printProfiles.first()
setState {
copy(
richSpot = richSpot,
uris = uris.split(",").map { it.trim().toUri() },
renderSettings = renderPreviewDefaultSettings(greyscale = printProfile.grayscale),
printProfile = printProfile
)
.catchTimber { }
.collect { res ->
when (res) {
FlowResult.Loading -> setState { copy(rendering = true) }
is FlowResult.Success -> onSourceFileListReady(res.result)
}
}
}
......@@ -98,7 +100,12 @@ class RenderViewModel @Inject constructor(
}
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)
.catchTimber { Timber.e(it) }
.collect { flowResult ->
......@@ -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
updateRenderResults(refreshRender)
......@@ -135,7 +148,7 @@ class RenderViewModel @Inject constructor(
if (refreshRender) {
onPageOpened(0)
onPageOpened(page ?: 0)
}
}
......@@ -149,15 +162,37 @@ class RenderViewModel @Inject constructor(
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
setState { copy(printProfilesSelector = null, printProfile = printProfile) }
if (profileId != null)
updateRenderSettings(increaseCopy = null, orientation = null, size = null)
updateRenderSettings(increaseCopy = null, orientation = null, size = null, page = page)
}
// 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) {
_renderResults[page] = result
_renderResultsFlow.emit(_renderResults)
......
......@@ -67,7 +67,7 @@ private fun PagerContent(
modifier: Modifier,
onPageOpened: (Int) -> Unit,
) {
val pageCount = state.value.uris.size
val pageCount = state.value.pageCount
val renderResults = renderResultsFlow?.collectAsState(initial = null)
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
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
fun Uri.getDate(context: Context): Date {
val cursor = context.contentResolver.query(this, null, null, null, null)
......@@ -42,4 +46,56 @@ fun Uri.getDate(context: Context): Date {
cursor?.close()
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