diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderdetail/FolderDetailScreen.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderdetail/FolderDetailScreen.kt index e6c69c5..fc2197a 100644 --- a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderdetail/FolderDetailScreen.kt +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderdetail/FolderDetailScreen.kt @@ -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) -> 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, 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) + } } } } diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderlist/FolderListScreen.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderlist/FolderListScreen.kt index c1f71d8..7557038 100644 --- a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderlist/FolderListScreen.kt +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/folderlist/FolderListScreen.kt @@ -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) -> 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, diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/navigation/NavGraph.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/navigation/NavGraph.kt index eb7625d..ae16b0b 100644 --- a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/navigation/NavGraph.kt +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/navigation/NavGraph.kt @@ -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 = "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 diff --git a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/search/SearchScreen.kt b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/search/SearchScreen.kt index 28f746c..4c9bdd8 100644 --- a/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/search/SearchScreen.kt +++ b/mobile-kotlin/app/src/main/java/com/vaultdrop/mobile/ui/search/SearchScreen.kt @@ -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) -> 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, + ) + } } } } diff --git a/mobile-kotlin/app/src/main/res/values-en/strings.xml b/mobile-kotlin/app/src/main/res/values-en/strings.xml index 83c7334..429184b 100644 --- a/mobile-kotlin/app/src/main/res/values-en/strings.xml +++ b/mobile-kotlin/app/src/main/res/values-en/strings.xml @@ -74,4 +74,28 @@ Incorrect username or password Continue without account Local mode — your folders stay on this device and are not synced. + + + %1$d selected + Assemble PDF + Cancel selection + Assemble a PDF + Nothing here yet. Add files or a note to start assembling. + Add a file + Pick files + Add a note + Text note + Type your note text… + Add + Cancel + Assemble PDF + Remove + Move up + Move down + Unreadable — skipped + Select at least one file or add a note. + Could not generate the PDF. + Could not save the PDF. + PDF created + Generated PDFs \ No newline at end of file diff --git a/mobile-kotlin/app/src/main/res/values/strings.xml b/mobile-kotlin/app/src/main/res/values/strings.xml index 4a0bb7b..55be93b 100644 --- a/mobile-kotlin/app/src/main/res/values/strings.xml +++ b/mobile-kotlin/app/src/main/res/values/strings.xml @@ -82,4 +82,28 @@ Vérification… Mode local Statut serveur — appuyer pour vérifier + + + %1$d sélectionné(s) + Assembler en PDF + Annuler la sélection + Assembler un PDF + Aucun élément. Ajoute des fichiers ou une note pour commencer l\'assemblage. + Ajouter un fichier + Choisir des fichiers + Ajouter une note + Note texte + Saisis le texte de la note… + Ajouter + Annuler + Assembler le PDF + Retirer + Monter + Descendre + Illisible — ignoré + Sélectionne au moins un fichier ou ajoute une note. + Impossible de générer le PDF. + Impossible d\'enregistrer le PDF. + PDF créé + PDF générés \ No newline at end of file