create folders
This commit is contained in:
+34
@@ -16,6 +16,7 @@ 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.CreateNewFolder
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.MergeType
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
@@ -26,11 +27,16 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -44,6 +50,7 @@ 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.FolderNameDialog
|
||||
import com.vaultdrop.mobile.ui.components.SelectionState
|
||||
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
|
||||
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
|
||||
@@ -64,11 +71,21 @@ fun FolderDetailScreen(
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
|
||||
val selection = rememberSelectionState()
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(uiState.folderMissing) {
|
||||
if (uiState.folderMissing) onBack()
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.createError) {
|
||||
val createError = uiState.createError
|
||||
if (createError != null) {
|
||||
snackbarHostState.showSnackbar(createError)
|
||||
viewModel.clearCreateError()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = selection.active) { selection.clear() }
|
||||
|
||||
Scaffold(
|
||||
@@ -114,6 +131,12 @@ fun FolderDetailScreen(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { showCreateDialog = true }) {
|
||||
Icon(
|
||||
Icons.Filled.CreateNewFolder,
|
||||
contentDescription = stringResource(R.string.create_folder),
|
||||
)
|
||||
}
|
||||
ServerStatusBadge(
|
||||
status = connectionStatus,
|
||||
onClick = connectionStatusViewModel::checkNow,
|
||||
@@ -125,6 +148,7 @@ fun FolderDetailScreen(
|
||||
},
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { padding ->
|
||||
FolderDetailContent(
|
||||
subFolders = uiState.subFolders,
|
||||
@@ -137,6 +161,16 @@ fun FolderDetailScreen(
|
||||
modifier = Modifier.padding(padding),
|
||||
)
|
||||
}
|
||||
|
||||
if (showCreateDialog) {
|
||||
FolderNameDialog(
|
||||
onDismiss = { showCreateDialog = false },
|
||||
onConfirm = { name ->
|
||||
showCreateDialog = false
|
||||
viewModel.createFolder(name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+50
@@ -1,14 +1,19 @@
|
||||
package com.vaultdrop.mobile.ui.folderdetail
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
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.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.data.repository.FileRepository
|
||||
import com.vaultdrop.mobile.data.repository.FolderRepository
|
||||
import com.vaultdrop.mobile.data.repository.SaveFolderInput
|
||||
import com.vaultdrop.mobile.features.saf.SafFolderCreator
|
||||
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
|
||||
@@ -23,6 +28,8 @@ data class FolderDetailUiState(
|
||||
val files: List<FileEntity> = emptyList(),
|
||||
val isRefreshing: Boolean = false,
|
||||
val error: String? = null,
|
||||
/** Erreur transitoire de création — affichée en Snackbar puis effacée. */
|
||||
val createError: String? = null,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
@@ -31,6 +38,8 @@ class FolderDetailViewModel @Inject constructor(
|
||||
private val folderRepository: FolderRepository,
|
||||
private val fileRepository: FileRepository,
|
||||
private val tokenProvider: TokenProvider,
|
||||
private val safFolderCreator: SafFolderCreator,
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ViewModel() {
|
||||
|
||||
private val folderResourceId: String =
|
||||
@@ -78,4 +87,45 @@ class FolderDetailViewModel @Inject constructor(
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un sous-dossier physique dans le dossier courant puis l'enregistre
|
||||
* en Room. Impossible si le dossier parent est cloud-only (pas d'uri SAF).
|
||||
*/
|
||||
fun createFolder(name: String) {
|
||||
val trimmed = name.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
_uiState.update {
|
||||
it.copy(createError = context.getString(R.string.new_folder_name_required))
|
||||
}
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val folder = _uiState.value.folder
|
||||
?: run {
|
||||
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_error)) }
|
||||
return@launch
|
||||
}
|
||||
val physical = folder.uri != null
|
||||
val created = safFolderCreator.createFolder(folder.uri, trimmed)
|
||||
if (created == null) {
|
||||
val message = if (physical) {
|
||||
context.getString(R.string.new_folder_error)
|
||||
} else {
|
||||
context.getString(R.string.new_folder_cloud_only)
|
||||
}
|
||||
_uiState.update { it.copy(createError = message) }
|
||||
return@launch
|
||||
}
|
||||
folderRepository.saveFolder(
|
||||
input = SaveFolderInput(uri = created.toString(), name = trimmed, exists = true),
|
||||
parentResourceId = folder.resourceId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Consomme une erreur transitoire de création (Snackbar). */
|
||||
fun clearCreateError() {
|
||||
_uiState.update { it.copy(createError = null) }
|
||||
}
|
||||
}
|
||||
+95
@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -28,6 +29,8 @@ 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.CreateNewFolder
|
||||
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.Button
|
||||
@@ -38,7 +41,10 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -60,9 +66,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vaultdrop.mobile.R
|
||||
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.features.sync.SyncViewModel
|
||||
import com.vaultdrop.mobile.ui.components.FileCategoryIcon
|
||||
import com.vaultdrop.mobile.ui.components.FolderNameDialog
|
||||
import com.vaultdrop.mobile.ui.components.SelectionState
|
||||
import com.vaultdrop.mobile.ui.components.SelectionStatusIcon
|
||||
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
|
||||
@@ -79,6 +87,7 @@ fun FolderListScreen(
|
||||
selectedTab: NavTab,
|
||||
onTabSelected: (NavTab) -> Unit,
|
||||
onOpenDocument: (String) -> Unit,
|
||||
onOpenFolder: (String) -> Unit,
|
||||
onBuildPdf: (List<String>) -> Unit,
|
||||
syncViewModel: SyncViewModel,
|
||||
connectionStatusViewModel: ConnectionStatusViewModel,
|
||||
@@ -91,6 +100,16 @@ fun FolderListScreen(
|
||||
val context = LocalContext.current
|
||||
val folderLabel = stringResource(R.string.folder)
|
||||
val selection = rememberSelectionState()
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val createError = uiState.createError
|
||||
LaunchedEffect(createError) {
|
||||
if (createError != null) {
|
||||
snackbarHostState.showSnackbar(createError)
|
||||
viewModel.clearCreateError()
|
||||
}
|
||||
}
|
||||
|
||||
// Racine par défaut : dossier VaultDrop choisi au premier lancement.
|
||||
val defaultRootLabel = stringResource(R.string.default_root_folder_label)
|
||||
@@ -210,6 +229,7 @@ fun FolderListScreen(
|
||||
bottomBar = {
|
||||
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { padding ->
|
||||
FolderListContent(
|
||||
uiState = uiState,
|
||||
@@ -217,10 +237,22 @@ fun FolderListScreen(
|
||||
error = uiState.error ?: importState.error,
|
||||
selection = selection,
|
||||
onAddFolder = { pickFolderLauncher.launch(null) },
|
||||
onCreateFolder = { showCreateDialog = true },
|
||||
onOpenFolder = onOpenFolder,
|
||||
onOpenDocument = onOpenDocument,
|
||||
modifier = Modifier.padding(padding),
|
||||
)
|
||||
}
|
||||
|
||||
if (showCreateDialog) {
|
||||
FolderNameDialog(
|
||||
onDismiss = { showCreateDialog = false },
|
||||
onConfirm = { name ->
|
||||
showCreateDialog = false
|
||||
viewModel.createFolderInDefaultRoot(name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Écran obligatoire du premier lancement : choix/création de la racine VaultDrop. */
|
||||
@@ -298,6 +330,8 @@ private fun FolderListContent(
|
||||
error: String?,
|
||||
selection: SelectionState,
|
||||
onAddFolder: () -> Unit,
|
||||
onCreateFolder: () -> Unit,
|
||||
onOpenFolder: (String) -> Unit,
|
||||
onOpenDocument: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -317,6 +351,17 @@ private fun FolderListContent(
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.add_folder))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onCreateFolder,
|
||||
enabled = !isImporting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 4.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.CreateNewFolder, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.create_folder))
|
||||
}
|
||||
}
|
||||
|
||||
if (isImporting) {
|
||||
@@ -342,6 +387,15 @@ private fun FolderListContent(
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.subFolders.isNotEmpty()) {
|
||||
item(key = "folders-header") {
|
||||
SectionHeader(stringResource(R.string.folders_section))
|
||||
}
|
||||
items(uiState.subFolders, key = { it.resourceId }) { folder ->
|
||||
FolderRow(folder, onClick = { onOpenFolder(folder.resourceId) })
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.sections.isEmpty() && !isImporting) {
|
||||
item(key = "empty") {
|
||||
Text(
|
||||
@@ -400,6 +454,47 @@ private fun SectionHeader(label: String) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Carte dossier de la section Dossiers — ouvre le dossier au tap. */
|
||||
@Composable
|
||||
private fun FolderRow(folder: FolderEntity, onClick: () -> Unit) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = folder.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.folder),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Carte fichier — clic long pour la sélection multi-fichiers, clic pour ouvrir. */
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
package com.vaultdrop.mobile.ui.folderlist
|
||||
|
||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||
|
||||
/** Section d'un jour dans la grille d'accueil — miroir de `FileSection` (app/index.tsx). */
|
||||
data class FileSection(
|
||||
@@ -18,7 +19,10 @@ data class FilePair(
|
||||
)
|
||||
|
||||
data class FolderListUiState(
|
||||
val subFolders: List<FolderEntity> = emptyList(),
|
||||
val sections: List<FileSection> = emptyList(),
|
||||
val isRefreshing: Boolean = false,
|
||||
val error: String? = null,
|
||||
/** Erreur transitoire de création de dossier — affichée en Snackbar puis effacée. */
|
||||
val createError: String? = null,
|
||||
)
|
||||
+67
@@ -8,14 +8,17 @@ 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.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.data.local.referenceDate
|
||||
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 com.vaultdrop.mobile.features.saf.SafFolderCreator
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -36,6 +39,7 @@ class FolderListViewModel @Inject constructor(
|
||||
private val fileRepository: FileRepository,
|
||||
private val tokenProvider: TokenProvider,
|
||||
private val defaultRootStore: DefaultRootStore,
|
||||
private val safFolderCreator: SafFolderCreator,
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -46,7 +50,10 @@ class FolderListViewModel @Inject constructor(
|
||||
private val _defaultRootId = MutableStateFlow(defaultRootStore.get())
|
||||
val defaultRootId: StateFlow<String?> = _defaultRootId.asStateFlow()
|
||||
|
||||
private var subFolderJob: Job? = null
|
||||
|
||||
init {
|
||||
observeSubFolders()
|
||||
observeFiles()
|
||||
refresh()
|
||||
}
|
||||
@@ -94,6 +101,66 @@ class FolderListViewModel @Inject constructor(
|
||||
return saved.resourceId
|
||||
}
|
||||
|
||||
/**
|
||||
* Sous-dossiers visibles de la racine par défaut, ré-abonnés quand la
|
||||
* racine change (onboarding). Un dossier créé y apparaît immédiatement.
|
||||
*/
|
||||
private fun observeSubFolders() {
|
||||
viewModelScope.launch {
|
||||
_defaultRootId.collect { rootId ->
|
||||
subFolderJob?.cancel()
|
||||
if (rootId == null) {
|
||||
_uiState.update { it.copy(subFolders = emptyList()) }
|
||||
return@collect
|
||||
}
|
||||
subFolderJob = viewModelScope.launch {
|
||||
folderRepository.observeSubFolders(rootId).collect { subFolders ->
|
||||
_uiState.update { it.copy(subFolders = subFolders) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un dossier physique dans la racine par défaut (VaultDrop) puis
|
||||
* l'enregistre en Room comme enfant de la racine.
|
||||
*/
|
||||
fun createFolderInDefaultRoot(name: String) {
|
||||
val trimmed = name.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_name_required)) }
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val rootId = _defaultRootId.value
|
||||
val root = rootId?.let { folderRepository.getFolder(it) }
|
||||
if (root == null) {
|
||||
_uiState.update { it.copy(createError = context.getString(R.string.new_folder_error)) }
|
||||
return@launch
|
||||
}
|
||||
val message = if (root.uri == null) {
|
||||
context.getString(R.string.new_folder_cloud_only)
|
||||
} else {
|
||||
context.getString(R.string.new_folder_error)
|
||||
}
|
||||
val created = safFolderCreator.createFolder(root.uri, trimmed)
|
||||
if (created == null) {
|
||||
_uiState.update { it.copy(createError = message) }
|
||||
return@launch
|
||||
}
|
||||
folderRepository.saveFolder(
|
||||
input = SaveFolderInput(uri = created.toString(), name = trimmed, exists = true),
|
||||
parentResourceId = root.resourceId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Consomme une erreur transitoire de création (Snackbar). */
|
||||
fun clearCreateError() {
|
||||
_uiState.update { it.copy(createError = null) }
|
||||
}
|
||||
|
||||
/** Grille d'accueil : tous les fichiers visibles, groupés par jour (date de référence). */
|
||||
private fun observeFiles() {
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -80,6 +80,7 @@ fun NavGraph(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected,
|
||||
onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
|
||||
onOpenFolder = { id -> navController.navigate(Routes.folder(id)) },
|
||||
onBuildPdf = { ids -> navController.navigate(Routes.pdfBuilder(ids)) },
|
||||
syncViewModel = syncViewModel,
|
||||
connectionStatusViewModel = connectionStatusViewModel,
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
<string name="default_root_pick">Choose my VaultDrop folder</string>
|
||||
<string name="default_root_folder_label">VaultDrop</string>
|
||||
<string name="add_folder">Add a folder</string>
|
||||
<string name="create_folder">Create a folder</string>
|
||||
<string name="new_folder_name_title">Folder name</string>
|
||||
<string name="new_folder_name_hint">e.g. invoices</string>
|
||||
<string name="new_folder_name_required">The folder name cannot be empty.</string>
|
||||
<string name="new_folder_cloud_only">This folder is cloud-only — import a physical folder first to create subfolders inside it.</string>
|
||||
<string name="new_folder_error">Could not create the folder.</string>
|
||||
<string name="folders_section">Folders</string>
|
||||
<string name="no_folders_yet">No folders yet. Tap "Add a folder" to sync a folder.</string>
|
||||
<string name="no_files_yet">No files yet. Tap "Add a folder" to sync your files.</string>
|
||||
<string name="empty_folder">Empty folder — next sync will refresh it.</string>
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
<string name="files">Fichiers</string>
|
||||
<string name="folder">Dossier</string>
|
||||
<string name="add_folder">Ajouter un dossier</string>
|
||||
<string name="create_folder">Créer un dossier</string>
|
||||
<string name="new_folder_name_title">Nom du dossier</string>
|
||||
<string name="new_folder_name_hint">ex. factures</string>
|
||||
<string name="new_folder_name_required">Le nom du dossier ne peut pas être vide.</string>
|
||||
<string name="new_folder_cloud_only">Ce dossier est disponible uniquement sur le cloud — importe d\'abord un dossier physique pour y créer des sous-dossiers.</string>
|
||||
<string name="new_folder_error">Impossible de créer le dossier.</string>
|
||||
<string name="folders_section">Dossiers</string>
|
||||
<string name="no_folders_yet">Aucun dossier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser un dossier.</string>
|
||||
<string name="no_files_yet">Aucun fichier pour le moment. Appuie sur « Ajouter un dossier » pour synchroniser tes fichiers.</string>
|
||||
<string name="empty_folder">Dossier vide — le prochain syncDevice l\'actualisera.</string>
|
||||
|
||||
Reference in New Issue
Block a user