multi select and generate pdf
This commit is contained in:
+82
-15
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
-12
@@ -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>
|
||||
Reference in New Issue
Block a user