generate pdf and save them to the directory
This commit is contained in:
+12
-7
@@ -70,6 +70,7 @@ import com.vaultdrop.mobile.ui.components.rememberSelectionState
|
||||
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
|
||||
import com.vaultdrop.mobile.ui.navigation.NavTab
|
||||
import kotlinx.coroutines.delay
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -103,24 +104,28 @@ fun FolderListScreen(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
)
|
||||
}
|
||||
}.onFailure { Timber.w(it, "persistable permission absent on default root pick") }
|
||||
pendingDefaultPick = uri
|
||||
syncViewModel.importRoot(uri = uri.toString(), name = defaultRootLabel)
|
||||
val rootName = uri.displayName(context) ?: uri.lastPathSegment ?: defaultRootLabel
|
||||
syncViewModel.importRoot(uri = uri.toString(), name = rootName)
|
||||
}
|
||||
|
||||
// Attend la création de la racine (importRoot la sauvegarde en Room puis
|
||||
// lance le walk) et l'enregistre comme racine par défaut.
|
||||
// Attend la création de la racine (importRoot la sauvegarde puis lance le
|
||||
// walk), crée le sous-dossier « VaultDrop » et l'enregistre comme racine.
|
||||
LaunchedEffect(pendingDefaultPick) {
|
||||
val uri = pendingDefaultPick ?: return@LaunchedEffect
|
||||
val target = uri.toString()
|
||||
var attempts = 0
|
||||
while (attempts < 100) { // ~10 s max
|
||||
val id = syncViewModel.rootResourceId(target)
|
||||
if (id != null) {
|
||||
val rootId = syncViewModel.rootResourceId(target)
|
||||
if (rootId != null) {
|
||||
val vaultFolderId = viewModel.ensureVaultDropFolder(uri, rootId)
|
||||
if (vaultFolderId != null) {
|
||||
pendingDefaultPick = null
|
||||
viewModel.setDefaultRoot(id)
|
||||
viewModel.setDefaultRoot(vaultFolderId)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
}
|
||||
delay(100)
|
||||
attempts++
|
||||
}
|
||||
|
||||
+45
@@ -1,7 +1,11 @@
|
||||
package com.vaultdrop.mobile.ui.folderlist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vaultdrop.mobile.R
|
||||
import com.vaultdrop.mobile.auth.TokenProvider
|
||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||
import com.vaultdrop.mobile.data.local.referenceDate
|
||||
@@ -9,13 +13,16 @@ import com.vaultdrop.mobile.data.preferences.DefaultRootStore
|
||||
import com.vaultdrop.mobile.data.remote.ApiException
|
||||
import com.vaultdrop.mobile.data.repository.FileRepository
|
||||
import com.vaultdrop.mobile.data.repository.FolderRepository
|
||||
import com.vaultdrop.mobile.data.repository.SaveFolderInput
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.FileNotFoundException
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -29,6 +36,7 @@ class FolderListViewModel @Inject constructor(
|
||||
private val fileRepository: FileRepository,
|
||||
private val tokenProvider: TokenProvider,
|
||||
private val defaultRootStore: DefaultRootStore,
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(FolderListUiState())
|
||||
@@ -49,6 +57,43 @@ class FolderListViewModel @Inject constructor(
|
||||
_defaultRootId.value = resourceId
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée (ou retrouve) un sous-dossier « VaultDrop » dans l'arbre choisi et
|
||||
* l'enregistre en Room comme enfant de la racine. Retourne son id — c'est
|
||||
* lui qui devient la racine par défaut où les PDF sont écrits.
|
||||
*/
|
||||
suspend fun ensureVaultDropFolder(rootUri: Uri, rootRoomId: String): String? {
|
||||
val resolver = context.contentResolver
|
||||
val treeDocId = DocumentsContract.getTreeDocumentId(rootUri)
|
||||
val label = context.getString(R.string.default_root_folder_label)
|
||||
val parentUri = if (DocumentsContract.isTreeUri(rootUri)) {
|
||||
DocumentsContract.buildDocumentUriUsingTree(rootUri, treeDocId)
|
||||
} else {
|
||||
rootUri
|
||||
}
|
||||
val vaultFolderUri = runCatching {
|
||||
DocumentsContract.createDocument(
|
||||
resolver,
|
||||
parentUri,
|
||||
DocumentsContract.Document.MIME_TYPE_DIR,
|
||||
label,
|
||||
)
|
||||
}.getOrElse { e ->
|
||||
if (e is FileNotFoundException) {
|
||||
DocumentsContract.buildDocumentUriUsingTree(rootUri, "$treeDocId/$label")
|
||||
} else {
|
||||
Timber.w(e, "cannot create VaultDrop folder")
|
||||
null
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
val saved = folderRepository.saveFolder(
|
||||
input = SaveFolderInput(uri = vaultFolderUri.toString(), name = label, exists = true),
|
||||
parentResourceId = rootRoomId,
|
||||
)
|
||||
return saved.resourceId
|
||||
}
|
||||
|
||||
/** Grille d'accueil : tous les fichiers visibles, groupés par jour (date de référence). */
|
||||
private fun observeFiles() {
|
||||
viewModelScope.launch {
|
||||
|
||||
+39
-6
@@ -206,7 +206,8 @@ class PdfBuilderViewModel @Inject constructor(
|
||||
runCatching {
|
||||
val root = folderRepository.getFolder(rootResourceId)
|
||||
?: error("default root not found: $rootResourceId")
|
||||
val rootUri = checkNotNull(Uri.parse(root.uri)) { "default root has no uri" }
|
||||
val rootUri = parentDocumentUri(root.uri)
|
||||
?: error("default root has no uri")
|
||||
|
||||
val fileName = if (trimmed.lowercase().endsWith(".pdf")) trimmed else "$trimmed.pdf"
|
||||
val createdUri = DocumentsContract.createDocument(
|
||||
@@ -251,13 +252,39 @@ class PdfBuilderViewModel @Inject constructor(
|
||||
_buildState.value = BuildPhase.Saved(resourceId)
|
||||
}.onFailure { e ->
|
||||
Timber.e(e, "PDF save failed")
|
||||
_buildState.value = BuildPhase.Failed(context.getString(R.string.pdf_builder_save_error))
|
||||
if (e is SecurityException) {
|
||||
defaultRootStore.clear()
|
||||
_buildState.value = BuildPhase.Failed(
|
||||
context.getString(R.string.pdf_builder_save_error_permission),
|
||||
)
|
||||
} else {
|
||||
_buildState.value = BuildPhase.Failed(
|
||||
e.message ?: context.getString(R.string.pdf_builder_save_error),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ helpers
|
||||
|
||||
/**
|
||||
* `DocumentsContract.createDocument` attend un URI *document*, pas un URI
|
||||
* *tree*. Convertit un tree URI en document URI (équivalent au dossier
|
||||
* racine de l'arbre) — les deux autorités sont identiques.
|
||||
*/
|
||||
private fun parentDocumentUri(uri: String?): Uri? {
|
||||
val raw = uri?.let(Uri::parse) ?: return null
|
||||
return if (DocumentsContract.isTreeUri(raw)) {
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
raw,
|
||||
DocumentsContract.getTreeDocumentId(raw),
|
||||
)
|
||||
} else {
|
||||
raw
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyInto(uri: Uri, source: File, resolver: ContentResolver) {
|
||||
val target = resolver.openOutputStream(uri)
|
||||
?: error("cannot open output stream")
|
||||
@@ -281,20 +308,26 @@ class PdfBuilderViewModel @Inject constructor(
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* Associe l'URI créé au dossier SAF importé qui le contient (par préfixe
|
||||
* du documentId), sinon à un dossier racine local « PDF générés ».
|
||||
* Associe l'URI créé au dossier (racine ou sous-dossier) dont le documentId
|
||||
* est le préfixe — plus long match gagne (ex. le sous-dossier « VaultDrop »
|
||||
* plutôt que l'arbre racine). Sinon dossier racine local « PDF générés ».
|
||||
*/
|
||||
private suspend fun resolveTargetFolder(uri: Uri): String {
|
||||
val documentId = runCatching { DocumentsContract.getDocumentId(uri) }.getOrNull()
|
||||
if (documentId != null) {
|
||||
folderRepository.getRootFolders().filter { it.uri != null }.forEach { folder ->
|
||||
val matches = folderRepository.getAll()
|
||||
.filter { it.uri != null }
|
||||
.mapNotNull { folder ->
|
||||
val treeDocId = runCatching {
|
||||
DocumentsContract.getDocumentId(Uri.parse(folder.uri))
|
||||
}.getOrNull()
|
||||
if (treeDocId != null && documentId.startsWith("$treeDocId/")) {
|
||||
return folder.resourceId
|
||||
folder to treeDocId.length
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
matches.maxByOrNull { it.second }?.first?.resourceId?.let { return it }
|
||||
}
|
||||
return generatedFolderResourceId()
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
<string name="pdf_builder_no_items">Select at least one file or add a note.</string>
|
||||
<string name="pdf_builder_error">Could not generate the PDF.</string>
|
||||
<string name="pdf_builder_save_error">Could not save the PDF.</string>
|
||||
<string name="pdf_builder_save_error_permission">Permission denied for the VaultDrop folder. Choose a folder again at next launch.</string>
|
||||
<string name="pdf_builder_name_required">The file name cannot be empty.</string>
|
||||
<string name="pdf_builder_name_title">PDF name</string>
|
||||
<string name="pdf_builder_name_hint">e.g. january_invoices</string>
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
<string name="pdf_builder_no_items">Sélectionne au moins un fichier ou ajoute une note.</string>
|
||||
<string name="pdf_builder_error">Impossible de générer le PDF.</string>
|
||||
<string name="pdf_builder_save_error">Impossible d\'enregistrer le PDF.</string>
|
||||
<string name="pdf_builder_save_error_permission">Permission refusée pour le dossier VaultDrop. Choisis à nouveau un dossier au prochain lancement.</string>
|
||||
<string name="pdf_builder_name_required">Le nom du fichier ne peut pas être vide.</string>
|
||||
<string name="pdf_builder_name_title">Nom du PDF</string>
|
||||
<string name="pdf_builder_name_hint">ex. factures_janvier</string>
|
||||
|
||||
Reference in New Issue
Block a user