add local status

This commit is contained in:
m
2026-09-12 10:01:43 +02:00
parent 2d23ba1bde
commit 9fed148a12
8 changed files with 83 additions and 6 deletions
@@ -58,6 +58,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.R import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.data.local.entity.FileEntity import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.domain.FileCategory import com.vaultdrop.mobile.domain.FileCategory
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.categoryValue import com.vaultdrop.mobile.ui.components.categoryValue
import com.vaultdrop.mobile.ui.document.content.DocumentContentViewer import com.vaultdrop.mobile.ui.document.content.DocumentContentViewer
import com.vaultdrop.mobile.ui.document.content.PdfFocusViewer import com.vaultdrop.mobile.ui.document.content.PdfFocusViewer
@@ -81,9 +83,11 @@ import java.util.Locale
fun DocumentViewerScreen( fun DocumentViewerScreen(
initialResourceId: String, initialResourceId: String,
onBack: () -> Unit, onBack: () -> Unit,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: DocumentViewerViewModel = hiltViewModel(), viewModel: DocumentViewerViewModel = hiltViewModel(),
) { ) {
val documents by viewModel.documents.collectAsStateWithLifecycle() val documents by viewModel.documents.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val pagerState = rememberPagerState(pageCount = { documents.size }) val pagerState = rememberPagerState(pageCount = { documents.size })
// Démarre une seule fois sur le document demandé, dès que la liste est chargée. // Démarre une seule fois sur le document demandé, dès que la liste est chargée.
@@ -160,6 +164,12 @@ fun DocumentViewerScreen(
) )
} }
}, },
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
},
) )
} }
}, },
@@ -37,7 +37,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.R import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.data.local.entity.FileEntity import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.data.local.entity.FolderEntity 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.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import java.util.Locale import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -47,9 +49,11 @@ fun FolderDetailScreen(
onBack: () -> Unit, onBack: () -> Unit,
onOpenFolder: (String) -> Unit, onOpenFolder: (String) -> Unit,
onOpenDocument: (String) -> Unit, onOpenDocument: (String) -> Unit,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: FolderDetailViewModel = hiltViewModel(), viewModel: FolderDetailViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
LaunchedEffect(uiState.folderMissing) { LaunchedEffect(uiState.folderMissing) {
if (uiState.folderMissing) onBack() if (uiState.folderMissing) onBack()
@@ -68,6 +72,10 @@ fun FolderDetailScreen(
} }
}, },
actions = { actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
IconButton(onClick = viewModel::refresh) { IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh)) Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh))
} }
@@ -49,8 +49,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.R import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.data.local.entity.FileEntity 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.features.sync.SyncViewModel
import com.vaultdrop.mobile.ui.components.FileCategoryIcon import com.vaultdrop.mobile.ui.components.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab import com.vaultdrop.mobile.ui.navigation.NavTab
import java.util.Locale import java.util.Locale
@@ -62,10 +64,12 @@ fun FolderListScreen(
onTabSelected: (NavTab) -> Unit, onTabSelected: (NavTab) -> Unit,
onOpenDocument: (String) -> Unit, onOpenDocument: (String) -> Unit,
syncViewModel: SyncViewModel, syncViewModel: SyncViewModel,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: FolderListViewModel = hiltViewModel(), viewModel: FolderListViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val importState by syncViewModel.importState.collectAsStateWithLifecycle() val importState by syncViewModel.importState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
val folderLabel = stringResource(R.string.folder) val folderLabel = stringResource(R.string.folder)
@@ -91,6 +95,10 @@ fun FolderListScreen(
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.files)) }, title = { Text(stringResource(R.string.files)) },
actions = { actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
IconButton(onClick = viewModel::refresh) { IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh)) Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.refresh))
} }
@@ -15,6 +15,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.features.sync.SyncViewModel import com.vaultdrop.mobile.features.sync.SyncViewModel
import com.vaultdrop.mobile.ui.auth.AuthViewModel import com.vaultdrop.mobile.ui.auth.AuthViewModel
import com.vaultdrop.mobile.ui.document.DocumentViewerScreen import com.vaultdrop.mobile.ui.document.DocumentViewerScreen
@@ -47,6 +48,7 @@ private fun String?.toNavTab(): NavTab = when (this) {
fun NavGraph( fun NavGraph(
authViewModel: AuthViewModel, authViewModel: AuthViewModel,
syncViewModel: SyncViewModel, syncViewModel: SyncViewModel,
connectionStatusViewModel: ConnectionStatusViewModel,
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
@@ -75,6 +77,7 @@ fun NavGraph(
onTabSelected = onTabSelected, onTabSelected = onTabSelected,
onOpenDocument = { id -> navController.navigate(Routes.document(id)) }, onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
syncViewModel = syncViewModel, syncViewModel = syncViewModel,
connectionStatusViewModel = connectionStatusViewModel,
) )
} }
composable(Routes.SEARCH) { composable(Routes.SEARCH) {
@@ -82,6 +85,7 @@ fun NavGraph(
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = onTabSelected, onTabSelected = onTabSelected,
onOpenDocument = { id -> navController.navigate(Routes.document(id)) }, onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
connectionStatusViewModel = connectionStatusViewModel,
) )
} }
composable(Routes.SETTINGS) { composable(Routes.SETTINGS) {
@@ -89,6 +93,7 @@ fun NavGraph(
selectedTab = selectedTab, selectedTab = selectedTab,
onTabSelected = onTabSelected, onTabSelected = onTabSelected,
authViewModel = authViewModel, authViewModel = authViewModel,
connectionStatusViewModel = connectionStatusViewModel,
) )
} }
composable( composable(
@@ -105,6 +110,7 @@ fun NavGraph(
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onOpenFolder = { id -> navController.navigate(Routes.folder(id)) }, onOpenFolder = { id -> navController.navigate(Routes.folder(id)) },
onOpenDocument = { id -> navController.navigate(Routes.document(id)) }, onOpenDocument = { id -> navController.navigate(Routes.document(id)) },
connectionStatusViewModel = connectionStatusViewModel,
) )
} }
composable( composable(
@@ -119,6 +125,7 @@ fun NavGraph(
DocumentViewerScreen( DocumentViewerScreen(
initialResourceId = documentId, initialResourceId = documentId,
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
connectionStatusViewModel = connectionStatusViewModel,
) )
} }
} }
@@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.features.sync.SyncViewModel import com.vaultdrop.mobile.features.sync.SyncViewModel
import com.vaultdrop.mobile.ui.auth.AuthState import com.vaultdrop.mobile.ui.auth.AuthState
import com.vaultdrop.mobile.ui.auth.AuthViewModel import com.vaultdrop.mobile.ui.auth.AuthViewModel
@@ -35,18 +36,30 @@ private fun SplashScreen() {
fun VaultDropApp( fun VaultDropApp(
authViewModel: AuthViewModel = hiltViewModel(), authViewModel: AuthViewModel = hiltViewModel(),
syncViewModel: SyncViewModel = hiltViewModel(), syncViewModel: SyncViewModel = hiltViewModel(),
connectionStatusViewModel: ConnectionStatusViewModel = hiltViewModel(),
) { ) {
val authState by authViewModel.authState.collectAsStateWithLifecycle() val authState by authViewModel.authState.collectAsStateWithLifecycle()
// Une fois la session résolue, lancer la boucle de fond (idempotente). // Une fois la session résolue, lancer les boucles de fond (idempotentes).
LaunchedEffect(authState) { LaunchedEffect(authState) {
if (authState != AuthState.Loading) syncViewModel.ensureStarted() if (authState != AuthState.Loading) {
syncViewModel.ensureStarted()
connectionStatusViewModel.start()
}
} }
when (authState) { when (authState) {
AuthState.Loading -> SplashScreen() AuthState.Loading -> SplashScreen()
AuthState.SignedOut -> LoginScreen() AuthState.SignedOut -> LoginScreen()
AuthState.Local -> NavGraph(authViewModel = authViewModel, syncViewModel = syncViewModel) AuthState.Local -> NavGraph(
is AuthState.SignedIn -> NavGraph(authViewModel = authViewModel, syncViewModel = syncViewModel) authViewModel = authViewModel,
syncViewModel = syncViewModel,
connectionStatusViewModel = connectionStatusViewModel,
)
is AuthState.SignedIn -> NavGraph(
authViewModel = authViewModel,
syncViewModel = syncViewModel,
connectionStatusViewModel = connectionStatusViewModel,
)
} }
} }
@@ -44,7 +44,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.R import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.data.local.entity.FileEntity import com.vaultdrop.mobile.data.local.entity.FileEntity
import com.vaultdrop.mobile.domain.FileCategory 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.FileCategoryIcon
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.components.color import com.vaultdrop.mobile.ui.components.color
import com.vaultdrop.mobile.ui.components.icon import com.vaultdrop.mobile.ui.components.icon
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
@@ -57,13 +59,23 @@ fun SearchScreen(
selectedTab: NavTab, selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit, onTabSelected: (NavTab) -> Unit,
onOpenDocument: (String) -> Unit, onOpenDocument: (String) -> Unit,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: SearchViewModel = hiltViewModel(), viewModel: SearchViewModel = hiltViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar(title = { Text(stringResource(R.string.search)) }) TopAppBar(
title = { Text(stringResource(R.string.search)) },
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
},
)
}, },
bottomBar = { bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected) FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
@@ -46,9 +46,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vaultdrop.mobile.R import com.vaultdrop.mobile.R
import com.vaultdrop.mobile.domain.ThemePreference import com.vaultdrop.mobile.domain.ThemePreference
import com.vaultdrop.mobile.features.connection.ConnectionStatusViewModel
import com.vaultdrop.mobile.ui.auth.AuthState import com.vaultdrop.mobile.ui.auth.AuthState
import com.vaultdrop.mobile.ui.auth.AuthViewModel import com.vaultdrop.mobile.ui.auth.AuthViewModel
import com.vaultdrop.mobile.ui.auth.authErrorResFor import com.vaultdrop.mobile.ui.auth.authErrorResFor
import com.vaultdrop.mobile.ui.components.ServerStatusBadge
import com.vaultdrop.mobile.ui.navigation.FloatingNavBar import com.vaultdrop.mobile.ui.navigation.FloatingNavBar
import com.vaultdrop.mobile.ui.navigation.NavTab import com.vaultdrop.mobile.ui.navigation.NavTab
@@ -58,11 +60,13 @@ fun SettingsScreen(
selectedTab: NavTab, selectedTab: NavTab,
onTabSelected: (NavTab) -> Unit, onTabSelected: (NavTab) -> Unit,
authViewModel: AuthViewModel, authViewModel: AuthViewModel,
connectionStatusViewModel: ConnectionStatusViewModel,
viewModel: SettingsViewModel = hiltViewModel(), viewModel: SettingsViewModel = hiltViewModel(),
) { ) {
val theme by viewModel.theme.collectAsStateWithLifecycle() val theme by viewModel.theme.collectAsStateWithLifecycle()
val serverUrl by viewModel.serverUrl.collectAsStateWithLifecycle() val serverUrl by viewModel.serverUrl.collectAsStateWithLifecycle()
val testUiState by viewModel.testUiState.collectAsStateWithLifecycle() val testUiState by viewModel.testUiState.collectAsStateWithLifecycle()
val connectionStatus by connectionStatusViewModel.status.collectAsStateWithLifecycle()
val authState by authViewModel.authState.collectAsStateWithLifecycle() val authState by authViewModel.authState.collectAsStateWithLifecycle()
val loginUi by authViewModel.loginUiState.collectAsStateWithLifecycle() val loginUi by authViewModel.loginUiState.collectAsStateWithLifecycle()
@@ -77,7 +81,15 @@ fun SettingsScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar(title = { Text(stringResource(R.string.settings)) }) TopAppBar(
title = { Text(stringResource(R.string.settings)) },
actions = {
ServerStatusBadge(
status = connectionStatus,
onClick = connectionStatusViewModel::checkNow,
)
},
)
}, },
bottomBar = { bottomBar = {
FloatingNavBar(selected = selectedTab, onSelect = onTabSelected) FloatingNavBar(selected = selectedTab, onSelect = onTabSelected)
@@ -75,4 +75,11 @@
<string name="login_error_unauthorized">Nom d\'utilisateur ou mot de passe incorrect</string> <string name="login_error_unauthorized">Nom d\'utilisateur ou mot de passe incorrect</string>
<string name="login_skip">Continuer sans compte</string> <string name="login_skip">Continuer sans compte</string>
<string name="skip_message">Mode local — tes dossiers restent sur cet appareil et ne sont pas synchronisés.</string> <string name="skip_message">Mode local — tes dossiers restent sur cet appareil et ne sont pas synchronisés.</string>
<!-- Statut serveur (badge du header) -->
<string name="status_online">En ligne</string>
<string name="status_offline">Hors ligne</string>
<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>
</resources> </resources>