diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0b9c0f3..1781f70 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,10 +3,12 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
Unit) {
@@ -302,7 +305,7 @@ fun NavGraphBuilder.settingsScreen(onNavigateBack: () -> Unit, viewModel: Naviga
}
}
-fun NavGraphBuilder.cameraScreen(onNavigateBack: () -> Unit,viewModel: NavigationViewModel) {
+fun NavGraphBuilder.cameraScreen(onNavigateBack: () -> Unit, onNavigateTo: (Screen) -> Unit, viewModel: NavigationViewModel) {
composable(
route = Screen.CameraScreen.route,
enterTransition = {
@@ -324,7 +327,39 @@ fun NavGraphBuilder.cameraScreen(onNavigateBack: () -> Unit,viewModel: Navigatio
val profileViewModel = hiltViewModel()
CameraScreen(
navigateBack = onNavigateBack,
- profileViewModel = profileViewModel
+ profileViewModel = profileViewModel,
+ navigateToVideoPlayer = { videoUri ->
+ onNavigateTo(Screen.VideoPlayerScreen.navWith(videoUri))
+ }
)
}
+}
+
+fun NavGraphBuilder.videoPlayerScreen(viewModel: NavigationViewModel) {
+ composable(
+ route = Screen.VideoPlayerScreen.withVideoUri(),
+ arguments = listOf(
+ navArgument(VIDEO_URI) {
+ type = NavType.StringType
+ }
+ ),
+ enterTransition = {
+ slideIntoContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Down,
+ animationSpec = tween(500)
+ )
+ },
+ exitTransition = {
+ slideOutOfContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Down,
+ animationSpec = tween(200)
+ )
+ }
+ ) {
+ LaunchedEffect(key1 = viewModel.isBottomBarVisible.value) {
+ viewModel.setBottomBarVisible(false)
+ }
+ val videoPlayerViewModel = hiltViewModel()
+ VideoPlayerScreen(viewModel = videoPlayerViewModel)
+ }
}
\ No newline at end of file
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 b23e239..27ea7ba 100644
--- a/core/navigation/src/main/java/com/loki/navigation/Screen.kt
+++ b/core/navigation/src/main/java/com/loki/navigation/Screen.kt
@@ -6,6 +6,7 @@ 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.REPORT_ID
+import com.loki.ui.utils.Constants.VIDEO_URI
sealed class Screen(
val route: String,
@@ -24,11 +25,15 @@ sealed class Screen(
return "${ReportScreen.route}/{$REPORT_ID}"
}
+ fun withVideoUri(): String {
+ return "${VideoPlayerScreen.route}/{$VIDEO_URI}"
+ }
+
object LoginScreen: Screen("login_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)
+ object ReportListScreen: Screen(route = "report_list_screen", title = "Home", icon = Icons.Filled.Home, restoreState = false)
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)
@@ -36,5 +41,6 @@ sealed class Screen(
object UsernameChangeScreen: Screen("change_username_screen")
object SettingsScreen: Screen("settings_screen")
object CameraScreen: Screen("camera_screen")
+ object VideoPlayerScreen: Screen("video_player_screen")
}
\ No newline at end of file
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 293b2d5..b87c0b0 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
@@ -11,6 +11,7 @@ import com.loki.navigation.ext.navigateTo
import com.loki.navigation.profileScreen
import com.loki.navigation.settingsScreen
import com.loki.navigation.usernameChangeScreen
+import com.loki.navigation.videoPlayerScreen
@Composable
fun AccountNavGraph(
@@ -28,6 +29,7 @@ fun AccountNavGraph(
profileScreen(onNavigateTo = navController::navigateTo, viewModel = viewModel, onNavigateToLogin = onNavigateToLogin)
usernameChangeScreen(onNavigateBack = navController::navigateUp, viewModel = viewModel)
settingsScreen(onNavigateBack = navController::navigateUp, viewModel = viewModel)
- cameraScreen(onNavigateBack = navController::navigateUp, viewModel = viewModel)
+ cameraScreen(onNavigateBack = navController::navigateUp, onNavigateTo = navController::navigateTo, viewModel = viewModel)
+ videoPlayerScreen(viewModel = viewModel)
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/loki/ui/components/NotificationBubble.kt b/core/ui/src/main/java/com/loki/ui/components/NotificationBubble.kt
new file mode 100644
index 0000000..132e2c7
--- /dev/null
+++ b/core/ui/src/main/java/com/loki/ui/components/NotificationBubble.kt
@@ -0,0 +1,83 @@
+package com.loki.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import com.loki.ui.theme.md_theme_light_primary
+import kotlinx.coroutines.delay
+
+@Composable
+fun NotificationBubble(
+ modifier: Modifier = Modifier,
+ message: String = ""
+) {
+
+ var isBubbleVisible by remember { mutableStateOf(false) }
+ var mess by remember { mutableStateOf(message) }
+
+ LaunchedEffect(key1 = mess) {
+
+ isBubbleVisible = true
+
+ delay(3000L)
+
+ isBubbleVisible = false
+ mess = ""
+ }
+
+ AnimatedVisibility(
+ visible = isBubbleVisible,
+ enter = slideInVertically { -(it / 2) },
+ exit = slideOutVertically { 0 }
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = 24.dp,
+ vertical = 16.dp
+ )
+ .clip(RoundedCornerShape(12.dp))
+ .height(70.dp)
+ .background(md_theme_light_primary.copy(.5f))
+ .border(
+ border = BorderStroke(width = 1.dp, color = md_theme_light_primary),
+ shape = RoundedCornerShape(12.dp)
+ )
+ .alpha(1f)
+ .align(Alignment.TopCenter),
+ contentAlignment = Alignment.CenterStart
+ ) {
+
+ Text(
+ text = message,
+ color = md_theme_light_primary,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+
+ }
+ }
+ }
+}
\ 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 7b5950c..088bfb4 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
@@ -18,50 +18,71 @@ import androidx.core.content.ContextCompat
@Composable
fun PermissionDialog(
context: Context,
- permission: String,
- permissionRationale: String,
+ permissions: List,
+ permissionRationale: Map,
snackbarHostState: SnackbarHostState,
- permissionAction: (PermissionAction) -> Unit
+ permissionAction: (Map) -> Unit
) {
- val isPermissionGranted = checkIfPermissionGranted(context, permission)
+ val grantedPermissions = mutableMapOf()
- if (isPermissionGranted) {
- permissionAction(PermissionAction.PermissionAlreadyGranted)
- return
- }
+ permissions.forEach { permission ->
+ val isPermissionGranted = checkIfPermissionGranted(context, permission)
- val permissionLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted ->
- if (isGranted) {
- permissionAction(PermissionAction.PermissionGranted)
+ if (isPermissionGranted) {
+ grantedPermissions[permission] = PermissionAction.PermissionAlreadyGranted
}
else {
- permissionAction(PermissionAction.PermissionDenied)
- }
- }
- val showPermissionRationale = shouldShowPermissionRationale(context, permission)
+ val permissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
- if (showPermissionRationale) {
- LaunchedEffect(key1 = showPermissionRationale ) {
- val snackbarResult = snackbarHostState.showSnackbar(
- message = permissionRationale,
- actionLabel = "Grant Access",
- duration = SnackbarDuration.Long
+ val action = if (isGranted) {
+ PermissionAction.PermissionGranted
+ } else {
+ PermissionAction.PermissionDenied
+ }
- )
+ grantedPermissions[permission] = action
- when(snackbarResult) {
- SnackbarResult.Dismissed -> permissionAction(PermissionAction.PermissionDenied)
- SnackbarResult.ActionPerformed -> permissionLauncher.launch(permission)
+ //checks if we all collected responses for all permission
+ if (grantedPermissions.size == permissions.size) {
+ permissionAction(grantedPermissions)
+ }
+ }
+
+ val showPermissionRationale = shouldShowPermissionRationale(context, permission)
+
+ 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
+
+ // Check if we have collected responses for all permissions
+ if (grantedPermissions.size == permissions.size) {
+ permissionAction(grantedPermissions)
+ }
+ }
+ 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 8b9084e..1d4910d 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
@@ -3,4 +3,5 @@ package com.loki.ui.utils
object Constants {
const val REPORT_ID = "reportId"
+ const val VIDEO_URI = "videoUri"
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/loki/ui/utils/DateUtil.kt b/core/ui/src/main/java/com/loki/ui/utils/DateUtil.kt
index 12e0bbf..b46f5a7 100644
--- a/core/ui/src/main/java/com/loki/ui/utils/DateUtil.kt
+++ b/core/ui/src/main/java/com/loki/ui/utils/DateUtil.kt
@@ -29,6 +29,11 @@ object DateUtil {
return dateFormat.format(date)
}
+ fun getFileName(): String {
+ return SimpleDateFormat(FILE_NAME_FORMAT, Locale.ENGLISH).format(System.currentTimeMillis())
+ }
+
private const val DATE_FORMAT = "EEE, d MMM yyyy"
private const val TIME_FORMAT = "hh:mm a"
+ private const val FILE_NAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/loki/ui/viewmodel/ReetViewModel.kt b/core/ui/src/main/java/com/loki/ui/viewmodel/ReetViewModel.kt
index c644063..3c587b1 100644
--- a/core/ui/src/main/java/com/loki/ui/viewmodel/ReetViewModel.kt
+++ b/core/ui/src/main/java/com/loki/ui/viewmodel/ReetViewModel.kt
@@ -1,6 +1,5 @@
package com.loki.ui.viewmodel
-import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -8,13 +7,10 @@ import com.google.firebase.FirebaseException
import com.loki.local.datastore.DataStoreStorage
import com.loki.local.datastore.model.LocalProfile
import com.loki.local.datastore.model.LocalUser
-import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-import javax.inject.Inject
-
open class ReetViewModel(
private val dataStore: DataStoreStorage
diff --git a/di/build.gradle.kts b/di/build.gradle.kts
index b8a92b3..94ce5b2 100644
--- a/di/build.gradle.kts
+++ b/di/build.gradle.kts
@@ -46,6 +46,7 @@ dependencies {
implementation(libs.androidx.datastore)
implementation(libs.bundles.retrofit)
+ implementation(libs.bundles.exoPlayer)
testImplementation(libs.bundles.test.common)
androidTestImplementation(libs.bundles.android.test)
diff --git a/di/src/main/java/com/loki/di/VideoPlayerModule.kt b/di/src/main/java/com/loki/di/VideoPlayerModule.kt
new file mode 100644
index 0000000..0ed6c6b
--- /dev/null
+++ b/di/src/main/java/com/loki/di/VideoPlayerModule.kt
@@ -0,0 +1,22 @@
+package com.loki.di
+
+import android.app.Application
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object VideoPlayerModule {
+
+ @Provides
+ @ViewModelScoped
+ fun provideVideoPlayer(app: Application): Player {
+ return ExoPlayer.Builder(app)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/feature/camera/build.gradle.kts b/feature/camera/build.gradle.kts
index 0df9ab2..ed698d6 100644
--- a/feature/camera/build.gradle.kts
+++ b/feature/camera/build.gradle.kts
@@ -57,7 +57,10 @@ dependencies {
implementation(platform(libs.firebase.bom))
implementation(libs.bundles.firebase)
implementation(libs.coil.kt.compose)
+
+ implementation(libs.permissions)
implementation(libs.bundles.cameraX)
+ implementation(libs.bundles.exoPlayer)
testImplementation(libs.bundles.test.common)
androidTestImplementation(libs.bundles.android.test)
diff --git a/feature/camera/src/main/AndroidManifest.xml b/feature/camera/src/main/AndroidManifest.xml
index a5918e6..3939fdd 100644
--- a/feature/camera/src/main/AndroidManifest.xml
+++ b/feature/camera/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/feature/camera/src/main/java/com/loki/camera/CameraPreview.kt b/feature/camera/src/main/java/com/loki/camera/CameraPreview.kt
deleted file mode 100644
index eee0328..0000000
--- a/feature/camera/src/main/java/com/loki/camera/CameraPreview.kt
+++ /dev/null
@@ -1,200 +0,0 @@
-package com.loki.camera
-
-import android.util.Log
-import android.view.ViewGroup
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
-import androidx.camera.core.Preview
-import androidx.camera.core.UseCase
-import androidx.camera.view.PreviewView
-import androidx.compose.foundation.Image
-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.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Cameraswitch
-import androidx.compose.material.icons.filled.Image
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-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 kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.io.File
-
-@Composable
-fun CameraPreview(
- modifier: Modifier = Modifier,
- scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
- onUseCase: (UseCase) -> Unit = {}
-) {
-
- AndroidView(
- modifier = modifier,
- factory = { context ->
- val previewView = PreviewView(context).apply {
- this.scaleType = scaleType
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
- }
-
- // camerax preview usecase
- onUseCase(
- Preview.Builder()
- .build().also {
- it.setSurfaceProvider(previewView.surfaceProvider)
- }
- )
-
- previewView
- }
- )
-}
-
-@Composable
-fun CameraCapture(
- modifier: Modifier = Modifier,
- onImageFile: (File) -> Unit,
- onGalleryClick: () -> Unit
-) {
-
- Box(modifier = modifier) {
-
- val context = LocalContext.current
- val lifecycleOwner = LocalLifecycleOwner.current
- val coroutineScope = rememberCoroutineScope()
-
- var previewUseCase by remember { mutableStateOf(Preview.Builder().build()) }
- val imageCaptureUseCase by remember {
- mutableStateOf(
- ImageCapture.Builder().setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
- .build()
- )
- }
-
- var isFrontCamera by remember { mutableStateOf(false) }
-
- val cameraSelector = if (isFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA
- else CameraSelector.DEFAULT_BACK_CAMERA
-
- Box(
- modifier = Modifier.background(md_theme_dark_background)
- ) {
- CameraPreview(
- modifier = Modifier.fillMaxSize(),
- onUseCase = {
- previewUseCase = it
- }
- )
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 32.dp)
- .align(Alignment.BottomCenter)
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
-
- IconButton(
- onClick = onGalleryClick,
- modifier = Modifier
- .padding(start = 24.dp),
- ) {
- Image(
- imageVector = Icons.Filled.Image,
- contentDescription = "image_icon",
- modifier = Modifier.size(30.dp),
- colorFilter = ColorFilter.tint(
- Color.White
- )
- )
- }
-
- Spacer(modifier = Modifier.weight(1f))
- Box(
- modifier = Modifier
- .size(70.dp)
- .clip(CircleShape)
- .background(Color.White.copy(.8f))
- .clickable {
- coroutineScope.launch(Dispatchers.IO) {
- imageCaptureUseCase
- .takePicture(context.executor)
- .let {
- onImageFile(it)
- }
- }
- }
- )
- Spacer(modifier = Modifier.weight(1f))
-
- IconButton(
- onClick = {
- isFrontCamera = !isFrontCamera
- },
- modifier = Modifier
- .padding(end = 24.dp)
- ) {
- Image(
- imageVector = Icons.Filled.Cameraswitch,
- contentDescription = "rotate_camera_icon",
- modifier = Modifier.size(30.dp),
- colorFilter = ColorFilter.tint(
- Color.White
- )
- )
- }
- }
- }
- }
-
- 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)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/camera/src/main/java/com/loki/camera/CameraScreen.kt b/feature/camera/src/main/java/com/loki/camera/CameraScreen.kt
deleted file mode 100644
index 7611a86..0000000
--- a/feature/camera/src/main/java/com/loki/camera/CameraScreen.kt
+++ /dev/null
@@ -1,200 +0,0 @@
-package com.loki.camera
-
-import android.net.Uri
-import android.widget.Toast
-import androidx.activity.compose.BackHandler
-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.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Done
-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.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-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.LocalView
-import androidx.compose.ui.unit.dp
-import androidx.core.net.toUri
-import coil.compose.rememberAsyncImagePainter
-import com.loki.camera.util.StatusBarUtil
-import com.loki.profile.ProfileViewModel
-import com.loki.ui.components.Loading
-import com.loki.ui.theme.md_theme_dark_background
-import com.loki.ui.theme.md_theme_dark_secondaryContainer
-
-private val EMPTY_IMAGE_URI: Uri = Uri.parse("file://dev/null")
-
-@Composable
-fun CameraScreen(
- profileViewModel: ProfileViewModel,
- navigateBack: () -> Unit
-) {
-
- val appTheme by profileViewModel.isDarkTheme
-
- val context = LocalContext.current
- val view = LocalView.current
-
- 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 navBarColor by remember { mutableStateOf(md_theme_dark_background.toArgb()) }
-
- val defaultNavColor = StatusBarUtil.defaultNavColor(darkTheme = appTheme)
- val defaultStatusBarColor = StatusBarUtil.statusBarColor()
-
- if (!appTheme) {
- StatusBarUtil.DefaultStatusColors(
- view = view,
- isDark = isDark,
- statusBarColor = statusBarColor,
- navigationBarColor = navBarColor
- )
-
- BackHandler(true) {
- statusBarColor = defaultStatusBarColor
- navBarColor = defaultNavColor
- isDark = !isDark
- navigateBack()
- }
- }
-
- // open files to select images
- val galleryLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.PickVisualMedia()
- ) { uri ->
- isGalleryClicked = false
-
- uri?.let {
- imageUri = it
- }
- }
-
- if (isGalleryClicked) {
- SideEffect {
- galleryLauncher.launch(
- PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
- )
- }
- }
-
- // onBackPress will not navigate back if image has been captured or selected
- if (imageUri != EMPTY_IMAGE_URI) {
- BackHandler(true) {
- imageUri = EMPTY_IMAGE_URI
- }
- }
-
- if (profileViewModel.errorMessage.value.isNotBlank()) {
- LaunchedEffect(key1 = profileViewModel.errorMessage.value) {
- Toast.makeText(
- context,
- profileViewModel.errorMessage.value,
- Toast.LENGTH_LONG
- ).show()
- }
- }
-
- if (imageUri != EMPTY_IMAGE_URI) {
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(md_theme_dark_background)
- ) {
- Image(
- modifier = Modifier.fillMaxSize(),
- painter = rememberAsyncImagePainter(imageUri),
- contentDescription = "Captured image"
- )
- IconButton(
- modifier = Modifier
- .align(Alignment.BottomStart)
- .padding(16.dp),
- colors = IconButtonDefaults.iconButtonColors(
- contentColor = MaterialTheme.colorScheme.onBackground,
- containerColor = md_theme_dark_secondaryContainer
- ),
- onClick = {
- imageUri = EMPTY_IMAGE_URI
- }
- ) {
- Image(
- imageVector = Icons.Filled.Sync,
- contentDescription = "Retry icon",
- modifier = Modifier.size(30.dp),
- colorFilter = ColorFilter.tint(
- Color.White
- )
- )
- }
- IconButton(
- modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(16.dp),
- colors = IconButtonDefaults.iconButtonColors(
- contentColor = MaterialTheme.colorScheme.onBackground,
- containerColor = md_theme_dark_secondaryContainer
- ),
- onClick = {
- profileViewModel.updateProfilePicture(imageUri) {
- navigateBack()
- }
- }
- ) {
- Image(
- imageVector = Icons.Filled.Done,
- contentDescription = "Done icon",
- modifier = Modifier.size(30.dp),
- colorFilter = ColorFilter.tint(
- Color.White
- )
- )
- }
-
- if (profileViewModel.isLoading.value) {
- Loading()
- }
- }
- } else {
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(md_theme_dark_background)
- ) {
- CameraCapture(
- modifier = Modifier.fillMaxSize(),
- onImageFile = { file ->
- imageUri = file.toUri()
- },
- onGalleryClick = {
- isGalleryClicked = true
- }
- )
- }
- }
-}
\ No newline at end of file
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
new file mode 100644
index 0000000..8c5dc78
--- /dev/null
+++ b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraPreview.kt
@@ -0,0 +1,428 @@
+package com.loki.camera.camera_screen
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import android.view.ViewGroup
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.video.FallbackStrategy
+import androidx.camera.video.FileOutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.Recorder
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Cameraswitch
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.Mic
+import androidx.compose.material.icons.filled.MicOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.net.toUri
+import androidx.core.util.Consumer
+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(
+ modifier: Modifier = Modifier,
+ scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER,
+ onUseCase: (UseCase) -> Unit = {}
+) {
+
+ AndroidView(
+ modifier = modifier,
+ factory = { context ->
+ val previewView = PreviewView(context).apply {
+ this.scaleType = scaleType
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ }
+
+ // camerax preview usecase
+ onUseCase(
+ Preview.Builder()
+ .build().also {
+ it.setSurfaceProvider(previewView.surfaceProvider)
+ }
+ )
+
+ previewView
+ }
+ )
+}
+
+@Composable
+fun CameraCapture(
+ modifier: Modifier = Modifier,
+ isCameraView: Boolean,
+ isPermissionGranted: Boolean,
+ onImageUri: (imageUri: Uri) -> Unit,
+ onGalleryClick: () -> Unit,
+ onPermissionRequired: (isPermissionRequired: Boolean) -> Unit,
+ bottomSwitchContent: @Composable () -> Unit
+) {
+
+ Box(modifier = modifier) {
+
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val coroutineScope = rememberCoroutineScope()
+
+ var previewUseCase by remember { mutableStateOf(Preview.Builder().build()) }
+ val imageCaptureUseCase by remember {
+ mutableStateOf(
+ ImageCapture.Builder().setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
+ .build()
+ )
+ }
+
+ var isFrontCamera by remember { mutableStateOf(false) }
+ val cameraSelector = if (isFrontCamera) CameraSelector.DEFAULT_FRONT_CAMERA
+ else CameraSelector.DEFAULT_BACK_CAMERA
+
+ Box(
+ modifier = Modifier.background(md_theme_dark_background)
+ ) {
+ CameraPreview(
+ modifier = Modifier.fillMaxSize(),
+ onUseCase = {
+ previewUseCase = it
+ }
+ )
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ ) {
+ CaptureControls(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 32.dp),
+ isCamera = isCameraView,
+ onLeftControlClick = onGalleryClick,
+ onRotateCamera = {
+ isFrontCamera = !isFrontCamera
+ },
+ onCapture = {
+ if (isCameraView && isPermissionGranted) {
+ coroutineScope.launch(Dispatchers.IO) {
+ imageCaptureUseCase
+ .takePicture(context.executor)
+ .let {
+ onImageUri(it.toUri())
+ }
+ }
+ }
+ 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
+) {
+ 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)
+
+ 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 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)
+ }
+ }
+ }
+ )
+ bottomSwitchContent()
+ }
+ }
+
+ LaunchedEffect(key1 = recordingStarted, key2 = cameraSelector, key3 = recording) {
+ val cameraProvider = context.getCameraProvider()
+
+ try {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ previewUseCase,
+ videoCaptureUseCase
+ )
+ } catch (e: Exception) {
+ Log.e("Camera Capture", "Failed to bind camera use case", e)
+ }
+ }
+}
+
+@SuppressLint("MissingPermission")
+fun startRecordingVideo(
+ context: Context,
+ videoCapture: VideoCapture,
+ outputDirectory: File,
+ executor: Executor,
+ audioEnabled: Boolean,
+ consumer: Consumer
+): Recording {
+
+ val videoFile = File(
+ outputDirectory,
+ DateUtil.getFileName() + ".mp4"
+ )
+
+ val outputOptions = FileOutputOptions.Builder(videoFile).build()
+
+
+ return videoCapture.output
+ .prepareRecording(context, outputOptions)
+ .apply {
+ if (audioEnabled) withAudioEnabled()
+ }
+ .start(executor, consumer)
+
+}
+
+@Composable
+fun CaptureControls(
+ modifier: Modifier = Modifier,
+ isCamera: Boolean,
+ isRecording: Boolean = false,
+ audioEnabled: Boolean = true,
+ onLeftControlClick: () -> Unit,
+ onRotateCamera: () -> Unit,
+ onCapture: () -> Unit
+) {
+
+ val recordingBackground = if (isRecording) Color.Red else Color.Red.copy(.4f)
+ val captureBackground = if (isCamera) 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
+
+ Box(
+ modifier = modifier
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+
+ IconButton(
+ onClick = onLeftControlClick,
+ modifier = Modifier
+ .padding(start = 24.dp),
+ ) {
+ Icon(
+ imageVector = leftControl,
+ contentDescription = "image_icon",
+ modifier = Modifier.size(30.dp),
+ tint = Color.White
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+ Box(
+ modifier = Modifier
+ .size(70.dp)
+ .clip(CircleShape)
+ .background(captureBackground)
+ .border(
+ width = if (isRecording) 2.dp else 0.dp,
+ color = if (isRecording) Color.White else Color.Transparent,
+ shape = CircleShape
+ )
+ .clickable {
+ onCapture()
+ }
+ )
+ Spacer(modifier = Modifier.weight(1f))
+
+ IconButton(
+ onClick = onRotateCamera,
+ modifier = Modifier
+ .padding(end = 24.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Cameraswitch,
+ contentDescription = "rotate_camera_icon",
+ modifier = Modifier.size(30.dp),
+ tint = Color.White
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..3fc65f8
--- /dev/null
+++ b/feature/camera/src/main/java/com/loki/camera/camera_screen/CameraScreen.kt
@@ -0,0 +1,379 @@
+package com.loki.camera.camera_screen
+
+import android.Manifest
+import android.net.Uri
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
+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.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+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.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+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.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.LocalView
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.rememberAsyncImagePainter
+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.theme.md_theme_dark_background
+import com.loki.ui.theme.md_theme_dark_secondaryContainer
+
+private val EMPTY_IMAGE_URI: Uri = Uri.parse("file://dev/null")
+
+@Composable
+fun CameraScreen(
+ profileViewModel: ProfileViewModel,
+ navigateToVideoPlayer: (uri: String) -> Unit,
+ navigateBack: () -> Unit
+) {
+
+ val appTheme by profileViewModel.isDarkTheme
+
+ val context = LocalContext.current
+ val view = LocalView.current
+
+ val snackbarHostState = remember { SnackbarHostState() }
+ var showPermissionDialog by remember { mutableStateOf(false) }
+ var isPermissionGranted by remember { mutableStateOf(false) }
+
+ // 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 navBarColor by remember { mutableStateOf(md_theme_dark_background.toArgb()) }
+
+ val defaultNavColor = StatusBarUtil.defaultNavColor(darkTheme = appTheme)
+ val defaultStatusBarColor = StatusBarUtil.statusBarColor()
+
+ if (!appTheme) {
+ StatusBarUtil.DefaultStatusColors(
+ view = view,
+ isDark = isDark,
+ statusBarColor = statusBarColor,
+ navigationBarColor = navBarColor
+ )
+
+ BackHandler(true) {
+ statusBarColor = defaultStatusBarColor
+ navBarColor = defaultNavColor
+ isDark = !isDark
+ navigateBack()
+ }
+ }
+
+ 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"
+ )
+
+ if (showPermissionDialog) {
+ PermissionDialog(
+ context = context,
+ permissions = permissions,
+ permissionRationale = permissionRationales,
+ snackbarHostState = snackbarHostState,
+ permissionAction = { permissionsAction ->
+
+ for ((permission, action) in permissionsAction) {
+ 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
+ }
+ }
+ }
+ }
+ )
+ }
+
+ // open files to select images
+ val galleryLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia()
+ ) { uri ->
+ isGalleryClicked = false
+
+ uri?.let {
+ imageUri = it
+ }
+ }
+
+ if (isGalleryClicked) {
+ SideEffect {
+ galleryLauncher.launch(
+ PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+ )
+ }
+ }
+
+ // onBackPress will not navigate back if image has been captured or selected
+ if (imageUri != EMPTY_IMAGE_URI) {
+ BackHandler(true) {
+ imageUri = EMPTY_IMAGE_URI
+ }
+ }
+
+ if (profileViewModel.errorMessage.value.isNotBlank()) {
+ LaunchedEffect(key1 = profileViewModel.errorMessage.value) {
+ Toast.makeText(
+ context,
+ profileViewModel.errorMessage.value,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ if (imageUri != EMPTY_IMAGE_URI) {
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(md_theme_dark_background)
+ ) {
+ Image(
+ modifier = Modifier.fillMaxSize(),
+ painter = rememberAsyncImagePainter(imageUri),
+ contentDescription = "Captured image"
+ )
+ IconButton(
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(16.dp),
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = MaterialTheme.colorScheme.onBackground,
+ containerColor = md_theme_dark_secondaryContainer
+ ),
+ onClick = {
+ imageUri = EMPTY_IMAGE_URI
+ }
+ ) {
+ Image(
+ imageVector = Icons.Filled.Sync,
+ contentDescription = "Retry icon",
+ modifier = Modifier.size(30.dp),
+ colorFilter = ColorFilter.tint(
+ Color.White
+ )
+ )
+ }
+ IconButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = MaterialTheme.colorScheme.onBackground,
+ containerColor = md_theme_dark_secondaryContainer
+ ),
+ onClick = {
+ profileViewModel.updateProfilePicture(imageUri) {
+ navigateBack()
+ }
+ }
+ ) {
+ Image(
+ imageVector = Icons.Filled.Done,
+ contentDescription = "Done icon",
+ modifier = Modifier.size(30.dp),
+ colorFilter = ColorFilter.tint(
+ Color.White
+ )
+ )
+ }
+
+ if (profileViewModel.isLoading.value) {
+ Loading()
+ }
+ }
+
+ } else {
+
+ if (isCameraView) {
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(md_theme_dark_background)
+ ) {
+ CameraCapture(
+ modifier = Modifier.fillMaxSize(),
+ isCameraView = isCameraView,
+ isPermissionGranted = isPermissionGranted,
+ onImageUri = { uri ->
+ imageUri = uri
+ },
+ onPermissionRequired = {
+ showPermissionDialog = it
+ },
+ onGalleryClick = {
+ isGalleryClicked = true
+ }
+ ) {
+ 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
+ }
+ )
+ }
+ )
+ }
+ }
+}
+
+@Composable
+fun BottomContent(
+ modifier: Modifier = Modifier,
+ onViewSelected: (Preview) -> Unit
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+
+ val titles = listOf("Camera", "Video")
+ var selectedTitle by remember { mutableStateOf(titles[0]) }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+
+ titles.forEachIndexed { index, title ->
+
+ val selected = titles[index] == title
+
+ PreviewSelectables(
+ title = title,
+ selected = selected,
+ onClick = {
+ selectedTitle = title
+ if (title == "Camera") {
+ onViewSelected(Preview.CAMERA)
+ } else {
+ onViewSelected(Preview.VIDEO)
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
+enum class Preview {
+ CAMERA,
+ VIDEO
+}
+
+@Composable
+fun PreviewSelectables(
+ modifier: Modifier = Modifier,
+ title: String,
+ titleSize: Int = 18,
+ cornerRadius: Dp = 16.dp,
+ spacing: Dp = 8.dp,
+ selected: Boolean,
+ selectedBackground: Color = Color.Black.copy(.3f),
+ selectedTitleColor: Color = Color.White,
+ onClick: () -> Unit
+) {
+
+ val backgroundColor by rememberUpdatedState( if (selected) selectedBackground else Color.Transparent)
+ val titleColor by rememberUpdatedState( if(selected) selectedTitleColor else Color.White.copy(.5f))
+
+ Box(
+ modifier = modifier
+ .background(
+ color = backgroundColor,
+ shape = RoundedCornerShape(cornerRadius)
+ )
+ .clip(RoundedCornerShape(cornerRadius))
+ .clickable { onClick() }
+ ) {
+ Text(
+ text = title,
+ color = titleColor,
+ fontSize = titleSize.sp,
+ modifier = Modifier.padding(spacing)
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/camera/src/main/java/com/loki/camera/util/CameraUtils.kt b/feature/camera/src/main/java/com/loki/camera/util/CameraUtils.kt
index e344fa5..c6f0903 100644
--- a/feature/camera/src/main/java/com/loki/camera/util/CameraUtils.kt
+++ b/feature/camera/src/main/java/com/loki/camera/util/CameraUtils.kt
@@ -2,10 +2,19 @@ package com.loki.camera.util
import android.content.Context
import android.util.Log
+import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.video.FallbackStrategy
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.Recorder
+import androidx.camera.video.VideoCapture
+import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
+import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
diff --git a/feature/camera/src/main/java/com/loki/camera/video_screen/VideoPlayerScreen.kt b/feature/camera/src/main/java/com/loki/camera/video_screen/VideoPlayerScreen.kt
new file mode 100644
index 0000000..6fcf9e4
--- /dev/null
+++ b/feature/camera/src/main/java/com/loki/camera/video_screen/VideoPlayerScreen.kt
@@ -0,0 +1,66 @@
+package com.loki.camera.video_screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+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.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.media3.ui.PlayerView
+
+@Composable
+fun VideoPlayerScreen(
+ viewModel: VideoPlayerViewModel
+) {
+
+ var lifecycle by remember { mutableStateOf(Lifecycle.Event.ON_CREATE) }
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ DisposableEffect(key1 = lifecycleOwner) {
+ val observer = LifecycleEventObserver { _, event ->
+ lifecycle = event
+ }
+
+ lifecycleOwner.lifecycle.addObserver(observer)
+
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ AndroidView(
+ factory = { context ->
+ PlayerView(context).also {
+ it.player = viewModel.player
+ }
+ },
+ update = {
+ when(lifecycle) {
+ Lifecycle.Event.ON_PAUSE -> {
+ it.onPause()
+ it.player?.pause()
+ }
+
+ Lifecycle.Event.ON_RESUME -> {
+ it.onResume()
+ }
+
+ else -> Unit
+ }
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/camera/src/main/java/com/loki/camera/video_screen/VideoPlayerViewModel.kt b/feature/camera/src/main/java/com/loki/camera/video_screen/VideoPlayerViewModel.kt
new file mode 100644
index 0000000..5802def
--- /dev/null
+++ b/feature/camera/src/main/java/com/loki/camera/video_screen/VideoPlayerViewModel.kt
@@ -0,0 +1,35 @@
+package com.loki.camera.video_screen
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import com.loki.ui.utils.Constants.VIDEO_URI
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class VideoPlayerViewModel @Inject constructor(
+ val player: Player,
+ savedStateHandle: SavedStateHandle
+): ViewModel() {
+
+ init {
+ savedStateHandle.get(VIDEO_URI)?.let {
+ playVideo(it)
+ }
+ player.prepare()
+ }
+
+ private fun playVideo(uri: String) {
+ player.setMediaItem(
+ MediaItem.fromUri(uri)
+ )
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ player.release()
+ }
+}
\ No newline at end of file
diff --git a/feature/home/src/main/java/com/loki/home/home/HomeScreen.kt b/feature/home/src/main/java/com/loki/home/home/HomeScreen.kt
index e628444..9858081 100644
--- a/feature/home/src/main/java/com/loki/home/home/HomeScreen.kt
+++ b/feature/home/src/main/java/com/loki/home/home/HomeScreen.kt
@@ -2,7 +2,6 @@ package com.loki.home.home
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -13,8 +12,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
nestedNavGraph: @Composable () -> Unit,
@@ -26,7 +23,9 @@ fun HomeScreen(
Scaffold(bottomBar = bottomBar) { padding ->
Surface(
- modifier = Modifier.fillMaxSize().padding(padding),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
color = if (color == Color.Unspecified) Color.Transparent else color
) {
CompositionLocalProvider(
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 479b9d2..ac7f6b0 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
@@ -1,6 +1,5 @@
package com.loki.profile.profile
-import android.Manifest
import android.content.Intent
import android.net.Uri
import android.widget.Toast
@@ -33,15 +32,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarHostState
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.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -55,8 +49,6 @@ import com.loki.profile.ProfileViewModel
import com.loki.ui.components.AppTopBar
import com.loki.ui.components.ExtendedRowItem
import com.loki.ui.components.ProfileCircleBox
-import com.loki.ui.permission.PermissionAction
-import com.loki.ui.permission.PermissionDialog
@Composable
fun ProfileScreen(
@@ -70,9 +62,6 @@ fun ProfileScreen(
val localProfile by viewModel.localProfile.collectAsStateWithLifecycle()
val localUser by viewModel.localUser.collectAsStateWithLifecycle()
val context = LocalContext.current
- val snackbarHostState = remember { SnackbarHostState() }
-
- var isPermissionDialogClicked by rememberSaveable { mutableStateOf(false) }
val openIntent = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
@@ -89,43 +78,6 @@ fun ProfileScreen(
}
}
- if (isPermissionDialogClicked) {
- PermissionDialog(
- context = context,
- permission = Manifest.permission.CAMERA,
- permissionRationale = Manifest.permission.CAMERA,
- snackbarHostState = snackbarHostState,
- permissionAction = { action ->
-
- when (action) {
- is PermissionAction.PermissionGranted -> {
- Toast.makeText(
- context,
- "Permission Granted",
- Toast.LENGTH_SHORT
- ).show()
- isPermissionDialogClicked = false
- navigateToCamera()
- }
-
- is PermissionAction.PermissionDenied -> {
- Toast.makeText(
- context,
- "Permission Not Granted",
- Toast.LENGTH_SHORT
- ).show()
- isPermissionDialogClicked = false
- }
-
- is PermissionAction.PermissionAlreadyGranted -> {
- isPermissionDialogClicked = false
- navigateToCamera()
- }
- }
- }
- )
- }
-
Scaffold (
topBar = {
AppTopBar(
@@ -149,9 +101,7 @@ fun ProfileScreen(
backgroundColor = Color(localProfile.profileBackground),
initials = localProfile.userNameInitials,
imageUri = localProfile.profileImage,
- onEditClick = {
- isPermissionDialogClicked = true
- }
+ onEditClick = navigateToCamera
)
}
diff --git a/feature/report/src/main/java/com/loki/report/ReportViewModel.kt b/feature/report/src/main/java/com/loki/report/ReportViewModel.kt
index 440575e..e686545 100644
--- a/feature/report/src/main/java/com/loki/report/ReportViewModel.kt
+++ b/feature/report/src/main/java/com/loki/report/ReportViewModel.kt
@@ -149,6 +149,7 @@ class ReportViewModel @Inject constructor(
fun deleteComment() {
launchCatching {
comments.deleteComment(editableComment.value.id)
+ message.value = "Comment Deleted"
}
}
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f71ad18..551a2fd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -44,6 +44,8 @@ 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"
[libraries]
@@ -140,6 +142,14 @@ androidx-camera-video = { group = "androidx.camera", name = "camera-video", ver
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
androidx-camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camerax" }
+
+# exo player
+exoPlayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref="exoPlayer" }
+exoPlayerUi = { group = "androidx.media3", name = "media3-ui", version.ref="exoPlayer" }
+
+# accompanist libs
+permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermission" }
+
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" }
@@ -242,4 +252,9 @@ cameraX = [
"androidx-camera-view",
"androidx-camera-camera2",
"androidx-camera-extensions"
+]
+
+exoPlayer = [
+ "exoPlayer",
+ "exoPlayerUi"
]
\ No newline at end of file