Plug input data to EditPageScreen
This commit is contained in:
@@ -124,6 +124,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val importState by cameraViewModel.importState.collectAsStateWithLifecycle()
|
val importState by cameraViewModel.importState.collectAsStateWithLifecycle()
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
val documentUiState by viewModel.documentUiState.collectAsStateWithLifecycle()
|
val documentUiState by viewModel.documentUiState.collectAsStateWithLifecycle()
|
||||||
|
val cropInitialState by viewModel.cropInitState.collectAsStateWithLifecycle()
|
||||||
val exportUiState by exportViewModel.uiState.collectAsStateWithLifecycle()
|
val exportUiState by exportViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val cameraPermission = rememberCameraPermissionState()
|
val cameraPermission = rememberCameraPermissionState()
|
||||||
CollectCameraEvents(cameraViewModel, viewModel)
|
CollectCameraEvents(cameraViewModel, viewModel)
|
||||||
@@ -179,10 +180,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Screen.Main.EditImage -> {
|
is Screen.Main.EditImage -> {
|
||||||
val pageIndex = (currentScreen as Screen.Main.EditImage).pageIndex
|
|
||||||
EditPageScreen(
|
EditPageScreen(
|
||||||
pageId = documentUiState.document.pages[pageIndex].key.pageId,
|
pageId = documentUiState.currentPage?.key?.pageId ?: "",
|
||||||
imageRepository = imageRepository,
|
onLoad = { id -> viewModel.loadCropInitialState(id)},
|
||||||
|
initState = cropInitialState,
|
||||||
navigation = navigation,
|
navigation = navigation,
|
||||||
onUpdatePageQuad = { id, quad, onComplete -> },
|
onUpdatePageQuad = { id, quad, onComplete -> },
|
||||||
)
|
)
|
||||||
@@ -467,7 +468,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun navigation(viewModel: MainViewModel, launchMode: LaunchMode): Navigation = Navigation(
|
private fun navigation(viewModel: MainViewModel, launchMode: LaunchMode): Navigation = Navigation(
|
||||||
toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) },
|
toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) },
|
||||||
toEditImageScreen = { pageIndex -> viewModel.navigateTo(Screen.Main.EditImage(pageIndex)) },
|
toEditImageScreen = { viewModel.navigateTo(Screen.Main.EditImage) },
|
||||||
toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) },
|
toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) },
|
||||||
toExportScreen = { viewModel.navigateTo(Screen.Main.Export) },
|
toExportScreen = { viewModel.navigateTo(Screen.Main.Export) },
|
||||||
toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) },
|
toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) },
|
||||||
|
|||||||
@@ -14,12 +14,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app
|
package org.fairscan.app
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -34,14 +38,17 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.fairscan.app.data.ImageRepository
|
import org.fairscan.app.data.ImageRepository
|
||||||
import org.fairscan.app.domain.CapturedPage
|
import org.fairscan.app.domain.CapturedPage
|
||||||
|
import org.fairscan.app.domain.Rotation
|
||||||
import org.fairscan.app.domain.ScanPage
|
import org.fairscan.app.domain.ScanPage
|
||||||
import org.fairscan.app.ui.NavigationState
|
import org.fairscan.app.ui.NavigationState
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.screens.document.CurrentPageUiState
|
import org.fairscan.app.ui.screens.document.CurrentPageUiState
|
||||||
import org.fairscan.app.ui.screens.document.DocumentUiState
|
import org.fairscan.app.ui.screens.document.DocumentUiState
|
||||||
|
import org.fairscan.app.ui.screens.edit.CropInitState
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
import org.fairscan.app.ui.state.PageThumbnail
|
import org.fairscan.app.ui.state.PageThumbnail
|
||||||
import org.fairscan.imageprocessing.ColorMode
|
import org.fairscan.imageprocessing.ColorMode
|
||||||
|
import org.fairscan.imageprocessing.ImageSize
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@@ -95,7 +102,8 @@ class MainViewModel(val imageRepository: ImageRepository): ViewModel() {
|
|||||||
page?.let {
|
page?.let {
|
||||||
val isLoading = (it.id == loadingId)
|
val isLoading = (it.id == loadingId)
|
||||||
val bitmap = imageRepository.jpegBytes(it.key())?.toBitmap()
|
val bitmap = imageRepository.jpegBytes(it.key())?.toBitmap()
|
||||||
CurrentPageUiState(it.key(), bitmap, it.colorMode, isLoading)
|
val canBeCropped = page.metadata != null
|
||||||
|
CurrentPageUiState(it.key(), bitmap, it.colorMode, canBeCropped, isLoading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
@@ -212,4 +220,47 @@ class MainViewModel(val imageRepository: ImageRepository): ViewModel() {
|
|||||||
_pages.value = pages
|
_pages.value = pages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _cropInitState = MutableStateFlow<CropInitState>(CropInitState.Loading)
|
||||||
|
val cropInitState: StateFlow<CropInitState> = _cropInitState
|
||||||
|
|
||||||
|
private var cropInitialStateJob: Job? = null
|
||||||
|
fun loadCropInitialState(pageId: String) {
|
||||||
|
cropInitialStateJob?.cancel()
|
||||||
|
cropInitialStateJob = viewModelScope.launch {
|
||||||
|
_cropInitState.value = CropInitState.Loading
|
||||||
|
|
||||||
|
val page = _pages.value.find { it.id == pageId }
|
||||||
|
?: return@launch
|
||||||
|
|
||||||
|
val metadata = page.metadata
|
||||||
|
val baseRotation = metadata?.baseRotation ?: Rotation.R0
|
||||||
|
val rotation = baseRotation.add(page.manualRotation)
|
||||||
|
|
||||||
|
val bitmap = withContext(Dispatchers.IO) {
|
||||||
|
val source = imageRepository.source(page.id)
|
||||||
|
val bytes = source?.bytes ?: return@withContext null
|
||||||
|
|
||||||
|
val original = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||||
|
if (original != null && rotation != Rotation.R0) {
|
||||||
|
val matrix = Matrix().apply { postRotate(rotation.degrees.toFloat()) }
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
original, 0, 0, original.width, original.height, matrix, true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val quad = metadata?.normalizedQuad?.rotate90(
|
||||||
|
rotation.degrees / 90,
|
||||||
|
ImageSize(1, 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
_cropInitState.value = if (bitmap == null || quad == null)
|
||||||
|
CropInitState.Error
|
||||||
|
else
|
||||||
|
CropInitState.Ready(page.id, bitmap, quad)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ package org.fairscan.app.ui
|
|||||||
sealed class Screen {
|
sealed class Screen {
|
||||||
sealed class Main : Screen() {
|
sealed class Main : Screen() {
|
||||||
object Camera : Main()
|
object Camera : Main()
|
||||||
data class EditImage(val pageIndex: Int) : Main()
|
object EditImage : Main()
|
||||||
data class Document(val initialPage: Int = 0) : Main()
|
data class Document(val initialPage: Int = 0) : Main()
|
||||||
object Export : Main()
|
object Export : Main()
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ sealed class Screen {
|
|||||||
|
|
||||||
data class Navigation(
|
data class Navigation(
|
||||||
val toCameraScreen: () -> Unit,
|
val toCameraScreen: () -> Unit,
|
||||||
val toEditImageScreen: (Int) -> Unit,
|
val toEditImageScreen: () -> Unit,
|
||||||
val toDocumentScreen: () -> Unit,
|
val toDocumentScreen: () -> Unit,
|
||||||
val toExportScreen: () -> Unit,
|
val toExportScreen: () -> Unit,
|
||||||
val toAboutScreen: () -> Unit,
|
val toAboutScreen: () -> Unit,
|
||||||
@@ -64,7 +64,7 @@ data class NavigationState private constructor(val stack: List<Screen>, val root
|
|||||||
root -> this // Back handled by system
|
root -> this // Back handled by system
|
||||||
is Screen.Main.Camera -> this // Back handled by system
|
is Screen.Main.Camera -> this // Back handled by system
|
||||||
is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera))
|
is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera))
|
||||||
is Screen.Main.EditImage -> copy(stack = listOf(Screen.Main.Document(initialPage = (current as Screen.Main.EditImage).pageIndex)))
|
is Screen.Main.EditImage -> copy(stack = listOf(Screen.Main.Document()))
|
||||||
is Screen.Main.Export -> copy(stack = listOf(Screen.Main.Camera))
|
is Screen.Main.Export -> copy(stack = listOf(Screen.Main.Camera))
|
||||||
is Screen.Overlay -> copy(stack = stack.dropLast(1))
|
is Screen.Overlay -> copy(stack = stack.dropLast(1))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Contrast
|
import androidx.compose.material.icons.filled.Contrast
|
||||||
|
import androidx.compose.material.icons.filled.Crop
|
||||||
import androidx.compose.material.icons.filled.Done
|
import androidx.compose.material.icons.filled.Done
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material.icons.filled.RotateLeft
|
import androidx.compose.material.icons.filled.RotateLeft
|
||||||
@@ -132,6 +133,7 @@ fun DocumentScreen(
|
|||||||
{ showDeletePageDialog.value = true },
|
{ showDeletePageDialog.value = true },
|
||||||
onRotateImage,
|
onRotateImage,
|
||||||
onToggleColorMode,
|
onToggleColorMode,
|
||||||
|
navigation,
|
||||||
modifier
|
modifier
|
||||||
)
|
)
|
||||||
if (showDeletePageDialog.value) {
|
if (showDeletePageDialog.value) {
|
||||||
@@ -150,6 +152,7 @@ private fun DocumentPreview(
|
|||||||
onDeleteImage: () -> Unit,
|
onDeleteImage: () -> Unit,
|
||||||
onRotateImage: (Boolean) -> Unit,
|
onRotateImage: (Boolean) -> Unit,
|
||||||
onToggleColorMode: () -> Unit,
|
onToggleColorMode: () -> Unit,
|
||||||
|
navigation: Navigation,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val currentPageIndex = uiState.currentPageIndex
|
val currentPageIndex = uiState.currentPageIndex
|
||||||
@@ -194,15 +197,12 @@ private fun DocumentPreview(
|
|||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uiState.currentPage?.colorMode?.let {
|
EditButtons(
|
||||||
ColorModeButton(
|
uiState,
|
||||||
currentColorMode = it,
|
onToggleColorMode,
|
||||||
onToggle = { onToggleColorMode() },
|
navigation,
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.BottomStart)
|
||||||
.align(Alignment.BottomStart)
|
)
|
||||||
.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RotationButtons(onRotateImage, Modifier.align(Alignment.BottomCenter))
|
RotationButtons(onRotateImage, Modifier.align(Alignment.BottomCenter))
|
||||||
SecondaryActionButton(
|
SecondaryActionButton(
|
||||||
Icons.Outlined.Delete,
|
Icons.Outlined.Delete,
|
||||||
@@ -253,6 +253,31 @@ fun RotationButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditButtons(
|
||||||
|
uiState: DocumentUiState,
|
||||||
|
onToggleColorMode: () -> Unit,
|
||||||
|
navigation: Navigation,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier.padding(8.dp)) {
|
||||||
|
uiState.currentPage?.colorMode?.let {
|
||||||
|
ColorModeButton(
|
||||||
|
currentColorMode = it,
|
||||||
|
onToggle = { onToggleColorMode() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
if (uiState.currentPage?.canBeCropped ?: false) {
|
||||||
|
SecondaryActionButton(
|
||||||
|
icon = Icons.Default.Crop,
|
||||||
|
contentDescription = "Crop", // TODO externalize string
|
||||||
|
onClick = navigation.toEditImageScreen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ColorModeButton(
|
fun ColorModeButton(
|
||||||
currentColorMode: ColorMode,
|
currentColorMode: ColorMode,
|
||||||
@@ -348,7 +373,7 @@ fun DocumentScreenPreview() {
|
|||||||
)
|
)
|
||||||
val key = PageViewKey("123", Rotation.R0, null)
|
val key = PageViewKey("123", Rotation.R0, null)
|
||||||
DocumentScreen(
|
DocumentScreen(
|
||||||
uiState = DocumentUiState(1, CurrentPageUiState(key,image, COLOR), document),
|
uiState = DocumentUiState(1, CurrentPageUiState(key,image, COLOR, true), document),
|
||||||
navigation = dummyNavigation(),
|
navigation = dummyNavigation(),
|
||||||
onExportClick = {},
|
onExportClick = {},
|
||||||
onDeleteImage = { },
|
onDeleteImage = { },
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ data class CurrentPageUiState(
|
|||||||
val key: PageViewKey,
|
val key: PageViewKey,
|
||||||
val bitmap: Bitmap?,
|
val bitmap: Bitmap?,
|
||||||
val colorMode: ColorMode?,
|
val colorMode: ColorMode?,
|
||||||
|
val canBeCropped: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ package org.fairscan.app.ui.screens.edit
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Matrix
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
@@ -39,7 +39,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -50,100 +49,43 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.fairscan.app.R
|
import org.fairscan.app.R
|
||||||
import org.fairscan.app.data.ImageRepository
|
|
||||||
import org.fairscan.app.data.ImageTransformations
|
|
||||||
import org.fairscan.app.domain.Rotation
|
|
||||||
import org.fairscan.app.ui.Navigation
|
import org.fairscan.app.ui.Navigation
|
||||||
import org.fairscan.app.ui.components.AppOverflowMenu
|
import org.fairscan.app.ui.components.AppOverflowMenu
|
||||||
import org.fairscan.app.ui.components.BackButton
|
import org.fairscan.app.ui.components.BackButton
|
||||||
import org.fairscan.app.ui.components.ConfirmationDialog
|
|
||||||
import org.fairscan.app.ui.components.MainActionButton
|
import org.fairscan.app.ui.components.MainActionButton
|
||||||
import org.fairscan.app.ui.components.isLandscape
|
import org.fairscan.app.ui.components.isLandscape
|
||||||
import org.fairscan.app.ui.dummyNavigation
|
import org.fairscan.app.ui.dummyNavigation
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
import org.fairscan.app.ui.theme.FairScanTheme
|
||||||
import org.fairscan.imageprocessing.ImageSize
|
|
||||||
import org.fairscan.imageprocessing.Point
|
import org.fairscan.imageprocessing.Point
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPageScreen(
|
fun EditPageScreen(
|
||||||
pageId: String,
|
pageId: String,
|
||||||
imageRepository: ImageRepository,
|
onLoad: (String) -> Unit,
|
||||||
|
initState: CropInitState,
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
onUpdatePageQuad: (String, Quad, onComplete: () -> Unit) -> Unit,
|
onUpdatePageQuad: (String, Quad, onComplete: () -> Unit) -> Unit,
|
||||||
onReportProblem: () -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val showDiscardChangesDialog = rememberSaveable { mutableStateOf(false) }
|
|
||||||
val state = remember { EditPageScreenState() }
|
val state = remember { EditPageScreenState() }
|
||||||
val quadHandler = remember { QuadEditingHandler() }
|
val quadHandler = remember { QuadEditingHandler() }
|
||||||
|
|
||||||
val handleBack = {
|
if (initState is CropInitState.Ready && initState.pageId == pageId) {
|
||||||
if (state.hasUnsavedChanges()) {
|
state.bitmap = initState.bitmap
|
||||||
showDiscardChangesDialog.value = true
|
state.setInitialQuad(initState.quad)
|
||||||
} else {
|
|
||||||
navigation.back()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler { handleBack() }
|
BackHandler { navigation.back() }
|
||||||
|
|
||||||
val isPreview = LocalInspectionMode.current
|
|
||||||
if (isPreview) {
|
|
||||||
val dummyImage = LocalContext.current.assets.open("gallica.bnf.fr-bpt6k5530456s-1.jpg").use { input ->
|
|
||||||
BitmapFactory.decodeStream(input)
|
|
||||||
}
|
|
||||||
state.bitmap = dummyImage
|
|
||||||
state.setInitialQuad(Quad(Point(.1, .1), Point(.9, .1), Point(.9, .9), Point(.1, .9)))
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalRotation = remember { mutableStateOf(Rotation.R0) }
|
|
||||||
|
|
||||||
LaunchedEffect(pageId) {
|
LaunchedEffect(pageId) {
|
||||||
val metadata = imageRepository.getPageMetadata(pageId)
|
onLoad(pageId)
|
||||||
val baseRotation = metadata?.baseRotation ?: Rotation.R0
|
|
||||||
val manualRotation = imageRepository.getManualRotation(pageId)
|
|
||||||
val rotation = baseRotation.add(manualRotation)
|
|
||||||
totalRotation.value = rotation
|
|
||||||
|
|
||||||
val bitmap = withContext(Dispatchers.IO) {
|
|
||||||
val sourceJpegBytes = imageRepository.sourceJpegBytes(pageId)
|
|
||||||
if (sourceJpegBytes != null) {
|
|
||||||
val original = BitmapFactory.decodeByteArray(sourceJpegBytes, 0, sourceJpegBytes.size)
|
|
||||||
if (original != null && rotation != Rotation.R0) {
|
|
||||||
// Adjust the displayed bitmap's rotation to what is in the metadata
|
|
||||||
val matrix = Matrix().apply { postRotate(rotation.degrees.toFloat()) }
|
|
||||||
val rotated = android.graphics.Bitmap.createBitmap(
|
|
||||||
original, 0, 0, original.width, original.height, matrix, true
|
|
||||||
)
|
|
||||||
if (rotated !== original) {
|
|
||||||
original.recycle()
|
|
||||||
}
|
|
||||||
rotated
|
|
||||||
} else {
|
|
||||||
original
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
state.bitmap = bitmap // assigned on the main thread after withContext returns
|
|
||||||
if (metadata?.normalizedQuad != null) {
|
|
||||||
// Rotate the quad to match the rotated bitmap display
|
|
||||||
val rotatedQuad = metadata.normalizedQuad.rotate90(
|
|
||||||
rotation.degrees / 90,
|
|
||||||
ImageSize(1, 1)
|
|
||||||
)
|
|
||||||
state.setInitialQuad(rotatedQuad)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLandscape = isLandscape(LocalConfiguration.current)
|
val isLandscape = isLandscape(LocalConfiguration.current)
|
||||||
@@ -178,7 +120,7 @@ fun EditPageScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BackButton(
|
BackButton(
|
||||||
onClick = handleBack,
|
onClick = navigation.back,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
@@ -198,6 +140,7 @@ fun EditPageScreen(
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
|
/*
|
||||||
val quad = state.editableQuad
|
val quad = state.editableQuad
|
||||||
if (quad != null) {
|
if (quad != null) {
|
||||||
// Reverse the total rotation to get back to original source image coordinates
|
// Reverse the total rotation to get back to original source image coordinates
|
||||||
@@ -210,21 +153,11 @@ fun EditPageScreen(
|
|||||||
} else {
|
} else {
|
||||||
navigation.back()
|
navigation.back()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDiscardChangesDialog.value) {
|
|
||||||
ConfirmationDialog(
|
|
||||||
title = stringResource(R.string.discard_changes),
|
|
||||||
message = stringResource(R.string.discard_changes_warning),
|
|
||||||
showDialog = showDiscardChangesDialog
|
|
||||||
) {
|
|
||||||
state.revertToInitial()
|
|
||||||
navigation.back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -246,7 +179,7 @@ private fun ActionButtons(
|
|||||||
private fun DragQuadOverlay(
|
private fun DragQuadOverlay(
|
||||||
state: EditPageScreenState,
|
state: EditPageScreenState,
|
||||||
quadHandler: QuadEditingHandler,
|
quadHandler: QuadEditingHandler,
|
||||||
bmp: android.graphics.Bitmap
|
bmp: Bitmap
|
||||||
) {
|
) {
|
||||||
if (state.editableQuad == null || state.containerSize == null) return
|
if (state.editableQuad == null || state.containerSize == null) return
|
||||||
|
|
||||||
@@ -409,22 +342,16 @@ private fun DragMagnifyingGlass(state: EditPageScreenState) {
|
|||||||
@Preview(name = "RTL", locale = "ar", showSystemUi = true)
|
@Preview(name = "RTL", locale = "ar", showSystemUi = true)
|
||||||
fun EditPageScreenPreview() {
|
fun EditPageScreenPreview() {
|
||||||
FairScanTheme {
|
FairScanTheme {
|
||||||
|
val dummyImage = LocalContext.current.assets.open("gallica.bnf.fr-bpt6k5530456s-1.jpg").use { input ->
|
||||||
// Minimal no-op ImageTransformations implementation used only for preview.
|
BitmapFactory.decodeStream(input)
|
||||||
val dummyTransformations = object : ImageTransformations {
|
|
||||||
override fun rotate(inputFile: File, outputFile: File, rotationDegrees: Int, jpegQuality: Int) = Unit
|
|
||||||
override fun resize(inputFile: File, outputFile: File, maxSize: Int) = Unit
|
|
||||||
}
|
}
|
||||||
|
val quad = Quad(Point(.1, .1), Point(.9, .1), Point(.9, .9), Point(.1, .9))
|
||||||
// Use a temporary directory for the repository in preview.
|
|
||||||
val tempDir = File(System.getProperty("java.io.tmpdir") ?: "/tmp")
|
|
||||||
val dummyImageRepo = ImageRepository(tempDir, dummyTransformations, 128)
|
|
||||||
|
|
||||||
EditPageScreen(
|
EditPageScreen(
|
||||||
pageId = "preview-page-id",
|
pageId = "123",
|
||||||
imageRepository = dummyImageRepo,
|
onLoad = {},
|
||||||
|
initState = CropInitState.Ready("123",dummyImage, quad),
|
||||||
navigation = dummyNavigation(),
|
navigation = dummyNavigation(),
|
||||||
onUpdatePageQuad = { _, _, onComplete -> onComplete() }
|
onUpdatePageQuad = { _,_,_ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app.ui.screens.edit
|
package org.fairscan.app.ui.screens.edit
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -23,6 +24,16 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
|
|
||||||
|
sealed interface CropInitState {
|
||||||
|
object Loading : CropInitState
|
||||||
|
object Error : CropInitState
|
||||||
|
data class Ready(
|
||||||
|
val pageId: String,
|
||||||
|
val bitmap: Bitmap,
|
||||||
|
val quad: Quad
|
||||||
|
) : CropInitState
|
||||||
|
}
|
||||||
|
|
||||||
class EditPageScreenState {
|
class EditPageScreenState {
|
||||||
companion object {
|
companion object {
|
||||||
val LIFT_WIGGLE_MAX_DISTANCE = 8.dp
|
val LIFT_WIGGLE_MAX_DISTANCE = 8.dp
|
||||||
@@ -116,12 +127,4 @@ class EditPageScreenState {
|
|||||||
initialQuad = quad
|
initialQuad = quad
|
||||||
editableQuad = quad
|
editableQuad = quad
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasUnsavedChanges(): Boolean {
|
|
||||||
return editableQuad != initialQuad
|
|
||||||
}
|
|
||||||
|
|
||||||
fun revertToInitial() {
|
|
||||||
editableQuad = initialQuad
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
<string name="delete_page">Delete page</string>
|
<string name="delete_page">Delete page</string>
|
||||||
<string name="delete_page_warning">Do you want to delete this page?</string>
|
<string name="delete_page_warning">Do you want to delete this page?</string>
|
||||||
<string name="developer">Developer</string>
|
<string name="developer">Developer</string>
|
||||||
<string name="discard_changes">Discard changes</string>
|
|
||||||
<string name="discard_changes_warning">You have unsaved changes. Do you want to discard them?</string>
|
|
||||||
<string name="discard_scan">Discard scan</string>
|
<string name="discard_scan">Discard scan</string>
|
||||||
<string name="download_dirname">Downloads</string>
|
<string name="download_dirname">Downloads</string>
|
||||||
<string name="error">Error: %1$s</string>
|
<string name="error">Error: %1$s</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user