add saf config
This commit is contained in:
@@ -24,6 +24,10 @@ interface FolderDao {
|
||||
@Query("SELECT * FROM folders WHERE parent_resource_id IS NULL ORDER BY name ASC")
|
||||
fun observeRootFolders(): Flow<List<FolderEntity>>
|
||||
|
||||
/** Racines physiques surveillées (uri SAF non nulle) — distinctes des racines cloud-only. */
|
||||
@Query("SELECT * FROM folders WHERE parent_resource_id IS NULL AND uri IS NOT NULL ORDER BY name ASC")
|
||||
fun observeSafRoots(): Flow<List<FolderEntity>>
|
||||
|
||||
@Query("SELECT * FROM folders WHERE parent_resource_id IS NULL ORDER BY name ASC")
|
||||
suspend fun getRootFolders(): List<FolderEntity>
|
||||
|
||||
|
||||
+3
@@ -25,6 +25,9 @@ class FolderRepository @Inject constructor(
|
||||
|
||||
fun observeRootFolders(): Flow<List<FolderEntity>> = folderDao.observeRootFolders()
|
||||
|
||||
/** Racines SAF surveillées par le sync (celles qui déclenchent un walk périodique). */
|
||||
fun observeSafRoots(): Flow<List<FolderEntity>> = folderDao.observeSafRoots()
|
||||
|
||||
/** Sous-dossiers visibles d'un dossier (filtre `exists = 1` dans le DAO). */
|
||||
fun observeSubFolders(parentResourceId: String): Flow<List<FolderEntity>> =
|
||||
folderDao.observeByParent(parentResourceId)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.vaultdrop.mobile.features.saf
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.OpenableColumns
|
||||
|
||||
/**
|
||||
* Conversions d'URI SAF partagées par SafFolderCreator et FileMover.
|
||||
@@ -39,3 +41,18 @@ object SafUris {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Nom affiché d'une racine SAF via DocumentsContract (colonne DISPLAY_NAME). */
|
||||
fun Uri.safDisplayName(context: Context): String? = runCatching {
|
||||
val docId = DocumentsContract.getTreeDocumentId(this)
|
||||
val docUri = DocumentsContract.buildDocumentUriUsingTree(this, docId)
|
||||
context.contentResolver.query(
|
||||
docUri,
|
||||
arrayOf(OpenableColumns.DISPLAY_NAME),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) cursor.getString(0) else null
|
||||
}
|
||||
}.getOrNull()
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.vaultdrop.mobile.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vaultdrop.mobile.R
|
||||
import com.vaultdrop.mobile.ui.watchedfolders.WatchedFoldersViewModel
|
||||
|
||||
/** Seuil au-delà duquel le badge affiche « 99+ » au lieu du compte exact. */
|
||||
private const val COUNT_CAP = 99
|
||||
|
||||
/**
|
||||
* Pilule du header affichant le nombre de dossiers SAF surveillés. Un appui
|
||||
* ouvre l'écran listant ces dossiers.
|
||||
*/
|
||||
@Composable
|
||||
fun WatchedFoldersBadge(
|
||||
onClick: () -> Unit,
|
||||
viewModel: WatchedFoldersViewModel = hiltViewModel(),
|
||||
) {
|
||||
val count by viewModel.count.collectAsStateWithLifecycle(initialValue = 0)
|
||||
val label = if (count > COUNT_CAP) {
|
||||
stringResource(R.string.watched_count_many)
|
||||
} else {
|
||||
count.toString()
|
||||
}
|
||||
val contentDescription = stringResource(R.string.settings_saf_folders)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(percent = 50))
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClickLabel = contentDescription,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Visibility,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-55
@@ -1,10 +1,7 @@
|
||||
package com.vaultdrop.mobile.ui.folderlist
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
@@ -73,6 +70,7 @@ 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.saf.safDisplayName
|
||||
import com.vaultdrop.mobile.features.sync.SyncViewModel
|
||||
import com.vaultdrop.mobile.ui.components.FileCategoryIcon
|
||||
import com.vaultdrop.mobile.ui.components.FolderNameDialog
|
||||
@@ -104,7 +102,6 @@ fun FolderListScreen(
|
||||
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
|
||||
val defaultRootId by viewModel.defaultRootId.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val folderLabel = stringResource(R.string.folder)
|
||||
val selection = rememberSelectionState()
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -146,7 +143,7 @@ fun FolderListScreen(
|
||||
)
|
||||
}.onFailure { Timber.w(it, "persistable permission absent on default root pick") }
|
||||
pendingDefaultPick = uri
|
||||
val rootName = uri.displayName(context) ?: uri.lastPathSegment ?: defaultRootLabel
|
||||
val rootName = uri.safDisplayName(context) ?: uri.lastPathSegment ?: defaultRootLabel
|
||||
syncViewModel.importRoot(uri = uri.toString(), name = rootName)
|
||||
}
|
||||
|
||||
@@ -172,23 +169,6 @@ fun FolderListScreen(
|
||||
pendingDefaultPick = null
|
||||
}
|
||||
|
||||
val pickFolderLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree(),
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
// Permissions persistantes : l'app ré-ouvrira le dossier aux prochains lancements.
|
||||
runCatching {
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
)
|
||||
}
|
||||
syncViewModel.importRoot(
|
||||
uri = uri.toString(),
|
||||
name = uri.displayName(context) ?: uri.lastPathSegment ?: folderLabel,
|
||||
)
|
||||
}
|
||||
|
||||
if (defaultRootId == null) {
|
||||
DefaultRootOnboarding(
|
||||
importing = importState.isImporting,
|
||||
@@ -289,7 +269,6 @@ fun FolderListScreen(
|
||||
selection = selection,
|
||||
onBrowseUp = viewModel::browseUp,
|
||||
onOpenBrowseFolder = viewModel::openBrowseFolder,
|
||||
onAddFolder = { pickFolderLauncher.launch(null) },
|
||||
onCreateFolder = { showCreateDialog = true },
|
||||
onOpenDocument = onOpenDocument,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -416,7 +395,6 @@ private fun HomeViewContent(
|
||||
selection: SelectionState,
|
||||
onBrowseUp: () -> Unit,
|
||||
onOpenBrowseFolder: (String) -> Unit,
|
||||
onAddFolder: () -> Unit,
|
||||
onCreateFolder: () -> Unit,
|
||||
onOpenDocument: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -439,7 +417,6 @@ private fun HomeViewContent(
|
||||
error = error,
|
||||
onBrowseUp = onBrowseUp,
|
||||
onOpenBrowseFolder = onOpenBrowseFolder,
|
||||
onAddFolder = onAddFolder,
|
||||
onCreateFolder = onCreateFolder,
|
||||
modifier = modifier,
|
||||
)
|
||||
@@ -541,7 +518,6 @@ private fun FolderBrowserContent(
|
||||
error: String?,
|
||||
onBrowseUp: () -> Unit,
|
||||
onOpenBrowseFolder: (String) -> Unit,
|
||||
onAddFolder: () -> Unit,
|
||||
onCreateFolder: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -582,25 +558,12 @@ private fun FolderBrowserContent(
|
||||
|
||||
if (!moveMode) {
|
||||
item(key = "create") {
|
||||
if (atRoot) {
|
||||
Button(
|
||||
onClick = onAddFolder,
|
||||
enabled = !isImporting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.add_folder))
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onCreateFolder,
|
||||
enabled = !isImporting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = if (atRoot) 8.dp else 4.dp, bottom = 4.dp),
|
||||
.padding(top = 8.dp, bottom = 4.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.CreateNewFolder, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
@@ -705,21 +668,6 @@ private fun DefaultRootOnboarding(
|
||||
}
|
||||
}
|
||||
|
||||
/** Nom affiché d'un dossier SAF via DocumentsContract (DISPLAY_NAME). */
|
||||
private fun Uri.displayName(context: Context): String? = runCatching {
|
||||
val docId = DocumentsContract.getTreeDocumentId(this)
|
||||
val docUri = DocumentsContract.buildDocumentUriUsingTree(this, docId)
|
||||
context.contentResolver.query(
|
||||
docUri,
|
||||
arrayOf(OpenableColumns.DISPLAY_NAME),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) cursor.getString(0) else null
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(label: String) {
|
||||
Text(
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.vaultdrop.mobile.ui.pdfbuilder.PdfBuilderScreen
|
||||
import com.vaultdrop.mobile.ui.review.SwipeReviewScreen
|
||||
import com.vaultdrop.mobile.ui.search.SearchScreen
|
||||
import com.vaultdrop.mobile.ui.settings.SettingsScreen
|
||||
import com.vaultdrop.mobile.ui.watchedfolders.WatchedFoldersScreen
|
||||
|
||||
object Routes {
|
||||
const val FILES = "files"
|
||||
@@ -34,6 +35,7 @@ object Routes {
|
||||
const val SETTINGS = "settings"
|
||||
const val DASHBOARD = "dashboard"
|
||||
const val REVIEW = "review"
|
||||
const val WATCHED_FOLDERS = "watched"
|
||||
const val FOLDER_DETAIL = "folder/{folderResourceId}"
|
||||
const val ARG_FOLDER = "folderResourceId"
|
||||
const val DOCUMENT = "document/{documentResourceId}"
|
||||
@@ -67,6 +69,8 @@ fun NavGraph(
|
||||
|
||||
val importState by syncViewModel.importState.collectAsStateWithLifecycle()
|
||||
|
||||
val onOpenWatchedFolders = { navController.navigate(Routes.WATCHED_FOLDERS) }
|
||||
|
||||
val onTabSelected: (NavTab) -> Unit = { tab ->
|
||||
if (tab.route != selectedTab.route) {
|
||||
navController.navigate(tab.route) {
|
||||
@@ -108,6 +112,7 @@ fun NavGraph(
|
||||
SettingsScreen(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected,
|
||||
onOpenWatchedFolders = onOpenWatchedFolders,
|
||||
authViewModel = authViewModel,
|
||||
connectionStatusViewModel = connectionStatusViewModel,
|
||||
)
|
||||
@@ -126,6 +131,12 @@ fun NavGraph(
|
||||
connectionStatusViewModel = connectionStatusViewModel,
|
||||
)
|
||||
}
|
||||
composable(Routes.WATCHED_FOLDERS) {
|
||||
WatchedFoldersScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
syncViewModel = syncViewModel,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Routes.FOLDER_DETAIL,
|
||||
arguments = listOf(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vaultdrop.mobile.ui.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -14,6 +15,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -51,6 +53,7 @@ import com.vaultdrop.mobile.ui.auth.AuthState
|
||||
import com.vaultdrop.mobile.ui.auth.AuthViewModel
|
||||
import com.vaultdrop.mobile.ui.auth.authErrorResFor
|
||||
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
|
||||
import com.vaultdrop.mobile.ui.components.WatchedFoldersBadge
|
||||
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
|
||||
import com.vaultdrop.mobile.ui.navigation.NavTab
|
||||
|
||||
@@ -59,6 +62,7 @@ import com.vaultdrop.mobile.ui.navigation.NavTab
|
||||
fun SettingsScreen(
|
||||
selectedTab: NavTab,
|
||||
onTabSelected: (NavTab) -> Unit,
|
||||
onOpenWatchedFolders: () -> Unit,
|
||||
authViewModel: AuthViewModel,
|
||||
connectionStatusViewModel: ConnectionStatusViewModel,
|
||||
viewModel: SettingsViewModel = hiltViewModel(),
|
||||
@@ -119,6 +123,17 @@ fun SettingsScreen(
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
SettingsRow(
|
||||
icon = { Icon(Icons.Filled.Folder, contentDescription = null) },
|
||||
title = stringResource(R.string.settings_watched_folders),
|
||||
subtitle = stringResource(R.string.settings_watched_folders_hint),
|
||||
trailing = {
|
||||
WatchedFoldersBadge(onClick = onOpenWatchedFolders)
|
||||
},
|
||||
onClick = onOpenWatchedFolders,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
SettingsSectionTitle(stringResource(R.string.settings_server))
|
||||
|
||||
@@ -241,6 +256,8 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
@@ -251,11 +268,13 @@ private fun SettingsRow(
|
||||
icon: @Composable () -> Unit,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
trailing: @Composable () -> Unit,
|
||||
trailing: @Composable () -> Unit = {},
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package com.vaultdrop.mobile.ui.watchedfolders
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vaultdrop.mobile.R
|
||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.features.saf.safDisplayName
|
||||
import com.vaultdrop.mobile.features.sync.SyncViewModel
|
||||
|
||||
/**
|
||||
* Dossiers SAF surveillés par l'application. D'ici on ajoute une racine
|
||||
* (picker OpenDocumentTree) et on consulte la liste des racines déjà suivies.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WatchedFoldersScreen(
|
||||
onBack: () -> Unit,
|
||||
syncViewModel: SyncViewModel,
|
||||
viewModel: WatchedFoldersViewModel = hiltViewModel(),
|
||||
) {
|
||||
val folders by viewModel.folders.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val importState by syncViewModel.importState.collectAsStateWithLifecycle()
|
||||
|
||||
val context = LocalContext.current
|
||||
val folderLabel = stringResource(R.string.folder)
|
||||
val pickFolderLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree(),
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
// Permissions persistantes : l'app ré-ouvrira le dossier aux prochains lancements.
|
||||
runCatching {
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
)
|
||||
}
|
||||
syncViewModel.importRoot(
|
||||
uri = uri.toString(),
|
||||
name = uri.safDisplayName(context) ?: uri.lastPathSegment ?: folderLabel,
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_saf_folders)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
item(key = "add") {
|
||||
Button(
|
||||
onClick = { pickFolderLauncher.launch(null) },
|
||||
enabled = !importState.isImporting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.add_folder))
|
||||
}
|
||||
}
|
||||
|
||||
if (importState.isImporting) {
|
||||
item(key = "importing") {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(folders, key = { it.resourceId }) { root ->
|
||||
WatchedFolderRow(root)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (folders.isEmpty() && !importState.isImporting) {
|
||||
item(key = "empty") {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_saf_folders_empty),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 32.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchedFolderRow(folder: FolderEntity) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.padding(8.dp),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = folder.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
folder.uri?.let { uri ->
|
||||
Text(
|
||||
text = uri,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.vaultdrop.mobile.ui.watchedfolders
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.data.repository.FolderRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/** État des dossiers SAF surveillés, partagé entre le badge du header et l'écran de liste. */
|
||||
@HiltViewModel
|
||||
class WatchedFoldersViewModel @Inject constructor(
|
||||
folderRepository: FolderRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
/** Liste live des racines SAF surveillées (uri non nulle). */
|
||||
val folders: Flow<List<FolderEntity>> = folderRepository.observeSafRoots()
|
||||
|
||||
/** Nombre de racines surveillées — l'UI plafonne l'affichage à 99 (« 99+ »). */
|
||||
val count: StateFlow<Int> = folderRepository.observeSafRoots()
|
||||
.map { it.size }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
}
|
||||
@@ -71,6 +71,11 @@
|
||||
<string name="settings_connect">Sign in</string>
|
||||
<string name="settings_disconnect">Sign out</string>
|
||||
<string name="settings_connected_as">Signed in as %1$s</string>
|
||||
<string name="settings_saf_folders">Watched SAF folders</string>
|
||||
<string name="settings_saf_folders_empty">No watched folders yet.</string>
|
||||
<string name="settings_watched_folders">Watched system folders</string>
|
||||
<string name="settings_watched_folders_hint">View watched folders</string>
|
||||
<string name="watched_count_many">99+</string>
|
||||
|
||||
<!-- Search -->
|
||||
<string name="search">Search</string>
|
||||
|
||||
@@ -88,6 +88,11 @@
|
||||
<string name="settings_connect">Se connecter</string>
|
||||
<string name="settings_disconnect">Se déconnecter</string>
|
||||
<string name="settings_connected_as">Connecté en tant que %1$s</string>
|
||||
<string name="settings_saf_folders">Dossiers SAF surveillés</string>
|
||||
<string name="settings_saf_folders_empty">Aucun dossier surveillé pour le moment.</string>
|
||||
<string name="settings_watched_folders">Dossiers système surveillés</string>
|
||||
<string name="settings_watched_folders_hint">Consulter les dossiers surveillés</string>
|
||||
<string name="watched_count_many">99+</string>
|
||||
|
||||
<!-- Recherche -->
|
||||
<string name="search">Recherche</string>
|
||||
|
||||
Reference in New Issue
Block a user