focus on document
This commit is contained in:
+146
-4
@@ -1,6 +1,11 @@
|
|||||||
package com.vaultdrop.mobile.ui.document
|
package com.vaultdrop.mobile.ui.document
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -8,8 +13,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -21,26 +28,41 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
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.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vaultdrop.mobile.R
|
import com.vaultdrop.mobile.R
|
||||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||||
|
import com.vaultdrop.mobile.domain.FileCategory
|
||||||
|
import com.vaultdrop.mobile.ui.components.categoryValue
|
||||||
import com.vaultdrop.mobile.ui.document.content.DocumentContentViewer
|
import com.vaultdrop.mobile.ui.document.content.DocumentContentViewer
|
||||||
|
import com.vaultdrop.mobile.ui.document.content.PdfPageViewer
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -52,6 +74,9 @@ import java.util.Locale
|
|||||||
* Consultation d'un document : lecteur intégré (PDF / image / texte) ou fallback
|
* Consultation d'un document : lecteur intégré (PDF / image / texte) ou fallback
|
||||||
* externe, carte de métadonnées, navigation au swipe entre les documents (même
|
* externe, carte de métadonnées, navigation au swipe entre les documents (même
|
||||||
* fil que l'écran d'accueil).
|
* fil que l'écran d'accueil).
|
||||||
|
*
|
||||||
|
* Les PDF locaux passent en mode plein écran sur tap : barres système et barre
|
||||||
|
* d'outils masquées, document verrouillé, indication de page en bas.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -86,9 +111,41 @@ fun DocumentViewerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fullscreen by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val canFullscreen = currentFile?.canPdfFullscreen() == true
|
||||||
|
|
||||||
|
// Mode immersif : masque les barres système en plein écran, les restaure
|
||||||
|
// à la sortie ou si l'écran est composé autrement.
|
||||||
|
val view = LocalView.current
|
||||||
|
DisposableEffect(fullscreen, canFullscreen) {
|
||||||
|
val window = (view.context as? Activity)?.window
|
||||||
|
if (window != null) {
|
||||||
|
val controller = WindowCompat.getInsetsController(window, view)
|
||||||
|
if (fullscreen && canFullscreen) {
|
||||||
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
|
} else {
|
||||||
|
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDispose {
|
||||||
|
val restoredWindow = (view.context as? Activity)?.window
|
||||||
|
if (restoredWindow != null) {
|
||||||
|
WindowCompat.getInsetsController(restoredWindow, view)
|
||||||
|
.show(WindowInsetsCompat.Type.systemBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// En plein écran, retour système = retour à la vue classique plutôt que de
|
||||||
|
// quitter le document.
|
||||||
|
BackHandler(enabled = fullscreen && canFullscreen) { fullscreen = false }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
|
if (!(fullscreen && canFullscreen)) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -106,8 +163,18 @@ fun DocumentViewerScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
val current = currentFile
|
||||||
|
if (fullscreen && canFullscreen && current != null) {
|
||||||
|
PdfFullscreenReader(
|
||||||
|
file = current,
|
||||||
|
onOpenExternalFailed = onOpenExternalFailed,
|
||||||
|
onExitFullscreen = { fullscreen = false },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -115,7 +182,68 @@ fun DocumentViewerScreen(
|
|||||||
.padding(padding),
|
.padding(padding),
|
||||||
) { page ->
|
) { page ->
|
||||||
val file = documents.getOrNull(page)
|
val file = documents.getOrNull(page)
|
||||||
if (file != null) DocumentViewerPage(file = file, onOpenExternalFailed = onOpenExternalFailed)
|
if (file != null) {
|
||||||
|
DocumentViewerPage(
|
||||||
|
file = file,
|
||||||
|
onOpenExternalFailed = onOpenExternalFailed,
|
||||||
|
onEnterFullscreen = file.canPdfFullscreen()
|
||||||
|
.takeIf { it }
|
||||||
|
?.let { { fullscreen = true } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lecture PDF focus : le document remplit l'écran (fond noir), un tap quitte le
|
||||||
|
* plein écran, le défilement vertical reste actif et une pastille indique la
|
||||||
|
* page courante.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PdfFullscreenReader(
|
||||||
|
file: FileEntity,
|
||||||
|
onOpenExternalFailed: () -> Unit,
|
||||||
|
onExitFullscreen: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val contentResolver = LocalContext.current.contentResolver
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
var pageCount by remember(file.resourceId) { mutableIntStateOf(0) }
|
||||||
|
val currentPage by remember { derivedStateOf { listState.firstVisibleItemIndex + 1 } }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
.pointerInput(file.resourceId) {
|
||||||
|
detectTapGestures(onTap = { onExitFullscreen() })
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
PdfPageViewer(
|
||||||
|
file = file,
|
||||||
|
contentResolver = contentResolver,
|
||||||
|
onOpenExternalFailed = onOpenExternalFailed,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
listState = listState,
|
||||||
|
onPageCountChanged = { pageCount = it },
|
||||||
|
)
|
||||||
|
if (pageCount > 0) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
shape = RoundedCornerShape(percent = 50),
|
||||||
|
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.85f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$currentPage / $pageCount",
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.inverseOnSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,22 +252,32 @@ fun DocumentViewerScreen(
|
|||||||
private fun DocumentViewerPage(
|
private fun DocumentViewerPage(
|
||||||
file: FileEntity,
|
file: FileEntity,
|
||||||
onOpenExternalFailed: () -> Unit,
|
onOpenExternalFailed: () -> Unit,
|
||||||
|
onEnterFullscreen: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.then(
|
||||||
|
if (onEnterFullscreen != null) Modifier.pointerInput(onEnterFullscreen) {
|
||||||
|
detectTapGestures(onTap = { onEnterFullscreen() })
|
||||||
|
} else Modifier,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
// Lecteur central : dispatch par catégorie (PDF / image / texte /
|
// Lecteur central : dispatch par catégorie (PDF / image / texte /
|
||||||
// délégation externe), état dédié pour les fichiers cloud-only.
|
// délégation externe), état dédié pour les fichiers cloud-only.
|
||||||
DocumentContentViewer(
|
DocumentContentViewer(
|
||||||
file = file,
|
file = file,
|
||||||
onOpenExternalFailed = onOpenExternalFailed,
|
onOpenExternalFailed = onOpenExternalFailed,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -187,6 +325,10 @@ private fun fileTypeLabel(file: FileEntity): String =
|
|||||||
?: file.mimeType?.uppercase(Locale.getDefault())
|
?: file.mimeType?.uppercase(Locale.getDefault())
|
||||||
?: "—"
|
?: "—"
|
||||||
|
|
||||||
|
/** Vrai si le fichier est un PDF avec un contenu local consultable en plein écran. */
|
||||||
|
private fun FileEntity.canPdfFullscreen(): Boolean =
|
||||||
|
uri != null && categoryValue() == FileCategory.PDF
|
||||||
|
|
||||||
private fun formatDateTime(millis: Long): String {
|
private fun formatDateTime(millis: Long): String {
|
||||||
val locale = Locale.getDefault()
|
val locale = Locale.getDefault()
|
||||||
val dateTime = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDateTime()
|
val dateTime = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDateTime()
|
||||||
|
|||||||
+6
@@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BrokenImage
|
import androidx.compose.material.icons.filled.BrokenImage
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -58,6 +60,8 @@ fun PdfPageViewer(
|
|||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
onOpenExternalFailed: () -> Unit,
|
onOpenExternalFailed: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
listState: LazyListState = rememberLazyListState(),
|
||||||
|
onPageCountChanged: ((Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val uri = file.uri
|
val uri = file.uri
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
@@ -80,6 +84,7 @@ fun PdfPageViewer(
|
|||||||
loadFailed = false
|
loadFailed = false
|
||||||
if (document.load()) {
|
if (document.load()) {
|
||||||
pageCount = document.pageCount
|
pageCount = document.pageCount
|
||||||
|
onPageCountChanged?.invoke(pageCount)
|
||||||
} else {
|
} else {
|
||||||
loadFailed = true
|
loadFailed = true
|
||||||
}
|
}
|
||||||
@@ -101,6 +106,7 @@ fun PdfPageViewer(
|
|||||||
val maxHeightPx = with(density) { maxHeight.toPx().toInt() }
|
val maxHeightPx = with(density) { maxHeight.toPx().toInt() }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
|||||||
Reference in New Issue
Block a user