Refactor UI to remove blocking image loading and support suspend APIs
This commit is contained in:
@@ -53,7 +53,7 @@ import org.fairscan.app.data.ImageRepository
|
|||||||
import org.fairscan.app.ui.Navigation
|
import org.fairscan.app.ui.Navigation
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.components.rememberCameraPermissionState
|
import org.fairscan.app.ui.components.rememberCameraPermissionState
|
||||||
import org.fairscan.app.ui.screens.DocumentScreen
|
import org.fairscan.app.ui.screens.document.DocumentScreen
|
||||||
import org.fairscan.app.ui.screens.LibrariesScreen
|
import org.fairscan.app.ui.screens.LibrariesScreen
|
||||||
import org.fairscan.app.ui.screens.about.AboutEvent
|
import org.fairscan.app.ui.screens.about.AboutEvent
|
||||||
import org.fairscan.app.ui.screens.about.AboutScreen
|
import org.fairscan.app.ui.screens.about.AboutScreen
|
||||||
@@ -62,6 +62,7 @@ import org.fairscan.app.ui.screens.about.createEmailWithImageIntent
|
|||||||
import org.fairscan.app.ui.screens.camera.CameraEvent
|
import org.fairscan.app.ui.screens.camera.CameraEvent
|
||||||
import org.fairscan.app.ui.screens.camera.CameraScreen
|
import org.fairscan.app.ui.screens.camera.CameraScreen
|
||||||
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
||||||
|
import org.fairscan.app.ui.screens.document.DocumentUiState
|
||||||
import org.fairscan.app.ui.screens.export.ExportActions
|
import org.fairscan.app.ui.screens.export.ExportActions
|
||||||
import org.fairscan.app.ui.screens.export.ExportEvent
|
import org.fairscan.app.ui.screens.export.ExportEvent
|
||||||
import org.fairscan.app.ui.screens.export.ExportResult
|
import org.fairscan.app.ui.screens.export.ExportResult
|
||||||
@@ -123,6 +124,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
||||||
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
|
val currentPageIndex by viewModel.currentPageIndex.collectAsStateWithLifecycle()
|
||||||
|
val currentPageBitmap by viewModel.currentPageBitmap.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)
|
||||||
@@ -152,7 +155,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
navigation.toExportScreen
|
navigation.toExportScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val screen = currentScreen) {
|
when (currentScreen) {
|
||||||
is Screen.Main.Home -> {
|
is Screen.Main.Home -> {
|
||||||
val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle()
|
val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle()
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
@@ -177,13 +180,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
is Screen.Main.Document -> {
|
is Screen.Main.Document -> {
|
||||||
DocumentScreen (
|
DocumentScreen (
|
||||||
document = document,
|
uiState = DocumentUiState(currentPageIndex, currentPageBitmap, document),
|
||||||
initialPage = screen.initialPage,
|
|
||||||
navigation = navigation,
|
navigation = navigation,
|
||||||
onExportClick = onExportClick,
|
onExportClick = onExportClick,
|
||||||
onDeleteImage = { id -> viewModel.deletePage(id) },
|
onDeleteImage = { id -> viewModel.deletePage(id) },
|
||||||
onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) },
|
onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) },
|
||||||
onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) },
|
onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) },
|
||||||
|
onPageSelected = viewModel::onPageSelected
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Screen.Main.Export -> {
|
is Screen.Main.Export -> {
|
||||||
|
|||||||
@@ -21,21 +21,26 @@ 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.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
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.PageViewKey
|
|
||||||
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.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
|
import org.fairscan.app.ui.state.PageThumbnail
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
||||||
|
|
||||||
@@ -53,18 +58,40 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
val documentUiModel: StateFlow<DocumentUiModel> =
|
val documentUiModel: StateFlow<DocumentUiModel> =
|
||||||
_pages.map { pages ->
|
_pages.map { pages ->
|
||||||
DocumentUiModel(
|
pages.map {
|
||||||
pageKeys = pages.map { it.key() }.toImmutableList(),
|
val jpeg = imageRepository.getThumbnail(it.key())
|
||||||
imageLoader = ::getBitmap,
|
PageThumbnail(it.key(), jpeg?.toBitmap())
|
||||||
thumbnailLoader = ::getThumbnail,
|
}.toImmutableList()
|
||||||
)
|
}
|
||||||
}.stateIn(
|
.flowOn(Dispatchers.IO)
|
||||||
|
.map { DocumentUiModel(it) }
|
||||||
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
initialValue = DocumentUiModel(persistentListOf(), ::getBitmap, ::getThumbnail)
|
initialValue = DocumentUiModel(persistentListOf())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val _currentPageIndex = MutableStateFlow(0)
|
||||||
|
val currentPageIndex: StateFlow<Int> =
|
||||||
|
_currentPageIndex.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val currentPageBitmap: StateFlow<Bitmap?> =
|
||||||
|
_currentPageIndex
|
||||||
|
.combine(_pages) { index, pages -> pages.getOrNull(index) }
|
||||||
|
.mapLatest { page ->
|
||||||
|
page?.let { imageRepository.jpegBytes(it.key())?.toBitmap() }
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
fun onPageSelected(index: Int) {
|
||||||
|
_currentPageIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
fun navigateTo(destination: Screen) {
|
fun navigateTo(destination: Screen) {
|
||||||
|
if (destination is Screen.Main.Document) {
|
||||||
|
_currentPageIndex.value = min(_pages.value.size - 1, destination.initialPage)
|
||||||
|
}
|
||||||
_navigationState.update { it.navigateTo(destination) }
|
_navigationState.update { it.navigateTo(destination) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +126,13 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
imageRepository.pages()
|
imageRepository.pages()
|
||||||
}
|
}
|
||||||
_pages.value = pages
|
_pages.value = pages
|
||||||
|
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
navigateTo(Screen.Main.Camera)
|
||||||
|
_currentPageIndex.value = 0
|
||||||
|
} else if (_currentPageIndex.value >= pages.size) {
|
||||||
|
_currentPageIndex.value = pages.size - 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,15 +145,8 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBitmap(key: PageViewKey): Bitmap? {
|
private fun ByteArray.toBitmap() : Bitmap =
|
||||||
val bytes = imageRepository.jpegBytes(key)
|
BitmapFactory.decodeByteArray(this, 0, this.size)
|
||||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getThumbnail(key: PageViewKey): Bitmap? {
|
|
||||||
val bytes = imageRepository.getThumbnail(key)
|
|
||||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleImageCaptured(capturedPage: CapturedPage) {
|
fun handleImageCaptured(capturedPage: CapturedPage) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package org.fairscan.app.ui
|
package org.fairscan.app.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -22,21 +23,25 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
import org.fairscan.app.domain.PageViewKey
|
import org.fairscan.app.domain.PageViewKey
|
||||||
import org.fairscan.app.domain.Rotation
|
import org.fairscan.app.domain.Rotation
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
|
import org.fairscan.app.ui.state.PageThumbnail
|
||||||
|
|
||||||
fun dummyNavigation(): Navigation {
|
fun dummyNavigation(): Navigation {
|
||||||
return Navigation({}, {}, {}, {}, {}, {}, {}, {})
|
return Navigation({}, {}, {}, {}, {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fakeDocument(): DocumentUiModel {
|
fun fakeDocument(): DocumentUiModel {
|
||||||
return DocumentUiModel(persistentListOf(), { _ -> null }, { _ -> null })
|
return DocumentUiModel(persistentListOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {
|
||||||
val loader = { key: PageViewKey ->
|
val pageKeys = pageIds.map {
|
||||||
context.assets.open("${key.pageId}.jpg").use { input ->
|
PageThumbnail(PageViewKey(it, Rotation.R0), fakeImage(it, context))
|
||||||
BitmapFactory.decodeStream(input)
|
}.toImmutableList()
|
||||||
}
|
return DocumentUiModel(pageKeys)
|
||||||
}
|
|
||||||
val pageKeys = pageIds.map { PageViewKey(it, Rotation.R0) }.toImmutableList()
|
|
||||||
return DocumentUiModel(pageKeys, loader, loader)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fakeImage(id: String, context: Context): Bitmap =
|
||||||
|
context.assets.open("${id}.jpg").use { input ->
|
||||||
|
BitmapFactory.decodeStream(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ fun CommonPageList(
|
|||||||
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
val reorderableLazyListState = rememberReorderableLazyListState(state.listState) { from, to ->
|
val reorderableLazyListState = rememberReorderableLazyListState(state.listState) { from, to ->
|
||||||
val pageId = state.document.pageKeys[from.index].pageId
|
val pageId = state.document.pages[from.index].key.pageId
|
||||||
state.onPageReorder(pageId, to.index)
|
state.onPageReorder(pageId, to.index)
|
||||||
}
|
}
|
||||||
val content: LazyListScope.() -> Unit = {
|
val content: LazyListScope.() -> Unit = {
|
||||||
itemsIndexed(state.document.pageKeys, key = { _, item -> item.saveKey}) { index, item ->
|
itemsIndexed(state.document.pages.map { it.key }, key = { _, item -> item.saveKey}) { index, item ->
|
||||||
ReorderableItem(reorderableLazyListState, key = item.saveKey) { isDragging ->
|
ReorderableItem(reorderableLazyListState, key = item.saveKey) { isDragging ->
|
||||||
val borderColor =
|
val borderColor =
|
||||||
if (isDragging) MaterialTheme.colorScheme.primary else Color.Transparent
|
if (isDragging) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||||
@@ -94,7 +94,7 @@ fun CommonPageList(
|
|||||||
color = borderColor,
|
color = borderColor,
|
||||||
shape = RoundedCornerShape(6.dp)
|
shape = RoundedCornerShape(6.dp)
|
||||||
)
|
)
|
||||||
val image = state.document.loadThumbnail(index)
|
val image = state.document.thumbnail(index)
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
PageThumbnail(image, index, state, modifier)
|
PageThumbnail(image, index, state, modifier)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.fairscan.app.ui.screens
|
package org.fairscan.app.ui.screens.document
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
@@ -45,8 +45,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableIntState
|
|
||||||
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.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@@ -74,44 +72,38 @@ import org.fairscan.app.ui.components.MyScaffold
|
|||||||
import org.fairscan.app.ui.components.SecondaryActionButton
|
import org.fairscan.app.ui.components.SecondaryActionButton
|
||||||
import org.fairscan.app.ui.dummyNavigation
|
import org.fairscan.app.ui.dummyNavigation
|
||||||
import org.fairscan.app.ui.fakeDocument
|
import org.fairscan.app.ui.fakeDocument
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.fakeImage
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
import org.fairscan.app.ui.theme.FairScanTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DocumentScreen(
|
fun DocumentScreen(
|
||||||
document: DocumentUiModel,
|
uiState: DocumentUiState,
|
||||||
initialPage: Int,
|
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
onExportClick: () -> Unit,
|
onExportClick: () -> Unit,
|
||||||
onDeleteImage: (String) -> Unit,
|
onDeleteImage: (String) -> Unit,
|
||||||
onRotateImage: (String, Boolean) -> Unit,
|
onRotateImage: (String, Boolean) -> Unit,
|
||||||
onPageReorder: (String, Int) -> Unit,
|
onPageReorder: (String, Int) -> Unit,
|
||||||
|
onPageSelected: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
// TODO Check how often images are loaded
|
|
||||||
val showDeletePageDialog = rememberSaveable { mutableStateOf(false) }
|
val showDeletePageDialog = rememberSaveable { mutableStateOf(false) }
|
||||||
val currentPageIndex = rememberSaveable { mutableIntStateOf(initialPage) }
|
|
||||||
if (currentPageIndex.intValue >= document.pageCount()) {
|
val document = uiState.document
|
||||||
currentPageIndex.intValue = document.pageCount() - 1
|
val currentPageIndex = uiState.currentPageIndex
|
||||||
}
|
|
||||||
if (currentPageIndex.intValue < 0) {
|
|
||||||
navigation.toCameraScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
BackHandler { navigation.back() }
|
BackHandler { navigation.back() }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
LaunchedEffect(initialPage) {
|
LaunchedEffect(currentPageIndex) {
|
||||||
listState.scrollToItem(initialPage)
|
listState.scrollToItem(currentPageIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
MyScaffold(
|
MyScaffold(
|
||||||
navigation = navigation,
|
navigation = navigation,
|
||||||
pageListState = CommonPageListState(
|
pageListState = CommonPageListState(
|
||||||
document,
|
document,
|
||||||
onPageClick = { index -> currentPageIndex.intValue = index },
|
onPageClick = { index -> onPageSelected(index) },
|
||||||
onPageReorder = onPageReorder,
|
onPageReorder = onPageReorder,
|
||||||
currentPageIndex = currentPageIndex.intValue,
|
currentPageIndex = currentPageIndex,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
showPageNumbers = true,
|
showPageNumbers = true,
|
||||||
),
|
),
|
||||||
@@ -121,8 +113,7 @@ fun DocumentScreen(
|
|||||||
},
|
},
|
||||||
) { modifier ->
|
) { modifier ->
|
||||||
DocumentPreview(
|
DocumentPreview(
|
||||||
document,
|
uiState,
|
||||||
currentPageIndex,
|
|
||||||
{ showDeletePageDialog.value = true },
|
{ showDeletePageDialog.value = true },
|
||||||
onRotateImage,
|
onRotateImage,
|
||||||
modifier
|
modifier
|
||||||
@@ -132,20 +123,21 @@ fun DocumentScreen(
|
|||||||
title = stringResource(R.string.delete_page),
|
title = stringResource(R.string.delete_page),
|
||||||
message = stringResource(R.string.delete_page_warning),
|
message = stringResource(R.string.delete_page_warning),
|
||||||
showDialog = showDeletePageDialog
|
showDialog = showDeletePageDialog
|
||||||
) { onDeleteImage(document.pageId(currentPageIndex.intValue)) }
|
) { onDeleteImage(document.pageId(currentPageIndex)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DocumentPreview(
|
private fun DocumentPreview(
|
||||||
document: DocumentUiModel,
|
uiState: DocumentUiState,
|
||||||
currentPageIndex: MutableIntState,
|
|
||||||
onDeleteImage: (String) -> Unit,
|
onDeleteImage: (String) -> Unit,
|
||||||
onRotateImage: (String, Boolean) -> Unit,
|
onRotateImage: (String, Boolean) -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val imageId = document.pageId(currentPageIndex.intValue)
|
val currentPageIndex = uiState.currentPageIndex
|
||||||
|
val document = uiState.document
|
||||||
|
val imageId = document.pageId(currentPageIndex)
|
||||||
Column (
|
Column (
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
@@ -153,7 +145,7 @@ private fun DocumentPreview(
|
|||||||
Box (
|
Box (
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
val bitmap = document.load(currentPageIndex.intValue)
|
val bitmap = uiState.currentPageBitmap
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val imageBitmap = bitmap.asImageBitmap()
|
val imageBitmap = bitmap.asImageBitmap()
|
||||||
val zoomState = remember(imageId) {
|
val zoomState = remember(imageId) {
|
||||||
@@ -184,7 +176,7 @@ private fun DocumentPreview(
|
|||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
)
|
)
|
||||||
Text("${currentPageIndex.intValue + 1} / ${document.pageCount()}",
|
Text("${currentPageIndex + 1} / ${document.pageCount()}",
|
||||||
color = MaterialTheme.colorScheme.inverseOnSurface,
|
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
@@ -266,17 +258,19 @@ private fun BottomBar(
|
|||||||
@Preview(name = "Landscape", showBackground = true, widthDp = 640, heightDp = 320)
|
@Preview(name = "Landscape", showBackground = true, widthDp = 640, heightDp = 320)
|
||||||
fun DocumentScreenPreview() {
|
fun DocumentScreenPreview() {
|
||||||
FairScanTheme {
|
FairScanTheme {
|
||||||
|
val image = fakeImage("gallica.bnf.fr-bpt6k5530456s-1", LocalContext.current)
|
||||||
|
val document = fakeDocument(
|
||||||
|
listOf(1, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it" }.toImmutableList(),
|
||||||
|
LocalContext.current
|
||||||
|
)
|
||||||
DocumentScreen(
|
DocumentScreen(
|
||||||
fakeDocument(
|
uiState = DocumentUiState(1, image, document),
|
||||||
listOf(1, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it" }.toImmutableList(),
|
|
||||||
LocalContext.current
|
|
||||||
),
|
|
||||||
initialPage = 1,
|
|
||||||
navigation = dummyNavigation(),
|
navigation = dummyNavigation(),
|
||||||
onExportClick = {},
|
onExportClick = {},
|
||||||
onDeleteImage = { _ -> },
|
onDeleteImage = { _ -> },
|
||||||
onRotateImage = { _,_ -> },
|
onRotateImage = { _,_ -> },
|
||||||
onPageReorder = { _,_ -> },
|
onPageReorder = { _,_ -> },
|
||||||
|
onPageSelected = { _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025-2026 Pierre-Yves Nicolas
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.fairscan.app.ui.screens.document
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
|
|
||||||
|
data class DocumentUiState(
|
||||||
|
val currentPageIndex: Int,
|
||||||
|
val currentPageBitmap: Bitmap?,
|
||||||
|
val document: DocumentUiModel,
|
||||||
|
)
|
||||||
@@ -259,7 +259,7 @@ private fun PdfInfos(
|
|||||||
val result = uiState.result
|
val result = uiState.result
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
val thumbnail = currentDocument.loadThumbnail(0)
|
val thumbnail = currentDocument.thumbnail(0)
|
||||||
thumbnail?.let {
|
thumbnail?.let {
|
||||||
Card(
|
Card(
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ fun OngoingScanBanner(
|
|||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
currentDocument.load(0)?.let {
|
currentDocument.thumbnail(0)?.let {
|
||||||
Image(
|
Image(
|
||||||
bitmap = it.asImageBitmap(),
|
bitmap = it.asImageBitmap(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|||||||
@@ -19,26 +19,26 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
import org.fairscan.app.domain.PageViewKey
|
import org.fairscan.app.domain.PageViewKey
|
||||||
|
|
||||||
data class DocumentUiModel(
|
data class DocumentUiModel(
|
||||||
val pageKeys: ImmutableList<PageViewKey>,
|
val pages: ImmutableList<PageThumbnail>,
|
||||||
private val imageLoader: (PageViewKey) -> Bitmap?,
|
|
||||||
private val thumbnailLoader: (PageViewKey) -> Bitmap?
|
|
||||||
) {
|
) {
|
||||||
fun pageCount(): Int {
|
fun pageCount(): Int {
|
||||||
return pageKeys.size
|
return pages.size
|
||||||
}
|
}
|
||||||
fun pageId(index: Int): String {
|
fun pageId(index: Int): String {
|
||||||
return pageKeys[index].pageId
|
return pages[index].key.pageId
|
||||||
}
|
}
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return pageKeys.isEmpty()
|
return pages.isEmpty()
|
||||||
}
|
}
|
||||||
fun lastIndex(): Int {
|
fun lastIndex(): Int {
|
||||||
return pageKeys.lastIndex
|
return pages.lastIndex
|
||||||
}
|
}
|
||||||
fun load(index: Int): Bitmap? {
|
fun thumbnail(index: Int): Bitmap? {
|
||||||
return imageLoader(pageKeys[index])
|
return pages[index].thumbnail
|
||||||
}
|
}
|
||||||
fun loadThumbnail(index: Int): Bitmap? {
|
}
|
||||||
return thumbnailLoader(pageKeys[index])
|
|
||||||
}
|
data class PageThumbnail(
|
||||||
}
|
val key: PageViewKey,
|
||||||
|
val thumbnail: Bitmap?,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user