multi select and generate pdf

This commit is contained in:
m
2026-09-12 11:33:33 +02:00
parent 9d44703400
commit 3a232138bd
6 changed files with 319 additions and 41 deletions
@@ -1,5 +1,8 @@
package com.vaultdrop.mobile.ui.folderdetail
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -12,7 +15,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -39,7 +44,10 @@ import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.data.local.entity.FolderEntity
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.SelectionState
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.rememberSelectionState
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@@ -49,35 +57,70 @@ fun FolderDetailScreen(
onBack: () -> Unit,
onOpenFolder: (String) -> Unit,
onOpenDocument: (String) -> Unit,
onBuildPdf: (List<String>) -> Unit,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: FolderDetailViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val selection = rememberSelectionState()
LaunchedEffect(uiState.folderMissing) {
if (uiState.folderMissing) onBack()
}
BackHandler(enabled = selection.active) { selection.clear() }
Scaffold(
topBar = {
TopAppBar(
title = { Text(uiState.folder?.name ?: stringResource(R.string.folder)) },
title = {
if (selection.active) {
Text(stringResource(R.string.selection_count, selection.ids.size))
} else {
Text(uiState.folder?.name ?: stringResource(R.string.folder))
}
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
)
if (selection.active) {
IconButton(onClick = { selection.clear() }) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.selection_cancel),
)
}
} else {
IconButton(onClick = onBack) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
}
},
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh))
if (selection.active) {
IconButton(
onClick = {
val ids = selection.ids.toList()
selection.clear()
onBuildPdf(ids)
},
enabled = selection.ids.isNotEmpty(),
) {
Icon(
Icons.Filled.MergeType,
contentDescription = stringResource(R.string.selection_assemble),
)
}
} else {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh))
}
}
},
)
@@ -88,6 +131,7 @@ fun FolderDetailScreen(
files = uiState.files,
isRefreshing = uiState.isRefreshing,
error = uiState.error,
selection = selection,
onOpenFolder = onOpenFolder,
onOpenDocument = onOpenDocument,
modifier = Modifier.padding(padding),
@@ -101,6 +145,7 @@ private fun FolderDetailContent(
files: List<FileEntity>,
isRefreshing: Boolean,
error: String?,
selection: SelectionState,
onOpenFolder: (String) -> Unit,
onOpenDocument: (String) -> Unit,
modifier: Modifier = Modifier,
@@ -116,7 +161,11 @@ private fun FolderDetailContent(
FolderRow(folder, onClick = { onOpenFolder(folder.resourceId) })
}
items(files, key = { it.resourceId }) { file ->
FileRow(file, onClick = { onOpenDocument(file.resourceId) })
FileRow(
file = file,
selection = selection,
onOpenDocument = { onOpenDocument(file.resourceId) },
)
}
when {
empty -> {
@@ -186,12 +235,27 @@ private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FileRow(file: FileEntity, onClick: () -> Unit) {
private fun FileRow(
file: FileEntity,
selection: SelectionState,
onOpenDocument: () -> Unit,
) {
val selected = file.resourceId in selection.ids
Card(
onClick = onClick,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
if (selection.active) selection.toggle(file.resourceId) else onOpenDocument()
},
onLongClick = {
if (!selection.active) selection.start(file.resourceId)
},
),
) {
Row(
modifier = Modifier
@@ -213,6 +277,9 @@ private fun FileRow(file: FileEntity, onClick: () -> Unit) {
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (selection.active) {
SelectionStatusIcon(selected = selected)
}
}
}
}
@@ -5,9 +5,12 @@ import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -23,6 +26,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@@ -52,7 +57,10 @@ import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.features.sync.SyncViewModel
import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.SelectionState
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.rememberSelectionState
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
import java.util.Locale
@@ -63,6 +71,7 @@ fun FolderListScreen(
selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit,
onOpenDocument: (String) -> Unit,
onBuildPdf: (List<String>) -> Unit,
syncViewModel: SyncViewModel,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: FolderListViewModel = hiltViewModel(),
@@ -72,6 +81,7 @@ fun FolderListScreen(
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val context = LocalContext.current
val folderLabel = stringResource(R.string.folder)
val selection = rememberSelectionState()
val pickFolderLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
@@ -90,17 +100,51 @@ fun FolderListScreen(
)
}
BackHandler(enabled = selection.active) { selection.clear() }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.files)) },
title = {
if (selection.active) {
Text(stringResource(R.string.selection_count, selection.ids.size))
} else {
Text(stringResource(R.string.files))
}
},
navigationIcon = {
if (selection.active) {
IconButton(onClick = { selection.clear() }) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.selection_cancel),
)
}
}
},
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh))
if (selection.active) {
IconButton(
onClick = {
val ids = selection.ids.toList()
selection.clear()
onBuildPdf(ids)
},
enabled = selection.ids.isNotEmpty(),
) {
Icon(
Icons.Filled.MergeType,
contentDescription = stringResource(R.string.selection_assemble),
)
}
} else {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh))
}
}
},
)
@@ -113,6 +157,7 @@ fun FolderListScreen(
uiState = uiState,
isImporting = importState.isImporting,
error = uiState.error ?: importState.error,
selection = selection,
onAddFolder = { pickFolderLauncher.launch(null) },
onOpenDocument = onOpenDocument,
modifier = Modifier.padding(padding),
@@ -140,6 +185,7 @@ private fun FolderListContent(
uiState: FolderListUiState,
isImporting: Boolean,
error: String?,
selection: SelectionState,
onAddFolder: () -> Unit,
onOpenDocument: (String) -> Unit,
modifier: Modifier = Modifier,
@@ -212,12 +258,14 @@ private fun FolderListContent(
) {
FileCard(
file = row.left,
selection = selection,
onClick = { onOpenDocument(row.left.resourceId) },
modifier = Modifier.weight(1f),
)
if (row.right != null) {
FileCard(
file = row.right,
selection = selection,
onClick = { onOpenDocument(row.right.resourceId) },
modifier = Modifier.weight(1f),
)
@@ -241,19 +289,38 @@ private fun SectionHeader(label: String) {
)
}
/** Carte fichier — le clic ouvre la consultation du document. */
/** Carte fichier — clic long pour la sélection multi-fichiers, clic pour ouvrir. */
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FileCard(file: FileEntity, onClick: () -> Unit, modifier: Modifier = Modifier) {
private fun FileCard(
file: FileEntity,
selection: SelectionState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val selected = file.resourceId in selection.ids
Card(
onClick = onClick,
colors = CardDefaults.cardColors(containerColor = Color(0xFFF7F8FA)),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
border = BorderStroke(1.dp, Color(0xFFEAEAEA)),
shape = RoundedCornerShape(10.dp),
modifier = modifier,
modifier = modifier.combinedClickable(
onClick = {
if (selection.active) selection.toggle(file.resourceId) else onClick()
},
onLongClick = {
if (!selection.active) selection.start(file.resourceId)
},
),
) {
Column(modifier = Modifier.padding(12.dp)) {
FileCategoryIcon(file = file, size = 36.dp)
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
FileCategoryIcon(file = file, size = 36.dp)
if (selection.active) {
Spacer(Modifier.weight(1f))
SelectionStatusIcon(selected = selected)
}
}
Spacer(Modifier.height(8.dp))
Text(
text = file.name,
@@ -21,6 +21,7 @@ import com.vaultdrop.mobile.ui.auth.AuthViewModel
import com.vaultdrop.mobile.ui.document.DocumentViewerScreen
import com.vaultdrop.mobile.ui.folderdetail.FolderDetailScreen
import com.vaultdrop.mobile.ui.folderlist.FolderListScreen
import com.vaultdrop.mobile.ui.pdfbuilder.PdfBuilderScreen
import com.vaultdrop.mobile.ui.search.SearchScreen
import com.vaultdrop.mobile.ui.settings.SettingsScreen
@@ -32,9 +33,12 @@ object Routes {
const val ARG_FOLDER = "folderResourceId"
const val DOCUMENT = "document/{documentResourceId}"
const val ARG_DOCUMENT = "documentResourceId"
const val PDF_BUILDER = "pdfbuilder/{ids}"
const val ARG_BUILDER_IDS = "ids"
fun folder(folderResourceId: String): String = "folder/$folderResourceId"
fun document(documentResourceId: String): String = "document/$documentResourceId"
fun pdfBuilder(resourceIds: List<String>): String = "pdfbuilder/${resourceIds.joinToString(",")}"
}
private fun String?.toNavTab(): NavTab = when (this) {
@@ -76,6 +80,7 @@ fun NavGraph(
selectedTab = selectedTab,
onTabSelected = onTabSelected,
onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
onBuildPdf = { ids -> navController.navigate(Routes.pdfBuilder(ids)) },
syncViewModel = syncViewModel,
connectionStatusViewModel = connectionStatusViewModel,
)
@@ -85,6 +90,7 @@ fun NavGraph(
selectedTab = selectedTab,
onTabSelected = onTabSelected,
onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
onBuildPdf = { ids -> navController.navigate(Routes.pdfBuilder(ids)) },
connectionStatusViewModel = connectionStatusViewModel,
)
}
@@ -110,6 +116,7 @@ fun NavGraph(
onBack = { navController.popBackStack() },
onOpenFolder = { id -> navController.navigate(Routes.folder(id)) },
onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
onBuildPdf = { ids -> navController.navigate(Routes.pdfBuilder(ids)) },
connectionStatusViewModel = connectionStatusViewModel,
)
}
@@ -128,6 +135,22 @@ fun NavGraph(
connectionStatusViewModel = connectionStatusViewModel,
)
}
composable(
route = Routes.PDF_BUILDER,
arguments = listOf(
navArgument(Routes.ARG_BUILDER_IDS) { type = NavType.StringType },
),
) { backStackEntry ->
val ids = backStackEntry.arguments
?.getString(Routes.ARG_BUILDER_IDS)
.orEmpty()
.split(",")
.filter { it.isNotBlank() }
PdfBuilderScreen(
initialResourceIds = ids,
onBack = { navController.popBackStack() },
)
}
}
// Bandeau global : visible sur tous les onglets tant qu'un import SAF
@@ -1,6 +1,9 @@
package com.vaultdrop.mobile.ui.search
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -17,6 +20,8 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MergeType
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -24,6 +29,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
@@ -46,9 +52,12 @@ import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.domain.FileCategory
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.SelectionState
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.color
import com.vaultdrop.mobile.ui.components.icon
import com.vaultdrop.mobile.ui.components.rememberSelectionState
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab
import java.util.Locale
@@ -59,21 +68,57 @@ fun SearchScreen(
selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit,
onOpenDocument: (String) -> Unit,
onBuildPdf: (List<String>) -> Unit,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: SearchViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val selection = rememberSelectionState()
BackHandler(enabled = selection.active) { selection.clear() }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.search)) },
title = {
if (selection.active) {
Text(stringResource(R.string.selection_count, selection.ids.size))
} else {
Text(stringResource(R.string.search))
}
},
navigationIcon = {
if (selection.active) {
IconButton(onClick = { selection.clear() }) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.selection_cancel),
)
}
}
},
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
if (selection.active) {
IconButton(
onClick = {
val ids = selection.ids.toList()
selection.clear()
onBuildPdf(ids)
},
enabled = selection.ids.isNotEmpty(),
) {
Icon(
Icons.Filled.MergeType,
contentDescription = stringResource(R.string.selection_assemble),
)
}
} else {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
}
},
)
},
@@ -83,6 +128,7 @@ fun SearchScreen(
) { padding ->
SearchContent(
uiState = uiState,
selection = selection,
onQueryChange = viewModel::onQueryChange,
onCategorySelect = viewModel::onCategorySelect,
onOpenDocument = onOpenDocument,
@@ -94,6 +140,7 @@ fun SearchScreen(
@Composable
private fun SearchContent(
uiState: SearchUiState,
selection: SelectionState,
onQueryChange: (String) -> Unit,
onCategorySelect: (String?) -> Unit,
onOpenDocument: (String) -> Unit,
@@ -113,6 +160,7 @@ private fun SearchContent(
leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
placeholder = { Text(stringResource(R.string.search_hint)) },
singleLine = true,
enabled = !selection.active,
)
LazyRow(
@@ -138,6 +186,7 @@ private fun SearchContent(
SearchResults(
uiState = uiState,
selection = selection,
onOpenDocument = onOpenDocument,
modifier = Modifier.weight(1f),
)
@@ -185,6 +234,7 @@ private fun categoryLabel(category: FileCategory): String = when (category) {
@Composable
private fun SearchResults(
uiState: SearchUiState,
selection: SelectionState,
onOpenDocument: (String) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -231,21 +281,40 @@ private fun SearchResults(
}
}
items(uiState.results, key = { it.resourceId }) { file ->
SearchResultCard(file = file, onClick = { onOpenDocument(file.resourceId) })
SearchResultCard(
file = file,
selection = selection,
onOpenDocument = { onOpenDocument(file.resourceId) },
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SearchResultCard(file: FileEntity, onClick: () -> Unit, modifier: Modifier = Modifier) {
private fun SearchResultCard(
file: FileEntity,
selection: SelectionState,
onOpenDocument: () -> Unit,
modifier: Modifier = Modifier,
) {
val selected = file.resourceId in selection.ids
Card(
onClick = onClick,
colors = CardDefaults.cardColors(containerColor = Color(0xFFF7F8FA)),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
border = BorderStroke(1.dp, Color(0xFFEAEAEA)),
shape = RoundedCornerShape(10.dp),
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
if (selection.active) selection.toggle(file.resourceId) else onOpenDocument()
},
onLongClick = {
if (!selection.active) selection.start(file.resourceId)
},
),
) {
Row(
modifier = Modifier.padding(12.dp),
@@ -269,11 +338,15 @@ private fun SearchResultCard(file: FileEntity, onClick: () -> Unit, modifier: Mo
)
}
Spacer(Modifier.width(12.dp))
Text(
text = formatSize(file.size),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (selection.active) {
SelectionStatusIcon(selected = selected)
} else {
Text(
text = formatSize(file.size),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
@@ -74,4 +74,28 @@
<string name="login_error_unauthorized">Incorrect username or password</string>
<string name="login_skip">Continue without account</string>
<string name="skip_message">Local mode — your folders stay on this device and are not synced.</string>
<!-- Multi-select + PDF assembly -->
<string name="selection_count">%1$d selected</string>
<string name="selection_assemble">Assemble PDF</string>
<string name="selection_cancel">Cancel selection</string>
<string name="pdf_builder_title">Assemble a PDF</string>
<string name="pdf_builder_empty">Nothing here yet. Add files or a note to start assembling.</string>
<string name="pdf_builder_add_files">Add a file</string>
<string name="pdf_builder_pick_files">Pick files</string>
<string name="pdf_builder_add_note">Add a note</string>
<string name="pdf_builder_note">Text note</string>
<string name="pdf_builder_note_hint">Type your note text…</string>
<string name="pdf_builder_add">Add</string>
<string name="pdf_builder_cancel">Cancel</string>
<string name="pdf_builder_assemble">Assemble PDF</string>
<string name="pdf_builder_remove">Remove</string>
<string name="pdf_builder_move_up">Move up</string>
<string name="pdf_builder_move_down">Move down</string>
<string name="pdf_builder_unreadable">Unreadable — skipped</string>
<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_saved">PDF created</string>
<string name="pdf_generated_folder">Generated PDFs</string>
</resources>
@@ -82,4 +82,28 @@
<string name="status_checking">Vérification…</string>
<string name="status_local">Mode local</string>
<string name="status_tap_to_recheck">Statut serveur — appuyer pour vérifier</string>
<!-- Sélection multi-fichiers + assemblage PDF -->
<string name="selection_count">%1$d sélectionné(s)</string>
<string name="selection_assemble">Assembler en PDF</string>
<string name="selection_cancel">Annuler la sélection</string>
<string name="pdf_builder_title">Assembler un PDF</string>
<string name="pdf_builder_empty">Aucun élément. Ajoute des fichiers ou une note pour commencer l\'assemblage.</string>
<string name="pdf_builder_add_files">Ajouter un fichier</string>
<string name="pdf_builder_pick_files">Choisir des fichiers</string>
<string name="pdf_builder_add_note">Ajouter une note</string>
<string name="pdf_builder_note">Note texte</string>
<string name="pdf_builder_note_hint">Saisis le texte de la note…</string>
<string name="pdf_builder_add">Ajouter</string>
<string name="pdf_builder_cancel">Annuler</string>
<string name="pdf_builder_assemble">Assembler le PDF</string>
<string name="pdf_builder_remove">Retirer</string>
<string name="pdf_builder_move_up">Monter</string>
<string name="pdf_builder_move_down">Descendre</string>
<string name="pdf_builder_unreadable">Illisible — ignoré</string>
<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_saved">PDF créé</string>
<string name="pdf_generated_folder">PDF générés</string>
</resources>