From fe8182ea34ec0fe8c14dd28eb4c6c125ecab30eb Mon Sep 17 00:00:00 2001 From: hue Date: Fri, 5 Jan 2024 13:09:29 -0500 Subject: [PATCH 01/29] Translate work type statuses correctly --- app/build.gradle.kts | 2 +- .../LanguageTranslationsRepository.kt | 14 +++- .../feature/cases/CasesFilterViewModel.kt | 2 +- .../feature/cases/ui/CasesFilterScreen.kt | 64 +++++++++---------- gradle/libs.versions.toml | 2 +- 5 files changed, 46 insertions(+), 38 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 50ab34d78..c98eb3722 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,7 @@ plugins { android { defaultConfig { - val buildVersion = 179 + val buildVersion = 180 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt index b223b932d..342b8e0dc 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt @@ -217,6 +217,18 @@ class OfflineFirstLanguageTranslationsRepository @Inject constructor( } override fun translate(phraseKey: String): String? { - return translations.value[phraseKey] ?: statusRepository.translateStatus(phraseKey) + translations.value[phraseKey]?.let { + return it + } + + statusRepository.translateStatus(phraseKey)?.let { statusTranslated -> + return if (statusTranslated.contains(phraseKey)) { + translations.value[statusTranslated] + } else { + statusTranslated + } + } + + return null } } diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesFilterViewModel.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesFilterViewModel.kt index 58bc0f742..5077c852e 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesFilterViewModel.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesFilterViewModel.kt @@ -50,7 +50,7 @@ class CasesFilterViewModel @Inject constructor( accountDataRepository: AccountDataRepository, organizationsRepository: OrganizationsRepository, private val permissionManager: PermissionManager, - val translator: KeyResourceTranslator, + translator: KeyResourceTranslator, @Dispatcher(CrisisCleanupDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, @Logger(CrisisCleanupLoggers.Cases) private val logger: AppLogger, ) : ViewModel() { diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt index e5b586d3f..e45c84ea1 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt @@ -85,45 +85,41 @@ internal fun CasesFilterRoute( onBack: () -> Unit = {}, viewModel: CasesFilterViewModel = hiltViewModel(), ) { - val translator = viewModel.translator - CompositionLocalProvider( - LocalAppTranslator provides translator, - ) { - val filters by viewModel.casesFilters.collectAsStateWithLifecycle() - val updateFilters = - remember(viewModel) { { filters: CasesFilter -> viewModel.changeFilters(filters) } } - - Column( - Modifier - .fillMaxSize() - .background(Color.White), - ) { - TopAppBarBackAction( - title = translator("worksiteFilters.filters"), - onAction = onBack, - modifier = Modifier.testTag("workFilterBackBtn"), - ) + val translator = LocalAppTranslator.current + val filters by viewModel.casesFilters.collectAsStateWithLifecycle() + val updateFilters = + remember(viewModel) { { filters: CasesFilter -> viewModel.changeFilters(filters) } } - FilterControls( - filters, - updateFilters = updateFilters, - ) + Column( + Modifier + .fillMaxSize() + .background(Color.White), + ) { + TopAppBarBackAction( + title = translator("worksiteFilters.filters"), + onAction = onBack, + modifier = Modifier.testTag("workFilterBackBtn"), + ) - BottomActionBar( - onBack = onBack, - filters = filters, - ) - } + FilterControls( + filters, + updateFilters = updateFilters, + ) - val closePermissionDialog = - remember(viewModel) { { viewModel.showExplainPermissionLocation = false } } - val explainPermission = viewModel.showExplainPermissionLocation - ExplainLocationPermissionDialog( - showDialog = explainPermission, - closeDialog = closePermissionDialog, - explanation = translator("worksiteFilters.location_required_to_filter_by_distance"), + BottomActionBar( + onBack = onBack, + filters = filters, ) } + + val closePermissionDialog = + remember(viewModel) { { viewModel.showExplainPermissionLocation = false } } + val explainPermission = viewModel.showExplainPermissionLocation + ExplainLocationPermissionDialog( + showDialog = explainPermission, + closeDialog = closePermissionDialog, + explanation = translator("worksiteFilters.location_required_to_filter_by_distance"), + ) } private val collapsibleFilterSections = listOf( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a29fdab4e..7982173e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanist = "0.31.2-alpha" androidDesugarJdkLibs = "2.0.4" -androidGradlePlugin = "8.1.1" +androidGradlePlugin = "8.2.0" androidMapsUtil = "2.3.0" androidMapsUtilKtx = "3.4.0" androidMaterial = "1.11.0" From f01796688d417ab6a99dcc2254bdca91168812c6 Mon Sep 17 00:00:00 2001 From: hue Date: Fri, 5 Jan 2024 20:22:19 -0500 Subject: [PATCH 02/29] Move app header bar into top bar for Menu screen Remove and reduce unused/unnecessary features. --- app/build.gradle.kts | 7 - .../ui/CrisisCleanupAppStateTest.kt | 4 - .../crisiscleanup/FirebaseFeedbackReceiver.kt | 20 -- .../java/com/crisiscleanup/MainActivity.kt | 5 - .../crisiscleanup/MainActivityViewModel.kt | 26 -- .../java/com/crisiscleanup/di/AppModule.kt | 11 - .../com/crisiscleanup/di/FeedbackModule.kt | 15 -- .../navigation/CrisisCleanupNavHost.kt | 2 + .../CrisisCleanupNavigationObserver.kt | 26 -- .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 114 +-------- .../ui/CrisisCleanupAppHeaderUiState.kt | 31 --- .../crisiscleanup/ui/CrisisCleanupAppState.kt | 22 -- core/appheader/build.gradle.kts | 15 -- .../core/appheader/AppHeaderUiState.kt | 12 - .../core/common/NavigationObserver.kt | 7 - .../core/domain/IncidentsData.kt | 13 + ...tDataUseCase.kt => LoadSelectIncidents.kt} | 61 ++--- core/{appheader => selectincident}/.gitignore | 0 core/selectincident/build.gradle.kts | 25 ++ .../src/main/AndroidManifest.xml | 1 + .../selectincident/SelectIncidentDialog.kt | 37 +-- .../testing/util/TestNavigationObserver.kt | 8 - feature/cases/build.gradle.kts | 1 + .../feature/cases/CasesViewModel.kt | 13 +- .../feature/cases/SelectIncidentViewModel.kt | 36 --- .../feature/cases/ui/CasesScreen.kt | 18 +- feature/menu/build.gradle.kts | 6 + .../crisiscleanup/feature/menu/MenuScreen.kt | 232 +++++++++++++----- .../feature/menu/MenuViewModel.kt | 64 +++++ .../feature/menu/navigation/MenuNavigation.kt | 2 + .../InviteTeammateViewModel.kt | 17 +- settings.gradle.kts | 2 +- 32 files changed, 364 insertions(+), 489 deletions(-) delete mode 100644 app/src/main/java/com/crisiscleanup/FirebaseFeedbackReceiver.kt delete mode 100644 app/src/main/java/com/crisiscleanup/di/FeedbackModule.kt delete mode 100644 app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavigationObserver.kt delete mode 100644 app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppHeaderUiState.kt delete mode 100644 core/appheader/build.gradle.kts delete mode 100644 core/appheader/src/main/java/com/crisiscleanup/core/appheader/AppHeaderUiState.kt delete mode 100644 core/common/src/main/java/com/crisiscleanup/core/common/NavigationObserver.kt create mode 100644 core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt rename core/domain/src/main/java/com/crisiscleanup/core/domain/{LoadIncidentDataUseCase.kt => LoadSelectIncidents.kt} (53%) rename core/{appheader => selectincident}/.gitignore (100%) create mode 100644 core/selectincident/build.gradle.kts create mode 100644 core/selectincident/src/main/AndroidManifest.xml rename feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/SelectIncidentScreen.kt => core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt (84%) delete mode 100644 core/testing/src/main/java/com/crisiscleanup/core/testing/util/TestNavigationObserver.kt delete mode 100644 feature/cases/src/main/java/com/crisiscleanup/feature/cases/SelectIncidentViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c98eb3722..2e7abb91c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,17 +117,12 @@ dependencies { implementation(project(":feature:team")) implementation(project(":feature:userfeedback")) - implementation(project(":core:appheader")) implementation(project(":core:appnav")) implementation(project(":core:common")) - implementation(project(":core:commonassets")) - implementation(project(":core:commoncase")) implementation(project(":core:data")) implementation(project(":core:designsystem")) implementation(project(":core:model")) implementation(project(":core:network")) - implementation(project(":core:testerfeedbackapi")) - demoImplementation(project(":core:testerfeedback")) implementation(project(":core:ui")) implementation(project(":sync:work")) @@ -164,8 +159,6 @@ dependencies { // For Firebase support implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.appdistribution.api) - demoImplementation(libs.firebase.appdistribution) implementation(libs.kotlinx.coroutines.playservices) implementation(libs.playservices.maps) diff --git a/app/src/androidTest/java/com/crisiscleanup/ui/CrisisCleanupAppStateTest.kt b/app/src/androidTest/java/com/crisiscleanup/ui/CrisisCleanupAppStateTest.kt index b07b58cf6..d7f61f747 100644 --- a/app/src/androidTest/java/com/crisiscleanup/ui/CrisisCleanupAppStateTest.kt +++ b/app/src/androidTest/java/com/crisiscleanup/ui/CrisisCleanupAppStateTest.kt @@ -14,7 +14,6 @@ import androidx.navigation.compose.ComposeNavigator import androidx.navigation.compose.composable import androidx.navigation.createGraph import androidx.navigation.testing.TestNavHostController -import com.crisiscleanup.core.testing.util.TestNavigationObserver import com.crisiscleanup.core.testing.util.TestNetworkMonitor import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse @@ -42,8 +41,6 @@ class CrisisCleanupAppStateTest { // Create the test dependencies. private val networkMonitor = TestNetworkMonitor() - private val navigationObserver = TestNavigationObserver() - // Subject under test. private lateinit var state: CrisisCleanupAppState @@ -80,7 +77,6 @@ class CrisisCleanupAppStateTest { state = rememberCrisisCleanupAppState( windowSizeClass = getCompactWindowClass(), networkMonitor = networkMonitor, - navigationObserver = navigationObserver, ) } diff --git a/app/src/main/java/com/crisiscleanup/FirebaseFeedbackReceiver.kt b/app/src/main/java/com/crisiscleanup/FirebaseFeedbackReceiver.kt deleted file mode 100644 index 3f9bdd959..000000000 --- a/app/src/main/java/com/crisiscleanup/FirebaseFeedbackReceiver.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.crisiscleanup - -import android.os.Bundle -import com.crisiscleanup.core.common.AppEnv -import com.crisiscleanup.core.testerfeedbackapi.FeedbackReceiver -import com.google.firebase.appdistribution.ktx.appDistribution -import com.google.firebase.ktx.Firebase -import javax.inject.Inject - -class FirebaseFeedbackReceiver @Inject constructor( - private val appEnv: AppEnv, -) : FeedbackReceiver { - override fun onStartFeedback(starterKey: String, payload: Bundle) { - if (appEnv.isProduction) { - return - } - - Firebase.appDistribution.startFeedback("Do share O_o") - } -} diff --git a/app/src/main/java/com/crisiscleanup/MainActivity.kt b/app/src/main/java/com/crisiscleanup/MainActivity.kt index 1da8c90d2..444eb42f4 100644 --- a/app/src/main/java/com/crisiscleanup/MainActivity.kt +++ b/app/src/main/java/com/crisiscleanup/MainActivity.kt @@ -23,7 +23,6 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.metrics.performance.JankStats import com.crisiscleanup.MainActivityUiState.Loading import com.crisiscleanup.MainActivityUiState.Success -import com.crisiscleanup.core.common.NavigationObserver import com.crisiscleanup.core.common.NetworkMonitor import com.crisiscleanup.core.common.PermissionManager import com.crisiscleanup.core.common.VisualAlertManager @@ -61,9 +60,6 @@ class MainActivity : ComponentActivity() { @Inject internal lateinit var networkMonitor: NetworkMonitor - @Inject - internal lateinit var navigationObserver: NavigationObserver - @Inject internal lateinit var trimMemoryEventManager: TrimMemoryEventManager @@ -140,7 +136,6 @@ class MainActivity : ComponentActivity() { CrisisCleanupApp( windowSizeClass = calculateWindowSizeClass(this), networkMonitor = networkMonitor, - navigationObserver = navigationObserver, ) } } diff --git a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt index 147662821..d1b5aacf1 100644 --- a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt +++ b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt @@ -3,7 +3,6 @@ package com.crisiscleanup import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.crisiscleanup.core.appheader.AppHeaderUiState import com.crisiscleanup.core.common.AppEnv import com.crisiscleanup.core.common.AppVersionProvider import com.crisiscleanup.core.common.KeyResourceTranslator @@ -15,16 +14,12 @@ import com.crisiscleanup.core.common.log.Logger import com.crisiscleanup.core.common.network.CrisisCleanupDispatchers.IO import com.crisiscleanup.core.common.network.Dispatcher import com.crisiscleanup.core.common.sync.SyncPuller -import com.crisiscleanup.core.commonassets.R -import com.crisiscleanup.core.commonassets.getDisasterIcon import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.AccountDataRefresher import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.AppDataManagementRepository -import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.LocalAppMetricsRepository import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository -import com.crisiscleanup.core.data.repository.WorksitesRepository import com.crisiscleanup.core.model.data.AccountData import com.crisiscleanup.core.model.data.AppMetricsData import com.crisiscleanup.core.model.data.AppOpenInstant @@ -59,9 +54,6 @@ class MainActivityViewModel @Inject constructor( private val appMetricsRepository: LocalAppMetricsRepository, accountDataRepository: AccountDataRepository, incidentSelector: IncidentSelector, - val appHeaderUiState: AppHeaderUiState, - incidentsRepository: IncidentsRepository, - worksitesRepository: WorksitesRepository, appDataRepository: AppDataManagementRepository, accountDataRefresher: AccountDataRefresher, val translator: KeyResourceTranslator, @@ -121,24 +113,6 @@ class MainActivityViewModel @Inject constructor( val translationCount = translator.translationCount - val disasterIconResId = incidentSelector.incident.map { getDisasterIcon(it.disaster) } - .stateIn( - scope = viewModelScope, - initialValue = R.drawable.ic_disaster_other, - started = SharingStarted.WhileSubscribed(), - ) - - private val isSyncingWorksitesFull = combine( - incidentSelector.incidentId, - worksitesRepository.syncWorksitesFullIncidentId, - ) { incidentId, syncingIncidentId -> incidentId == syncingIncidentId } - - val showHeaderLoading = combine( - incidentsRepository.isLoading, - worksitesRepository.isLoading, - isSyncingWorksitesFull, - ) { b0, b1, b2 -> b0 || b1 || b2 } - val buildEndOfLife: BuildEndOfLife? get() { if (appEnv.isEarlybird) { diff --git a/app/src/main/java/com/crisiscleanup/di/AppModule.kt b/app/src/main/java/com/crisiscleanup/di/AppModule.kt index 88d1f06a0..8a97afc31 100644 --- a/app/src/main/java/com/crisiscleanup/di/AppModule.kt +++ b/app/src/main/java/com/crisiscleanup/di/AppModule.kt @@ -8,16 +8,13 @@ import com.crisiscleanup.AndroidPermissionManager import com.crisiscleanup.AppVisualAlertManager import com.crisiscleanup.CrisisCleanupAppEnv import com.crisiscleanup.ZxingQrCodeGenerator -import com.crisiscleanup.core.appheader.AppHeaderUiState import com.crisiscleanup.core.common.* import com.crisiscleanup.core.common.log.TagLogger import com.crisiscleanup.core.network.AuthInterceptorProvider import com.crisiscleanup.core.network.RetrofitInterceptorProvider import com.crisiscleanup.log.CrisisCleanupAppLogger -import com.crisiscleanup.navigation.CrisisCleanupNavigationObserver import com.crisiscleanup.network.CrisisCleanupAuthInterceptorProvider import com.crisiscleanup.network.CrisisCleanupInterceptorProvider -import com.crisiscleanup.ui.CrisisCleanupAppHeaderUiState import com.google.firebase.analytics.FirebaseAnalytics import dagger.Binds import dagger.Module @@ -49,14 +46,6 @@ interface AppModule { provider: CrisisCleanupInterceptorProvider, ): RetrofitInterceptorProvider - @Singleton - @Binds - fun bindsAppHeaderUiState(headerUiState: CrisisCleanupAppHeaderUiState): AppHeaderUiState - - @Singleton - @Binds - fun bindsNavigationObserver(navigationObserver: CrisisCleanupNavigationObserver): NavigationObserver - @Singleton @Binds fun bindsPermissionManager(manager: AndroidPermissionManager): PermissionManager diff --git a/app/src/main/java/com/crisiscleanup/di/FeedbackModule.kt b/app/src/main/java/com/crisiscleanup/di/FeedbackModule.kt deleted file mode 100644 index cc967efb9..000000000 --- a/app/src/main/java/com/crisiscleanup/di/FeedbackModule.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.crisiscleanup.di - -import com.crisiscleanup.FirebaseFeedbackReceiver -import com.crisiscleanup.core.testerfeedbackapi.FeedbackReceiver -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -interface FeedbackModule { - @Binds - fun bindsFeedbackReceiver(receiver: FirebaseFeedbackReceiver): FeedbackReceiver -} diff --git a/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt b/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt index 51fc26831..2e50e82c0 100644 --- a/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt +++ b/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt @@ -52,6 +52,7 @@ import com.crisiscleanup.feature.userfeedback.navigation.userFeedbackScreen fun CrisisCleanupNavHost( navController: NavHostController, onBack: () -> Unit, + openAuthentication: () -> Unit = {}, modifier: Modifier = Modifier, startDestination: String = casesGraphRoutePattern, ) { @@ -134,6 +135,7 @@ fun CrisisCleanupNavHost( dashboardScreen() teamScreen() menuScreen( + openAuthentication = openAuthentication, openInviteTeammate = openInviteTeammate, openUserFeedback = openUserFeedback, openSyncLogs = openSyncLogs, diff --git a/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavigationObserver.kt b/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavigationObserver.kt deleted file mode 100644 index 8ca2c9281..000000000 --- a/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavigationObserver.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.crisiscleanup.navigation - -import android.os.Bundle -import com.crisiscleanup.core.appheader.AppHeaderUiState -import com.crisiscleanup.core.common.NavigationObserver -import com.crisiscleanup.core.common.di.ApplicationScope -import com.crisiscleanup.core.common.log.AppLogger -import com.crisiscleanup.core.common.log.CrisisCleanupLoggers -import com.crisiscleanup.core.common.log.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CrisisCleanupNavigationObserver @Inject constructor( - private val headerUiState: AppHeaderUiState, - @ApplicationScope private val coroutineScope: CoroutineScope, - @Logger(CrisisCleanupLoggers.Navigation) logger: AppLogger, -) : NavigationObserver { - private val navigationRoute = MutableStateFlow>(Pair(null, null)) - - override fun onRouteChange(route: String?, arguments: Bundle?) { - navigationRoute.value = Pair(navigationRoute.value.second, route) - } -} diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index 86273d097..f9cc256a5 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -1,6 +1,5 @@ package com.crisiscleanup.ui -import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.slideIn @@ -21,7 +20,6 @@ import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -34,7 +32,6 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -55,18 +52,13 @@ import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import com.crisiscleanup.AuthState import com.crisiscleanup.MainActivityViewModel -import com.crisiscleanup.core.common.NavigationObserver import com.crisiscleanup.core.common.NetworkMonitor -import com.crisiscleanup.core.commoncase.ui.IncidentDropdownSelect import com.crisiscleanup.core.designsystem.LocalAppTranslator import com.crisiscleanup.core.designsystem.component.CrisisCleanupBackground import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationBar import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationBarItem import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationRail import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationRailItem -import com.crisiscleanup.core.designsystem.component.TopAppBarDefault -import com.crisiscleanup.core.designsystem.component.TruncatedAppBarText -import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.icon.Icon.DrawableResourceIcon import com.crisiscleanup.core.designsystem.icon.Icon.ImageVectorIcon import com.crisiscleanup.core.ui.AppLayoutArea @@ -76,7 +68,6 @@ import com.crisiscleanup.feature.authentication.navigation.navigateToMagicLinkLo import com.crisiscleanup.feature.authentication.navigation.navigateToOrgPersistentInvite import com.crisiscleanup.feature.authentication.navigation.navigateToPasswordReset import com.crisiscleanup.feature.authentication.navigation.navigateToRequestAccess -import com.crisiscleanup.feature.cases.ui.SelectIncidentDialog import com.crisiscleanup.navigation.CrisisCleanupAuthNavHost import com.crisiscleanup.navigation.CrisisCleanupNavHost import com.crisiscleanup.navigation.TopLevelDestination @@ -86,11 +77,9 @@ import com.crisiscleanup.feature.authentication.R as authenticationR fun CrisisCleanupApp( windowSizeClass: WindowSizeClass, networkMonitor: NetworkMonitor, - navigationObserver: NavigationObserver, appState: CrisisCleanupAppState = rememberCrisisCleanupAppState( networkMonitor = networkMonitor, windowSizeClass = windowSizeClass, - navigationObserver = navigationObserver, ), viewModel: MainActivityViewModel = hiltViewModel(), ) { @@ -189,30 +178,10 @@ private fun LoadedContent( } } } else { - val accountData = (authState as AuthState.Authenticated).accountData - val profilePictureUri = accountData.profilePictureUri - val appHeaderBar = viewModel.appHeaderUiState - val appHeaderTitle by appHeaderBar.title.collectAsStateWithLifecycle() - val isHeaderLoading by viewModel.showHeaderLoading.collectAsState(false) - - var showIncidentPicker by remember { mutableStateOf(false) } - val openIncidentsSelect = remember(viewModel) { - { showIncidentPicker = true } - } - - val disasterIconResId by viewModel.disasterIconResId.collectAsStateWithLifecycle() - NavigableContent( snackbarHostState, appState, - appHeaderTitle, - isHeaderLoading, - { openAuthentication = true }, - profilePictureUri, - isAccountExpired, - disasterIconResId, - openIncidentsSelect, - ) + ) { openAuthentication = true } if ( isAccountExpired && @@ -223,11 +192,6 @@ private fun LoadedContent( } } - if (showIncidentPicker) { - val closeDialog = { showIncidentPicker = false } - SelectIncidentDialog(closeDialog) - } - if (showPasswordReset) { appState.navController.navigateToPasswordReset(true) } @@ -278,16 +242,9 @@ private fun AuthenticateContent( private fun NavigableContent( snackbarHostState: SnackbarHostState, appState: CrisisCleanupAppState, - headerTitle: String = "", - isHeaderLoading: Boolean, openAuthentication: () -> Unit, - profilePictureUri: String, - isAccountExpired: Boolean, - @DrawableRes disasterIconResId: Int, - openIncidentsSelect: () -> Unit, ) { val showNavigation = appState.isTopLevelRoute - val showAppBar = appState.isMenuRoute val isFullscreen = appState.isFullscreenRoute Scaffold( modifier = Modifier.semantics { @@ -297,30 +254,6 @@ private fun NavigableContent( contentColor = MaterialTheme.colorScheme.onBackground, contentWindowInsets = WindowInsets(0, 0, 0, 0), snackbarHost = { SnackbarHost(snackbarHostState) }, - topBar = { - AnimatedVisibility( - visible = showAppBar, - enter = slideIn { IntOffset.Zero }, - exit = slideOut { IntOffset.Zero }, - ) { - val title = headerTitle.ifBlank { - appState.currentTopLevelDestination?.let { destination -> - LocalAppTranslator.current(destination.titleTranslateKey) - } ?: "" - } - val onOpenIncidents = if (appState.isMenuRoute) openIncidentsSelect else null - AppHeader( - modifier = Modifier, - title = title, - isAppHeaderLoading = isHeaderLoading, - profilePictureUri = profilePictureUri, - isAccountExpired = isAccountExpired, - openAuthentication = openAuthentication, - disasterIconResId = disasterIconResId, - onOpenIncidents = onOpenIncidents, - ) - } - }, bottomBar = { val showBottomBar = showNavigation && appState.shouldShowBottomBar AnimatedVisibility( @@ -388,6 +321,7 @@ private fun NavigableContent( CrisisCleanupNavHost( navController = appState.navController, onBack = appState::onBack, + openAuthentication = openAuthentication, modifier = Modifier.weight(1f), startDestination = appState.lastTopLevelRoute(), ) @@ -426,49 +360,6 @@ private fun ExpiredAccountAlert( } } -@OptIn( - ExperimentalMaterial3Api::class, -) -@Composable -private fun AppHeader( - modifier: Modifier = Modifier, - title: String = "", - isAppHeaderLoading: Boolean = false, - profilePictureUri: String = "", - isAccountExpired: Boolean = false, - openAuthentication: () -> Unit = {}, - @DrawableRes disasterIconResId: Int = 0, - onOpenIncidents: (() -> Unit)? = null, -) { - val t = LocalAppTranslator.current - val actionText = t("actions.account") - TopAppBarDefault( - modifier = modifier, - title = title, - profilePictureUri = profilePictureUri, - actionIcon = CrisisCleanupIcons.Account, - actionText = actionText, - isActionAttention = isAccountExpired, - onActionClick = openAuthentication, - onNavigationClick = null, - titleContent = @Composable { - // TODO Match height of visible part of app bar (not the entire app bar) - if (onOpenIncidents == null) { - TruncatedAppBarText(title = title) - } else { - IncidentDropdownSelect( - modifier = Modifier.testTag("appIncidentSelector"), - onOpenIncidents, - disasterIconResId, - title = title, - contentDescription = t("nav.change_incident"), - isLoading = isAppHeaderLoading, - ) - } - }, - ) -} - @Composable private fun TopLevelDestination.Icon(isSelected: Boolean, description: String) { val icon = if (isSelected) { @@ -498,6 +389,7 @@ private fun CrisisCleanupNavRail( ) { val translator = LocalAppTranslator.current CrisisCleanupNavigationRail(modifier = modifier) { + Spacer(Modifier.weight(1f)) destinations.forEach { destination -> val title = translator(destination.titleTranslateKey) val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppHeaderUiState.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppHeaderUiState.kt deleted file mode 100644 index 2bdb470e6..000000000 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppHeaderUiState.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.crisiscleanup.ui - -import com.crisiscleanup.core.appheader.AppHeaderUiState -import com.crisiscleanup.core.common.di.ApplicationScope -import com.crisiscleanup.core.data.IncidentSelectManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CrisisCleanupAppHeaderUiState @Inject constructor( - incidentSelectManager: IncidentSelectManager, - @ApplicationScope scope: CoroutineScope, -) : AppHeaderUiState { - override var title = MutableStateFlow("") - private set - - init { - incidentSelectManager.incident.onEach { - setTitle(it.shortName) - } - .launchIn(scope) - } - - override fun setTitle(title: String) { - this.title.value = title - } -} diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt index 76036174a..667f04ef4 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt @@ -3,7 +3,6 @@ package com.crisiscleanup.ui import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -23,7 +22,6 @@ import com.crisiscleanup.core.appnav.RouteConstant.teamRoute import com.crisiscleanup.core.appnav.RouteConstant.topLevelRoutes import com.crisiscleanup.core.appnav.RouteConstant.userFeedbackRoute import com.crisiscleanup.core.appnav.RouteConstant.viewImageRoute -import com.crisiscleanup.core.common.NavigationObserver import com.crisiscleanup.core.common.NetworkMonitor import com.crisiscleanup.core.ui.TrackDisposableJank import com.crisiscleanup.feature.cases.navigation.navigateToCases @@ -44,12 +42,10 @@ import kotlinx.coroutines.flow.stateIn fun rememberCrisisCleanupAppState( windowSizeClass: WindowSizeClass, networkMonitor: NetworkMonitor, - navigationObserver: NavigationObserver, coroutineScope: CoroutineScope = rememberCoroutineScope(), navController: NavHostController = rememberNavController(), ): CrisisCleanupAppState { NavigationTrackingSideEffect(navController) - NavigationObserverSideEffect(navController, navigationObserver) return remember(navController, coroutineScope, windowSizeClass, networkMonitor) { CrisisCleanupAppState(navController, coroutineScope, windowSizeClass, networkMonitor) } @@ -181,21 +177,3 @@ private fun NavigationTrackingSideEffect(navController: NavHostController) { } } } - -@Composable -private fun NavigationObserverSideEffect( - navController: NavHostController, - navigationObserver: NavigationObserver, -) { - DisposableEffect(navController, navigationObserver) { - val listener = NavController.OnDestinationChangedListener { _, destination, arguments -> - navigationObserver.onRouteChange(destination.route, arguments) - } - - navController.addOnDestinationChangedListener(listener) - - onDispose { - navController.removeOnDestinationChangedListener(listener) - } - } -} diff --git a/core/appheader/build.gradle.kts b/core/appheader/build.gradle.kts deleted file mode 100644 index 83074ca20..000000000 --- a/core/appheader/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -// Decouples top app bar state from individual modules allowing app to implement behavior - -plugins { - id("nowinandroid.android.library") - id("nowinandroid.android.library.compose") - id("nowinandroid.android.library.jacoco") -} - -android { - namespace = "com.crisiscleanup.core.appheader" -} - -dependencies { - implementation(libs.androidx.compose.runtime) -} \ No newline at end of file diff --git a/core/appheader/src/main/java/com/crisiscleanup/core/appheader/AppHeaderUiState.kt b/core/appheader/src/main/java/com/crisiscleanup/core/appheader/AppHeaderUiState.kt deleted file mode 100644 index 56e1e204e..000000000 --- a/core/appheader/src/main/java/com/crisiscleanup/core/appheader/AppHeaderUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.crisiscleanup.core.appheader - -import kotlinx.coroutines.flow.StateFlow - -/** - * State for app header UI - */ -interface AppHeaderUiState { - val title: StateFlow - - fun setTitle(title: String) -} diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/NavigationObserver.kt b/core/common/src/main/java/com/crisiscleanup/core/common/NavigationObserver.kt deleted file mode 100644 index fb294cbd5..000000000 --- a/core/common/src/main/java/com/crisiscleanup/core/common/NavigationObserver.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.crisiscleanup.core.common - -import android.os.Bundle - -interface NavigationObserver { - fun onRouteChange(route: String?, arguments: Bundle?) -} diff --git a/core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt b/core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt new file mode 100644 index 000000000..f390e5ca7 --- /dev/null +++ b/core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt @@ -0,0 +1,13 @@ +package com.crisiscleanup.core.domain + +import com.crisiscleanup.core.model.data.Incident + +sealed interface IncidentsData { + data object Loading : IncidentsData + + data class Incidents( + val incidents: List, + ) : IncidentsData + + data object Empty : IncidentsData +} diff --git a/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadIncidentDataUseCase.kt b/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadSelectIncidents.kt similarity index 53% rename from core/domain/src/main/java/com/crisiscleanup/core/domain/LoadIncidentDataUseCase.kt rename to core/domain/src/main/java/com/crisiscleanup/core/domain/LoadSelectIncidents.kt index b0738d147..1d28c7fb3 100644 --- a/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadIncidentDataUseCase.kt +++ b/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadSelectIncidents.kt @@ -1,31 +1,24 @@ package com.crisiscleanup.core.domain -import com.crisiscleanup.core.common.di.ApplicationScope import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.Incident -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn -import javax.inject.Inject -import javax.inject.Singleton +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch -@Singleton -class LoadIncidentDataUseCase @Inject constructor( +class LoadSelectIncidents( incidentsRepository: IncidentsRepository, private val incidentSelector: IncidentSelector, private val appPreferencesRepository: LocalAppPreferencesRepository, - @ApplicationScope coroutineScope: CoroutineScope, + private val coroutineScope: CoroutineScope, ) { - private val data = incidentsRepository.incidents.map { incidents -> + val data = incidentsRepository.incidents.map { incidents -> var selectedId = incidentSelector.incidentId.first() if (selectedId == EmptyIncident.id) { selectedId = appPreferencesRepository.userPreferences.first().selectedIncidentId @@ -45,39 +38,21 @@ class LoadIncidentDataUseCase @Inject constructor( } else { IncidentsData.Incidents(incidents) } - }.shareIn( + }.stateIn( scope = coroutineScope, - started = SharingStarted.WhileSubscribed(1_000), - replay = 1, + initialValue = IncidentsData.Loading, + started = SharingStarted.WhileSubscribed(3_000), ) - operator fun invoke() = data -} - -sealed interface IncidentsData { - data object Loading : IncidentsData + fun selectIncident(incident: Incident) { + coroutineScope.launch { + (data.first() as? IncidentsData.Incidents)?.let { incidentsData -> + incidentsData.incidents.find { it.id == incident.id }?.let { matchingIncident -> + appPreferencesRepository.setSelectedIncident(matchingIncident.id) - data class Incidents( - val incidents: List, - ) : IncidentsData - - data object Empty : IncidentsData -} - -@Module -@InstallIn(SingletonComponent::class) -object LoadIncidentsDataModule { - @Provides - @Singleton - fun providesLoadIncidentsData( - incidentsRepository: IncidentsRepository, - incidentSelector: IncidentSelector, - appPreferencesRepository: LocalAppPreferencesRepository, - @ApplicationScope coroutineScope: CoroutineScope, - ) = LoadIncidentDataUseCase( - incidentsRepository, - incidentSelector, - appPreferencesRepository, - coroutineScope, - ) + incidentSelector.setIncident(matchingIncident) + } + } + } + } } diff --git a/core/appheader/.gitignore b/core/selectincident/.gitignore similarity index 100% rename from core/appheader/.gitignore rename to core/selectincident/.gitignore diff --git a/core/selectincident/build.gradle.kts b/core/selectincident/build.gradle.kts new file mode 100644 index 000000000..204632bef --- /dev/null +++ b/core/selectincident/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("nowinandroid.android.library") + id("nowinandroid.android.library.compose") + id("nowinandroid.android.library.jacoco") + kotlin("kapt") +} + +android { + namespace = "com.crisiscleanup.core.selectincident" +} + +dependencies { + implementation(project(":core:common")) + implementation(project(":core:designsystem")) + implementation(project(":core:domain")) + implementation(project(":core:model")) + + api(libs.androidx.compose.foundation) + api(libs.androidx.compose.foundation.layout) + api(libs.androidx.compose.material3) + debugApi(libs.androidx.compose.ui.tooling) + api(libs.androidx.compose.ui.tooling.preview) + api(libs.androidx.compose.ui.util) + api(libs.androidx.compose.runtime) +} \ No newline at end of file diff --git a/core/selectincident/src/main/AndroidManifest.xml b/core/selectincident/src/main/AndroidManifest.xml new file mode 100644 index 000000000..25df38676 --- /dev/null +++ b/core/selectincident/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/SelectIncidentScreen.kt b/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt similarity index 84% rename from feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/SelectIncidentScreen.kt rename to core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt index c59b884a9..0b813c940 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/SelectIncidentScreen.kt +++ b/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt @@ -1,4 +1,4 @@ -package com.crisiscleanup.feature.cases.ui +package com.crisiscleanup.core.selectincident import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -28,14 +28,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.crisiscleanup.core.designsystem.LocalAppTranslator import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.domain.IncidentsData import com.crisiscleanup.core.model.data.Incident -import com.crisiscleanup.feature.cases.SelectIncidentViewModel @Composable private fun WrapInDialog( @@ -59,15 +56,14 @@ private fun WrapInDialog( @Composable fun SelectIncidentDialog( + rememberKey: Any, onBackClick: () -> Unit, - selectIncidentViewModel: SelectIncidentViewModel = hiltViewModel(), + incidentsData: IncidentsData, + selectedIncidentId: Long, + onSelectIncident: (Incident) -> Unit, padding: Dp = 16.dp, textPadding: Dp = 16.dp, ) { - val incidentsData by selectIncidentViewModel.incidentsData.collectAsStateWithLifecycle( - IncidentsData.Loading, - ) - WrapInDialog(onBackClick) { when (incidentsData) { IncidentsData.Loading -> { @@ -79,15 +75,19 @@ fun SelectIncidentDialog( is IncidentsData.Incidents -> { Column { Text( - modifier = Modifier.testTag("selectIncidentHeader").padding(textPadding), + modifier = Modifier + .testTag("selectIncidentHeader") + .padding(textPadding), text = LocalAppTranslator.current("nav.change_incident"), style = LocalFontStyles.current.header3, ) - val incidents = (incidentsData as IncidentsData.Incidents).incidents + val incidents = incidentsData.incidents IncidentSelectContent( - selectIncidentViewModel, - incidents, + rememberKey = rememberKey, + selectedIncidentId = selectedIncidentId, + incidents = incidents, + onSelectIncident = onSelectIncident, onBackClick = onBackClick, padding = padding, ) @@ -106,23 +106,24 @@ fun SelectIncidentDialog( @Composable private fun ColumnScope.IncidentSelectContent( - selectIncidentViewModel: SelectIncidentViewModel, + rememberKey: Any, + selectedIncidentId: Long, incidents: List, + onSelectIncident: (Incident) -> Unit, modifier: Modifier = Modifier, onBackClick: () -> Unit = {}, padding: Dp = 16.dp, ) { var enableInput by rememberSaveable { mutableStateOf(true) } - val onSelectIncident = remember(selectIncidentViewModel) { + val rememberOnSelectIncident = remember(rememberKey) { { incident: Incident -> if (enableInput) { enableInput = false - selectIncidentViewModel.selectIncident(incident) + onSelectIncident(incident) onBackClick() } } } - val selectedIncidentId by selectIncidentViewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() Box(Modifier.weight(weight = 1f, fill = false)) { val listState = rememberLazyListState() @@ -143,7 +144,7 @@ private fun ColumnScope.IncidentSelectContent( .testTag("selectIncidentItem_$id") .fillParentMaxWidth() .clickable(enabled = enableInput) { - onSelectIncident(incident) + rememberOnSelectIncident(incident) } .padding(padding), text = incident.name, diff --git a/core/testing/src/main/java/com/crisiscleanup/core/testing/util/TestNavigationObserver.kt b/core/testing/src/main/java/com/crisiscleanup/core/testing/util/TestNavigationObserver.kt deleted file mode 100644 index 96c3db1d6..000000000 --- a/core/testing/src/main/java/com/crisiscleanup/core/testing/util/TestNavigationObserver.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.crisiscleanup.core.testing.util - -import android.os.Bundle -import com.crisiscleanup.core.common.NavigationObserver - -class TestNavigationObserver : NavigationObserver { - override fun onRouteChange(route: String?, arguments: Bundle?) {} -} diff --git a/feature/cases/build.gradle.kts b/feature/cases/build.gradle.kts index 5cdad6d6a..668f5ea0c 100644 --- a/feature/cases/build.gradle.kts +++ b/feature/cases/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":core:commonassets")) implementation(project(":core:commoncase")) implementation(project(":core:mapmarker")) + implementation(project(":core:selectincident")) implementation(libs.kotlinx.datetime) diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt index c5804854d..3649b7495 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt @@ -41,7 +41,7 @@ import com.crisiscleanup.core.data.repository.WorksiteChangeRepository import com.crisiscleanup.core.data.repository.WorksitesRepository import com.crisiscleanup.core.data.util.IncidentDataPullReporter import com.crisiscleanup.core.data.util.IncidentDataPullStats -import com.crisiscleanup.core.domain.LoadIncidentDataUseCase +import com.crisiscleanup.core.domain.LoadSelectIncidents import com.crisiscleanup.core.mapmarker.IncidentBoundsProvider import com.crisiscleanup.core.mapmarker.MapCaseIconProvider import com.crisiscleanup.core.mapmarker.model.MapViewCameraZoom @@ -97,8 +97,7 @@ class CasesViewModel @Inject constructor( incidentsRepository: IncidentsRepository, incidentBoundsProvider: IncidentBoundsProvider, private val worksitesRepository: WorksitesRepository, - private val incidentSelector: IncidentSelector, - loadIncidentDataUseCase: LoadIncidentDataUseCase, + val incidentSelector: IncidentSelector, dataPullReporter: IncidentDataPullReporter, private val mapCaseIconProvider: MapCaseIconProvider, private val worksiteInteractor: WorksiteInteractor, @@ -123,7 +122,13 @@ class CasesViewModel @Inject constructor( @Logger(CrisisCleanupLoggers.Cases) private val logger: AppLogger, val appEnv: AppEnv, ) : ViewModel(), TrimMemoryListener { - val incidentsData = loadIncidentDataUseCase() + val loadSelectIncidents = LoadSelectIncidents( + incidentsRepository = incidentsRepository, + incidentSelector = incidentSelector, + appPreferencesRepository = appPreferencesRepository, + coroutineScope = viewModelScope, + ) + val incidentsData = loadSelectIncidents.data val incidentId: Long get() = incidentSelector.incidentId.value diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/SelectIncidentViewModel.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/SelectIncidentViewModel.kt deleted file mode 100644 index e6ec2bf0c..000000000 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/SelectIncidentViewModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.crisiscleanup.feature.cases - -import androidx.lifecycle.ViewModel -import com.crisiscleanup.core.common.di.ApplicationScope -import com.crisiscleanup.core.data.IncidentSelector -import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository -import com.crisiscleanup.core.domain.IncidentsData -import com.crisiscleanup.core.domain.LoadIncidentDataUseCase -import com.crisiscleanup.core.model.data.Incident -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SelectIncidentViewModel @Inject constructor( - val incidentSelector: IncidentSelector, - private val appPreferencesRepository: LocalAppPreferencesRepository, - loadIncidentDataUseCase: LoadIncidentDataUseCase, - @ApplicationScope private val coroutineScope: CoroutineScope, -) : ViewModel() { - val incidentsData = loadIncidentDataUseCase() - - fun selectIncident(incident: Incident) { - coroutineScope.launch { - (incidentsData.first() as? IncidentsData.Incidents)?.let { data -> - data.incidents.find { it.id == incident.id }?.let { matchingIncident -> - appPreferencesRepository.setSelectedIncident(matchingIncident.id) - - incidentSelector.setIncident(matchingIncident) - } - } - } - } -} diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt index 0ee527374..b130fd34d 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt @@ -75,8 +75,10 @@ import com.crisiscleanup.core.mapmarker.model.MapViewCameraZoomDefault import com.crisiscleanup.core.mapmarker.ui.rememberMapProperties import com.crisiscleanup.core.mapmarker.ui.rememberMapUiSettings import com.crisiscleanup.core.model.data.EmptyIncident +import com.crisiscleanup.core.model.data.Incident import com.crisiscleanup.core.model.data.Worksite import com.crisiscleanup.core.model.data.WorksiteMapMark +import com.crisiscleanup.core.selectincident.SelectIncidentDialog import com.crisiscleanup.core.ui.LocalAppLayout import com.crisiscleanup.feature.cases.CasesViewModel import com.crisiscleanup.feature.cases.R @@ -117,7 +119,7 @@ internal fun CasesRoute( openTransferWorkType() } - val incidentsData by viewModel.incidentsData.collectAsStateWithLifecycle(IncidentsData.Loading) + val incidentsData by viewModel.incidentsData.collectAsStateWithLifecycle() val isIncidentLoading by viewModel.isIncidentLoading.collectAsState(true) val isLoadingData by viewModel.isLoadingData.collectAsState(true) if (incidentsData is IncidentsData.Incidents) { @@ -216,7 +218,19 @@ internal fun CasesRoute( if (showChangeIncident) { val closeDialog = remember(viewModel) { { showChangeIncident = false } } - SelectIncidentDialog(closeDialog) + val selectedIncidentId by viewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() + val setSelected = remember(viewModel) { + { incident: Incident -> + viewModel.loadSelectIncidents.selectIncident(incident) + } + } + SelectIncidentDialog( + rememberKey = viewModel, + onBackClick = closeDialog, + incidentsData = incidentsData, + selectedIncidentId = selectedIncidentId, + onSelectIncident = setSelected, + ) } val closePermissionDialog = diff --git a/feature/menu/build.gradle.kts b/feature/menu/build.gradle.kts index 6fb38d888..407501da3 100644 --- a/feature/menu/build.gradle.kts +++ b/feature/menu/build.gradle.kts @@ -7,3 +7,9 @@ plugins { android { namespace = "com.crisiscleanup.feature.menu" } + +dependencies { + implementation(project(":core:commonassets")) + implementation(project(":core:commoncase")) + implementation(project(":core:selectincident")) +} \ No newline at end of file diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt index b121eb324..38154e0ed 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt @@ -1,5 +1,6 @@ package com.crisiscleanup.feature.menu +import androidx.annotation.DrawableRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -8,32 +9,47 @@ 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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState 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.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.crisiscleanup.core.commoncase.ui.IncidentDropdownSelect import com.crisiscleanup.core.designsystem.LocalAppTranslator import com.crisiscleanup.core.designsystem.component.CrisisCleanupButton import com.crisiscleanup.core.designsystem.component.CrisisCleanupOutlinedButton import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton +import com.crisiscleanup.core.designsystem.component.TopAppBarDefault +import com.crisiscleanup.core.designsystem.component.TruncatedAppBarText import com.crisiscleanup.core.designsystem.component.actionHeight +import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.theme.listItemModifier import com.crisiscleanup.core.designsystem.theme.listItemPadding import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy +import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.selectincident.SelectIncidentDialog @Composable internal fun MenuRoute( + openAuthentication: () -> Unit = {}, openInviteTeammate: () -> Unit = {}, openUserFeedback: () -> Unit = {}, openSyncLogs: () -> Unit = {}, ) { MenuScreen( + openAuthentication = openAuthentication, openInviteTeammate = openInviteTeammate, openUserFeedback = openUserFeedback, openSyncLogs = openSyncLogs, @@ -43,11 +59,26 @@ internal fun MenuRoute( @Composable internal fun MenuScreen( viewModel: MenuViewModel = hiltViewModel(), + openAuthentication: () -> Unit = {}, openInviteTeammate: () -> Unit = {}, openUserFeedback: () -> Unit = {}, openSyncLogs: () -> Unit = {}, ) { - val translator = LocalAppTranslator.current + val t = LocalAppTranslator.current + + val screenTitle by viewModel.screenTitle.collectAsStateWithLifecycle() + val isHeaderLoading by viewModel.showHeaderLoading.collectAsState(false) + + val incidentsData by viewModel.incidentsData.collectAsStateWithLifecycle() + val disasterIconResId by viewModel.disasterIconResId.collectAsStateWithLifecycle() + + var showIncidentPicker by remember { mutableStateOf(false) } + val openIncidentsSelect = remember(viewModel) { + { showIncidentPicker = true } + } + + val isAccountExpired by viewModel.isAccountExpired.collectAsStateWithLifecycle() + val profilePictureUri by viewModel.profilePictureUri.collectAsStateWithLifecycle() val isSharingAnalytics by viewModel.isSharingAnalytics.collectAsStateWithLifecycle(false) val shareAnalytics = remember(viewModel) { @@ -58,85 +89,162 @@ internal fun MenuScreen( Box(Modifier.fillMaxSize()) { Column(Modifier.fillMaxWidth()) { - Text( - modifier = listItemModifier, - text = viewModel.versionText, + TopBar( + modifier = Modifier, + title = screenTitle, + isAppHeaderLoading = isHeaderLoading, + profilePictureUri = profilePictureUri, + isAccountExpired = isAccountExpired, + openAuthentication = openAuthentication, + disasterIconResId = disasterIconResId, + onOpenIncidents = openIncidentsSelect, ) - CrisisCleanupButton( - modifier = Modifier - .fillMaxWidth() - .listItemPadding(), - text = translator("usersVue.invite_new_user"), - onClick = openInviteTeammate, - ) - - CrisisCleanupOutlinedButton( - modifier = Modifier - .fillMaxWidth() - .listItemPadding() - .actionHeight(), - text = translator("info.give_app_feedback"), - onClick = openUserFeedback, - enabled = true, - ) - - Row( + Column( Modifier - .clickable( - onClick = { shareAnalytics(!isSharingAnalytics) }, - ) - .then(listItemModifier), - verticalAlignment = Alignment.CenterVertically, + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { Text( - translator("actions.share_analytics"), - Modifier.weight(1f), - ) - Switch( - checked = isSharingAnalytics, - onCheckedChange = shareAnalytics, + modifier = listItemModifier, + text = viewModel.versionText, ) - } - - if (viewModel.isDebuggable) { - MenuScreenDebug() - } - if (viewModel.isNotProduction) { - CrisisCleanupTextButton( - onClick = openSyncLogs, - text = "See sync logs", + CrisisCleanupButton( + modifier = Modifier + .fillMaxWidth() + .listItemPadding(), + text = t("usersVue.invite_new_user"), + onClick = openInviteTeammate, ) - } - - Spacer(modifier = Modifier.weight(1f)) - // TODO Open in WebView? - val uriHandler = LocalUriHandler.current - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - Text( - translator("publicNav.terms"), - Modifier + CrisisCleanupOutlinedButton( + modifier = Modifier + .fillMaxWidth() .listItemPadding() - .clickable { uriHandler.openUri("https://crisiscleanup.org/terms") }, + .actionHeight(), + text = t("info.give_app_feedback"), + onClick = openUserFeedback, + enabled = true, ) - Text( - translator("nav.privacy"), + + Row( Modifier - .listItemPadding() - .clickable { uriHandler.openUri("https://crisiscleanup.org/privacy") }, - ) + .clickable( + onClick = { shareAnalytics(!isSharingAnalytics) }, + ) + .then(listItemModifier), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + t("actions.share_analytics"), + Modifier.weight(1f), + ) + Switch( + checked = isSharingAnalytics, + onCheckedChange = shareAnalytics, + ) + } + + if (viewModel.isDebuggable) { + MenuScreenNonProductionView() + } + + if (viewModel.isNotProduction) { + CrisisCleanupTextButton( + onClick = openSyncLogs, + text = "See sync logs", + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + // TODO Open in WebView? + val uriHandler = LocalUriHandler.current + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + t("publicNav.terms"), + Modifier + .listItemPadding() + .clickable { uriHandler.openUri("https://crisiscleanup.org/terms") }, + ) + Text( + t("nav.privacy"), + Modifier + .listItemPadding() + .clickable { uriHandler.openUri("https://crisiscleanup.org/privacy") }, + ) + } } } + + if (showIncidentPicker) { + val closeDialog = { showIncidentPicker = false } + val selectedIncidentId by viewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() + val setSelected = remember(viewModel) { + { incident: Incident -> + viewModel.loadSelectIncidents.selectIncident(incident) + } + } + SelectIncidentDialog( + rememberKey = viewModel, + onBackClick = closeDialog, + incidentsData = incidentsData, + selectedIncidentId = selectedIncidentId, + onSelectIncident = setSelected, + ) + } } } +@OptIn( + ExperimentalMaterial3Api::class, +) +@Composable +private fun TopBar( + modifier: Modifier = Modifier, + title: String = "", + isAppHeaderLoading: Boolean = false, + profilePictureUri: String = "", + isAccountExpired: Boolean = false, + openAuthentication: () -> Unit = {}, + @DrawableRes disasterIconResId: Int = 0, + onOpenIncidents: (() -> Unit)? = null, +) { + val t = LocalAppTranslator.current + val actionText = t("actions.account") + TopAppBarDefault( + modifier = modifier, + title = title, + profilePictureUri = profilePictureUri, + actionIcon = CrisisCleanupIcons.Account, + actionText = actionText, + isActionAttention = isAccountExpired, + onActionClick = openAuthentication, + onNavigationClick = null, + titleContent = @Composable { + // TODO Match height of visible part of app bar (not the entire app bar) + if (onOpenIncidents == null) { + TruncatedAppBarText(title = title) + } else { + IncidentDropdownSelect( + modifier = Modifier.testTag("appIncidentSelector"), + onOpenIncidents, + disasterIconResId, + title = title, + contentDescription = t("nav.change_incident"), + isLoading = isAppHeaderLoading, + ) + } + }, + ) +} + @Composable -internal fun MenuScreenDebug( +internal fun MenuScreenNonProductionView( viewModel: MenuViewModel = hiltViewModel(), ) { val databaseText = viewModel.databaseVersionText diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt index e8427db0a..9e3e118b3 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt @@ -5,24 +5,38 @@ import androidx.lifecycle.viewModelScope import com.crisiscleanup.core.common.AppEnv import com.crisiscleanup.core.common.AppVersionProvider import com.crisiscleanup.core.common.DatabaseVersionProvider +import com.crisiscleanup.core.common.KeyResourceTranslator import com.crisiscleanup.core.common.di.ApplicationScope import com.crisiscleanup.core.common.network.CrisisCleanupDispatchers.IO import com.crisiscleanup.core.common.network.Dispatcher import com.crisiscleanup.core.common.sync.SyncPuller +import com.crisiscleanup.core.commonassets.R +import com.crisiscleanup.core.commonassets.getDisasterIcon +import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.AccountDataRefresher import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.CrisisCleanupAccountDataRepository +import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.data.repository.SyncLogRepository +import com.crisiscleanup.core.data.repository.WorksitesRepository +import com.crisiscleanup.core.domain.LoadSelectIncidents import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MenuViewModel @Inject constructor( + incidentsRepository: IncidentsRepository, + worksitesRepository: WorksitesRepository, + val incidentSelector: IncidentSelector, + private val translator: KeyResourceTranslator, syncLogRepository: SyncLogRepository, private val accountDataRepository: AccountDataRepository, private val accountDataRefresher: AccountDataRefresher, @@ -37,6 +51,56 @@ class MenuViewModel @Inject constructor( val isDebuggable = appEnv.isDebuggable val isNotProduction = appEnv.isNotProduction + val loadSelectIncidents = LoadSelectIncidents( + incidentsRepository = incidentsRepository, + incidentSelector = incidentSelector, + appPreferencesRepository = appPreferencesRepository, + coroutineScope = viewModelScope, + ) + val incidentsData = loadSelectIncidents.data + + val disasterIconResId = incidentSelector.incident.map { getDisasterIcon(it.disaster) } + .stateIn( + scope = viewModelScope, + initialValue = R.drawable.ic_disaster_other, + started = SharingStarted.WhileSubscribed(), + ) + + private val isSyncingWorksitesFull = combine( + incidentSelector.incidentId, + worksitesRepository.syncWorksitesFullIncidentId, + ) { incidentId, syncingIncidentId -> incidentId == syncingIncidentId } + + val showHeaderLoading = combine( + incidentsRepository.isLoading, + worksitesRepository.isLoading, + isSyncingWorksitesFull, + ) { b0, b1, b2 -> b0 || b1 || b2 } + + val screenTitle = incidentSelector.incident + .map { it.name.ifBlank { translator("nav.menu") } } + .stateIn( + scope = viewModelScope, + initialValue = "", + started = SharingStarted.WhileSubscribed(), + ) + + val isAccountExpired = accountDataRepository.accountData + .map { !it.areTokensValid } + .stateIn( + scope = viewModelScope, + initialValue = false, + started = SharingStarted.WhileSubscribed(), + ) + + val profilePictureUri = accountDataRepository.accountData + .map { it.profilePictureUri } + .stateIn( + scope = viewModelScope, + initialValue = "", + started = SharingStarted.WhileSubscribed(), + ) + val versionText: String get() { val version = appVersionProvider.version diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/navigation/MenuNavigation.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/navigation/MenuNavigation.kt index 975acdfe6..6f4f254bc 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/navigation/MenuNavigation.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/navigation/MenuNavigation.kt @@ -12,12 +12,14 @@ fun NavController.navigateToMenu(navOptions: NavOptions? = null) { } fun NavGraphBuilder.menuScreen( + openAuthentication: () -> Unit = {}, openInviteTeammate: () -> Unit = {}, openUserFeedback: () -> Unit = {}, openSyncLogs: () -> Unit = {}, ) { composable(route = menuRoute) { MenuRoute( + openAuthentication = openAuthentication, openInviteTeammate = openInviteTeammate, openUserFeedback = openUserFeedback, openSyncLogs = openSyncLogs, diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt index b6b1a1090..c0920d0ca 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt @@ -19,11 +19,14 @@ import com.crisiscleanup.core.common.network.CrisisCleanupDispatchers.IO import com.crisiscleanup.core.common.network.Dispatcher import com.crisiscleanup.core.common.throttleLatest import com.crisiscleanup.core.data.IncidentSelectManager +import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.AccountDataRepository +import com.crisiscleanup.core.data.repository.IncidentsRepository +import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.data.repository.OrgVolunteerRepository import com.crisiscleanup.core.data.repository.OrganizationsRepository import com.crisiscleanup.core.domain.IncidentsData -import com.crisiscleanup.core.domain.LoadIncidentDataUseCase +import com.crisiscleanup.core.domain.LoadSelectIncidents import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.Incident import com.crisiscleanup.core.model.data.IncidentOrganizationInviteInfo @@ -53,11 +56,13 @@ import javax.inject.Inject class InviteTeammateViewModel @Inject constructor( settingsProvider: AppSettingsProvider, accountDataRepository: AccountDataRepository, + incidentsRepository: IncidentsRepository, + incidentSelector: IncidentSelector, + appPreferencesRepository: LocalAppPreferencesRepository, organizationsRepository: OrganizationsRepository, private val orgVolunteerRepository: OrgVolunteerRepository, private val inputValidator: InputValidator, qrCodeGenerator: QrCodeGenerator, - loadIncidentDataUseCase: LoadIncidentDataUseCase, incidentSelectManager: IncidentSelectManager, private val translator: KeyResourceTranslator, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @@ -80,7 +85,13 @@ class InviteTeammateViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(), ) - private val incidentsData = loadIncidentDataUseCase() + private val loadSelectIncidents = LoadSelectIncidents( + incidentsRepository = incidentsRepository, + incidentSelector = incidentSelector, + appPreferencesRepository = appPreferencesRepository, + coroutineScope = viewModelScope, + ) + private val incidentsData = loadSelectIncidents.data val inviteToAnotherOrg = MutableStateFlow(false) private val affiliateOrganizationIds = MutableStateFlow?>(null) diff --git a/settings.gradle.kts b/settings.gradle.kts index d8e0f4d15..084e9f851 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,6 @@ dependencyResolutionManagement { rootProject.name = "crisiscleanup" include(":app") include(":core:addresssearch") -include(":core:appheader") include(":core:appnav") include(":core:common") include(":core:commonassets") @@ -34,6 +33,7 @@ include(":core:mapmarker") include(":core:model") include(":core:network") include(":core:renderscript-toolkit") +include(":core:selectincident") include(":core:testerfeedbackapi") include(":core:testerfeedback") include(":core:ui") From ff6ffe541a5698dd31482e286e98549e3ae7d06d Mon Sep 17 00:00:00 2001 From: hue Date: Fri, 5 Jan 2024 23:55:10 -0500 Subject: [PATCH 03/29] Layout main navigation according to screen orientation --- .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 6 +- .../crisiscleanup/ui/CrisisCleanupAppState.kt | 1 + .../core/designsystem/theme/Dimensions.kt | 3 + .../core/designsystem/theme/Theme.kt | 8 +- .../feature/cases/ui/CasesFilterScreen.kt | 102 +++++++++++++----- 5 files changed, 89 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index f9cc256a5..f2273fb71 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -61,6 +61,7 @@ import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationRail import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationRailItem import com.crisiscleanup.core.designsystem.icon.Icon.DrawableResourceIcon import com.crisiscleanup.core.designsystem.icon.Icon.ImageVectorIcon +import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.ui.AppLayoutArea import com.crisiscleanup.core.ui.LocalAppLayout import com.crisiscleanup.core.ui.rememberIsKeyboardOpen @@ -245,6 +246,7 @@ private fun NavigableContent( openAuthentication: () -> Unit, ) { val showNavigation = appState.isTopLevelRoute + val layoutBottomNav = appState.shouldShowBottomBar || LocalDimensions.current.isPortrait val isFullscreen = appState.isFullscreenRoute Scaffold( modifier = Modifier.semantics { @@ -255,7 +257,7 @@ private fun NavigableContent( contentWindowInsets = WindowInsets(0, 0, 0, 0), snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = { - val showBottomBar = showNavigation && appState.shouldShowBottomBar + val showBottomBar = showNavigation && layoutBottomNav AnimatedVisibility( visible = showBottomBar, enter = slideIn { IntOffset.Zero }, @@ -292,7 +294,7 @@ private fun NavigableContent( }, ), ) { - if (showNavigation && appState.shouldShowNavRail) { + if (showNavigation && !layoutBottomNav) { CrisisCleanupNavRail( destinations = appState.topLevelDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt index 667f04ef4..e34c665e8 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt @@ -23,6 +23,7 @@ import com.crisiscleanup.core.appnav.RouteConstant.topLevelRoutes import com.crisiscleanup.core.appnav.RouteConstant.userFeedbackRoute import com.crisiscleanup.core.appnav.RouteConstant.viewImageRoute import com.crisiscleanup.core.common.NetworkMonitor +import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.ui.TrackDisposableJank import com.crisiscleanup.feature.cases.navigation.navigateToCases import com.crisiscleanup.feature.dashboard.navigation.navigateToDashboard diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt index 1e46510ca..cc69b80ef 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt @@ -25,6 +25,9 @@ data class Dimensions( */ val itemInnerPaddingHorizontalFlexible: Dp = 16.dp, val isThinScreenWidth: Boolean = false, + val isLandscape: Boolean = false, + val isPortrait: Boolean = true, + val isListDetailWidth: Boolean = false, ) { val itemInnerSpacingHorizontalFlexible: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(itemInnerPaddingHorizontalFlexible) diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt index abe36c551..e75ba2acb 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt @@ -145,7 +145,13 @@ internal fun CrisisCleanupTheme( ) val configuration = LocalConfiguration.current - val dimensions = if (configuration.screenWidthDp <= 360) w360Dimensions else Dimensions() + val dimensions0 = if (configuration.screenWidthDp <= 360) w360Dimensions else Dimensions() + val isLandscape = configuration.screenWidthDp > configuration.screenHeightDp + val dimensions = dimensions0.copy( + isLandscape = isLandscape, + isPortrait = !isLandscape, + isListDetailWidth = true, + ) CompositionLocalProvider( LocalBackgroundTheme provides defaultBackgroundTheme, diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt index e45c84ea1..150260079 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt @@ -4,11 +4,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow 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.sizeIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.ExperimentalMaterial3Api @@ -53,6 +56,7 @@ import com.crisiscleanup.core.designsystem.component.rememberFocusSectionSliderS import com.crisiscleanup.core.designsystem.component.rememberSectionContentIndexLookup import com.crisiscleanup.core.designsystem.component.roundedOutline import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons +import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.listItemHeight import com.crisiscleanup.core.designsystem.theme.listItemHorizontalPadding @@ -79,37 +83,61 @@ private val dateFormatter = DateTimeFormatter .ofPattern("yyyy-MM-dd") .withZone(ZoneId.systemDefault()) -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CasesFilterRoute( onBack: () -> Unit = {}, viewModel: CasesFilterViewModel = hiltViewModel(), ) { - val translator = LocalAppTranslator.current + val isListDetailLayout = LocalDimensions.current.isListDetailWidth + + val t = LocalAppTranslator.current val filters by viewModel.casesFilters.collectAsStateWithLifecycle() val updateFilters = remember(viewModel) { { filters: CasesFilter -> viewModel.changeFilters(filters) } } - Column( - Modifier - .fillMaxSize() - .background(Color.White), - ) { - TopAppBarBackAction( - title = translator("worksiteFilters.filters"), - onAction = onBack, - modifier = Modifier.testTag("workFilterBackBtn"), - ) + val screenModifier = Modifier + .fillMaxSize() + .background(Color.White) - FilterControls( - filters, - updateFilters = updateFilters, - ) + if (isListDetailLayout) { + Row(screenModifier) { + Column(Modifier.weight(0.3f)) { + TopBar(onBack) - BottomActionBar( - onBack = onBack, - filters = filters, - ) + Spacer(Modifier.weight(1f)) + + BottomActionBar( + onBack = onBack, + filters = filters, + ) + } + Column( + Modifier + .weight(0.7f) + .sizeIn(maxWidth = 480.dp), + ) { + FilterContent( + filters, + updateFilters, + ) + } + } + + } else { + Column(screenModifier) { + TopBar(onBack) + + FilterContent( + filters, + updateFilters, + ) + + BottomActionBar( + onBack = onBack, + filters = filters, + horizontalLayout = true, + ) + } } val closePermissionDialog = @@ -118,7 +146,19 @@ internal fun CasesFilterRoute( ExplainLocationPermissionDialog( showDialog = explainPermission, closeDialog = closePermissionDialog, - explanation = translator("worksiteFilters.location_required_to_filter_by_distance"), + explanation = t("worksiteFilters.location_required_to_filter_by_distance"), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + onBack: () -> Unit, +) { + TopAppBarBackAction( + title = LocalAppTranslator.current("worksiteFilters.filters"), + onAction = onBack, + modifier = Modifier.testTag("workFilterBackBtn"), ) } @@ -132,10 +172,10 @@ private val collapsibleFilterSections = listOf( ) @Composable -private fun ColumnScope.FilterControls( +private fun ColumnScope.FilterContent( filters: CasesFilter, + updateFilters: (CasesFilter) -> Unit, viewModel: CasesFilterViewModel = hiltViewModel(), - updateFilters: (CasesFilter) -> Unit = {}, ) { val translator = LocalAppTranslator.current @@ -952,16 +992,21 @@ private fun LazyListScope.dateOptions( } } +@OptIn(ExperimentalLayoutApi::class) @Composable fun BottomActionBar( onBack: () -> Unit, filters: CasesFilter, + horizontalLayout: Boolean = false, viewModel: CasesFilterViewModel = hiltViewModel(), ) { - val translator = LocalAppTranslator.current - Row( - modifier = listItemModifier, + val t = LocalAppTranslator.current + val rowMaxItemCount = if (horizontalLayout) Int.MAX_VALUE else 1 + FlowRow( + listItemModifier, horizontalArrangement = listItemSpacedBy, + verticalArrangement = listItemSpacedBy, + maxItemsInEachRow = rowMaxItemCount, ) { val filterCount = filters.changeCount val hasFilters = filterCount > 0 @@ -969,12 +1014,13 @@ fun BottomActionBar( Modifier .testTag("filterClearFiltersBtn") .weight(1f), - text = translator("actions.clear_filters"), + text = t("actions.clear_filters"), enabled = hasFilters, onClick = viewModel::clearFilters, colors = cancelButtonColors(), ) - val applyFilters = translator("actions.apply_filters") + + val applyFilters = t("actions.apply_filters") val applyText = if (hasFilters) "$applyFilters ($filterCount)" else applyFilters BusyButton( Modifier From fa0052fd30df42e799fc79b6d43ff83aa3ff1f71 Mon Sep 17 00:00:00 2001 From: hue Date: Sat, 6 Jan 2024 00:23:42 -0500 Subject: [PATCH 04/29] Refactor app nav construction --- .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index f2273fb71..f40d0cdd2 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -382,6 +382,37 @@ private fun TopLevelDestination.Icon(isSelected: Boolean, description: String) { } } +@Composable +private fun NavItems( + destinations: List, + onNavigateToDestination: (TopLevelDestination) -> Unit, + currentDestination: NavDestination?, + itemContent: @Composable ( + Boolean, + String, + () -> Unit, + @Composable () -> Unit, + @Composable () -> Unit, + ) -> Unit, +) { + destinations.forEach { destination -> + val title = LocalAppTranslator.current(destination.titleTranslateKey) + val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) + itemContent( + selected, + title, + { onNavigateToDestination(destination) }, + { destination.Icon(selected, title) }, + { + Text( + title, + style = MaterialTheme.typography.bodySmall, + ) + }, + ) + } +} + @Composable private fun CrisisCleanupNavRail( destinations: List, @@ -389,22 +420,25 @@ private fun CrisisCleanupNavRail( currentDestination: NavDestination?, modifier: Modifier = Modifier, ) { - val translator = LocalAppTranslator.current CrisisCleanupNavigationRail(modifier = modifier) { Spacer(Modifier.weight(1f)) - destinations.forEach { destination -> - val title = translator(destination.titleTranslateKey) - val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) + NavItems( + destinations = destinations, + onNavigateToDestination = onNavigateToDestination, + currentDestination = currentDestination, + ) { + isSelected: Boolean, + title: String, + onClick: () -> Unit, + iconContent: @Composable () -> Unit, + labelContent: @Composable () -> Unit, + -> CrisisCleanupNavigationRailItem( - selected = selected, - onClick = { onNavigateToDestination(destination) }, - icon = { destination.Icon(selected, title) }, - label = { - Text( - title, - style = MaterialTheme.typography.bodySmall, - ) - }, + selected = isSelected, + onClick = onClick, + icon = iconContent, + label = labelContent, + modifier = Modifier.testTag("navItem_$title"), ) } } @@ -417,22 +451,24 @@ private fun CrisisCleanupBottomBar( currentDestination: NavDestination?, modifier: Modifier = Modifier, ) { - val translator = LocalAppTranslator.current CrisisCleanupNavigationBar(modifier = modifier) { - destinations.forEach { destination -> - val title = translator(destination.titleTranslateKey) - val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) + NavItems( + destinations = destinations, + onNavigateToDestination = onNavigateToDestination, + currentDestination = currentDestination, + ) { + isSelected: Boolean, + title: String, + onClick: () -> Unit, + iconContent: @Composable () -> Unit, + labelContent: @Composable () -> Unit, + -> CrisisCleanupNavigationBarItem( - selected = selected, - onClick = { onNavigateToDestination(destination) }, - icon = { destination.Icon(selected, title) }, + selected = isSelected, + onClick = onClick, + icon = iconContent, + label = labelContent, modifier = Modifier.testTag("navItem_$title"), - label = { - Text( - title, - style = MaterialTheme.typography.bodySmall, - ) - }, ) } } From bc93f3e6a0c7aaa6a6b8b2f39aed114c53df626a Mon Sep 17 00:00:00 2001 From: hue Date: Sat, 6 Jan 2024 16:05:54 -0500 Subject: [PATCH 05/29] Layout view case screen for landscape orientation --- .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 14 +- .../crisiscleanup/ui/CrisisCleanupAppState.kt | 1 - .../core/designsystem/LocalLayoutProvider.kt | 11 + .../designsystem/icon/CrisisCleanupIcons.kt | 3 +- ...iewModel.kt => CreateEditCaseViewModel.kt} | 2 +- ...gCaseViewModel.kt => ViewCaseViewModel.kt} | 7 +- ...ditorScreen.kt => CreateEditCaseScreen.kt} | 16 +- .../caseeditor/ui/ExistingCaseMediaViews.kt | 4 +- .../feature/caseeditor/ui/SectionHeader.kt | 4 +- .../feature/caseeditor/ui/ViewCaseHeader.kt | 280 ++++++++++ .../feature/caseeditor/ui/ViewCaseNav.kt | 235 +++++++++ ...xistingCaseScreen.kt => ViewCaseScreen.kt} | 493 ++++++++---------- .../feature/cases/ui/CasesFilterScreen.kt | 1 - .../lint/designsystem/DesignSystemDetector.kt | 4 +- 14 files changed, 782 insertions(+), 293 deletions(-) create mode 100644 core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/LocalLayoutProvider.kt rename feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/{CaseEditorViewModel.kt => CreateEditCaseViewModel.kt} (99%) rename feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/{ExistingCaseViewModel.kt => ViewCaseViewModel.kt} (99%) rename feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/{CaseEditorScreen.kt => CreateEditCaseScreen.kt} (98%) create mode 100644 feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseHeader.kt create mode 100644 feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt rename feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/{EditExistingCaseScreen.kt => ViewCaseScreen.kt} (78%) diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index f40d0cdd2..af88cc5ec 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -53,7 +53,9 @@ import androidx.navigation.NavDestination.Companion.hierarchy import com.crisiscleanup.AuthState import com.crisiscleanup.MainActivityViewModel import com.crisiscleanup.core.common.NetworkMonitor +import com.crisiscleanup.core.designsystem.LayoutProvider import com.crisiscleanup.core.designsystem.LocalAppTranslator +import com.crisiscleanup.core.designsystem.LocalLayoutProvider import com.crisiscleanup.core.designsystem.component.CrisisCleanupBackground import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationBar import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationBarItem @@ -111,7 +113,15 @@ fun CrisisCleanupApp( } else if (authState is AuthState.Loading) { // Splash screen should be showing } else { - CompositionLocalProvider(LocalAppTranslator provides translator) { + val layoutBottomNav = + appState.shouldShowBottomBar || LocalDimensions.current.isPortrait + val layoutProvider = LayoutProvider( + isBottomNav = layoutBottomNav, + ) + CompositionLocalProvider( + LocalAppTranslator provides translator, + LocalLayoutProvider provides layoutProvider, + ) { val endOfLife = viewModel.buildEndOfLife val minSupportedAppVersion = viewModel.supportedApp if (endOfLife?.isEndOfLife == true) { @@ -246,7 +256,7 @@ private fun NavigableContent( openAuthentication: () -> Unit, ) { val showNavigation = appState.isTopLevelRoute - val layoutBottomNav = appState.shouldShowBottomBar || LocalDimensions.current.isPortrait + val layoutBottomNav = LocalLayoutProvider.current.isBottomNav val isFullscreen = appState.isFullscreenRoute Scaffold( modifier = Modifier.semantics { diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt index e34c665e8..667f04ef4 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupAppState.kt @@ -23,7 +23,6 @@ import com.crisiscleanup.core.appnav.RouteConstant.topLevelRoutes import com.crisiscleanup.core.appnav.RouteConstant.userFeedbackRoute import com.crisiscleanup.core.appnav.RouteConstant.viewImageRoute import com.crisiscleanup.core.common.NetworkMonitor -import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.ui.TrackDisposableJank import com.crisiscleanup.feature.cases.navigation.navigateToCases import com.crisiscleanup.feature.dashboard.navigation.navigateToDashboard diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/LocalLayoutProvider.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/LocalLayoutProvider.kt new file mode 100644 index 000000000..cae29b427 --- /dev/null +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/LocalLayoutProvider.kt @@ -0,0 +1,11 @@ +package com.crisiscleanup.core.designsystem + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf + +@Immutable +data class LayoutProvider( + val isBottomNav: Boolean = false, +) + +val LocalLayoutProvider = staticCompositionLocalOf { LayoutProvider() } diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt index 21957f434..a4879a959 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt @@ -2,6 +2,7 @@ package com.crisiscleanup.core.designsystem.icon import androidx.annotation.DrawableRes import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.HelpOutline import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material.icons.filled.ArrowDropDown @@ -66,7 +67,7 @@ object CrisisCleanupIcons { val ExpandAll = icons.UnfoldMore val ExpandLess = icons.ExpandLess val ExpandMore = icons.ExpandMore - val Help = icons.HelpOutline + val Help = Icons.AutoMirrored.Filled.HelpOutline val Location = icons.LocationOn val Mail = icons.Mail val Minus = icons.Remove diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseEditorViewModel.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt similarity index 99% rename from feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseEditorViewModel.kt rename to feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt index e8d405c3b..eafd27c0b 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseEditorViewModel.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt @@ -68,7 +68,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @HiltViewModel -class CaseEditorViewModel @Inject constructor( +class CreateEditCaseViewModel @Inject constructor( savedStateHandle: SavedStateHandle, accountDataRepository: AccountDataRepository, incidentsRepository: IncidentsRepository, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ExistingCaseViewModel.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ViewCaseViewModel.kt similarity index 99% rename from feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ExistingCaseViewModel.kt rename to feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ViewCaseViewModel.kt index e30564dd4..35346371f 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ExistingCaseViewModel.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ViewCaseViewModel.kt @@ -86,7 +86,7 @@ import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject @HiltViewModel -class ExistingCaseViewModel @Inject constructor( +class ViewCaseViewModel @Inject constructor( savedStateHandle: SavedStateHandle, accountDataRepository: AccountDataRepository, private val incidentsRepository: IncidentsRepository, @@ -644,11 +644,6 @@ class ExistingCaseViewModel @Inject constructor( // TODO Show dialog save failed. Try again. If still fails seek help. } - fun takeNoteAdded(): Boolean { - val noteCount = referenceWorksite.notes.size - return previousNoteCount.getAndSet(noteCount) + 1 == noteCount - } - fun toggleFavorite() { val startingWorksite = referenceWorksite val changedWorksite = diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseEditorScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt similarity index 98% rename from feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseEditorScreen.kt rename to feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt index de13a513a..8e0f9e5ab 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseEditorScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt @@ -48,8 +48,8 @@ import com.crisiscleanup.core.ui.rememberCloseKeyboard import com.crisiscleanup.core.ui.rememberIsKeyboardOpen import com.crisiscleanup.core.ui.scrollFlingListener import com.crisiscleanup.feature.caseeditor.CaseEditorUiState -import com.crisiscleanup.feature.caseeditor.CaseEditorViewModel import com.crisiscleanup.feature.caseeditor.CasePropertyDataEditor +import com.crisiscleanup.feature.caseeditor.CreateEditCaseViewModel import com.crisiscleanup.feature.caseeditor.WorksiteSection import com.crisiscleanup.feature.caseeditor.model.FormFieldsInputData import com.crisiscleanup.core.common.R as commonR @@ -66,7 +66,7 @@ internal fun CaseEditorRoute( onEditSearchAddress: () -> Unit = {}, onEditMoveLocationOnMap: () -> Unit = {}, onBack: () -> Unit = {}, - viewModel: CaseEditorViewModel = hiltViewModel(), + viewModel: CreateEditCaseViewModel = hiltViewModel(), ) { val changeWorksiteIncidentId by viewModel.changeWorksiteIncidentId.collectAsStateWithLifecycle() val changeExistingWorksite by viewModel.changeExistingWorksite.collectAsStateWithLifecycle() @@ -124,7 +124,7 @@ internal fun CaseEditorRoute( @Composable internal fun ColumnScope.CaseEditorScreen( modifier: Modifier = Modifier, - viewModel: CaseEditorViewModel = hiltViewModel(), + viewModel: CreateEditCaseViewModel = hiltViewModel(), onNavigateCancel: () -> Unit = {}, onEditSearchAddress: () -> Unit = {}, onEditMoveLocationOnMap: () -> Unit = {}, @@ -172,7 +172,7 @@ internal fun ColumnScope.CaseEditorScreen( private fun ColumnScope.FullEditView( caseData: CaseEditorUiState.CaseData, modifier: Modifier = Modifier, - viewModel: CaseEditorViewModel = hiltViewModel(), + viewModel: CreateEditCaseViewModel = hiltViewModel(), onCancel: () -> Unit = {}, onMoveLocation: () -> Unit = {}, onSearchAddress: () -> Unit = {}, @@ -327,7 +327,7 @@ private fun ColumnScope.FullEditView( private fun LazyListScope.fullEditContent( caseData: CaseEditorUiState.CaseData, - viewModel: CaseEditorViewModel, + viewModel: CreateEditCaseViewModel, modifier: Modifier = Modifier, sectionTitles: List = emptyList(), onMoveLocation: () -> Unit = {}, @@ -386,7 +386,7 @@ private fun LazyListScope.fullEditContent( } private fun LazyListScope.propertyLocationSection( - viewModel: CaseEditorViewModel, + viewModel: CreateEditCaseViewModel, propertyEditor: CasePropertyDataEditor, sectionTitle: String, onMoveLocation: () -> Unit = {}, @@ -444,7 +444,7 @@ private fun LazyListScope.propertyLocationSection( } private fun LazyListScope.formDataSection( - viewModel: CaseEditorViewModel, + viewModel: CreateEditCaseViewModel, inputData: FormFieldsInputData, sectionTitle: String, sectionIndex: Int, @@ -510,7 +510,7 @@ private fun InvalidSaveDialog( onEditLocation: () -> Unit = {}, onEditLocationAddress: () -> Unit = {}, onEditFormData: (Int) -> Unit = {}, - viewModel: CaseEditorViewModel = hiltViewModel(), + viewModel: CreateEditCaseViewModel = hiltViewModel(), ) { val promptInvalidSave by viewModel.showInvalidWorksiteSave.collectAsStateWithLifecycle() if (promptInvalidSave) { diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt index 94a337107..360186ec1 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt @@ -54,8 +54,8 @@ import com.crisiscleanup.core.designsystem.theme.listItemModifier import com.crisiscleanup.core.designsystem.theme.primaryBlueColor import com.crisiscleanup.core.designsystem.theme.primaryBlueOneTenthColor import com.crisiscleanup.core.ui.touchDownConsumer -import com.crisiscleanup.feature.caseeditor.ExistingCaseViewModel import com.crisiscleanup.feature.caseeditor.R +import com.crisiscleanup.feature.caseeditor.ViewCaseViewModel import com.crisiscleanup.feature.caseeditor.model.CaseImage private val addMediaActionPadding = 4.dp @@ -240,7 +240,7 @@ internal fun PhotosSection( @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun TakePhotoSelectImage( - viewModel: ExistingCaseViewModel = hiltViewModel(), + viewModel: ViewCaseViewModel = hiltViewModel(), showOptions: Boolean = false, closeOptions: () -> Unit = {}, ) { diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/SectionHeader.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/SectionHeader.kt index d5deaf3c1..6e6823d17 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/SectionHeader.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/SectionHeader.kt @@ -26,7 +26,7 @@ import com.crisiscleanup.core.designsystem.theme.attentionBackgroundColor import com.crisiscleanup.core.designsystem.theme.listItemHeight import com.crisiscleanup.core.designsystem.theme.listItemPadding import com.crisiscleanup.core.designsystem.theme.listRowItemStartPadding -import com.crisiscleanup.feature.caseeditor.CaseEditorViewModel +import com.crisiscleanup.feature.caseeditor.CreateEditCaseViewModel @Composable private fun CircleNumber( @@ -60,7 +60,7 @@ private val headerTextStyle: TextStyle @Composable internal fun SectionHeaderCollapsible( - viewModel: CaseEditorViewModel, + viewModel: CreateEditCaseViewModel, modifier: Modifier = Modifier, sectionIndex: Int, sectionTitle: String, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseHeader.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseHeader.kt new file mode 100644 index 000000000..8cddb2a6c --- /dev/null +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseHeader.kt @@ -0,0 +1,280 @@ +package com.crisiscleanup.feature.caseeditor.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import com.crisiscleanup.core.designsystem.LocalAppTranslator +import com.crisiscleanup.core.designsystem.component.CrisisCleanupIconButton +import com.crisiscleanup.core.designsystem.component.TopBarBackAction +import com.crisiscleanup.core.designsystem.theme.LocalFontStyles +import com.crisiscleanup.core.designsystem.theme.disabledAlpha +import com.crisiscleanup.core.designsystem.theme.listItemModifier +import com.crisiscleanup.core.designsystem.theme.listItemSpacedByHalf +import com.crisiscleanup.core.designsystem.theme.neutralIconColor +import com.crisiscleanup.core.designsystem.theme.primaryRedColor +import com.crisiscleanup.feature.caseeditor.R + +private fun getTopIconActionColor( + isActive: Boolean, + isEditable: Boolean, +): Color { + var tint = if (isActive) { + primaryRedColor + } else { + neutralIconColor + } + if (!isEditable) { + tint = tint.disabledAlpha() + } + return tint +} + +@Composable +private fun HeaderActions( + isEditable: Boolean, + isHighPriority: Boolean, + isFavorite: Boolean, + toggleHighPriority: () -> Unit, + toggleFavorite: () -> Unit, +) { + val t = LocalAppTranslator.current + val highPriorityTranslateKey = if (isHighPriority) { + "actions.unmark_high_priority" + } else { + "flag.flag_high_priority" + } + val highPriorityTint = getTopIconActionColor(isHighPriority, isEditable) + CrisisCleanupIconButton( + iconResId = R.drawable.ic_important_filled, + contentDescription = t(highPriorityTranslateKey), + onClick = toggleHighPriority, + enabled = isEditable, + tint = highPriorityTint, + modifier = Modifier.testTag("editCaseHighPriorityToggleBtn"), + ) + + val iconResId = if (isFavorite) { + R.drawable.ic_heart_filled + } else { + R.drawable.ic_heart_outline + } + val favoriteDescription = if (isFavorite) { + t("actions.not_member_of_my_org") + } else { + t("actions.member_of_my_org") + } + val favoriteTint = getTopIconActionColor(isFavorite, isEditable) + CrisisCleanupIconButton( + iconResId = iconResId, + contentDescription = favoriteDescription, + onClick = toggleFavorite, + enabled = isEditable, + tint = favoriteTint, + modifier = Modifier.testTag("editCaseFavoriteToggleBtn"), + ) +} + +@Composable +private fun HeaderTitle( + title: String, + modifier: Modifier = Modifier, +) { + Text( + title, + style = LocalFontStyles.current.header3, + modifier = modifier.testTag("editCaseHeaderText"), + ) +} + +@Composable +private fun HeaderSubTitle( + subTitle: String, + modifier: Modifier = Modifier, +) { + if (subTitle.isNotBlank()) { + Text( + subTitle, + style = MaterialTheme.typography.bodySmall, + modifier = modifier.testTag("editCaseSubHeaderText"), + ) + } +} + +@Composable +private fun HeaderBack(onBack: () -> Unit) { + TopBarBackAction( + action = onBack, + modifier = Modifier.testTag("topBarBackAction"), + ) +} + +@Composable +internal fun ColumnScope.ViewCaseHeader( + title: String, + subTitle: String = "", + updatedAtText: String, + isFavorite: Boolean = false, + isHighPriority: Boolean = false, + onBack: () -> Unit = {}, + isLoading: Boolean = false, + toggleFavorite: () -> Unit = {}, + toggleHighPriority: () -> Unit = {}, + isEditable: Boolean = false, + onCaseLongPress: () -> Unit = {}, + isTopBar: Boolean = false, +) { + if (isTopBar) { + TopBarHeader( + title, + subTitle, + isFavorite, + isHighPriority, + onBack, + isLoading, + toggleFavorite, + toggleHighPriority, + isEditable, + onCaseLongPress, + ) + } else { + SideHeader( + title, + subTitle, + updatedAtText, + isFavorite, + isHighPriority, + onBack, + isLoading, + toggleFavorite, + toggleHighPriority, + isEditable, + onCaseLongPress, + ) + } +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, +) +@Composable +private fun TopBarHeader( + title: String, + subTitle: String = "", + isFavorite: Boolean = false, + isHighPriority: Boolean = false, + onBack: () -> Unit = {}, + isLoading: Boolean = false, + toggleFavorite: () -> Unit = {}, + toggleHighPriority: () -> Unit = {}, + isEditable: Boolean = false, + onCaseLongPress: () -> Unit = {}, +) { + val titleContent = @Composable { + Column( + modifier = Modifier.combinedClickable( + onClick = {}, + onLongClick = onCaseLongPress, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HeaderTitle(title) + HeaderSubTitle(subTitle) + } + } + + val navigationContent = @Composable { HeaderBack(onBack) } + + val actionsContent: (@Composable (RowScope.() -> Unit)) = if (isLoading) { + @Composable {} + } else { + @Composable { + HeaderActions( + isEditable = isEditable, + isHighPriority = isHighPriority, + isFavorite = isFavorite, + toggleHighPriority = toggleHighPriority, + toggleFavorite = toggleFavorite, + ) + } + } + + CenterAlignedTopAppBar( + title = titleContent, + navigationIcon = navigationContent, + actions = actionsContent, + ) +} + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, +) +@Composable +private fun ColumnScope.SideHeader( + title: String, + subTitle: String = "", + updatedAtText: String, + isFavorite: Boolean = false, + isHighPriority: Boolean = false, + onBack: () -> Unit = {}, + isLoading: Boolean = false, + toggleFavorite: () -> Unit = {}, + toggleHighPriority: () -> Unit = {}, + isEditable: Boolean = false, + onCaseLongPress: () -> Unit = {}, +) { + val titleContent = @Composable { + HeaderTitle( + title, + modifier = Modifier + .combinedClickable( + onClick = {}, + onLongClick = onCaseLongPress, + ) + .testTag("editCaseHeaderText"), + ) + } + + val navigationContent = @Composable { HeaderBack(onBack) } + + CenterAlignedTopAppBar( + title = titleContent, + navigationIcon = navigationContent, + actions = {}, + ) + + HeaderSubTitle(subTitle, listItemModifier) + + ViewCaseUpdatedAtView(updatedAtText, listItemModifier) + + if (!isLoading) { + Spacer(Modifier.weight(1f)) + Row( + listItemModifier, + horizontalArrangement = listItemSpacedByHalf, + ) { + Spacer(Modifier.weight(1f)) + HeaderActions( + isEditable = isEditable, + isHighPriority = isHighPriority, + isFavorite = isFavorite, + toggleHighPriority = toggleHighPriority, + toggleFavorite = toggleFavorite, + ) + } + } +} diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt new file mode 100644 index 000000000..3d6ee8049 --- /dev/null +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt @@ -0,0 +1,235 @@ +package com.crisiscleanup.feature.caseeditor.ui + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifier +import com.crisiscleanup.core.designsystem.LocalAppTranslator +import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationDefaults +import com.crisiscleanup.core.designsystem.theme.cardContainerColor +import com.crisiscleanup.core.designsystem.theme.disabledAlpha +import com.crisiscleanup.core.model.data.Worksite +import com.crisiscleanup.feature.caseeditor.R + +private val existingCaseActions = listOf( + IconTextAction( + iconResId = R.drawable.ic_share_small, + translationKey = "actions.share", + ), + IconTextAction( + iconResId = R.drawable.ic_flag_small, + translationKey = "nav.flag", + ), + IconTextAction( + iconResId = R.drawable.ic_history_small, + translationKey = "actions.history", + ), + IconTextAction( + iconResId = R.drawable.ic_edit_underscore_small, + translationKey = "actions.edit", + ), +) + +@Composable +private fun NavItems( + worksite: Worksite, + onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, + onCaseFlags: () -> Unit = {}, + onCaseShare: () -> Unit = {}, + onCaseHistory: () -> Unit = {}, + itemContent: @Composable ( + String, + () -> Unit, + @Composable () -> Unit, + @Composable () -> Unit, + ) -> Unit, +) { + existingCaseActions.forEachIndexed { index, action -> + var label = LocalAppTranslator.current(action.translationKey) + if (action.translationKey.isNotBlank()) { + if (label == action.translationKey && action.textResId != 0) { + label = stringResource(action.textResId) + } + } + if (label.isBlank() && action.textResId != 0) { + label = stringResource(action.textResId) + } + + itemContent( + label, + { + when (index) { + 0 -> onCaseShare() + 1 -> onCaseFlags() + 2 -> onCaseHistory() + 3 -> onFullEdit( + ExistingWorksiteIdentifier( + worksite.incidentId, + worksite.id, + ), + ) + } + }, + { + if (action.iconResId != 0) { + Icon( + painter = painterResource(action.iconResId), + contentDescription = label, + ) + } else if (action.imageVector != null) { + Icon( + imageVector = action.imageVector, + contentDescription = label, + ) + } + }, + { + Text( + label, + style = MaterialTheme.typography.bodySmall, + ) + }, + ) + } +} + +private fun navItemColor(isEditable: Boolean): Color { + var contentColor = Color.Black + if (!isEditable) { + contentColor = contentColor.disabledAlpha() + } + return contentColor +} + +@Composable +internal fun ViewCaseNav( + worksite: Worksite, + isEditable: Boolean, + onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, + onCaseFlags: () -> Unit = {}, + onCaseShare: () -> Unit = {}, + onCaseHistory: () -> Unit = {}, + isBottomNav: Boolean = false, +) { + if (isBottomNav) { + BottomNav( + worksite, + isEditable, + onFullEdit, + onCaseFlags, + onCaseShare, + onCaseHistory, + ) + } else { + RailNav( + worksite, + isEditable, + onFullEdit, + onCaseFlags, + onCaseShare, + onCaseHistory, + ) + } +} + +@Composable +private fun RailNav( + worksite: Worksite, + isEditable: Boolean, + onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, + onCaseFlags: () -> Unit = {}, + onCaseShare: () -> Unit = {}, + onCaseHistory: () -> Unit = {}, +) { + val contentColor = navItemColor(isEditable) + NavigationRail( + containerColor = cardContainerColor, + contentColor = contentColor, + ) { + Spacer(Modifier.weight(1f)) + NavItems( + worksite, + onFullEdit = onFullEdit, + onCaseFlags = onCaseFlags, + onCaseShare = onCaseShare, + onCaseHistory = onCaseHistory, + ) { + label: String, + onClick: () -> Unit, + iconContent: @Composable () -> Unit, + labelContent: @Composable () -> Unit, + -> + NavigationRailItem( + modifier = Modifier.testTag("editCaseNavItem_$label"), + selected = false, + onClick = onClick, + icon = iconContent, + label = labelContent, + colors = NavigationRailItemDefaults.colors( + unselectedIconColor = contentColor, + unselectedTextColor = contentColor, + indicatorColor = CrisisCleanupNavigationDefaults.navigationIndicatorColor(), + ), + enabled = isEditable, + ) + } + } +} + +@Composable +private fun BottomNav( + worksite: Worksite, + isEditable: Boolean, + onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, + onCaseFlags: () -> Unit = {}, + onCaseShare: () -> Unit = {}, + onCaseHistory: () -> Unit = {}, +) { + val contentColor = navItemColor(isEditable) + NavigationBar( + containerColor = cardContainerColor, + contentColor = contentColor, + tonalElevation = 0.dp, + ) { + NavItems( + worksite, + onFullEdit = onFullEdit, + onCaseFlags = onCaseFlags, + onCaseShare = onCaseShare, + onCaseHistory = onCaseHistory, + ) { + label: String, + onClick: () -> Unit, + iconContent: @Composable () -> Unit, + labelContent: @Composable () -> Unit, + -> + NavigationBarItem( + modifier = Modifier.testTag("editCaseNavItem_$label"), + selected = false, + onClick = onClick, + icon = iconContent, + label = labelContent, + colors = NavigationBarItemDefaults.colors( + unselectedIconColor = contentColor, + unselectedTextColor = contentColor, + indicatorColor = CrisisCleanupNavigationDefaults.navigationIndicatorColor(), + ), + enabled = isEditable, + ) + } + } +} diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/EditExistingCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt similarity index 78% rename from feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/EditExistingCaseScreen.kt rename to feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt index 9955b3d67..596cac56f 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/EditExistingCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -35,13 +34,8 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Tab import androidx.compose.material3.TabRow @@ -65,7 +59,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp @@ -73,40 +66,34 @@ import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.crisiscleanup.core.appnav.ViewImageArgs -import com.crisiscleanup.core.common.KeyResourceTranslator import com.crisiscleanup.core.common.filterNotBlankTrim import com.crisiscleanup.core.common.urlEncode import com.crisiscleanup.core.commoncase.model.addressQuery import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifier import com.crisiscleanup.core.designsystem.LocalAppTranslator +import com.crisiscleanup.core.designsystem.LocalLayoutProvider import com.crisiscleanup.core.designsystem.component.BusyIndicatorFloatingTopCenter import com.crisiscleanup.core.designsystem.component.CardSurface import com.crisiscleanup.core.designsystem.component.CollapsibleIcon import com.crisiscleanup.core.designsystem.component.CrisisCleanupButton import com.crisiscleanup.core.designsystem.component.CrisisCleanupFab -import com.crisiscleanup.core.designsystem.component.CrisisCleanupIconButton -import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationDefaults import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextArea import com.crisiscleanup.core.designsystem.component.LinkifyEmailText import com.crisiscleanup.core.designsystem.component.LinkifyLocationText import com.crisiscleanup.core.designsystem.component.LinkifyPhoneText import com.crisiscleanup.core.designsystem.component.TemporaryDialog -import com.crisiscleanup.core.designsystem.component.TopBarBackAction import com.crisiscleanup.core.designsystem.component.WorkTypeAction import com.crisiscleanup.core.designsystem.component.WorkTypePrimaryAction import com.crisiscleanup.core.designsystem.component.actionEdgeSpace import com.crisiscleanup.core.designsystem.component.fabPlusSpaceHeight import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.theme.LocalFontStyles -import com.crisiscleanup.core.designsystem.theme.cardContainerColor import com.crisiscleanup.core.designsystem.theme.disabledAlpha import com.crisiscleanup.core.designsystem.theme.listItemModifier import com.crisiscleanup.core.designsystem.theme.listItemPadding import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy import com.crisiscleanup.core.designsystem.theme.listItemVerticalPadding -import com.crisiscleanup.core.designsystem.theme.neutralIconColor import com.crisiscleanup.core.designsystem.theme.primaryOrangeColor -import com.crisiscleanup.core.designsystem.theme.primaryRedColor import com.crisiscleanup.core.mapmarker.ui.rememberMapProperties import com.crisiscleanup.core.mapmarker.ui.rememberMapUiSettings import com.crisiscleanup.core.model.data.EmptyWorksite @@ -118,8 +105,8 @@ import com.crisiscleanup.core.model.data.WorksiteNote import com.crisiscleanup.core.ui.rememberCloseKeyboard import com.crisiscleanup.core.ui.rememberIsKeyboardOpen import com.crisiscleanup.core.ui.scrollFlingListener -import com.crisiscleanup.feature.caseeditor.ExistingCaseViewModel import com.crisiscleanup.feature.caseeditor.R +import com.crisiscleanup.feature.caseeditor.ViewCaseViewModel import com.crisiscleanup.feature.caseeditor.WorkTypeProfile import com.crisiscleanup.feature.caseeditor.model.CaseImage import com.crisiscleanup.feature.caseeditor.model.ImageCategory @@ -149,7 +136,7 @@ private val flagColors = mapOf( @Composable internal fun EditExistingCaseRoute( - viewModel: ExistingCaseViewModel = hiltViewModel(), + viewModel: ViewCaseViewModel = hiltViewModel(), onBack: () -> Unit = {}, onBackToCases: () -> Unit = {}, onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, @@ -188,80 +175,66 @@ internal fun EditExistingCaseRoute( val onCaseLongPress = remember(viewModel) { { copyToClipboard(viewModel.editableWorksite.value.caseNumber) } } + val isRailNav = !LocalLayoutProvider.current.isBottomNav Box(Modifier.fillMaxSize()) { - Column { + val worksite by viewModel.editableWorksite.collectAsStateWithLifecycle() + Row(Modifier.fillMaxSize()) { + if (isRailNav) { + ViewCaseNav( + worksite, + isEditable, + onFullEdit = onFullEdit, + onCaseFlags = openAddFlag, + onCaseShare = openShareCase, + onCaseHistory = openCaseHistory, + ) + } + val title by viewModel.headerTitle.collectAsStateWithLifecycle() val subTitle by viewModel.subTitle.collectAsStateWithLifecycle() - val worksite by viewModel.editableWorksite.collectAsStateWithLifecycle() val isEmptyWorksite = worksite == EmptyWorksite - TopBar( - title, - subTitle, - isFavorite = worksite.isLocalFavorite, - isHighPriority = worksite.hasHighPriorityFlag, - onBack, - isEmptyWorksite, - toggleFavorite, - toggleHighPriority, - isEditable, - onCaseLongPress, - ) - val translator: KeyResourceTranslator = viewModel val tabTitles by viewModel.tabTitles.collectAsStateWithLifecycle() - val updatedAtText by viewModel.updatedAtText.collectAsStateWithLifecycle() - if (updatedAtText.isNotBlank()) { - Text( - updatedAtText, - Modifier - .testTag("editCaseUpdatedAtText") - .background(Color.White) - .then(listItemModifier), - style = MaterialTheme.typography.bodySmall, - ) - } - if (isEmptyWorksite) { - if (viewModel.worksiteIdArg == EmptyWorksite.id) { - Text( - translator("info.no_worksite_selected"), - Modifier.listItemPadding(), - ) - } else { - Box(Modifier.fillMaxSize()) { - BusyIndicatorFloatingTopCenter(true) - } - } - } else if (tabTitles.isNotEmpty()) { - val statusOptions by viewModel.statusOptions.collectAsStateWithLifecycle() - val caseEditor = CaseEditor( - isEditable, - statusOptions, - false, + if (isRailNav) { + ListDetailContent( + worksite = worksite, + title = title, + subTitle = subTitle, + onBack = onBack, + isEmptyWorksite = isEmptyWorksite, + toggleFavorite = toggleFavorite, + toggleHighPriority = toggleHighPriority, + isEditable = isEditable, + onCaseLongPress = onCaseLongPress, + updatedAtText = updatedAtText, + tabTitles = tabTitles, + isBusy = isBusy, + copyToClipboard = copyToClipboard, + openPhoto = openPhoto, + ) + } else { + PortraitContent( + worksite = worksite, + title = title, + subTitle = subTitle, + onBack = onBack, + isEmptyWorksite = isEmptyWorksite, + toggleFavorite = toggleFavorite, + toggleHighPriority = toggleHighPriority, + isEditable = isEditable, + onCaseLongPress = onCaseLongPress, + updatedAtText = updatedAtText, + tabTitles = tabTitles, + isBusy = isBusy, + copyToClipboard = copyToClipboard, + onFullEdit = onFullEdit, + openPhoto = openPhoto, + openAddFlag = openAddFlag, + openShareCase = openShareCase, + openCaseHistory = openCaseHistory, ) - CompositionLocalProvider( - LocalCaseEditor provides caseEditor, - LocalAppTranslator provides translator, - ) { - ExistingCaseContent( - tabTitles, - worksite, - isBusy, - openPhoto, - copyToClipboard, - ) - val isKeyboardOpen = rememberIsKeyboardOpen() - if (!isKeyboardOpen) { - BottomActions( - worksite, - onFullEdit, - onCaseFlags = openAddFlag, - onCaseShare = openShareCase, - onCaseHistory = openCaseHistory, - ) - } - } } } @@ -272,111 +245,192 @@ internal fun EditExistingCaseRoute( } } -private fun getTopIconActionColor( - isActive: Boolean, - isEditable: Boolean, -): Color { - var tint = if (isActive) { - primaryRedColor +@Composable +private fun NonWorksiteView(showEmpty: Boolean) { + if (showEmpty) { + Text( + LocalAppTranslator.current("info.no_worksite_selected"), + Modifier.listItemPadding(), + ) } else { - neutralIconColor + Box(Modifier.fillMaxSize()) { + BusyIndicatorFloatingTopCenter(true) + } } - if (!isEditable) { - tint = tint.disabledAlpha() +} + +@Composable +fun ColumnScope.CaseContent( + worksite: Worksite, + isEditable: Boolean, + tabTitles: List, + isBusy: Boolean, + copyToClipboard: (String?) -> Unit, + openPhoto: (ViewImageArgs) -> Unit, + viewModel: ViewCaseViewModel = hiltViewModel(), + nestedContent: @Composable ColumnScope.() -> Unit = {}, +) { + val statusOptions by viewModel.statusOptions.collectAsStateWithLifecycle() + val caseEditor = CaseEditor( + isEditable, + statusOptions, + false, + ) + CompositionLocalProvider( + LocalCaseEditor provides caseEditor, + LocalAppTranslator provides viewModel, + ) { + ExistingCaseContent( + tabTitles, + worksite, + isBusy, + openPhoto, + copyToClipboard, + ) + nestedContent() + } +} + +@Composable +internal fun ViewCaseUpdatedAtView( + updatedAtText: String, + modifier: Modifier = Modifier, +) { + if (updatedAtText.isNotBlank()) { + Text( + updatedAtText, + modifier.testTag("editCaseUpdatedAtText"), + style = MaterialTheme.typography.bodySmall, + ) } - return tint } -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalFoundationApi::class, -) @Composable -private fun TopBar( +private fun PortraitContent( + worksite: Worksite, title: String, - subTitle: String = "", - isFavorite: Boolean = false, - isHighPriority: Boolean = false, - onBack: () -> Unit = {}, - isLoading: Boolean = false, - toggleFavorite: () -> Unit = {}, - toggleHighPriority: () -> Unit = {}, - isEditable: Boolean = false, - onCaseLongPress: () -> Unit = {}, + subTitle: String, + onBack: () -> Unit, + isEmptyWorksite: Boolean, + toggleFavorite: () -> Unit, + toggleHighPriority: () -> Unit, + isEditable: Boolean, + onCaseLongPress: () -> Unit, + updatedAtText: String, + tabTitles: List, + isBusy: Boolean, + copyToClipboard: (String?) -> Unit, + viewModel: ViewCaseViewModel = hiltViewModel(), + onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, + openPhoto: (ViewImageArgs) -> Unit = { _ -> }, + openAddFlag: () -> Unit = {}, + openShareCase: () -> Unit = {}, + openCaseHistory: () -> Unit = {}, ) { - val titleContent = @Composable { - Column( - modifier = Modifier.combinedClickable( - onClick = {}, - onLongClick = onCaseLongPress, - ), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - title, - style = LocalFontStyles.current.header3, - modifier = Modifier.testTag("editCaseHeaderText"), - ) + Column { + ViewCaseHeader( + title, + subTitle, + updatedAtText, + isFavorite = worksite.isLocalFavorite, + isHighPriority = worksite.hasHighPriorityFlag, + onBack, + isEmptyWorksite, + toggleFavorite, + toggleHighPriority, + isEditable, + onCaseLongPress, + isTopBar = true, + ) - if (subTitle.isNotBlank()) { - Text( - subTitle, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.testTag("editCaseSubHeaderText"), - ) + ViewCaseUpdatedAtView( + updatedAtText, + modifier = Modifier + .background(Color.White) + .then(listItemModifier), + ) + + if (isEmptyWorksite) { + NonWorksiteView(viewModel.worksiteIdArg == EmptyWorksite.id) + } else if (tabTitles.isNotEmpty()) { + CaseContent( + worksite = worksite, + isEditable = isEditable, + tabTitles = tabTitles, + isBusy = isBusy, + copyToClipboard = copyToClipboard, + openPhoto = openPhoto, + ) { + val isKeyboardOpen = rememberIsKeyboardOpen() + if (!isKeyboardOpen) { + ViewCaseNav( + worksite, + isEditable, + onFullEdit = onFullEdit, + onCaseFlags = openAddFlag, + onCaseShare = openShareCase, + onCaseHistory = openCaseHistory, + isBottomNav = true, + ) + } } } } +} - val navigationContent = @Composable { - TopBarBackAction(action = onBack, modifier = Modifier.testTag("topBarBackAction")) - } - val actionsContent: (@Composable (RowScope.() -> Unit)) = if (isLoading) { - @Composable {} - } else { - @Composable { - val translator = LocalAppTranslator.current - val highPriorityTranslateKey = if (isHighPriority) { - "actions.unmark_high_priority" - } else { - "flag.flag_high_priority" - } - val highPriorityTint = getTopIconActionColor(isHighPriority, isEditable) - CrisisCleanupIconButton( - iconResId = R.drawable.ic_important_filled, - contentDescription = translator(highPriorityTranslateKey), - onClick = toggleHighPriority, - enabled = isEditable, - tint = highPriorityTint, - modifier = Modifier.testTag("editCaseHighPriorityToggleBtn"), +@Composable +private fun ListDetailContent( + worksite: Worksite, + title: String, + subTitle: String, + onBack: () -> Unit, + isEmptyWorksite: Boolean, + toggleFavorite: () -> Unit, + toggleHighPriority: () -> Unit, + isEditable: Boolean, + onCaseLongPress: () -> Unit, + updatedAtText: String, + tabTitles: List, + isBusy: Boolean, + copyToClipboard: (String?) -> Unit, + viewModel: ViewCaseViewModel = hiltViewModel(), + openPhoto: (ViewImageArgs) -> Unit = { _ -> }, +) { + Row { + Column(Modifier.weight(0.3f)) { + ViewCaseHeader( + title, + subTitle, + updatedAtText, + isFavorite = worksite.isLocalFavorite, + isHighPriority = worksite.hasHighPriorityFlag, + onBack, + isEmptyWorksite, + toggleFavorite, + toggleHighPriority, + isEditable, + onCaseLongPress, ) - - val iconResId = if (isFavorite) { - R.drawable.ic_heart_filled - } else { - R.drawable.ic_heart_outline - } - val favoriteDescription = if (isFavorite) { - translator("actions.not_member_of_my_org") - } else { - translator("actions.member_of_my_org") + } + Column( + Modifier + .weight(0.7f) + .sizeIn(maxWidth = 480.dp), + ) { + if (isEmptyWorksite) { + NonWorksiteView(viewModel.worksiteIdArg == EmptyWorksite.id) + } else if (tabTitles.isNotEmpty()) { + CaseContent( + worksite = worksite, + isEditable = isEditable, + tabTitles = tabTitles, + isBusy = isBusy, + copyToClipboard = copyToClipboard, + openPhoto = openPhoto, + ) } - val favoriteTint = getTopIconActionColor(isFavorite, isEditable) - CrisisCleanupIconButton( - iconResId = iconResId, - contentDescription = favoriteDescription, - onClick = toggleFavorite, - enabled = isEditable, - tint = favoriteTint, - modifier = Modifier.testTag("editCaseFavoriteToggleBtn"), - ) } } - CenterAlignedTopAppBar( - title = titleContent, - navigationIcon = navigationContent, - actions = actionsContent, - ) } @OptIn(ExperimentalFoundationApi::class) @@ -460,82 +514,6 @@ private fun ColumnScope.ExistingCaseContent( } } -@Composable -private fun BottomActions( - worksite: Worksite, - onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, - onCaseFlags: () -> Unit = {}, - onCaseShare: () -> Unit = {}, - onCaseHistory: () -> Unit = {}, -) { - val translator = LocalAppTranslator.current - val isEditable = LocalCaseEditor.current.isEditable - var contentColor = Color.Black - if (!isEditable) { - contentColor = contentColor.disabledAlpha() - } - NavigationBar( - containerColor = cardContainerColor, - contentColor = contentColor, - tonalElevation = 0.dp, - ) { - existingCaseActions.forEachIndexed { index, action -> - var label = translator(action.translationKey) - if (action.translationKey.isNotBlank()) { - if (label == action.translationKey && action.textResId != 0) { - label = stringResource(action.textResId) - } - } - if (label.isBlank() && action.textResId != 0) { - label = stringResource(action.textResId) - } - - NavigationBarItem( - modifier = Modifier.testTag("editCaseNavItem_$label"), - selected = false, - onClick = { - when (index) { - 0 -> onCaseShare() - 1 -> onCaseFlags() - 2 -> onCaseHistory() - 3 -> onFullEdit( - ExistingWorksiteIdentifier( - worksite.incidentId, - worksite.id, - ), - ) - } - }, - icon = { - if (action.iconResId != 0) { - Icon( - painter = painterResource(action.iconResId), - contentDescription = label, - ) - } else if (action.imageVector != null) { - Icon( - imageVector = action.imageVector, - contentDescription = label, - ) - } - }, - label = { - Text( - label, - style = MaterialTheme.typography.bodySmall, - ) - }, - colors = NavigationBarItemDefaults.colors( - unselectedIconColor = contentColor, - unselectedTextColor = contentColor, - indicatorColor = CrisisCleanupNavigationDefaults.navigationIndicatorColor(), - ), - enabled = isEditable, - ) - } - } -} - @Composable internal fun PropertyInfoRow( image: ImageVector, @@ -572,7 +550,7 @@ internal fun PropertyInfoRow( @Composable internal fun EditExistingCaseInfoView( worksite: Worksite, - viewModel: ExistingCaseViewModel = hiltViewModel(), + viewModel: ViewCaseViewModel = hiltViewModel(), copyToClipboard: (String?) -> Unit = {}, ) { val mapMarkerIcon by viewModel.mapMarkerIcon.collectAsStateWithLifecycle() @@ -909,7 +887,7 @@ private fun LazyListScope.workItems( @Composable internal fun EditExistingCasePhotosView( - viewModel: ExistingCaseViewModel = hiltViewModel(), + viewModel: ViewCaseViewModel = hiltViewModel(), setEnablePagerScroll: (Boolean) -> Unit = {}, onPhotoSelect: (ViewImageArgs) -> Unit = { _ -> }, ) { @@ -983,7 +961,7 @@ internal fun EditExistingCasePhotosView( @Composable internal fun EditExistingCaseNotesView( worksite: Worksite, - viewModel: ExistingCaseViewModel = hiltViewModel(), + viewModel: ViewCaseViewModel = hiltViewModel(), ) { val isEditable = LocalCaseEditor.current.isEditable val t = LocalAppTranslator.current @@ -1135,25 +1113,6 @@ data class IconTextAction( val translationKey: String = "", ) -private val existingCaseActions = listOf( - IconTextAction( - iconResId = R.drawable.ic_share_small, - translationKey = "actions.share", - ), - IconTextAction( - iconResId = R.drawable.ic_flag_small, - translationKey = "nav.flag", - ), - IconTextAction( - iconResId = R.drawable.ic_history_small, - translationKey = "actions.history", - ), - IconTextAction( - iconResId = R.drawable.ic_edit_underscore_small, - translationKey = "actions.edit", - ), -) - @Composable private fun PropertyInfoMapView( coordinates: LatLng, diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt index 150260079..840bc0267 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt @@ -122,7 +122,6 @@ internal fun CasesFilterRoute( ) } } - } else { Column(screenModifier) { TopBar(onBack) diff --git a/lint/src/main/java/com/crisiscleanup/lint/designsystem/DesignSystemDetector.kt b/lint/src/main/java/com/crisiscleanup/lint/designsystem/DesignSystemDetector.kt index be35bd360..dca8159b0 100644 --- a/lint/src/main/java/com/crisiscleanup/lint/designsystem/DesignSystemDetector.kt +++ b/lint/src/main/java/com/crisiscleanup/lint/designsystem/DesignSystemDetector.kt @@ -74,8 +74,8 @@ class DesignSystemDetector : Detector(), Detector.UastScanner { // "DropdownMenu" to "CrisisCleanupDropdownMenu", // "NavigationBar" to "CrisisCleanupNavigationBar", // "NavigationBarItem" to "CrisisCleanupNavigationBarItem", - "NavigationRail" to "CrisisCleanupNavigationRail", - "NavigationRailItem" to "CrisisCleanupNavigationRailItem", + // "NavigationRail" to "CrisisCleanupNavigationRail", + // "NavigationRailItem" to "CrisisCleanupNavigationRailItem", // "TabRow" to "CrisisCleanupTabRow", // "Tab" to "CrisisCleanupTab", "IconButton" to "CrisisCleanupIconButton", From b59b8f2b7e6cfb8c1d766b53e9453634c9b7745e Mon Sep 17 00:00:00 2001 From: hue Date: Sat, 6 Jan 2024 16:59:24 -0500 Subject: [PATCH 06/29] Layout Case photos view for landscape --- ...ingCaseMediaViews.kt => CaseMediaViews.kt} | 44 +++++++++++----- .../feature/caseeditor/ui/ViewCaseScreen.kt | 51 +++++++++---------- 2 files changed, 56 insertions(+), 39 deletions(-) rename feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/{ExistingCaseMediaViews.kt => CaseMediaViews.kt} (90%) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt similarity index 90% rename from feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt rename to feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt index 360186ec1..21053d9ea 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingCaseMediaViews.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt @@ -119,8 +119,9 @@ private fun AddMediaView( @Composable internal fun PhotosSection( title: String, - photoRowModifier: Modifier = Modifier, + modifier: Modifier = Modifier, photoRowGridCells: StaggeredGridCells = StaggeredGridCells.Fixed(1), + isInlineContent: Boolean = false, photos: List = emptyList(), syncingWorksiteImage: Long = 0L, onAddPhoto: () -> Unit = {}, @@ -128,11 +129,13 @@ internal fun PhotosSection( setEnableParentScroll: (Boolean) -> Unit = {}, addActionSize: Dp = 128.dp, ) { - Text( - title, - listItemModifier, - style = LocalFontStyles.current.header4, - ) + if (!isInlineContent) { + Text( + title, + listItemModifier, + style = LocalFontStyles.current.header4, + ) + } val gridState = rememberLazyStaggeredGridState() LaunchedEffect(gridState.isScrollInProgress) { @@ -141,35 +144,50 @@ internal fun PhotosSection( } } // TODO Common styles + val inlineContentCount = if (isInlineContent) 1 else 0 val itemSpacing = 4.dp LazyHorizontalStaggeredGrid( - modifier = photoRowModifier.touchDownConsumer { setEnableParentScroll(false) }, + modifier = modifier.touchDownConsumer { setEnableParentScroll(false) }, rows = photoRowGridCells, state = gridState, horizontalItemSpacing = itemSpacing, verticalArrangement = Arrangement.spacedBy(itemSpacing), ) { - val totalCount = 1 + photos.size + 1 + val categoryTitleIndex = if (isInlineContent) 0 else -99 + val addMediaIndex = if (isInlineContent) 1 else 0 + val photoOffsetIndex = addMediaIndex + 1 + val totalCount = inlineContentCount + 1 + photos.size + 1 val endSpaceIndex = totalCount - 1 items( totalCount, key = { when (it) { - 0 -> "add-media" + categoryTitleIndex -> "category-title" + addMediaIndex -> "add-media" endSpaceIndex -> "end-spacer" - else -> photos[it - 1].imageUri + else -> photos[it - photoOffsetIndex].imageUri } }, contentType = { when (it) { - 0 -> "add-media" + categoryTitleIndex -> "category-title" + addMediaIndex -> "add-media" endSpaceIndex -> "end-spacer" else -> "photo-image" } }, ) { index -> when (index) { - 0 -> { + categoryTitleIndex -> { + // TODO Refactor duplicate code + Text( + title, + listItemModifier, + style = LocalFontStyles.current.header4, + ) + } + + addMediaIndex -> { AddMediaView( Modifier .padding(addMediaActionPadding) @@ -183,7 +201,7 @@ internal fun PhotosSection( } else -> { - val photoIndex = index - 1 + val photoIndex = index - photoOffsetIndex val photo = photos[photoIndex] Box { AsyncImage( diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt index 596cac56f..5b6a33c32 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt @@ -54,14 +54,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -442,6 +444,7 @@ private fun ColumnScope.ExistingCaseContent( openPhoto: (ViewImageArgs) -> Unit = { _ -> }, copyToClipboard: (String?) -> Unit = {}, ) { + // TODO Page does not keep across first orientation change val pagerState = rememberPagerState( initialPage = 0, initialPageOffsetFraction = 0f, @@ -896,38 +899,34 @@ internal fun EditExistingCasePhotosView( var showCameraMediaSelect by remember { mutableStateOf(false) } - val translator = LocalAppTranslator.current + val t = LocalAppTranslator.current val sectionTitleResIds = mapOf( - ImageCategory.Before to translator("caseForm.before_photos"), - ImageCategory.After to translator("caseForm.after_photos"), + ImageCategory.Before to t("caseForm.before_photos"), + ImageCategory.After to t("caseForm.after_photos"), ) - // TODO Determine spacing and sizing based on available height. - // This viewport has - // - Top bar - // - Tab bar - // - Two rows of headers and items - // - Bottom bar - // - Optional snackbar which may wrap resulting in additional height - val twoRowHeight = 256.dp - val photoTwoRowModifier = Modifier - .height(twoRowHeight) - .listItemVerticalPadding() - val photoOneRowModifier = Modifier - .height(172.dp) - .listItemVerticalPadding() - val photoTwoRowGridCells = StaggeredGridCells.Adaptive(96.dp) - val photoOneRowGridCells = StaggeredGridCells.Fixed(1) - val isShortScreen = LocalConfiguration.current.screenHeightDp.dp < twoRowHeight.times(3) + var contentSize by remember { mutableStateOf(Size.Zero) } + val isShortScreen = contentSize.height.dp < 900.dp Column( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier + .fillMaxHeight() + .onGloballyPositioned { + contentSize = it.size.toSize() + }, ) { sectionTitleResIds.onEach { (imageCategory, sectionTitle) -> photos[imageCategory]?.let { rowPhotos -> - val isOneRow = isShortScreen || rowPhotos.size < 6 + val title = if (isShortScreen) { + sectionTitle.replace(" ", "\n") + } else { + sectionTitle + } PhotosSection( - sectionTitle, - if (isOneRow) photoOneRowModifier else photoTwoRowModifier, - if (isOneRow) photoOneRowGridCells else photoTwoRowGridCells, + title, + Modifier + .listItemVerticalPadding() + .weight(0.45f), + StaggeredGridCells.Fixed(1), + isShortScreen, photos = rowPhotos, setEnableParentScroll = setEnablePagerScroll, onAddPhoto = { From 0c36152802610ae0f39275ef939e10a4aba90e31 Mon Sep 17 00:00:00 2001 From: hue Date: Sat, 6 Jan 2024 18:12:58 -0500 Subject: [PATCH 07/29] Layout create/edit case screen for portrait vs landscape --- .../navigation/TopLevelDestination.kt | 1 + .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 4 +- .../component/ListDetailLayout.kt | 7 + .../core/designsystem/theme/Theme.kt | 2 +- .../authentication/ui/LoginWithEmailScreen.kt | 2 +- .../authentication/ui/LoginWithPhoneScreen.kt | 2 +- .../ui/OrgPersistentInviteScreen.kt | 4 +- .../authentication/ui/PasteOrgInviteScreen.kt | 4 +- .../ui/RequestOrgAccessScreen.kt | 2 +- .../authentication/ui/RootAuthScreen.kt | 2 +- .../authentication/ui/VolunteerOrgScreen.kt | 2 +- .../ui/VolunteerScanQrCodeScreen.kt | 2 +- .../navigation/CaseEditorNavigation.kt | 4 +- .../caseeditor/ui/CaseHistoryScreen.kt | 3 +- .../caseeditor/ui/CreateEditCaseScreen.kt | 189 +++++++++++++----- .../feature/caseeditor/ui/LocationScreen.kt | 28 --- .../caseeditor/ui/PropertyLocationViews.kt | 7 +- .../feature/caseeditor/ui/ShareCaseScreen.kt | 2 +- .../feature/caseeditor/ui/ViewCaseNav.kt | 2 + .../feature/caseeditor/ui/ViewCaseScreen.kt | 17 +- .../feature/cases/ui/CasesFilterScreen.kt | 11 +- .../feature/cases/ui/CasesScreen.kt | 2 +- .../feature/cases/ui/CasesSearchScreen.kt | 3 +- .../feature/dashboard/DashboardScreen.kt | 3 +- .../crisiscleanup/feature/menu/MenuScreen.kt | 2 +- .../ui/InviteTeammateScreen.kt | 4 +- .../syncinsights/ui/SyncInsightsScreen.kt | 2 +- .../crisiscleanup/feature/team/TeamScreen.kt | 3 +- .../userfeedback/ui/UserFeedbackScreen.kt | 3 +- 29 files changed, 196 insertions(+), 123 deletions(-) create mode 100644 core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListDetailLayout.kt diff --git a/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt b/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt index a219e22a5..a8e66a11d 100644 --- a/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt +++ b/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt @@ -10,6 +10,7 @@ enum class TopLevelDestination( val unselectedIcon: Icon, val titleTranslateKey: String, ) { + // TODO Icon color should change selected vs unselected. CASES( selectedIcon = DrawableResourceIcon(CrisisCleanupIcons.Cases), unselectedIcon = DrawableResourceIcon(CrisisCleanupIcons.Cases), diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index af88cc5ec..af120d2dc 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -87,7 +87,7 @@ fun CrisisCleanupApp( viewModel: MainActivityViewModel = hiltViewModel(), ) { CrisisCleanupBackground { - Box(Modifier.fillMaxSize()) { + Box { val snackbarHostState = remember { SnackbarHostState() } val isOffline by appState.isOffline.collectAsStateWithLifecycle() @@ -316,7 +316,7 @@ private fun NavigableContent( } val isKeyboardOpen = rememberIsKeyboardOpen() - Column(Modifier.fillMaxSize()) { + Column { val snackbarAreaHeight = if (!showNavigation && snackbarHostState.currentSnackbarData != null && diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListDetailLayout.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListDetailLayout.kt new file mode 100644 index 000000000..59dd5c83c --- /dev/null +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/ListDetailLayout.kt @@ -0,0 +1,7 @@ +package com.crisiscleanup.core.designsystem.component + +import androidx.compose.ui.unit.dp + +const val listDetailListWeight = 0.3f +const val listDetailDetailWeight = 0.7f +val listDetailDetailMaxWidth = 480.dp diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt index e75ba2acb..b3fdac43f 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Theme.kt @@ -150,7 +150,7 @@ internal fun CrisisCleanupTheme( val dimensions = dimensions0.copy( isLandscape = isLandscape, isPortrait = !isLandscape, - isListDetailWidth = true, + isListDetailWidth = configuration.screenWidthDp >= 600, ) CompositionLocalProvider( diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithEmailScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithEmailScreen.kt index a661f59b0..614b28304 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithEmailScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithEmailScreen.kt @@ -67,7 +67,7 @@ fun LoginWithEmailRoute( val uiState by viewModel.uiState.collectAsStateWithLifecycle() when (uiState) { is AuthenticateScreenUiState.Loading -> { - Box(Modifier.fillMaxSize()) { + Box { CircularProgressIndicator(Modifier.align(Alignment.Center)) } } diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt index e7efb6e8c..7dfaf6396 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt @@ -90,7 +90,7 @@ fun LoginWithPhoneRoute( val uiState by viewModel.uiState.collectAsStateWithLifecycle() when (uiState) { is AuthenticateScreenUiState.Loading -> { - Box(Modifier.fillMaxSize()) { + Box { CircularProgressIndicator(Modifier.align(Alignment.Center)) } } diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/OrgPersistentInviteScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/OrgPersistentInviteScreen.kt index 6821f76c5..00606a967 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/OrgPersistentInviteScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/OrgPersistentInviteScreen.kt @@ -67,8 +67,8 @@ fun OrgPersistentInviteRoute( val inviteFailMessage by viewModel.inviteFailMessage.collectAsStateWithLifecycle() val inviteDisplay by viewModel.inviteDisplay.collectAsStateWithLifecycle() - Box(Modifier.fillMaxSize()) { - Column(Modifier.fillMaxSize()) { + Box { + Column { TopAppBarBackAction( title = t("actions.sign_up"), onAction = onClose, diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/PasteOrgInviteScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/PasteOrgInviteScreen.kt index b07d3ceda..630d87153 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/PasteOrgInviteScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/PasteOrgInviteScreen.kt @@ -1,7 +1,6 @@ package com.crisiscleanup.feature.authentication.ui import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -9,7 +8,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.hilt.navigation.compose.hiltViewModel @@ -44,7 +42,7 @@ fun VolunteerPasteInviteLinkRoute( val isVerifying by viewModel.isVerifyingCode.collectAsStateWithLifecycle() - Column(Modifier.fillMaxSize()) { + Column { TopAppBarBackAction( title = t("nav.invitation_link"), onAction = onBack, diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RequestOrgAccessScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RequestOrgAccessScreen.kt index 00b1170bd..52614af91 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RequestOrgAccessScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RequestOrgAccessScreen.kt @@ -81,7 +81,7 @@ fun RequestOrgAccessRoute( val inviteInfoErrorMessage by viewModel.inviteInfoErrorMessage.collectAsStateWithLifecycle() - Column(Modifier.fillMaxSize()) { + Column { TopAppBarBackAction( title = screenTitle, onAction = clearStateOnBack, diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RootAuthScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RootAuthScreen.kt index f5d2bb223..fdca1e9ec 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RootAuthScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/RootAuthScreen.kt @@ -75,7 +75,7 @@ internal fun RootAuthScreen( val authState by viewModel.authState.collectAsStateWithLifecycle() when (authState) { is AuthState.Loading -> { - Box(Modifier.fillMaxSize()) { + Box { CircularProgressIndicator(Modifier.align(Alignment.Center)) } } diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerOrgScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerOrgScreen.kt index bf15e439d..802b3eda6 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerOrgScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerOrgScreen.kt @@ -39,7 +39,7 @@ fun VolunteerOrgRoute( val t = LocalAppTranslator.current val closeKeyboard = rememberCloseKeyboard(onBack) - Column(Modifier.fillMaxSize()) { + Column { TopAppBarBackAction( title = t("actions.sign_up"), onAction = onBack, diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerScanQrCodeScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerScanQrCodeScreen.kt index 0e2f29d68..e35457635 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerScanQrCodeScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/VolunteerScanQrCodeScreen.kt @@ -41,7 +41,7 @@ fun VolunteerScanQrCodeRoute( ) { val t = LocalAppTranslator.current - Column(Modifier.fillMaxSize()) { + Column { TopAppBarBackAction( title = t("volunteerOrg.scan_qr_code"), onAction = onBack, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt index 6b6a7f3cc..6f89393ff 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt @@ -25,7 +25,7 @@ import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.EmptyWorksite import com.crisiscleanup.feature.caseeditor.ui.CaseEditCaseHistoryRoute import com.crisiscleanup.feature.caseeditor.ui.CaseEditShareCaseRoute -import com.crisiscleanup.feature.caseeditor.ui.CaseEditorRoute +import com.crisiscleanup.feature.caseeditor.ui.CreateEditCaseRoute import com.crisiscleanup.feature.caseeditor.ui.EditCaseAddressSearchRoute import com.crisiscleanup.feature.caseeditor.ui.EditCaseMapMoveLocationRoute import com.crisiscleanup.feature.caseeditor.ui.EditExistingCaseRoute @@ -103,7 +103,7 @@ fun NavGraphBuilder.caseEditorScreen( remember(navController) { { navController.navigateToCaseEditSearchAddress() } } val onEditMoveLocationOnMap = remember(navController) { { navController.navigateToCaseEditLocationMapMove() } } - CaseEditorRoute( + CreateEditCaseRoute( onBack = onBackClick, changeNewIncidentCase = navToNewCase, changeExistingIncidentCase = navToChangedIncident, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseHistoryScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseHistoryScreen.kt index b08b90201..5b3b47e42 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseHistoryScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseHistoryScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -54,7 +53,7 @@ fun CaseEditCaseHistoryRoute( val historyEvents by viewModel.historyEvents.collectAsStateWithLifecycle() val hasEvents by viewModel.hasEvents.collectAsStateWithLifecycle() - Column(Modifier.fillMaxSize()) { + Column { TopAppBarBackAction( title = viewModel.screenTitle, onAction = onBack, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt index 8e0f9e5ab..c5ddb0662 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CreateEditCaseScreen.kt @@ -6,10 +6,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow 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.sizeIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.ExperimentalMaterial3Api @@ -38,11 +42,15 @@ import com.crisiscleanup.core.designsystem.component.FocusSectionSlider import com.crisiscleanup.core.designsystem.component.FormListSectionSeparator import com.crisiscleanup.core.designsystem.component.TopAppBarBackAction import com.crisiscleanup.core.designsystem.component.cancelButtonColors +import com.crisiscleanup.core.designsystem.component.listDetailDetailMaxWidth +import com.crisiscleanup.core.designsystem.component.listDetailDetailWeight +import com.crisiscleanup.core.designsystem.component.listDetailListWeight import com.crisiscleanup.core.designsystem.component.rememberFocusSectionSliderState import com.crisiscleanup.core.designsystem.component.rememberSectionContentIndexLookup import com.crisiscleanup.core.designsystem.theme.CrisisCleanupTheme import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.designsystem.theme.LocalFontStyles +import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.ui.rememberCloseKeyboard import com.crisiscleanup.core.ui.rememberIsKeyboardOpen @@ -57,9 +65,8 @@ import com.crisiscleanup.core.common.R as commonR private const val SectionHeaderContentType = "section-header-content-type" private const val SectionSeparatorContentType = "section-header-content-type" -@OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun CaseEditorRoute( +internal fun CreateEditCaseRoute( changeNewIncidentCase: (Long) -> Unit = {}, changeExistingIncidentCase: (ExistingWorksiteIdentifier) -> Unit = {}, onOpenExistingCase: (ExistingWorksiteIdentifier) -> Unit = {}, @@ -88,48 +95,117 @@ internal fun CaseEditorRoute( } } - val headerTitle by viewModel.headerTitle.collectAsStateWithLifecycle() - val onNavigateBack = remember(viewModel) { - { - if (viewModel.onNavigateBack()) { - onBack() - } - } + CompositionLocalProvider(LocalAppTranslator provides viewModel) { + ArrangeLayout( + onEditSearchAddress = onEditSearchAddress, + onEditMoveLocationOnMap = onEditMoveLocationOnMap, + onBack = onBack, + ) } - val onNavigateCancel = remember(viewModel) { - { - if (viewModel.onNavigateCancel()) { - onBack() - } - } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ArrangeLayout( + onEditSearchAddress: () -> Unit, + onEditMoveLocationOnMap: () -> Unit, + onBack: () -> Unit, + viewModel: CreateEditCaseViewModel = hiltViewModel(), +) { + val headerTitle by viewModel.headerTitle.collectAsStateWithLifecycle() + val onNavigateBack = remember(viewModel) { + { + if (viewModel.onNavigateBack()) { + onBack() + } + } + } + val onNavigateCancel = remember(viewModel) { + { + if (viewModel.onNavigateCancel()) { + onBack() } - Column(Modifier.background(color = Color.White)) { + } + } + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val areEditorsReady by viewModel.areEditorsReady.collectAsStateWithLifecycle() + val isSaving by viewModel.isSavingWorksite.collectAsStateWithLifecycle() + var isCaseLoaded = false + var isEditable = false + (uiState as? CaseEditorUiState.CaseData)?.let { caseData -> + isCaseLoaded = true + isEditable = areEditorsReady && caseData.isNetworkLoadFinished && !isSaving + } + + val isListDetailLayout = LocalDimensions.current.isListDetailWidth + val screenModifier = Modifier.background(color = Color.White) + if (isListDetailLayout) { + Row(screenModifier) { + Column(Modifier.weight(listDetailListWeight)) { TopAppBarBackAction( title = headerTitle, onAction = onNavigateBack, ) - CompositionLocalProvider(LocalAppTranslator provides viewModel) { - CaseEditorScreen( - onNavigateCancel = onNavigateCancel, - onEditSearchAddress = onEditSearchAddress, - onEditMoveLocationOnMap = onEditMoveLocationOnMap, + Spacer(Modifier.weight(1f)) + + if (isCaseLoaded) { + KeyboardSaveActionBar( + enable = isEditable, + isSaving = isSaving, + onCancel = onNavigateCancel, ) } } + Column( + Modifier + .weight(listDetailDetailWeight) + .sizeIn(maxWidth = listDetailDetailMaxWidth), + ) { + CreateEditCaseContent( + uiState, + isEditable = isEditable, + onEditSearchAddress = onEditSearchAddress, + onEditMoveLocationOnMap = onEditMoveLocationOnMap, + ) + } + } + } else { + Column(screenModifier) { + TopAppBarBackAction( + title = headerTitle, + onAction = onNavigateBack, + ) + + CreateEditCaseContent( + uiState, + isEditable = isEditable, + onEditSearchAddress = onEditSearchAddress, + onEditMoveLocationOnMap = onEditMoveLocationOnMap, + ) { + KeyboardSaveActionBar( + enable = isEditable, + isSaving = isSaving, + onCancel = onNavigateCancel, + horizontalLayout = true, + ) + } } } } @Composable -internal fun ColumnScope.CaseEditorScreen( +private fun ColumnScope.CreateEditCaseContent( + uiState: CaseEditorUiState, + isEditable: Boolean, modifier: Modifier = Modifier, - viewModel: CreateEditCaseViewModel = hiltViewModel(), - onNavigateCancel: () -> Unit = {}, onEditSearchAddress: () -> Unit = {}, onEditMoveLocationOnMap: () -> Unit = {}, + bottomCaseContent: @Composable () -> Unit = {}, ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() when (uiState) { is CaseEditorUiState.Loading -> { Box( @@ -143,11 +219,13 @@ internal fun ColumnScope.CaseEditorScreen( is CaseEditorUiState.CaseData -> { FullEditView( - uiState as CaseEditorUiState.CaseData, - onCancel = onNavigateCancel, + uiState, + isEditable = isEditable, onSearchAddress = onEditSearchAddress, onMoveLocation = onEditMoveLocationOnMap, ) + + bottomCaseContent() } else -> { @@ -171,9 +249,9 @@ internal fun ColumnScope.CaseEditorScreen( @Composable private fun ColumnScope.FullEditView( caseData: CaseEditorUiState.CaseData, + isEditable: Boolean, modifier: Modifier = Modifier, viewModel: CreateEditCaseViewModel = hiltViewModel(), - onCancel: () -> Unit = {}, onMoveLocation: () -> Unit = {}, onSearchAddress: () -> Unit = {}, ) { @@ -210,10 +288,6 @@ private fun ColumnScope.FullEditView( sectionCollapseStates, ) - val areEditorsReady by viewModel.areEditorsReady.collectAsStateWithLifecycle() - val isSavingData by viewModel.isSavingWorksite.collectAsStateWithLifecycle() - val isEditable = areEditorsReady && caseData.isNetworkLoadFinished && !isSavingData - val isSectionCollapsed = remember(viewModel) { { sectionIndex: Int -> sectionCollapseStates[sectionIndex] } } val toggleSectionCollapse = remember(viewModel) { @@ -265,23 +339,6 @@ private fun ColumnScope.FullEditView( BusyIndicatorFloatingTopCenter(isLoadingWorksite) } - val isKeyboardOpen = rememberIsKeyboardOpen() - if (!isKeyboardOpen) { - val claimAndSaveChanges = remember(viewModel) { { viewModel.saveChanges(true) } } - val saveChanges = remember(viewModel) { { viewModel.saveChanges(false) } } - val translator = LocalAppTranslator.current - SaveActionBar( - enable = isEditable, - isSaving = isSavingData, - onCancel = onCancel, - onClaimAndSave = claimAndSaveChanges, - onSave = saveChanges, - saveText = translator("actions.save"), - saveClaimText = translator("actions.save_claim"), - cancelText = translator("actions.cancel"), - ) - } - val showBackChangesDialog by viewModel.promptUnsavedChanges val showCancelChangesDialog by viewModel.promptCancelChanges val abandonChanges = remember(viewModel) { { viewModel.abandonChanges() } } @@ -560,6 +617,34 @@ private fun InvalidSaveDialog( } } +@Composable +private fun KeyboardSaveActionBar( + enable: Boolean, + isSaving: Boolean, + onCancel: () -> Unit, + horizontalLayout: Boolean = false, + viewModel: CreateEditCaseViewModel = hiltViewModel(), +) { + val isKeyboardOpen = rememberIsKeyboardOpen() + if (!isKeyboardOpen) { + val claimAndSaveChanges = remember(viewModel) { { viewModel.saveChanges(true) } } + val saveChanges = remember(viewModel) { { viewModel.saveChanges(false) } } + val t = LocalAppTranslator.current + SaveActionBar( + enable = enable, + isSaving = isSaving, + onCancel = onCancel, + onClaimAndSave = claimAndSaveChanges, + onSave = saveChanges, + saveText = t("actions.save"), + saveClaimText = t("actions.save_claim"), + cancelText = t("actions.cancel"), + horizontalLayout = horizontalLayout, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) @Composable private fun SaveActionBar( enable: Boolean = false, @@ -570,15 +655,19 @@ private fun SaveActionBar( saveText: String = "", saveClaimText: String = "", cancelText: String = "", + horizontalLayout: Boolean = false, ) { val dimensions = LocalDimensions.current val isSharpCorners = dimensions.isThinScreenWidth - Row( + val rowMaxItemCount = if (horizontalLayout) Int.MAX_VALUE else 1 + FlowRow( modifier = Modifier.padding( horizontal = dimensions.edgePaddingFlexible, vertical = dimensions.edgePadding, ), horizontalArrangement = dimensions.itemInnerSpacingHorizontalFlexible, + verticalArrangement = listItemSpacedBy, + maxItemsInEachRow = rowMaxItemCount, ) { val style = LocalFontStyles.current.header5 BusyButton( diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt index efd9f9560..4004c0f7a 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -23,14 +22,11 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.crisiscleanup.core.designsystem.LocalAppTranslator @@ -115,30 +111,6 @@ private fun AddressSummaryInColumn( } } -// TODO Use common dimensions/sizes for all static map views -@Composable -internal fun getLayoutParameters(isMoveLocationMode: Boolean): Pair { - val configuration = LocalConfiguration.current - val screenWidth = configuration.screenWidthDp.dp - val screenHeight = configuration.screenHeightDp.dp - val minScreenDimension = screenWidth.coerceAtMost(screenHeight) - // TODO Revisit for all screen sizes. Adjust map size as necessary - val isRowOriented = screenWidth > screenHeight.times(1.3f) - - val mapWidth: Dp - val mapHeight: Dp - if (isMoveLocationMode) { - mapWidth = screenWidth - mapHeight = screenHeight - } else { - mapWidth = minScreenDimension - mapHeight = minScreenDimension.times(0.5f).coerceAtMost(240.dp) - } - val mapModifier = Modifier.sizeIn(maxWidth = mapWidth, maxHeight = mapHeight) - - return Pair(isRowOriented, mapModifier) -} - @Composable internal fun BoxScope.LocationMapView( viewModel: EditCaseBaseViewModel, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/PropertyLocationViews.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/PropertyLocationViews.kt index ae7e355a3..7bcedcbd2 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/PropertyLocationViews.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/PropertyLocationViews.kt @@ -5,14 +5,17 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifier @@ -89,7 +92,9 @@ internal fun PropertyLocationView( ) } - val (_, mapModifier) = getLayoutParameters(false) + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val mapHeight = screenHeight.times(0.5f).coerceAtMost(240.dp) + val mapModifier = Modifier.sizeIn(maxHeight = mapHeight) val cameraPositionState = rememberCameraPositionState() Box(mapModifier) { LocationMapView( diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ShareCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ShareCaseScreen.kt index 8b6c5514b..8345beddf 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ShareCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ShareCaseScreen.kt @@ -115,7 +115,7 @@ fun CaseEditShareCaseRoute( CompositionLocalProvider( LocalAppTranslator provides translator, ) { - Column(Modifier.fillMaxSize()) { + Column { val screenTitle = translator("actions.share") if (isOnSecondStep) { TopAppBarBackAction( diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt index 3d6ee8049..f05dd0d71 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt @@ -107,6 +107,8 @@ private fun NavItems( } } +// TODO Icon color is not correct on first screen load +// Is correct when navigates back private fun navItemColor(isEditable: Boolean): Color { var contentColor = Color.Black if (!isEditable) { diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt index 5b6a33c32..57a6a7d01 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt @@ -88,6 +88,9 @@ import com.crisiscleanup.core.designsystem.component.WorkTypeAction import com.crisiscleanup.core.designsystem.component.WorkTypePrimaryAction import com.crisiscleanup.core.designsystem.component.actionEdgeSpace import com.crisiscleanup.core.designsystem.component.fabPlusSpaceHeight +import com.crisiscleanup.core.designsystem.component.listDetailDetailMaxWidth +import com.crisiscleanup.core.designsystem.component.listDetailDetailWeight +import com.crisiscleanup.core.designsystem.component.listDetailListWeight import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.disabledAlpha @@ -178,9 +181,9 @@ internal fun EditExistingCaseRoute( remember(viewModel) { { copyToClipboard(viewModel.editableWorksite.value.caseNumber) } } val isRailNav = !LocalLayoutProvider.current.isBottomNav - Box(Modifier.fillMaxSize()) { + Box { val worksite by viewModel.editableWorksite.collectAsStateWithLifecycle() - Row(Modifier.fillMaxSize()) { + Row { if (isRailNav) { ViewCaseNav( worksite, @@ -255,7 +258,7 @@ private fun NonWorksiteView(showEmpty: Boolean) { Modifier.listItemPadding(), ) } else { - Box(Modifier.fillMaxSize()) { + Box { BusyIndicatorFloatingTopCenter(true) } } @@ -399,7 +402,7 @@ private fun ListDetailContent( openPhoto: (ViewImageArgs) -> Unit = { _ -> }, ) { Row { - Column(Modifier.weight(0.3f)) { + Column(Modifier.weight(listDetailListWeight)) { ViewCaseHeader( title, subTitle, @@ -416,8 +419,8 @@ private fun ListDetailContent( } Column( Modifier - .weight(0.7f) - .sizeIn(maxWidth = 480.dp), + .weight(listDetailDetailWeight) + .sizeIn(maxWidth = listDetailDetailMaxWidth), ) { if (isEmptyWorksite) { NonWorksiteView(viewModel.worksiteIdArg == EmptyWorksite.id) @@ -983,7 +986,7 @@ internal fun EditExistingCaseNotesView( listState.firstVisibleItemIndex > 0 } } - ConstraintLayout(Modifier.fillMaxSize()) { + ConstraintLayout { val (noteContent, newNoteFab) = createRefs() val notes = worksite.notes diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt index 840bc0267..c49ae02d4 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesFilterScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow 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.sizeIn @@ -52,6 +51,9 @@ import com.crisiscleanup.core.designsystem.component.TopAppBarBackAction import com.crisiscleanup.core.designsystem.component.WithHelpDialog import com.crisiscleanup.core.designsystem.component.actionHeight import com.crisiscleanup.core.designsystem.component.cancelButtonColors +import com.crisiscleanup.core.designsystem.component.listDetailDetailMaxWidth +import com.crisiscleanup.core.designsystem.component.listDetailDetailWeight +import com.crisiscleanup.core.designsystem.component.listDetailListWeight import com.crisiscleanup.core.designsystem.component.rememberFocusSectionSliderState import com.crisiscleanup.core.designsystem.component.rememberSectionContentIndexLookup import com.crisiscleanup.core.designsystem.component.roundedOutline @@ -96,12 +98,11 @@ internal fun CasesFilterRoute( remember(viewModel) { { filters: CasesFilter -> viewModel.changeFilters(filters) } } val screenModifier = Modifier - .fillMaxSize() .background(Color.White) if (isListDetailLayout) { Row(screenModifier) { - Column(Modifier.weight(0.3f)) { + Column(Modifier.weight(listDetailListWeight)) { TopBar(onBack) Spacer(Modifier.weight(1f)) @@ -113,8 +114,8 @@ internal fun CasesFilterRoute( } Column( Modifier - .weight(0.7f) - .sizeIn(maxWidth = 480.dp), + .weight(listDetailDetailWeight) + .sizeIn(maxWidth = listDetailDetailMaxWidth), ) { FilterContent( filters, diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt index b130fd34d..da76ca7ab 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt @@ -308,7 +308,7 @@ internal fun NoCasesScreen( isLoading: Boolean = false, onRetryLoad: () -> Unit = {}, ) { - Box(modifier.fillMaxSize()) { + Box { if (isLoading) { BusyIndicatorFloatingTopCenter(true) } else { diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt index 641ded692..1925fe102 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt @@ -4,7 +4,6 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -67,7 +66,7 @@ internal fun CasesSearchRoute( val closeKeyboard = rememberCloseKeyboard(viewModel) val translator = LocalAppTranslator.current - Box(Modifier.fillMaxSize()) { + Box { Column { Row( // TODO Common dimensions and color diff --git a/feature/dashboard/src/main/java/com/crisiscleanup/feature/dashboard/DashboardScreen.kt b/feature/dashboard/src/main/java/com/crisiscleanup/feature/dashboard/DashboardScreen.kt index aa63f2abe..461dbb3f4 100644 --- a/feature/dashboard/src/main/java/com/crisiscleanup/feature/dashboard/DashboardScreen.kt +++ b/feature/dashboard/src/main/java/com/crisiscleanup/feature/dashboard/DashboardScreen.kt @@ -1,7 +1,6 @@ package com.crisiscleanup.feature.dashboard import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -26,7 +25,7 @@ internal fun DashboardRoute( internal fun DashboardScreen( modifier: Modifier = Modifier, ) { - Box(Modifier.fillMaxSize()) { + Box { Text( text = "Dash", modifier = Modifier diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt index 38154e0ed..0f52e9864 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt @@ -87,7 +87,7 @@ internal fun MenuScreen( } } - Box(Modifier.fillMaxSize()) { + Box { Column(Modifier.fillMaxWidth()) { TopBar( modifier = Modifier, diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt index d1050e9ae..c12860e00 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/ui/InviteTeammateScreen.kt @@ -90,14 +90,14 @@ fun InviteTeammateRoute( val hasValidTokens by viewModel.hasValidTokens.collectAsStateWithLifecycle() val t = LocalAppTranslator.current - Column(Modifier.fillMaxSize()) { + Column { TopAppBarBackAction( title = t("nav.invite_teammates"), onAction = onBack, ) if (isLoading) { - Box(Modifier.fillMaxSize()) { + Box { BusyIndicatorFloatingTopCenter(true) } } else if (isInviteSent) { diff --git a/feature/syncinsights/src/main/java/com/crisiscleanup/feature/syncinsights/ui/SyncInsightsScreen.kt b/feature/syncinsights/src/main/java/com/crisiscleanup/feature/syncinsights/ui/SyncInsightsScreen.kt index 4404f5ff8..6ed4ff0ae 100644 --- a/feature/syncinsights/src/main/java/com/crisiscleanup/feature/syncinsights/ui/SyncInsightsScreen.kt +++ b/feature/syncinsights/src/main/java/com/crisiscleanup/feature/syncinsights/ui/SyncInsightsScreen.kt @@ -33,7 +33,7 @@ internal fun SyncInsightsRoute( viewModel: SyncInsightsViewModel = hiltViewModel(), openCase: (Long, Long) -> Boolean = { _, _ -> false }, ) { - Column(Modifier.fillMaxSize()) { + Column { val pendingSync by viewModel.worksitesPendingSync.collectAsStateWithLifecycle() val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle(false) Row( diff --git a/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamScreen.kt b/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamScreen.kt index baad9e8f5..02a0ef768 100644 --- a/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamScreen.kt +++ b/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamScreen.kt @@ -1,7 +1,6 @@ package com.crisiscleanup.feature.team import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -26,7 +25,7 @@ internal fun TeamRoute( internal fun TeamScreen( modifier: Modifier = Modifier, ) { - Box(Modifier.fillMaxSize()) { + Box { Text( text = "Team", modifier = Modifier diff --git a/feature/userfeedback/src/main/java/com/crisiscleanup/feature/userfeedback/ui/UserFeedbackScreen.kt b/feature/userfeedback/src/main/java/com/crisiscleanup/feature/userfeedback/ui/UserFeedbackScreen.kt index ae1f099e8..016cabfb3 100644 --- a/feature/userfeedback/src/main/java/com/crisiscleanup/feature/userfeedback/ui/UserFeedbackScreen.kt +++ b/feature/userfeedback/src/main/java/com/crisiscleanup/feature/userfeedback/ui/UserFeedbackScreen.kt @@ -12,7 +12,6 @@ import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -72,7 +71,7 @@ fun UserFeedbackRoute( }, ) } else { - Box(Modifier.fillMaxSize()) { + Box { BusyIndicatorFloatingTopCenter(true) } } From b9a8e7ebba7016a6d4b31391be483eea14e6eeb3 Mon Sep 17 00:00:00 2001 From: hue Date: Sat, 6 Jan 2024 18:30:51 -0500 Subject: [PATCH 08/29] Layout change location for portrait or list-detail --- .../caseeditor/ui/MoveLocationOnMapScreen.kt | 161 +++++++++++++++--- 1 file changed, 139 insertions(+), 22 deletions(-) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/MoveLocationOnMapScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/MoveLocationOnMapScreen.kt index 0400f13cd..be2804bb7 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/MoveLocationOnMapScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/MoveLocationOnMapScreen.kt @@ -3,9 +3,13 @@ package com.crisiscleanup.feature.caseeditor.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable @@ -24,6 +28,10 @@ import com.crisiscleanup.core.designsystem.LocalAppTranslator import com.crisiscleanup.core.designsystem.component.BusyButton import com.crisiscleanup.core.designsystem.component.TopAppBarBackAction import com.crisiscleanup.core.designsystem.component.cancelButtonColors +import com.crisiscleanup.core.designsystem.component.listDetailDetailMaxWidth +import com.crisiscleanup.core.designsystem.component.listDetailDetailWeight +import com.crisiscleanup.core.designsystem.component.listDetailListWeight +import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.listItemHeight import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy @@ -45,9 +53,9 @@ import com.google.maps.android.compose.rememberMarkerState @Composable internal fun EditCaseMapMoveLocationRoute( - viewModel: EditCaseLocationViewModel = hiltViewModel(), onBack: () -> Unit = {}, openExistingCase: (ids: ExistingWorksiteIdentifier) -> Unit = { _ -> }, + viewModel: EditCaseLocationViewModel = hiltViewModel(), ) { val editor = viewModel.editor val editDifferentWorksite by editor.editIncidentWorksite.collectAsStateWithLifecycle() @@ -63,31 +71,90 @@ internal fun EditCaseMapMoveLocationRoute( val isEditable = !isCheckingOutOfBounds val isOnline by viewModel.isOnline.collectAsStateWithLifecycle() - EditCaseMapMoveLocationScreen(viewModel, editor, isOnline, onBack, isEditable) + + val isListDetailLayout = LocalDimensions.current.isListDetailWidth + + val t = LocalAppTranslator.current + val title = t("caseForm.select_on_map") + + val locationQuery by editor.locationInputData.locationQuery.collectAsStateWithLifecycle() + + val onUseMyLocation = remember(viewModel) { { editor.useMyLocation() } } + + if (isListDetailLayout) { + ListDetailLayout( + title, + locationQuery, + viewModel, + editor, + isOnline, + onBack, + isEditable, + onUseMyLocation, + ) + } else { + PortraitLayout( + title, + locationQuery, + viewModel, + editor, + isOnline, + onBack, + isEditable, + onUseMyLocation, + ) + } LocationOutOfBoundsDialog(editor) } } +@Composable +private fun UseMyLocationAction( + isEditable: Boolean, + onUseMyLocation: () -> Unit, +) { + val useMyLocationText = LocalAppTranslator.current("caseForm.use_my_location") + CompositionLocalProvider( + LocalTextStyle provides LocalFontStyles.current.header4, + ) { + CrisisCleanupIconTextButton( + modifier = Modifier + .listItemHeight() + .fillMaxWidth(), + iconResId = R.drawable.ic_use_my_location, + label = useMyLocationText, + onClick = onUseMyLocation, + enabled = isEditable, + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun EditCaseMapMoveLocationScreen( +private fun PortraitLayout( + title: String, + locationQuery: String, viewModel: EditCaseBaseViewModel, editor: CaseLocationDataEditor, isOnline: Boolean, onBack: () -> Unit = {}, isEditable: Boolean = false, + onUseMyLocation: () -> Unit = {}, ) { - val translator = LocalAppTranslator.current Column { TopAppBarBackAction( - title = translator("caseForm.select_on_map"), + title = title, onAction = onBack, ) - val locationQuery by editor.locationInputData.locationQuery.collectAsStateWithLifecycle() if (isOnline) { - FullAddressSearchInput(viewModel, editor, locationQuery, isEditable = isEditable) + FullAddressSearchInput( + viewModel, + editor, + locationQuery, + isEditable = isEditable, + ) } if (locationQuery.isBlank()) { @@ -95,20 +162,10 @@ private fun EditCaseMapMoveLocationScreen( MoveMapUnderLocation(viewModel, editor, isEditable) } - val useMyLocation = remember(viewModel) { { editor.useMyLocation() } } - CompositionLocalProvider( - LocalTextStyle provides LocalFontStyles.current.header4, - ) { - CrisisCleanupIconTextButton( - modifier = Modifier - .listItemHeight() - .fillMaxWidth(), - iconResId = R.drawable.ic_use_my_location, - label = translator("caseForm.use_my_location"), - onClick = useMyLocation, - enabled = isEditable, - ) - } + UseMyLocationAction( + isEditable = isEditable, + onUseMyLocation = onUseMyLocation, + ) SaveActionBar(viewModel, editor, onBack, isEditable) } else { @@ -118,6 +175,61 @@ private fun EditCaseMapMoveLocationScreen( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ListDetailLayout( + title: String, + locationQuery: String, + viewModel: EditCaseBaseViewModel, + editor: CaseLocationDataEditor, + isOnline: Boolean, + onBack: () -> Unit = {}, + isEditable: Boolean = false, + onUseMyLocation: () -> Unit = {}, +) { + Row { + Column(Modifier.weight(listDetailListWeight)) { + TopAppBarBackAction( + title = title, + onAction = onBack, + ) + + if (isOnline) { + FullAddressSearchInput( + viewModel, + editor, + locationQuery, + isEditable = isEditable, + ) + } + + Spacer(Modifier.weight(1f)) + + val isEditableListDetail = isEditable && locationQuery.isBlank() + UseMyLocationAction( + isEditable = isEditableListDetail, + onUseMyLocation = onUseMyLocation, + ) + + SaveActionBar(viewModel, editor, onBack, isEditableListDetail) + } + Column( + Modifier + .weight(listDetailDetailWeight) + .sizeIn(maxWidth = listDetailDetailMaxWidth), + ) { + if (locationQuery.isBlank()) { + Box(Modifier.weight(1f)) { + MoveMapUnderLocation(viewModel, editor, isEditable) + } + } else { + editor.isMapLoaded = false + AddressSearchResults(viewModel, editor, locationQuery, isEditable = isEditable) + } + } + } +} + @Composable private fun BoxScope.MoveMapUnderLocation( viewModel: EditCaseBaseViewModel, @@ -198,12 +310,14 @@ private fun BoxScope.MoveMapUnderLocation( ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun SaveActionBar( viewModel: EditCaseBaseViewModel, editor: CaseLocationDataEditor, onBack: () -> Unit = {}, isEditable: Boolean = false, + horizontalLayout: Boolean = false, ) { val translator = LocalAppTranslator.current val onSave = remember(viewModel) { @@ -213,11 +327,14 @@ private fun SaveActionBar( } } } - Row( + val rowMaxItemCount = if (horizontalLayout) Int.MAX_VALUE else 1 + FlowRow( modifier = Modifier // TODO Common dimensions .padding(16.dp), horizontalArrangement = listItemSpacedBy, + verticalArrangement = listItemSpacedBy, + maxItemsInEachRow = rowMaxItemCount, ) { BusyButton( Modifier.weight(1f), From 93f2db69193f70cb96cf7bb979ee24f2edb12a33 Mon Sep 17 00:00:00 2001 From: hue Date: Mon, 8 Jan 2024 19:07:02 -0500 Subject: [PATCH 09/29] Correct Cases screen loading layout to fill view --- app/build.gradle.kts | 2 +- .../java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2e7abb91c..0b6513769 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,7 @@ plugins { android { defaultConfig { - val buildVersion = 180 + val buildVersion = 181 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt index da76ca7ab..1682554d9 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt @@ -308,7 +308,7 @@ internal fun NoCasesScreen( isLoading: Boolean = false, onRetryLoad: () -> Unit = {}, ) { - Box { + Box(Modifier.fillMaxSize()) { if (isLoading) { BusyIndicatorFloatingTopCenter(true) } else { @@ -333,7 +333,6 @@ internal fun NoCasesScreen( @Composable internal fun CasesScreen( - modifier: Modifier = Modifier, showDataProgress: Boolean = false, dataProgress: Float = 0f, onSelectIncident: () -> Unit = {}, @@ -364,7 +363,7 @@ internal fun CasesScreen( onSyncData: () -> Unit = {}, hasIncidents: Boolean = false, ) { - Box(modifier.then(Modifier.fillMaxSize())) { + Box { if (isTableView) { CasesTableView( isLoadingData = isLoadingData, @@ -396,7 +395,7 @@ internal fun CasesScreen( ) } CasesOverlayElements( - modifier, + Modifier, onSelectIncident, disasterResId, onCasesAction, From 5e300fc4e84fe93da54037548aade4e71dd39d4f Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 9 Jan 2024 14:43:12 -0500 Subject: [PATCH 10/29] Update nav icon color correctly --- .../navigation/TopLevelDestination.kt | 1 - .../com/crisiscleanup/ui/CrisisCleanupApp.kt | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt b/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt index a8e66a11d..a219e22a5 100644 --- a/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt +++ b/app/src/main/java/com/crisiscleanup/navigation/TopLevelDestination.kt @@ -10,7 +10,6 @@ enum class TopLevelDestination( val unselectedIcon: Icon, val titleTranslateKey: String, ) { - // TODO Icon color should change selected vs unselected. CASES( selectedIcon = DrawableResourceIcon(CrisisCleanupIcons.Cases), unselectedIcon = DrawableResourceIcon(CrisisCleanupIcons.Cases), diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index af120d2dc..c5bf793df 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration @@ -385,10 +386,17 @@ private fun TopLevelDestination.Icon(isSelected: Boolean, description: String) { contentDescription = description, ) - is DrawableResourceIcon -> Icon( - painter = painterResource(id = icon.id), - contentDescription = description, - ) + is DrawableResourceIcon -> { + var tint = LocalContentColor.current + if (isSelected) { + tint = Color.White + } + Icon( + painter = painterResource(id = icon.id), + contentDescription = description, + tint = tint, + ) + } } } From 6a3e11fc3364349e11ca6b9d88652bb2dfe485a5 Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 9 Jan 2024 16:47:32 -0500 Subject: [PATCH 11/29] Add sandbox app for troubleshooting features in isolation --- app-sandbox/.gitignore | 1 + app-sandbox/build.gradle.kts | 85 +++++++ app-sandbox/src/main/AndroidManifest.xml | 27 ++ .../crisiscleanup/sandbox/SandboxActivity.kt | 32 +++ .../com/crisiscleanup/sandbox/SandboxApp.kt | 76 ++++++ .../sandbox/SandboxApplication.kt | 235 ++++++++++++++++++ .../res/drawable/ic_launcher_background.xml | 58 +++++ .../res/drawable/ic_launcher_foreground.xml | 42 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + app-sandbox/src/main/res/values/themes.xml | 8 + feature/caseeditor/build.gradle.kts | 5 + feature/cases/build.gradle.kts | 5 + settings.gradle.kts | 1 + 13 files changed, 580 insertions(+) create mode 100644 app-sandbox/.gitignore create mode 100644 app-sandbox/build.gradle.kts create mode 100644 app-sandbox/src/main/AndroidManifest.xml create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt create mode 100644 app-sandbox/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app-sandbox/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app-sandbox/src/main/res/values/themes.xml diff --git a/app-sandbox/.gitignore b/app-sandbox/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/app-sandbox/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app-sandbox/build.gradle.kts b/app-sandbox/build.gradle.kts new file mode 100644 index 000000000..b808c2057 --- /dev/null +++ b/app-sandbox/build.gradle.kts @@ -0,0 +1,85 @@ +import com.google.samples.apps.nowinandroid.NiaBuildType + +plugins { + id("nowinandroid.android.application") + id("nowinandroid.android.application.compose") + id("nowinandroid.android.application.flavors") + id("nowinandroid.android.hilt") +} + +android { + defaultConfig { + applicationId = "com.crisiscleanup.sandbox" + versionCode = 1 + versionName = "0.0.1" + + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + val debug by getting { + applicationIdSuffix = NiaBuildType.DEBUG.applicationIdSuffix + } + val release by getting { + applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix + } + } + + packaging { + resources { + excludes.add("/META-INF/{AL2.0,LGPL2.1}") + } + } + namespace = "com.crisiscleanup.sandbox" +} + +dependencies { + implementation(project(":feature:caseeditor")) + + implementation(project(":core:appnav")) + implementation(project(":core:common")) + implementation(project(":core:commoncase")) + implementation(project(":core:data")) + implementation(project(":core:designsystem")) + implementation(project(":core:network")) + implementation(project(":core:model")) + implementation(project(":core:ui")) + + androidTestImplementation(libs.androidx.navigation.testing) + androidTestImplementation(libs.accompanist.testharness) + androidTestImplementation(kotlin("test")) + debugImplementation(libs.androidx.compose.ui.testManifest) + debugImplementation(project(":ui-test-hilt-manifest")) + + implementation(libs.kotlinx.serialization.json) + + implementation(libs.accompanist.systemuicontroller) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.androidx.compose.runtime.tracing) + implementation(libs.androidx.compose.material3.windowSizeClass) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.window.manager) + implementation(libs.androidx.profileinstaller) + + implementation(libs.coil.kt) + + implementation(libs.kotlinx.coroutines.playservices) + implementation(libs.playservices.maps) +} + +// androidx.test is forcing JUnit, 4.12. This forces it to use 4.13 +configurations.configureEach { + resolutionStrategy { + force(libs.junit4) + // Temporary workaround for https://issuetracker.google.com/174733673 + force("org.objenesis:objenesis:2.6") + } +} diff --git a/app-sandbox/src/main/AndroidManifest.xml b/app-sandbox/src/main/AndroidManifest.xml new file mode 100644 index 000000000..1273be5c4 --- /dev/null +++ b/app-sandbox/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt new file mode 100644 index 000000000..a41555618 --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt @@ -0,0 +1,32 @@ +package com.crisiscleanup.sandbox + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.core.view.WindowCompat +import com.crisiscleanup.core.designsystem.theme.CrisisCleanupTheme +import com.google.android.gms.maps.MapsInitializer +import dagger.hilt.android.AndroidEntryPoint + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@AndroidEntryPoint +class SandboxActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + MapsInitializer.initialize(this, MapsInitializer.Renderer.LATEST) {} + + // Turn off the decor fitting system windows, which allows us to handle insets, + // including IME animations + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + CrisisCleanupTheme { + SandboxApp( + windowSizeClass = calculateWindowSizeClass(this), + ) + } + } + } +} \ No newline at end of file diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt new file mode 100644 index 000000000..4ee3fda79 --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt @@ -0,0 +1,76 @@ +package com.crisiscleanup.sandbox + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +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.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.crisiscleanup.core.designsystem.component.CrisisCleanupBackground +import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextCheckbox + +@Composable +fun SandboxApp( + windowSizeClass: WindowSizeClass, +) { + CrisisCleanupBackground { + Box(Modifier.fillMaxSize()) { + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onBackground, + contentWindowInsets = WindowInsets(0, 0, 0, 0), + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = {}, + bottomBar = {}, + ) { padding -> + Column( + Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .windowInsetsPadding(WindowInsets.safeDrawing), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("Show") + + Checkboxes() + } + } + } + } +} + +@Composable +private fun Checkboxes() { + CrisisCleanupTextCheckbox( + text = "Checkbox text and everything in between wrapping", + //wrapText = true, + ) { + Text("Longer trailing content") + } + CrisisCleanupTextCheckbox(text = "Short") { + Text("Longer trailing content") + } + CrisisCleanupTextCheckbox( + text = "Short", + spaceTrailingContent = true, + ) { + Text("Longer trailing content") + } +} \ No newline at end of file diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt new file mode 100644 index 000000000..13bc387cd --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt @@ -0,0 +1,235 @@ +package com.crisiscleanup.sandbox + +import android.app.Application +import android.content.ContentResolver +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import coil.ImageLoader +import coil.ImageLoaderFactory +import com.crisiscleanup.core.common.AppEnv +import com.crisiscleanup.core.common.LocationProvider +import com.crisiscleanup.core.common.PermissionManager +import com.crisiscleanup.core.common.PermissionStatus +import com.crisiscleanup.core.common.event.AuthEventBus +import com.crisiscleanup.core.common.log.TagLogger +import com.crisiscleanup.core.common.sync.SyncPuller +import com.crisiscleanup.core.common.sync.SyncPusher +import com.crisiscleanup.core.common.sync.SyncResult +import com.crisiscleanup.core.commoncase.WorksiteProvider +import com.crisiscleanup.core.model.data.EmptyWorksite +import com.crisiscleanup.core.network.AuthInterceptorProvider +import com.crisiscleanup.core.network.RetrofitInterceptorProvider +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.HiltAndroidApp +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.MutableStateFlow +import okhttp3.Interceptor +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton +import kotlin.coroutines.cancellation.CancellationException + +@HiltAndroidApp +class SandboxApplication : Application(), ImageLoaderFactory { + @Inject + lateinit var imageLoader: Provider + + override fun newImageLoader(): ImageLoader = imageLoader.get() +} + +@Singleton +class SandboxAppEnv @Inject constructor() : AppEnv { + override val isDebuggable = true + override val isProduction = false + override val isNotProduction = true + + override val isEarlybird = false + + override val apiEnvironment: String + get() = "Sandbox" + + override fun runInNonProd(block: () -> Unit) { + block() + } +} + +class AppLogger @Inject constructor() : TagLogger { + override var tag: String? = null + + override fun logDebug(vararg logs: Any) { + Log.d(tag, logs.joinToString(" ")) + } + + override fun logException(e: Exception) { + if (e is CancellationException) { + return + } + + Log.e(tag, e.message, e) + } + + override fun logCapture(message: String) { + Log.w(tag, message) + } +} + +@Singleton +class AppInterceptorProvider @Inject constructor() : RetrofitInterceptorProvider { + override val serverErrorInterceptor by lazy { + Interceptor { chain -> + val request = chain.request() + chain.proceed(request) + } + } + + override val interceptors: List = listOf( + serverErrorInterceptor, + ) +} + +@Singleton +class AppAuthInterceptProvider @Inject constructor() : AuthInterceptorProvider { + override val clientErrorInterceptor by lazy { + Interceptor { chain -> + val request = chain.request() + chain.proceed(request) + } + } +} + +@Singleton +class AppSyncer @Inject constructor() : SyncPuller, SyncPusher { + override fun appPull(force: Boolean, cancelOngoing: Boolean) {} + + override suspend fun syncPullAsync() = + CompletableDeferred(SyncResult.NotAttempted("")) + + override fun stopPull() {} + + override suspend fun syncPullWorksitesFullAsync() = + CompletableDeferred(SyncResult.NotAttempted("")) + + override fun stopSyncPullWorksitesFull() {} + + override fun scheduleSyncWorksitesFull() {} + + override fun appPullIncident(id: Long) {} + + override suspend fun syncPullIncidentAsync(id: Long) = + CompletableDeferred(SyncResult.NotAttempted("")) + + override fun stopPullIncident() {} + + override fun appPullIncidentWorksitesDelta() {} + + override fun appPullLanguage() {} + + override suspend fun syncPullLanguage() = SyncResult.NotAttempted("") + + override fun appPullStatuses() {} + + override suspend fun syncPullStatuses() = SyncResult.NotAttempted("") + + override fun appPushWorksite(worksiteId: Long) {} + + override suspend fun syncPushWorksitesAsync() = CompletableDeferred(SyncResult.NotAttempted("")) + + override fun stopPushWorksites() {} + + override suspend fun syncPushMedia() = SyncResult.NotAttempted("") + + override suspend fun syncPushWorksites() = SyncResult.NotAttempted("") + + override fun scheduleSyncMedia() {} + + override fun scheduleSyncWorksites() {} +} + +@Singleton +class AppAuthEventBus @Inject constructor() : AuthEventBus { + override val logouts = MutableStateFlow(false) + override val refreshedTokens = MutableStateFlow(false) + + override fun onLogout() {} + + override fun onTokensRefreshed() {} +} + +@Singleton +class AppLocationProvider @Inject constructor() : LocationProvider { + override val coordinates: Pair = Pair(0.0, 0.0) + + override suspend fun getLocation() = coordinates +} + +@Singleton +class AppPermissionManager @Inject constructor() : PermissionManager { + override val hasLocationPermission = MutableStateFlow(true) + override val permissionChanges = MutableStateFlow(Pair("", PermissionStatus.Requesting)) + + override fun requestLocationPermission() = PermissionStatus.Granted + + override fun requestCameraPermission() = PermissionStatus.Granted + + override fun requestScreenshotReadPermission() = PermissionStatus.Granted +} + +@Singleton +class AppWorksiteProvider @Inject constructor() : WorksiteProvider { + override val editableWorksite = MutableStateFlow(EmptyWorksite) + override var workTypeTranslationLookup: Map = mutableMapOf() + + override fun translate(key: String): String = key +} + +@Module +@InstallIn(SingletonComponent::class) +interface AppModule { + @Binds + fun bindsAppEnv(appEnv: SandboxAppEnv): AppEnv + + @Binds + fun bindsTagLogger(logger: AppLogger): TagLogger + + @Binds + fun bindsInterceptorProvider(provider: AppInterceptorProvider): RetrofitInterceptorProvider + + @Binds + fun bindsAuthInterceptorProvider(provider: AppAuthInterceptProvider): AuthInterceptorProvider + + @Binds + fun bindsSyncPuller(syncer: AppSyncer): SyncPuller + + @Binds + fun bindsSyncPusher(syncer: AppSyncer): SyncPusher + + @Binds + fun bindsAuthEventBus(eventBus: AppAuthEventBus): AuthEventBus + + @Binds + fun bindsLocationProvider(provider: AppLocationProvider): LocationProvider + + @Binds + fun bindsPermissionManager(manager: AppPermissionManager): PermissionManager + + @Binds + fun bindsWorksiteProvider(provider: AppWorksiteProvider): WorksiteProvider +} + +@Module +@InstallIn(SingletonComponent::class) +object AppObjectModule { + @Provides + fun providesPackageManager(@ApplicationContext context: Context): PackageManager = + context.packageManager + + @Provides + fun providesContentResolver(@ApplicationContext context: Context): ContentResolver = + context.contentResolver +} diff --git a/app-sandbox/src/main/res/drawable/ic_launcher_background.xml b/app-sandbox/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..25f68df3d --- /dev/null +++ b/app-sandbox/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/app-sandbox/src/main/res/drawable/ic_launcher_foreground.xml b/app-sandbox/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..d9ecad568 --- /dev/null +++ b/app-sandbox/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..bbd3e0212 --- /dev/null +++ b/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app-sandbox/src/main/res/values/themes.xml b/app-sandbox/src/main/res/values/themes.xml new file mode 100644 index 000000000..f92cf0e49 --- /dev/null +++ b/app-sandbox/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/feature/caseeditor/build.gradle.kts b/feature/caseeditor/build.gradle.kts index e75e7a613..f66faf712 100644 --- a/feature/caseeditor/build.gradle.kts +++ b/feature/caseeditor/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") id("nowinandroid.android.library.jacoco") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } android { @@ -18,6 +19,10 @@ android { namespace = "com.crisiscleanup.feature.caseeditor" } +secrets { + defaultPropertiesFileName = "secrets.defaults.properties" +} + dependencies { implementation(project(":core:addresssearch")) implementation(project(":core:commonassets")) diff --git a/feature/cases/build.gradle.kts b/feature/cases/build.gradle.kts index 668f5ea0c..e26c764c6 100644 --- a/feature/cases/build.gradle.kts +++ b/feature/cases/build.gradle.kts @@ -2,12 +2,17 @@ plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") id("nowinandroid.android.library.jacoco") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } android { namespace = "com.crisiscleanup.feature.cases" } +secrets { + defaultPropertiesFileName = "secrets.defaults.properties" +} + dependencies { implementation(project(":core:commonassets")) implementation(project(":core:commoncase")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 084e9f851..8aa090721 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ dependencyResolutionManagement { } rootProject.name = "crisiscleanup" include(":app") +include(":app-sandbox") include(":core:addresssearch") include(":core:appnav") include(":core:common") From 28cfc36b4fc9ecb84afab660a3ba6b65efb00474 Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 9 Jan 2024 17:40:36 -0500 Subject: [PATCH 12/29] Add sandbox app navigation --- .../com/crisiscleanup/sandbox/SandboxApp.kt | 49 +++++++++++-------- .../crisiscleanup/sandbox/SandboxAppState.kt | 32 ++++++++++++ .../sandbox/navigation/SandboxNavigation.kt | 34 +++++++++++++ .../crisiscleanup/sandbox/ui/Checkboxes.kt | 27 ++++++++++ 4 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxAppState.kt create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt index 4ee3fda79..2df7ac30e 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt @@ -3,6 +3,9 @@ package com.crisiscleanup.sandbox import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -13,19 +16,25 @@ 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.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.navigation.NavController import com.crisiscleanup.core.designsystem.component.CrisisCleanupBackground -import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextCheckbox +import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton +import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy +import com.crisiscleanup.sandbox.navigation.SandboxNavHost +import com.crisiscleanup.sandbox.navigation.navigateToCheckboxes @Composable fun SandboxApp( windowSizeClass: WindowSizeClass, + appState: SandboxAppState = rememberAppState( + windowSizeClass = windowSizeClass, + ), ) { CrisisCleanupBackground { Box(Modifier.fillMaxSize()) { @@ -47,30 +56,28 @@ fun SandboxApp( .windowInsetsPadding(WindowInsets.safeDrawing), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Text("Show") - - Checkboxes() + SandboxNavHost( + appState.navController, + ) } } } } } +@OptIn(ExperimentalLayoutApi::class) @Composable -private fun Checkboxes() { - CrisisCleanupTextCheckbox( - text = "Checkbox text and everything in between wrapping", - //wrapText = true, - ) { - Text("Longer trailing content") - } - CrisisCleanupTextCheckbox(text = "Short") { - Text("Longer trailing content") - } - CrisisCleanupTextCheckbox( - text = "Short", - spaceTrailingContent = true, - ) { - Text("Longer trailing content") +fun RootRoute(navController: NavController) { + Column { + Spacer(Modifier.weight(1f)) + FlowRow( + horizontalArrangement = listItemSpacedBy, + verticalArrangement = listItemSpacedBy, + maxItemsInEachRow = 6, + ) { + CrisisCleanupTextButton(text = "Checkboxes") { + navController.navigateToCheckboxes() + } + } } -} \ No newline at end of file +} diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxAppState.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxAppState.kt new file mode 100644 index 000000000..4fb78f803 --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxAppState.kt @@ -0,0 +1,32 @@ +package com.crisiscleanup.sandbox + +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.CoroutineScope + +@Composable +fun rememberAppState( + windowSizeClass: WindowSizeClass, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + navController: NavHostController = rememberNavController(), +): SandboxAppState { + return remember(navController, coroutineScope, windowSizeClass) { + SandboxAppState(navController, coroutineScope, windowSizeClass) + } +} + +@Stable +class SandboxAppState( + val navController: NavHostController, + val coroutineScope: CoroutineScope, + val windowSizeClass: WindowSizeClass, +) { + fun onBack() { + navController.popBackStack() + } +} diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt new file mode 100644 index 000000000..a70bf7884 --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt @@ -0,0 +1,34 @@ +package com.crisiscleanup.sandbox.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.crisiscleanup.sandbox.RootRoute +import com.crisiscleanup.sandbox.ui.CheckboxesRoute + +const val rootRoute = "root" +private const val checkboxesRoute = "checkboxes" + +fun NavController.navigateToCheckboxes() { + this.navigate(checkboxesRoute) +} + +@Composable +fun SandboxNavHost( + navController: NavHostController, +) { + NavHost( + navController = navController, + startDestination = rootRoute, + ) { + composable(route = rootRoute) { + RootRoute(navController) + } + + composable(route = checkboxesRoute) { + CheckboxesRoute() + } + } +} diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt new file mode 100644 index 000000000..5925c8338 --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt @@ -0,0 +1,27 @@ +package com.crisiscleanup.sandbox.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextCheckbox + +@Composable +fun CheckboxesRoute() { + Column { + CrisisCleanupTextCheckbox( + text = "Checkbox text and everything in between wrapping", + //wrapText = true, + ) { + Text("Longer trailing content") + } + CrisisCleanupTextCheckbox(text = "Short") { + Text("Longer trailing content") + } + CrisisCleanupTextCheckbox( + text = "Short", + spaceTrailingContent = true, + ) { + Text("Longer trailing content") + } + } +} From 87bb89406aa375b418577bc1a2d63dd74cd5db60 Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 9 Jan 2024 18:13:03 -0500 Subject: [PATCH 13/29] Add bottom nav to sandbox example --- .../com/crisiscleanup/sandbox/SandboxApp.kt | 4 ++ .../sandbox/navigation/SandboxNavigation.kt | 10 ++++ .../com/crisiscleanup/sandbox/ui/BottomNav.kt | 60 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt index 2df7ac30e..3441d74c4 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt @@ -27,6 +27,7 @@ import com.crisiscleanup.core.designsystem.component.CrisisCleanupBackground import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy import com.crisiscleanup.sandbox.navigation.SandboxNavHost +import com.crisiscleanup.sandbox.navigation.navigateToBottomNav import com.crisiscleanup.sandbox.navigation.navigateToCheckboxes @Composable @@ -78,6 +79,9 @@ fun RootRoute(navController: NavController) { CrisisCleanupTextButton(text = "Checkboxes") { navController.navigateToCheckboxes() } + CrisisCleanupTextButton(text = "Bottom nav") { + navController.navigateToBottomNav() + } } } } diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt index a70bf7884..816c0aec2 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt @@ -6,15 +6,21 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.crisiscleanup.sandbox.RootRoute +import com.crisiscleanup.sandbox.ui.BottomNavRoute import com.crisiscleanup.sandbox.ui.CheckboxesRoute const val rootRoute = "root" private const val checkboxesRoute = "checkboxes" +private const val bottomNavRoute = "bottom-nav" fun NavController.navigateToCheckboxes() { this.navigate(checkboxesRoute) } +fun NavController.navigateToBottomNav() { + this.navigate(bottomNavRoute) +} + @Composable fun SandboxNavHost( navController: NavHostController, @@ -30,5 +36,9 @@ fun SandboxNavHost( composable(route = checkboxesRoute) { CheckboxesRoute() } + + composable(route = bottomNavRoute) { + BottomNavRoute() + } } } diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt new file mode 100644 index 000000000..398bc9884 --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt @@ -0,0 +1,60 @@ +package com.crisiscleanup.sandbox.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationDefaults +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import com.crisiscleanup.feature.caseeditor.R as caseeditorR + +@Composable +fun BottomNavRoute() { + Column { + var enabled by remember { mutableStateOf(false) } + val contentColor = if (enabled) Color.Black else Color.Black.copy(alpha = 0.5f) + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(Unit) { + coroutineScope.launch { + delay(1000) + enabled = true + } + } + + NavigationBar( + containerColor = Color.White, + contentColor = contentColor, + ) { + NavigationBarItem( + selected = false, + onClick = {}, + icon = { + Icon( + painter = painterResource(caseeditorR.drawable.ic_flag_small), + contentDescription = null, + tint = contentColor, + ) + }, + label = { Text("Nav") }, + colors = NavigationBarItemDefaults.colors( + unselectedIconColor = contentColor, + unselectedTextColor = contentColor, + indicatorColor = CrisisCleanupNavigationDefaults.navigationIndicatorColor(), + ), + enabled = enabled, + ) + } + } +} \ No newline at end of file From d76539ecc9fcb1c682b293e64257a70f2415a12c Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 9 Jan 2024 18:18:14 -0500 Subject: [PATCH 14/29] Set nav item icon color according to editable state --- .../com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt index f05dd0d71..fb2fa3a55 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseNav.kt @@ -47,6 +47,7 @@ private val existingCaseActions = listOf( @Composable private fun NavItems( worksite: Worksite, + contentColor: Color, onFullEdit: (ExistingWorksiteIdentifier) -> Unit = {}, onCaseFlags: () -> Unit = {}, onCaseShare: () -> Unit = {}, @@ -89,11 +90,13 @@ private fun NavItems( Icon( painter = painterResource(action.iconResId), contentDescription = label, + tint = contentColor, ) } else if (action.imageVector != null) { Icon( imageVector = action.imageVector, contentDescription = label, + tint = contentColor, ) } }, @@ -107,8 +110,6 @@ private fun NavItems( } } -// TODO Icon color is not correct on first screen load -// Is correct when navigates back private fun navItemColor(isEditable: Boolean): Color { var contentColor = Color.Black if (!isEditable) { @@ -165,6 +166,7 @@ private fun RailNav( Spacer(Modifier.weight(1f)) NavItems( worksite, + contentColor, onFullEdit = onFullEdit, onCaseFlags = onCaseFlags, onCaseShare = onCaseShare, @@ -209,6 +211,7 @@ private fun BottomNav( ) { NavItems( worksite, + contentColor, onFullEdit = onFullEdit, onCaseFlags = onCaseFlags, onCaseShare = onCaseShare, From 9182a1a8730413371324aa4a19032c8adc82d3be Mon Sep 17 00:00:00 2001 From: hue Date: Mon, 15 Jan 2024 14:38:38 -0500 Subject: [PATCH 15/29] Increase zoom threshold for rendering markers on map view --- .../java/com/crisiscleanup/feature/cases/CasesConstant.kt | 2 +- .../crisiscleanup/feature/cases/CasesQueryStateManager.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesConstant.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesConstant.kt index f96a55f3e..7a3f08515 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesConstant.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesConstant.kt @@ -6,5 +6,5 @@ object CasesConstant { * * The next zoom level markers are interactive and can trigger details/navigation. */ - const val InteractiveZoomLevel = 9 + const val InteractiveZoomLevel = 10 } diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesQueryStateManager.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesQueryStateManager.kt index f6e76e1e2..9149763c5 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesQueryStateManager.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesQueryStateManager.kt @@ -1,5 +1,6 @@ package com.crisiscleanup.feature.cases +import com.crisiscleanup.core.common.throttleLatest import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.CasesFilterRepository import com.crisiscleanup.core.model.data.WorksiteSortBy @@ -7,7 +8,6 @@ import com.crisiscleanup.feature.cases.model.CoordinateBoundsDefault import com.crisiscleanup.feature.cases.model.WorksiteQueryStateDefault import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -15,7 +15,7 @@ internal class CasesQueryStateManager( incidentSelector: IncidentSelector, filterRepository: CasesFilterRepository, coroutineScope: CoroutineScope, - mapChangeDebounceTimeout: Long = 50, + mapChangeDebounceTimeout: Long = 100, ) { val isTableView = MutableStateFlow(false) @@ -39,14 +39,14 @@ internal class CasesQueryStateManager( .launchIn(coroutineScope) mapZoom - .debounce(mapChangeDebounceTimeout) + .throttleLatest(mapChangeDebounceTimeout) .onEach { worksiteQueryState.value = worksiteQueryState.value.copy(zoom = it) } .launchIn(coroutineScope) mapBounds - .debounce(mapChangeDebounceTimeout) + .throttleLatest(mapChangeDebounceTimeout) .onEach { worksiteQueryState.value = worksiteQueryState.value.copy(coordinateBounds = it) } From c935f9fd0e184d43b60ba7580a72a641d411a0ae Mon Sep 17 00:00:00 2001 From: hue Date: Mon, 15 Jan 2024 17:55:08 -0500 Subject: [PATCH 16/29] Decouple map dots and markers rendering --- app-sandbox/src/main/AndroidManifest.xml | 1 - .../com/crisiscleanup/sandbox/SandboxActivity.kt | 2 +- .../java/com/crisiscleanup/sandbox/ui/BottomNav.kt | 2 +- .../com/crisiscleanup/sandbox/ui/Checkboxes.kt | 2 +- .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 1 - app-sandbox/src/main/res/values/themes.xml | 1 - .../crisiscleanup/feature/cases/CasesConstant.kt | 8 ++------ .../crisiscleanup/feature/cases/CasesViewModel.kt | 3 ++- .../cases/map/CasesOverviewMapTileRenderer.kt | 14 ++------------ 9 files changed, 9 insertions(+), 25 deletions(-) diff --git a/app-sandbox/src/main/AndroidManifest.xml b/app-sandbox/src/main/AndroidManifest.xml index 1273be5c4..8017c0481 100644 --- a/app-sandbox/src/main/AndroidManifest.xml +++ b/app-sandbox/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt index a41555618..85820ff6d 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt @@ -29,4 +29,4 @@ class SandboxActivity : ComponentActivity() { } } } -} \ No newline at end of file +} diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt index 398bc9884..a80b39a13 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/BottomNav.kt @@ -57,4 +57,4 @@ fun BottomNavRoute() { ) } } -} \ No newline at end of file +} diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt index 5925c8338..17c1dcfb9 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/Checkboxes.kt @@ -10,7 +10,7 @@ fun CheckboxesRoute() { Column { CrisisCleanupTextCheckbox( text = "Checkbox text and everything in between wrapping", - //wrapText = true, + // wrapText = true, ) { Text("Longer trailing content") } diff --git a/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index bbd3e0212..6ae6a23dd 100644 --- a/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app-sandbox/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,4 +1,3 @@ - diff --git a/app-sandbox/src/main/res/values/themes.xml b/app-sandbox/src/main/res/values/themes.xml index f92cf0e49..20ef26d03 100644 --- a/app-sandbox/src/main/res/values/themes.xml +++ b/app-sandbox/src/main/res/values/themes.xml @@ -1,4 +1,3 @@ -