diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index c453547..7a63055 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -6,6 +6,9 @@ on: - 'master' tags: - 'v*' + pull_request: + types: + - closed jobs: apk: @@ -34,7 +37,7 @@ jobs: run: bash ./gradlew assembleDebug --stacktrace - name: Upload APK - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: name: apk path: app/build/outputs/apk/debug/app-debug.apk @@ -46,7 +49,7 @@ jobs: steps: - name: Download APK from build - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v2 with: name: apk @@ -56,12 +59,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: v1.2.0 - release_name: ${{ github.event.repository.name }} v1.2.0 + tag_name: v0.3.0-beta + release_name: ${{ github.event.repository.name }} v0.3.0-beta - name: Upload Release APK id: upload_release_asset - uses: actions/upload-release-asset@v1.0.1 + uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/core/navigation/src/main/java/com/loki/navigation/Navigation.kt b/core/navigation/src/main/java/com/loki/navigation/Navigation.kt index 8248885..ca5c581 100644 --- a/core/navigation/src/main/java/com/loki/navigation/Navigation.kt +++ b/core/navigation/src/main/java/com/loki/navigation/Navigation.kt @@ -39,7 +39,10 @@ import com.loki.report.ReportScreen import com.loki.report.ReportViewModel import com.loki.settings.SettingsScreen import com.loki.settings.SettingsViewModel +import com.loki.ui.utils.Constants.CAMERA_SCREEN_TYPE import com.loki.ui.utils.Constants.REPORT_ID +import com.loki.ui.utils.Constants.SCREEN_TYPE_PROFILE_CAMERA +import com.loki.ui.utils.Constants.SCREEN_TYPE_REPORT_CAMERA import com.loki.ui.utils.Constants.VIDEO_URI import kotlinx.coroutines.delay @@ -153,30 +156,60 @@ fun NavGraphBuilder.reportListScreen(onNavigateTo: (Screen) -> Unit, viewModel: } } -fun NavGraphBuilder.newReportScreen(onNavigateTo: (Screen) -> Unit, viewModel: NavigationViewModel) { +fun NavGraphBuilder.newReportScreen( + onNavigateTo: (Screen) -> Unit, + viewModel: NavigationViewModel, + newReportViewModel: NewReportViewModel +) { composable( route = Screen.NewReportScreen.route, enterTransition = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Up, - animationSpec = tween(600) - ) + when(initialState.destination.route) { + Screen.CameraScreen.route -> { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(600) + ) + } + + Screen.ReportListScreen.route -> { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Up, + animationSpec = tween(600) + ) + } + else -> null + } }, exitTransition = { - slideOutOfContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Down, - animationSpec = tween(200) - ) + when(targetState.destination.route) { + Screen.ReportListScreen.route -> { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Down, + animationSpec = tween(200) + ) + } + + Screen.CameraScreen.route -> { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(200) + ) + } + else -> null + } } ) { LaunchedEffect(key1 = viewModel.isBottomBarVisible.value) { viewModel.setBottomBarVisible(false) } - val newReportViewModel = hiltViewModel() NewReportScreen( viewModel = newReportViewModel, navigateToHome = { onNavigateTo(Screen.ReportListScreen) + }, + navigateToCamera = { + onNavigateTo(Screen.CameraScreen.navWith(SCREEN_TYPE_REPORT_CAMERA)) } ) } @@ -234,7 +267,11 @@ fun NavGraphBuilder.accountNavGraph(viewModel: NavigationViewModel, onNavigateTo } } -fun NavGraphBuilder.profileScreen(onNavigateTo: (Screen) -> Unit, onNavigateToLogin: (Screen) -> Unit, viewModel: NavigationViewModel) { +fun NavGraphBuilder.profileScreen( + onNavigateTo: (Screen) -> Unit, + onNavigateToLogin: (Screen) -> Unit, + viewModel: NavigationViewModel +) { composable(route = Screen.ProfileScreen.route) { LaunchedEffect(key1 = viewModel.isBottomBarVisible.value) { viewModel.setBottomBarVisible(true) @@ -246,7 +283,7 @@ fun NavGraphBuilder.profileScreen(onNavigateTo: (Screen) -> Unit, onNavigateToLo navigateToSettings = { onNavigateTo(Screen.SettingsScreen) }, navigateToChangeUsername = { onNavigateTo(Screen.UsernameChangeScreen) }, navigateToLogin = { onNavigateToLogin(Screen.LoginScreen) }, - navigateToCamera = { onNavigateTo(Screen.CameraScreen) } + navigateToCamera = { onNavigateTo(Screen.CameraScreen.navWith(SCREEN_TYPE_PROFILE_CAMERA)) } ) } } @@ -305,9 +342,19 @@ fun NavGraphBuilder.settingsScreen(onNavigateBack: () -> Unit, viewModel: Naviga } } -fun NavGraphBuilder.cameraScreen(onNavigateBack: () -> Unit, onNavigateTo: (Screen) -> Unit, viewModel: NavigationViewModel) { +fun NavGraphBuilder.cameraScreen( + onNavigateBack: () -> Unit, + onNavigateTo: (Screen) -> Unit, + viewModel: NavigationViewModel, + newReportViewModel: NewReportViewModel +) { composable( - route = Screen.CameraScreen.route, + route = Screen.CameraScreen.withCameraScreenType(), + arguments = listOf( + navArgument(CAMERA_SCREEN_TYPE) { + type = NavType.StringType + } + ), enterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Left, @@ -324,12 +371,31 @@ fun NavGraphBuilder.cameraScreen(onNavigateBack: () -> Unit, onNavigateTo: (Scre LaunchedEffect(key1 = viewModel.isBottomBarVisible.value) { viewModel.setBottomBarVisible(false) } + val profileViewModel = hiltViewModel() + val screenType = it.arguments?.getString(CAMERA_SCREEN_TYPE)!! + CameraScreen( navigateBack = onNavigateBack, + screenType = screenType, profileViewModel = profileViewModel, navigateToVideoPlayer = { videoUri -> onNavigateTo(Screen.VideoPlayerScreen.navWith(videoUri)) + }, + onSaveImage = { uri, screenSource -> + + when(screenSource) { + SCREEN_TYPE_PROFILE_CAMERA -> { + profileViewModel.updateProfilePicture( + imageUri = uri, + onSuccess = onNavigateBack + ) + } + SCREEN_TYPE_REPORT_CAMERA -> { + newReportViewModel.onChangeImageUri(uri) + onNavigateBack() + } + } } ) } @@ -345,7 +411,7 @@ fun NavGraphBuilder.videoPlayerScreen(viewModel: NavigationViewModel) { ), enterTransition = { slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Down, + towards = AnimatedContentTransitionScope.SlideDirection.Up, animationSpec = tween(500) ) }, diff --git a/core/navigation/src/main/java/com/loki/navigation/Screen.kt b/core/navigation/src/main/java/com/loki/navigation/Screen.kt index 27ea7ba..9283a3a 100644 --- a/core/navigation/src/main/java/com/loki/navigation/Screen.kt +++ b/core/navigation/src/main/java/com/loki/navigation/Screen.kt @@ -5,6 +5,7 @@ import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Newspaper import androidx.compose.ui.graphics.vector.ImageVector +import com.loki.ui.utils.Constants.CAMERA_SCREEN_TYPE import com.loki.ui.utils.Constants.REPORT_ID import com.loki.ui.utils.Constants.VIDEO_URI @@ -25,6 +26,10 @@ sealed class Screen( return "${ReportScreen.route}/{$REPORT_ID}" } + fun withCameraScreenType(): String { + return "${CameraScreen.route}/{$CAMERA_SCREEN_TYPE}" + } + fun withVideoUri(): String { return "${VideoPlayerScreen.route}/{$VIDEO_URI}" } @@ -33,7 +38,7 @@ sealed class Screen( object RegisterScreen: Screen("register_screen") object ForgotPasswordScreen: Screen("forgot_password_screen") object HomeScreen: Screen("home_screen") - object ReportListScreen: Screen(route = "report_list_screen", title = "Home", icon = Icons.Filled.Home, restoreState = false) + object ReportListScreen: Screen(route = "report_list_screen", title = "Home", icon = Icons.Filled.Home) object NewsScreen: Screen(route = "news_screen", title = "News", restoreState = false, icon = Icons.Filled.Newspaper) object NewReportScreen: Screen("new_report_screen") object ReportScreen: Screen("report_screen", restoreState = false) diff --git a/core/navigation/src/main/java/com/loki/navigation/ext/NavControllerExt.kt b/core/navigation/src/main/java/com/loki/navigation/ext/NavControllerExt.kt index 77a0f0a..2477f4f 100644 --- a/core/navigation/src/main/java/com/loki/navigation/ext/NavControllerExt.kt +++ b/core/navigation/src/main/java/com/loki/navigation/ext/NavControllerExt.kt @@ -1,7 +1,6 @@ package com.loki.navigation.ext import androidx.navigation.NavController -import androidx.navigation.NavGraph.Companion.findStartDestination import com.loki.navigation.Screen fun NavController.navigateTo( @@ -15,8 +14,10 @@ fun NavController.navigateTo( } ?: screen.route navigate(route) { - popUpTo(graph.findStartDestination().id) { - saveState = true + if (screen.icon != null) { + popUpTo(Screen.ReportListScreen.route) { + saveState = true + } } launchSingleTop = true restoreState = screen.restoreState diff --git a/core/navigation/src/main/java/com/loki/navigation/graph/AccountGraph.kt b/core/navigation/src/main/java/com/loki/navigation/graph/AccountGraph.kt index b87c0b0..e5bfeb4 100644 --- a/core/navigation/src/main/java/com/loki/navigation/graph/AccountGraph.kt +++ b/core/navigation/src/main/java/com/loki/navigation/graph/AccountGraph.kt @@ -2,6 +2,7 @@ package com.loki.navigation.graph import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import com.loki.navigation.NavigationViewModel @@ -12,6 +13,7 @@ import com.loki.navigation.profileScreen import com.loki.navigation.settingsScreen import com.loki.navigation.usernameChangeScreen import com.loki.navigation.videoPlayerScreen +import com.loki.new_report.NewReportViewModel @Composable fun AccountNavGraph( @@ -20,16 +22,25 @@ fun AccountNavGraph( viewModel: NavigationViewModel, onNavigateToLogin: (Screen) -> Unit ) { + val newReportViewModel = hiltViewModel() NavHost( navController = navController, startDestination = Screen.ProfileScreen.route, modifier = modifier ) { - profileScreen(onNavigateTo = navController::navigateTo, viewModel = viewModel, onNavigateToLogin = onNavigateToLogin) + profileScreen( + onNavigateTo = navController::navigateTo, + viewModel = viewModel, + onNavigateToLogin = onNavigateToLogin + ) usernameChangeScreen(onNavigateBack = navController::navigateUp, viewModel = viewModel) settingsScreen(onNavigateBack = navController::navigateUp, viewModel = viewModel) - cameraScreen(onNavigateBack = navController::navigateUp, onNavigateTo = navController::navigateTo, viewModel = viewModel) - videoPlayerScreen(viewModel = viewModel) + cameraScreen( + onNavigateBack = navController::navigateUp, + onNavigateTo = navController::navigateTo, + viewModel = viewModel, + newReportViewModel = newReportViewModel + ) } } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/loki/navigation/graph/ReportGraph.kt b/core/navigation/src/main/java/com/loki/navigation/graph/ReportGraph.kt index 07fa577..ceab9f1 100644 --- a/core/navigation/src/main/java/com/loki/navigation/graph/ReportGraph.kt +++ b/core/navigation/src/main/java/com/loki/navigation/graph/ReportGraph.kt @@ -2,14 +2,17 @@ package com.loki.navigation.graph import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import com.loki.navigation.NavigationViewModel import com.loki.navigation.Screen +import com.loki.navigation.cameraScreen import com.loki.navigation.ext.navigateTo import com.loki.navigation.newReportScreen import com.loki.navigation.reportListScreen import com.loki.navigation.reportScreen +import com.loki.new_report.NewReportViewModel @Composable fun ReportNavGraph( @@ -17,6 +20,7 @@ fun ReportNavGraph( navController: NavHostController, viewModel: NavigationViewModel ) { + val newReportViewModel = hiltViewModel() NavHost( navController = navController, @@ -25,7 +29,17 @@ fun ReportNavGraph( ) { reportListScreen(onNavigateTo = navController::navigateTo, viewModel = viewModel) - reportScreen(onNavigateBack = { navController.popBackStack() }, viewModel = viewModel) - newReportScreen(onNavigateTo = navController::navigateTo, viewModel = viewModel) + reportScreen(onNavigateBack = navController::navigateUp, viewModel = viewModel) + newReportScreen( + onNavigateTo = navController::navigateTo, + viewModel = viewModel, + newReportViewModel = newReportViewModel + ) + cameraScreen( + onNavigateBack = navController::navigateUp, + onNavigateTo = navController::navigateTo, + viewModel = viewModel, + newReportViewModel = newReportViewModel + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/loki/ui/components/Loading.kt b/core/ui/src/main/java/com/loki/ui/components/Loading.kt index 191ef59..d899c9b 100644 --- a/core/ui/src/main/java/com/loki/ui/components/Loading.kt +++ b/core/ui/src/main/java/com/loki/ui/components/Loading.kt @@ -11,14 +11,15 @@ import androidx.compose.ui.Modifier @Composable fun Loading( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center ) { Box( modifier = modifier.fillMaxSize() .background( MaterialTheme.colorScheme.background.copy(.5f) ), - contentAlignment = Alignment.Center + contentAlignment = alignment ) { CircularProgressIndicator() } diff --git a/core/ui/src/main/java/com/loki/ui/permission/PermissionAction.kt b/core/ui/src/main/java/com/loki/ui/permission/PermissionAction.kt index d423775..8402468 100644 --- a/core/ui/src/main/java/com/loki/ui/permission/PermissionAction.kt +++ b/core/ui/src/main/java/com/loki/ui/permission/PermissionAction.kt @@ -3,5 +3,4 @@ package com.loki.ui.permission sealed class PermissionAction { object PermissionGranted: PermissionAction() object PermissionDenied: PermissionAction() - object PermissionAlreadyGranted: PermissionAction() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/loki/ui/permission/PermissionDialog.kt b/core/ui/src/main/java/com/loki/ui/permission/PermissionDialog.kt index 088bfb4..02c4218 100644 --- a/core/ui/src/main/java/com/loki/ui/permission/PermissionDialog.kt +++ b/core/ui/src/main/java/com/loki/ui/permission/PermissionDialog.kt @@ -19,68 +19,71 @@ import androidx.core.content.ContextCompat fun PermissionDialog( context: Context, permissions: List, + permissionToRequest: List? = null, permissionRationale: Map, snackbarHostState: SnackbarHostState, permissionAction: (Map) -> Unit ) { val grantedPermissions = mutableMapOf() + val permissionToRequestSet = permissionToRequest?.toSet() permissions.forEach { permission -> - val isPermissionGranted = checkIfPermissionGranted(context, permission) - if (isPermissionGranted) { - grantedPermissions[permission] = PermissionAction.PermissionAlreadyGranted - } - else { + if (permissionToRequestSet == null || permissionToRequestSet.contains(permission)) { + val isPermissionGranted = checkIfPermissionGranted(context, permission) - val permissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> + if (isPermissionGranted) { + grantedPermissions[permission] = PermissionAction.PermissionGranted + } else { - val action = if (isGranted) { - PermissionAction.PermissionGranted - } else { - PermissionAction.PermissionDenied - } + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> - grantedPermissions[permission] = action + val action = if (isGranted) { + PermissionAction.PermissionGranted + } else { + PermissionAction.PermissionDenied + } - //checks if we all collected responses for all permission - if (grantedPermissions.size == permissions.size) { - permissionAction(grantedPermissions) - } - } + grantedPermissions[permission] = action - val showPermissionRationale = shouldShowPermissionRationale(context, permission) + //checks if we all collected responses for all permission + if (grantedPermissions.size == permissions.size) { + permissionAction(grantedPermissions) + } + } - if (showPermissionRationale) { - LaunchedEffect(key1 = showPermissionRationale ) { + val showPermissionRationale = shouldShowPermissionRationale(context, permission) - val rationale = permissionRationale[permission] - val snackbarResult = snackbarHostState.showSnackbar( - message = rationale ?: "Permission Required", - actionLabel = "Grant Access", - duration = SnackbarDuration.Long + if (showPermissionRationale) { + LaunchedEffect(key1 = showPermissionRationale) { - ) + val rationale = permissionRationale[permission] + val snackbarResult = snackbarHostState.showSnackbar( + message = rationale ?: "Permission Required", + actionLabel = "Grant Access", + duration = SnackbarDuration.Long + ) - when(snackbarResult) { - SnackbarResult.Dismissed -> { - grantedPermissions[permission] = PermissionAction.PermissionDenied + when (snackbarResult) { + SnackbarResult.Dismissed -> { + grantedPermissions[permission] = PermissionAction.PermissionDenied - // Check if we have collected responses for all permissions - if (grantedPermissions.size == permissions.size) { - permissionAction(grantedPermissions) + // Check if we have collected responses for all permissions + if (grantedPermissions.size == permissions.size) { + permissionAction(grantedPermissions) + } } + + SnackbarResult.ActionPerformed -> permissionLauncher.launch(permission) } - SnackbarResult.ActionPerformed -> permissionLauncher.launch(permission) } - } - } - else { - SideEffect { - permissionLauncher.launch(permission) + } else { + SideEffect { + permissionLauncher.launch(permission) + } } } } diff --git a/core/ui/src/main/java/com/loki/ui/utils/Constants.kt b/core/ui/src/main/java/com/loki/ui/utils/Constants.kt index 1d4910d..865bdc6 100644 --- a/core/ui/src/main/java/com/loki/ui/utils/Constants.kt +++ b/core/ui/src/main/java/com/loki/ui/utils/Constants.kt @@ -4,4 +4,8 @@ object Constants { const val REPORT_ID = "reportId" const val VIDEO_URI = "videoUri" + const val CAMERA_SCREEN_TYPE = "camera_screen_type" + const val SCREEN_TYPE_BOTH = "type_both" + const val SCREEN_TYPE_PROFILE_CAMERA = "type_profile_camera" + const val SCREEN_TYPE_REPORT_CAMERA = "type_report_camera" } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/loki/auth/login/LoginScreen.kt b/feature/auth/src/main/java/com/loki/auth/login/LoginScreen.kt index 43da000..2980cf3 100644 --- a/feature/auth/src/main/java/com/loki/auth/login/LoginScreen.kt +++ b/feature/auth/src/main/java/com/loki/auth/login/LoginScreen.kt @@ -54,7 +54,7 @@ fun LoginScreen( val uiState by viewModel.state val isDarkTheme by viewModel.isDarkTheme - val keyboardController = LocalSoftwareKeyboardController.current + //val keyboardController = LocalSoftwareKeyboardController.current val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -158,7 +158,7 @@ fun LoginScreen( Button( onClick = { - keyboardController?.hide() + //keyboardController?.hide() viewModel.login( navigateToHome = { Toast.makeText( diff --git a/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraPreview.kt b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraPreview.kt index 8c5dc78..41a0d55 100644 --- a/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraPreview.kt +++ b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraPreview.kt @@ -18,6 +18,7 @@ import androidx.camera.video.Recorder import androidx.camera.video.Recording import androidx.camera.video.VideoCapture import androidx.camera.video.VideoRecordEvent +import androidx.camera.video.VideoRecordEvent.Finalize import androidx.camera.view.PreviewView import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -56,17 +57,16 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import androidx.core.util.Consumer +import androidx.lifecycle.LifecycleOwner import com.loki.camera.util.executor import com.loki.camera.util.getCameraProvider import com.loki.camera.util.takePicture import com.loki.ui.theme.md_theme_dark_background import com.loki.ui.utils.DateUtil -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File import java.net.URLEncoder import java.nio.charset.StandardCharsets -import java.util.concurrent.Executor @Composable fun CameraPreview( @@ -102,11 +102,11 @@ fun CameraPreview( @Composable fun CameraCapture( modifier: Modifier = Modifier, - isCameraView: Boolean, - isPermissionGranted: Boolean, + screenPreview: ScreenPreview, onImageUri: (imageUri: Uri) -> Unit, + onVideoOutput: (VideoOutput) -> Unit, + onErrorMessage: (String) -> Unit, onGalleryClick: () -> Unit, - onPermissionRequired: (isPermissionRequired: Boolean) -> Unit, bottomSwitchContent: @Composable () -> Unit ) { @@ -117,6 +117,8 @@ fun CameraCapture( val coroutineScope = rememberCoroutineScope() var previewUseCase by remember { mutableStateOf(Preview.Builder().build()) } + + // camera capture val imageCaptureUseCase by remember { mutableStateOf( ImageCapture.Builder().setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) @@ -124,6 +126,24 @@ fun CameraCapture( ) } + // video capture + val qualitySelector = QualitySelector.fromOrderedList( + listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD), + FallbackStrategy.lowerQualityOrHigherThan(Quality.SD) + ) + val record = Recorder.Builder() + .setExecutor(context.executor) + .setQualitySelector(qualitySelector) + .setTargetVideoEncodingBitRate(5 * 1024 * 1024) + .build() + val videoCaptureUseCase = VideoCapture.withOutput(record) + + // video controls + var recording = remember { null } + var recordingStarted by remember { mutableStateOf(false) } + var audioEnabled by remember { mutableStateOf(true) } + + // camera controls var isFrontCamera by remember { mutableStateOf(false) } val cameraSelector = if (isFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA @@ -138,180 +158,127 @@ fun CameraCapture( } ) + when(screenPreview) { + ScreenPreview.CAMERA -> { + ImageCapture( + context = context, + lifecycleOwner = lifecycleOwner, + previewUseCase = previewUseCase, + cameraSelector = cameraSelector, + imageCaptureUseCase = imageCaptureUseCase + ) + } + + ScreenPreview.VIDEO -> { + VideoRecordCapture( + context = context, + lifecycleOwner = lifecycleOwner, + previewUseCase = previewUseCase, + cameraSelector = cameraSelector, + videoCaptureUseCase = videoCaptureUseCase + ) + } + } + Column( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) ) { + CaptureControls( modifier = Modifier .fillMaxWidth() .padding(bottom = 32.dp), - isCamera = isCameraView, + screenPreview = screenPreview, + isRecording = recordingStarted, onLeftControlClick = onGalleryClick, onRotateCamera = { isFrontCamera = !isFrontCamera }, onCapture = { - if (isCameraView && isPermissionGranted) { - coroutineScope.launch(Dispatchers.IO) { - imageCaptureUseCase - .takePicture(context.executor) - .let { - onImageUri(it.toUri()) - } + when(screenPreview) { + ScreenPreview.CAMERA -> { + coroutineScope.launch { + imageCapture( + imageCaptureUseCase, + context, + onImageUri + ) + } + } + + ScreenPreview.VIDEO -> { + if (!recordingStarted) { + recordingStarted = true + recording = startRecordingVideo( + context = context, + videoCapture = videoCaptureUseCase, + audioEnabled = audioEnabled, + consumer = { event -> + onVideoRecorded( + recording = recording, + event = event, + onVideoUri = onVideoOutput, + onErrorMessage = onErrorMessage + ) + } + ) + } + else { + recordingStarted = false + recording?.stop() + recording = null + } } - } - else { - onPermissionRequired(true) } } ) bottomSwitchContent() } - - } - - LaunchedEffect(key1 = cameraSelector) { - val cameraProvider = context.getCameraProvider() - - try { - cameraProvider.unbindAll() - cameraProvider.bindToLifecycle( - lifecycleOwner, - cameraSelector, - previewUseCase, - imageCaptureUseCase - ) - } catch (e: Exception) { - Log.e("Camera Capture", "Failed to bind camera use case", e) - } } } } @Composable -fun VideoCapture( - modifier: Modifier = Modifier, - isCameraView: Boolean, - isPermissionGranted: Boolean, - onVideoUri: (videoUri: String?) -> Unit, - onPermissionRequired: (isPermissionRequired: Boolean) -> Unit, - bottomSwitchContent: @Composable () -> Unit +fun ImageCapture( + context: Context, + lifecycleOwner: LifecycleOwner, + previewUseCase: UseCase, + cameraSelector: CameraSelector, + imageCaptureUseCase: ImageCapture ) { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val coroutineScope = rememberCoroutineScope() - - var previewUseCase by remember { mutableStateOf(Preview.Builder().build()) } - - val qualitySelector = QualitySelector.from( - Quality.HD, - FallbackStrategy.lowerQualityOrHigherThan(Quality.LOWEST) - ) - - val record = Recorder.Builder() - .setExecutor(context.executor) - .setQualitySelector(qualitySelector) - .build() - val videoCaptureUseCase = VideoCapture.withOutput(record) + LaunchedEffect(key1 = cameraSelector, key2 = previewUseCase) { - var recording = remember { null } - - var recordingStarted by remember { mutableStateOf(false) } - var audioEnabled by remember { mutableStateOf(true) } - - var isFrontCamera by remember { mutableStateOf(false) } - val cameraSelector = if (isFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA - else CameraSelector.DEFAULT_BACK_CAMERA - - Box(modifier = modifier) { - CameraPreview( - modifier = Modifier.fillMaxSize(), - onUseCase = { - previewUseCase = it - } - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - ) { - CaptureControls( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 32.dp), - isCamera = isCameraView, - isRecording = recordingStarted, - audioEnabled = audioEnabled, - onLeftControlClick = { - if (!recordingStarted && !isCameraView) { - audioEnabled = !audioEnabled - } - }, - onRotateCamera = { - if (!recordingStarted && !isCameraView) { - isFrontCamera = !isFrontCamera - } - }, - onCapture = { - - coroutineScope.launch(Dispatchers.IO) { - if (!isCameraView && isPermissionGranted) { - if (!recordingStarted) { - recordingStarted = true - - val mediaDir = context.externalCacheDirs.firstOrNull()?.let { - File(it, "Reet").apply { mkdirs() } - } + val cameraProvider = context.getCameraProvider() - val outputDirectory = - if (mediaDir != null && mediaDir.exists()) mediaDir else - context.filesDir - - recording = startRecordingVideo( - context = context, - videoCapture = videoCaptureUseCase, - outputDirectory = outputDirectory, - executor = context.executor, - audioEnabled = audioEnabled, - consumer = { event -> - - when (event) { - - is VideoRecordEvent.Finalize -> { - val uri = event.outputResults.outputUri - - if (uri != Uri.EMPTY) { - val uriEncoded = URLEncoder.encode( - uri.toString(), - StandardCharsets.UTF_8.toString() - ) - onVideoUri(uriEncoded) - } - } - } - } - ) - } else { - recordingStarted = false - recording?.stop() - recording = null - } - } else { - onPermissionRequired(true) - } - } - } + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + previewUseCase, + imageCaptureUseCase ) - bottomSwitchContent() + } catch (e: Exception) { + Log.e("Camera Capture", "Failed to bind camera use case", e) } } +} + +@Composable +fun VideoRecordCapture( + context: Context, + lifecycleOwner: LifecycleOwner, + previewUseCase: UseCase, + cameraSelector: CameraSelector, + videoCaptureUseCase: VideoCapture, +) { + + LaunchedEffect(key1 = cameraSelector, key2 = previewUseCase) { - LaunchedEffect(key1 = recordingStarted, key2 = cameraSelector, key3 = recording) { val cameraProvider = context.getCameraProvider() try { @@ -328,16 +295,75 @@ fun VideoCapture( } } +fun onVideoRecorded( + recording: Recording?, + event: VideoRecordEvent, + onVideoUri: (VideoOutput) -> Unit, + onErrorMessage: (String) -> Unit +) { + + when (event) { + + is Finalize -> { + recording?.stop() + + val uri = event.outputResults.outputUri + + when(event.error) { + Finalize.ERROR_INSUFFICIENT_STORAGE -> { + onErrorMessage("insufficient storage") + } + Finalize.ERROR_NO_VALID_DATA -> { + onErrorMessage("No data recorded") + } + } + + if (uri != Uri.EMPTY) { + + recording?.close() + + val uriEncoded = URLEncoder.encode( + uri.toString(), + StandardCharsets.UTF_8.toString() + ) + onVideoUri( + VideoOutput( + uri = uriEncoded.toString(), + ) + ) + } + } + } +} + +suspend fun imageCapture( + imageCaptureUseCase: ImageCapture, + context: Context, + onImageUri: (Uri) -> Unit +) { + imageCaptureUseCase + .takePicture(context.executor) + .let { + onImageUri(it.toUri()) + } +} + @SuppressLint("MissingPermission") fun startRecordingVideo( context: Context, videoCapture: VideoCapture, - outputDirectory: File, - executor: Executor, audioEnabled: Boolean, consumer: Consumer ): Recording { + val mediaDir = context.externalCacheDirs.firstOrNull()?.let { + File(it, "Reet").apply { mkdirs() } + } + + val outputDirectory = + if (mediaDir != null && mediaDir.exists()) mediaDir else + context.filesDir + val videoFile = File( outputDirectory, DateUtil.getFileName() + ".mp4" @@ -345,20 +371,19 @@ fun startRecordingVideo( val outputOptions = FileOutputOptions.Builder(videoFile).build() - return videoCapture.output .prepareRecording(context, outputOptions) .apply { if (audioEnabled) withAudioEnabled() } - .start(executor, consumer) + .start(context.executor, consumer) } @Composable fun CaptureControls( modifier: Modifier = Modifier, - isCamera: Boolean, + screenPreview: ScreenPreview, isRecording: Boolean = false, audioEnabled: Boolean = true, onLeftControlClick: () -> Unit, @@ -367,10 +392,10 @@ fun CaptureControls( ) { val recordingBackground = if (isRecording) Color.Red else Color.Red.copy(.4f) - val captureBackground = if (isCamera) Color.White.copy(.8f) else recordingBackground + val captureBackground = if (screenPreview == ScreenPreview.CAMERA) Color.White.copy(.8f) else recordingBackground val recordingLeftControlIcon = if (audioEnabled) Icons.Filled.Mic else Icons.Filled.MicOff - val leftControl = if (isCamera) Icons.Filled.Image else recordingLeftControlIcon + val leftControl = if (screenPreview == ScreenPreview.CAMERA) Icons.Filled.Image else recordingLeftControlIcon Box( modifier = modifier diff --git a/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraScreen.kt b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraScreen.kt index 3fc65f8..713ef7d 100644 --- a/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraScreen.kt +++ b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraScreen.kt @@ -1,7 +1,9 @@ package com.loki.camera.camera_screen import android.Manifest +import android.content.Intent import android.net.Uri +import android.provider.Settings import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult @@ -12,6 +14,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement 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 @@ -24,9 +27,11 @@ import androidx.compose.material.icons.filled.Sync import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue @@ -42,51 +47,67 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import coil.compose.rememberAsyncImagePainter +import com.loki.camera.R import com.loki.camera.util.StatusBarUtil import com.loki.profile.ProfileViewModel import com.loki.ui.components.Loading import com.loki.ui.permission.PermissionAction import com.loki.ui.permission.PermissionDialog +import com.loki.ui.permission.checkIfPermissionGranted import com.loki.ui.theme.md_theme_dark_background import com.loki.ui.theme.md_theme_dark_secondaryContainer +import com.loki.ui.utils.Constants.SCREEN_TYPE_BOTH +import com.loki.ui.utils.Constants.SCREEN_TYPE_PROFILE_CAMERA +import com.loki.ui.utils.Constants.SCREEN_TYPE_REPORT_CAMERA private val EMPTY_IMAGE_URI: Uri = Uri.parse("file://dev/null") @Composable fun CameraScreen( profileViewModel: ProfileViewModel, + screenType: String, navigateToVideoPlayer: (uri: String) -> Unit, + onSaveImage: (imageUri: Uri, screenSource: String) -> Unit, navigateBack: () -> Unit ) { - val appTheme by profileViewModel.isDarkTheme + val isDarkTheme by profileViewModel.isDarkTheme val context = LocalContext.current val view = LocalView.current + val lifecycleOwner = LocalLifecycleOwner.current + val lifecycle = lifecycleOwner.lifecycle val snackbarHostState = remember { SnackbarHostState() } var showPermissionDialog by remember { mutableStateOf(false) } var isPermissionGranted by remember { mutableStateOf(false) } + // preview + var screenPreview by remember { mutableStateOf(ScreenPreview.CAMERA) } + // camera states - var isCameraView by remember { mutableStateOf(true) } var imageUri by remember { mutableStateOf(EMPTY_IMAGE_URI) } var isGalleryClicked by rememberSaveable { mutableStateOf(false) } // if not dark theme change colors to dark theme in this screen composable var statusBarColor by remember { mutableStateOf(md_theme_dark_background.toArgb()) } - var isDark by remember { mutableStateOf(appTheme) } + var isDark by remember { mutableStateOf(isDarkTheme) } var navBarColor by remember { mutableStateOf(md_theme_dark_background.toArgb()) } - val defaultNavColor = StatusBarUtil.defaultNavColor(darkTheme = appTheme) - val defaultStatusBarColor = StatusBarUtil.statusBarColor() + val defaultNavColor = StatusBarUtil.defaultNavColor(darkTheme = isDarkTheme) + val defaultStatusBarColor = StatusBarUtil.defaultStatusBarColor() - if (!appTheme) { + if (!isDarkTheme) { StatusBarUtil.DefaultStatusColors( view = view, isDark = isDark, @@ -102,16 +123,46 @@ fun CameraScreen( } } + // permissions for camera and audio recorder val permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) val permissionRationales = mapOf( Manifest.permission.CAMERA to "Camera permission is required for taking photos and video recording", Manifest.permission.RECORD_AUDIO to "Record Audio permission is required for video recording" ) + val permissionToRequest = if (screenType == SCREEN_TYPE_PROFILE_CAMERA || screenType == SCREEN_TYPE_REPORT_CAMERA) + listOf(Manifest.permission.CAMERA) else permissions + + LaunchedEffect(key1 = showPermissionDialog, key2 = isPermissionGranted) { + showPermissionDialog = true + + val isGranted = checkIfPermissionGranted(context, Manifest.permission.CAMERA) + isPermissionGranted = isGranted + } + + // onresume, check if permission was allowed + DisposableEffect(key1 = lifecycle) { + val lifecycleObserver = LifecycleEventObserver { _, event -> + when(event) { + Lifecycle.Event.ON_RESUME -> { + val isGranted = checkIfPermissionGranted(context, Manifest.permission.CAMERA) + isPermissionGranted = isGranted + } + else -> {} + } + } + + lifecycle.addObserver(lifecycleObserver) + + onDispose { + lifecycle.removeObserver(lifecycleObserver) + } + } if (showPermissionDialog) { PermissionDialog( context = context, permissions = permissions, + permissionToRequest = permissionToRequest, permissionRationale = permissionRationales, snackbarHostState = snackbarHostState, permissionAction = { permissionsAction -> @@ -120,24 +171,10 @@ fun CameraScreen( when (action) { is PermissionAction.PermissionGranted -> { isPermissionGranted = true - Toast.makeText( - context, - "Permission Granted", - Toast.LENGTH_SHORT - ).show() } is PermissionAction.PermissionDenied -> { isPermissionGranted = false - Toast.makeText( - context, - "Permission Not Granted", - Toast.LENGTH_SHORT - ).show() - } - - is PermissionAction.PermissionAlreadyGranted -> { - isPermissionGranted = true } } } @@ -180,7 +217,7 @@ fun CameraScreen( ).show() } } - + if (imageUri != EMPTY_IMAGE_URI) { Box( @@ -223,9 +260,10 @@ fun CameraScreen( containerColor = md_theme_dark_secondaryContainer ), onClick = { - profileViewModel.updateProfilePicture(imageUri) { - navigateBack() - } + statusBarColor = defaultStatusBarColor + navBarColor = defaultNavColor + isDark = !isDark + onSaveImage(imageUri, screenType) } ) { Image( @@ -245,7 +283,35 @@ fun CameraScreen( } else { - if (isCameraView) { + if (!isPermissionGranted) { + + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.align(Alignment.Center) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.allow_permissions), + textAlign = TextAlign.Center + ) + OutlinedButton( + onClick = { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + context.startActivity(intent) + } + ) { + Text( + text = stringResource(R.string.open_settings), + color = MaterialTheme.colorScheme.onBackground + ) + } + } + } + } else { Box( modifier = Modifier @@ -254,46 +320,34 @@ fun CameraScreen( ) { CameraCapture( modifier = Modifier.fillMaxSize(), - isCameraView = isCameraView, - isPermissionGranted = isPermissionGranted, + screenPreview = screenPreview, onImageUri = { uri -> imageUri = uri }, - onPermissionRequired = { - showPermissionDialog = it + onVideoOutput = { output -> + output.uri?.let(navigateToVideoPlayer) }, onGalleryClick = { isGalleryClicked = true + }, + onErrorMessage = { + Toast.makeText( + context, + it, + Toast.LENGTH_LONG + ).show() } ) { - BottomContent( - onViewSelected = { - isCameraView = it == Preview.CAMERA - } - ) - } - } - } else { - - VideoCapture( - modifier = Modifier.fillMaxSize(), - isCameraView = isCameraView, - isPermissionGranted = isPermissionGranted, - onVideoUri = { uri -> - uri?.let(navigateToVideoPlayer) - }, - onPermissionRequired = { - showPermissionDialog = it - }, - bottomSwitchContent = { - BottomContent( - onViewSelected = { - isCameraView = it == Preview.CAMERA - } - ) + if (screenType == SCREEN_TYPE_BOTH) { + BottomContent( + onViewSelected = { + screenPreview = it + } + ) + } } - ) + } } } } @@ -301,7 +355,7 @@ fun CameraScreen( @Composable fun BottomContent( modifier: Modifier = Modifier, - onViewSelected: (Preview) -> Unit + onViewSelected: (ScreenPreview) -> Unit ) { Box( modifier = modifier @@ -310,6 +364,7 @@ fun BottomContent( contentAlignment = Alignment.Center ) { + val titles = listOf("Camera", "Video") var selectedTitle by remember { mutableStateOf(titles[0]) } @@ -318,19 +373,17 @@ fun BottomContent( horizontalArrangement = Arrangement.Center ) { - titles.forEachIndexed { index, title -> - - val selected = titles[index] == title + titles.forEachIndexed { _, title -> PreviewSelectables( title = title, - selected = selected, + selected = selectedTitle == title, onClick = { selectedTitle = title if (title == "Camera") { - onViewSelected(Preview.CAMERA) + onViewSelected(ScreenPreview.CAMERA) } else { - onViewSelected(Preview.VIDEO) + onViewSelected(ScreenPreview.VIDEO) } } ) @@ -339,7 +392,7 @@ fun BottomContent( } } -enum class Preview { +enum class ScreenPreview { CAMERA, VIDEO } diff --git a/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraViewModel.kt b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraViewModel.kt new file mode 100644 index 0000000..5a3fea6 --- /dev/null +++ b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraViewModel.kt @@ -0,0 +1,21 @@ +package com.loki.camera.camera_screen + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.loki.ui.utils.Constants.CAMERA_SCREEN_TYPE +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class CameraViewModel @Inject constructor( + savedStateHandle: SavedStateHandle +): ViewModel() { + + private var screenType = mutableStateOf("") + init { + savedStateHandle.get(CAMERA_SCREEN_TYPE)?.let { type -> + screenType.value = type + } + } +} \ No newline at end of file diff --git a/feature/camera/src/main/java/com/loki/camera/camera_screen/VideoOutput.kt b/feature/camera/src/main/java/com/loki/camera/camera_screen/VideoOutput.kt new file mode 100644 index 0000000..294cee6 --- /dev/null +++ b/feature/camera/src/main/java/com/loki/camera/camera_screen/VideoOutput.kt @@ -0,0 +1,7 @@ +package com.loki.camera.camera_screen + +data class VideoOutput( + val uri: String? = null, + val duration: Long? = null, + val fileSize: Long? = null +) diff --git a/feature/camera/src/main/java/com/loki/camera/util/StatusBarUtil.kt b/feature/camera/src/main/java/com/loki/camera/util/StatusBarUtil.kt index 4832322..66324ab 100644 --- a/feature/camera/src/main/java/com/loki/camera/util/StatusBarUtil.kt +++ b/feature/camera/src/main/java/com/loki/camera/util/StatusBarUtil.kt @@ -13,7 +13,7 @@ import androidx.core.view.WindowCompat object StatusBarUtil { @Composable - fun statusBarColor() = MaterialTheme.colorScheme.background.toArgb() + fun defaultStatusBarColor() = MaterialTheme.colorScheme.background.toArgb() @Composable fun defaultNavColor(darkTheme: Boolean) = @@ -24,7 +24,7 @@ object StatusBarUtil { fun DefaultStatusColors( view: View, isDark: Boolean, - statusBarColor: Int = statusBarColor(), + statusBarColor: Int = defaultStatusBarColor(), navigationBarColor: Int = defaultNavColor(isDark), ) { diff --git a/feature/camera/src/main/res/values/strings.xml b/feature/camera/src/main/res/values/strings.xml new file mode 100644 index 0000000..6406e98 --- /dev/null +++ b/feature/camera/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Permissions, and turn \"Camera\" on]]> + Open Settings + \ No newline at end of file diff --git a/feature/new_report/src/main/java/com/loki/new_report/NewReportScreen.kt b/feature/new_report/src/main/java/com/loki/new_report/NewReportScreen.kt index 93971db..1d81407 100644 --- a/feature/new_report/src/main/java/com/loki/new_report/NewReportScreen.kt +++ b/feature/new_report/src/main/java/com/loki/new_report/NewReportScreen.kt @@ -1,11 +1,14 @@ package com.loki.new_report +import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -15,9 +18,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.CameraAlt +import androidx.compose.material.icons.sharp.Cancel import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -46,16 +49,18 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.rememberAsyncImagePainter import com.loki.ui.components.AppTopBar +import com.loki.ui.components.Loading import com.loki.ui.components.ProfileCircleBox import kotlinx.coroutines.job -@OptIn(ExperimentalMaterial3Api::class) @Composable fun NewReportScreen( viewModel: NewReportViewModel, - navigateToHome: () -> Unit + navigateToHome: () -> Unit, + navigateToCamera: () -> Unit ) { + val isDarkTheme by viewModel.isDarkTheme val uiState by viewModel.state val localProfile by viewModel.localProfile.collectAsStateWithLifecycle() @@ -99,6 +104,12 @@ fun NewReportScreen( } } + if (viewModel.isLoading.value) { + Loading( + alignment = Alignment.TopCenter + ) + } + Column(modifier = Modifier.fillMaxSize()) { AppTopBar( @@ -111,7 +122,8 @@ fun NewReportScreen( onClick = { viewModel.addReport(navigateToHome) }, - enabled = !viewModel.isLoading.value && uiState.reportContent.isNotBlank() + enabled = !viewModel.isLoading.value && + (uiState.reportContent.isNotBlank() || uiState.imageUri != null) ) { Text(text = "Add") } @@ -131,6 +143,9 @@ fun NewReportScreen( Column { + val containerColor = if (isDarkTheme) MaterialTheme.colorScheme.primary.copy(.2f) + else MaterialTheme.colorScheme.primary.copy(.05f) + TextField( value = uiState.reportContent, onValueChange = viewModel::onChangeReportContent, @@ -143,13 +158,14 @@ fun NewReportScreen( .height(150.dp) .focusRequester(focusRequester), enabled = !viewModel.isLoading.value, - colors = TextFieldDefaults.textFieldColors( - containerColor = if (viewModel.isDarkTheme.value) MaterialTheme.colorScheme.primary.copy(.2f) - else MaterialTheme.colorScheme.primary.copy(.05f), + colors = TextFieldDefaults.colors( + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, - cursorColor = if (viewModel.isDarkTheme.value) MaterialTheme.colorScheme.onBackground - else MaterialTheme.colorScheme.primary + cursorColor = if (isDarkTheme) MaterialTheme.colorScheme.onBackground + else MaterialTheme.colorScheme.primary, + disabledContainerColor = Color.Transparent ), ) @@ -158,34 +174,61 @@ fun NewReportScreen( horizontalArrangement = Arrangement.Center, modifier = Modifier.padding(16.dp) ) { - IconButton( - onClick = { - isGalleryClicked = true - } + onClick = navigateToCamera ) { - - Icon( - imageVector = Icons.Filled.Image, - contentDescription = "Gallery_icon" - ) + Icon(imageVector = Icons.Filled.CameraAlt, contentDescription = "Camera_icon") } } uiState.imageUri?.let { - Image( - painter = rememberAsyncImagePainter(model = it), - contentDescription = null, - modifier = Modifier - .padding(vertical = 4.dp, horizontal = 16.dp) - .fillMaxWidth() - .height(300.dp) - .clip(RoundedCornerShape(8.dp)), - contentScale = ContentScale.Crop + ImageContainer( + imageUri = it, + onDeleteClick = { + viewModel.onChangeImageUri(null) + } ) } } + } +} +@Composable +fun ImageContainer( + imageUri: Uri, + onDeleteClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 16.dp) + .height(300.dp) + .clip(RoundedCornerShape(8.dp)) + ) { + Image( + painter = rememberAsyncImagePainter(model = imageUri), + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop + ) + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(.5f)) + ) { + IconButton( + onClick = onDeleteClick, + modifier = Modifier.align(Alignment.TopEnd) + ) { + Icon( + imageVector = Icons.Sharp.Cancel, + contentDescription = "cancel icon" + ) + } + } } } \ No newline at end of file diff --git a/feature/new_report/src/main/java/com/loki/new_report/NewReportViewModel.kt b/feature/new_report/src/main/java/com/loki/new_report/NewReportViewModel.kt index 559ab30..87fb7f7 100644 --- a/feature/new_report/src/main/java/com/loki/new_report/NewReportViewModel.kt +++ b/feature/new_report/src/main/java/com/loki/new_report/NewReportViewModel.kt @@ -30,7 +30,7 @@ class NewReportViewModel @Inject constructor( state.value = state.value.copy(reportContent = newValue) } - fun onChangeImageUri(newValue: Uri) { + fun onChangeImageUri(newValue: Uri?) { state.value = state.value.copy(imageUri = newValue) } diff --git a/feature/profile/src/main/java/com/loki/profile/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/loki/profile/profile/ProfileScreen.kt index ac7f6b0..a749057 100644 --- a/feature/profile/src/main/java/com/loki/profile/profile/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/loki/profile/profile/ProfileScreen.kt @@ -160,7 +160,7 @@ fun ProfileScreen( ) Text( - text = "v1.2.0", + text = "v0.3.0-beta", fontSize = 12.sp, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) diff --git a/feature/report/src/main/java/com/loki/report/ReportScreen.kt b/feature/report/src/main/java/com/loki/report/ReportScreen.kt index ae3d76d..f6137e1 100644 --- a/feature/report/src/main/java/com/loki/report/ReportScreen.kt +++ b/feature/report/src/main/java/com/loki/report/ReportScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material3.Button import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -57,7 +56,6 @@ import com.loki.ui.utils.DateUtil.formatDate import com.loki.ui.utils.DateUtil.formatTime import com.loki.ui.utils.ext.toInitials -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReportScreen( viewModel: ReportViewModel, @@ -364,8 +362,9 @@ fun ReportScreen( .width((deviceWidth - (deviceWidth / 2.4)).dp) .clip(RoundedCornerShape(12.dp)), enabled = !viewModel.isLoading.value, - colors = TextFieldDefaults.textFieldColors( - containerColor = MaterialTheme.colorScheme.primary.copy(.02f), + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.primary.copy(.02f), + unfocusedContainerColor = MaterialTheme.colorScheme.primary.copy(.02f), focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 551a2fd..d961a0d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,14 +2,14 @@ androidGradlePlugin = "8.0.2" androidxActivity = "1.7.2" -androidxComposeBom = "2023.06.01" +androidxComposeBom = "2023.09.00" androidxComposeCompiler = "1.4.4" androidxCore = "1.10.1" androidxComposeMaterial3 = "1.1.1" androidxCoreSplashscreen = "1.0.1" androidxEspresso = "3.5.1" -androidxLifecycle = "2.6.1" -androidxNavigation = "2.7.1" +androidxLifecycle = "2.6.2" +androidxNavigation = "2.7.2" androidxTestCore = "1.5.0" androidxTestExt = "1.1.5" androidxTestRules = "1.5.0" @@ -17,7 +17,7 @@ androidxTestRunner = "1.5.2" androidxDataStore = "1.0.0" junit4 = "4.13.2" kotlin = "1.8.21" -kotlinxCoroutines = "1.6.4" +kotlinxCoroutines = "1.7.1" retrofit = "2.9.0" okHttp3 = "4.10.0" room = "2.5.2" @@ -33,7 +33,7 @@ firebaseBom = "32.2.2" googleServices = "4.3.15" firebasePerf = "1.4.2" firebaseCrash = "2.9.6" -kotlinxPlayService = "1.6.4" +kotlinxPlayService = "1.7.1" ktlint = "11.0.0" spotless = "6.19.0" archCoreTest = "2.2.0" @@ -43,7 +43,6 @@ mockitoInline = "3.11.2" camerax = "1.3.0-rc01" junit = "1.1.5" appcompat = "1.6.1" -material = "1.8.0" exoPlayer = "1.1.1" accompanistPermission = "0.33.1-alpha" @@ -152,7 +151,6 @@ permissions = { group = "com.google.accompanist", name = "accompanist-permission junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d0d26d..f11f113 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,3 @@ -include(":feature:settings") pluginManagement { repositories { @@ -31,3 +30,4 @@ include(":data:remote") include(":data:local") include(":di") include(":feature:camera") +include(":feature:settings")