login screen vm , qr detect ml lit implemented ,hilt implemented ,data store implemented ,side navigation implemented
This commit is contained in:
3
.idea/deploymentTargetSelector.xml
generated
3
.idea/deploymentTargetSelector.xml
generated
@@ -5,6 +5,9 @@
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="lofpre">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +1,9 @@
|
||||
plugins {
|
||||
kotlin("kapt")
|
||||
id("com.google.dagger.hilt.android")
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -47,6 +50,10 @@ android {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
// Allow references to generated code
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -59,6 +66,8 @@ dependencies {
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.datastore.core.android)
|
||||
implementation(libs.androidx.datastore.preferences.core.jvm)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
@@ -66,4 +75,23 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation ("androidx.camera:camera-camera2:1.3.4")
|
||||
implementation ( "androidx.camera:camera-lifecycle:1.3.4")
|
||||
implementation ("androidx.camera:camera-view:1.3.4")
|
||||
implementation ("com.google.mlkit:barcode-scanning:17.3.0")
|
||||
implementation ("com.google.accompanist:accompanist-permissions:0.31.2-alpha")
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
|
||||
|
||||
implementation ("androidx.navigation:navigation-compose:2.7.2")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.51.1")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||
|
||||
val nav_version = "2.8.2"
|
||||
implementation("androidx.navigation:navigation-compose:$nav_version")
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<application
|
||||
android:name=".MyApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -15,11 +19,9 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.SMIAttendance">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
13
app/src/main/java/in/sminnovations/attendanceapp/AppClass.kt
Normal file
13
app/src/main/java/in/sminnovations/attendanceapp/AppClass.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package `in`.sminnovations.attendanceapp
|
||||
|
||||
// File: MyApp.kt
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MyApp : Application(){
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package `in`.sminnovations.attendanceapp
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import `in`.sminnovations.attendanceapp.screens.login.ui.LoginScreen
|
||||
import `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui.NavigationDrawer
|
||||
|
||||
@Composable
|
||||
fun AppNavigation() {
|
||||
val navController = rememberNavController()
|
||||
NavigationHost(navController = navController)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationHost(navController: NavHostController) {
|
||||
NavHost(navController = navController, startDestination = "login") {
|
||||
// Login Screen Route
|
||||
composable("login") {
|
||||
LoginScreen(
|
||||
loginViewModel = hiltViewModel(),
|
||||
onLoginSuccess = {
|
||||
// Navigate to the home screen after login success
|
||||
navController.navigate("home") {
|
||||
// Pop the login screen from the back stack
|
||||
popUpTo("login") { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
composable("home") {
|
||||
NavigationDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import `in`.sminnovations.attendanceapp.screens.navigationDrawer.ui.NavigationDrawer
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui.NavigationDrawer
|
||||
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
SMIAttendanceTheme {
|
||||
NavigationDrawer()
|
||||
AppNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package `in`.sminnovations.attendanceapp.data.dataStore
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.data
|
||||
|
||||
class EmployeeData
|
||||
@@ -1,4 +1,4 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.navigationDrawer.ui.components
|
||||
package `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -1,13 +1,17 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.navigationDrawer.ui.components
|
||||
package `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,22 +27,37 @@ import `in`.sminnovations.attendanceapp.R
|
||||
|
||||
|
||||
@Composable
|
||||
fun TopComponents() {
|
||||
fun TopComponents(
|
||||
onclose : () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Image with rounded corners
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Absolute.Right
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {onclose()},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
Icon(painter = painterResource(id =R.drawable.baseline_close_24 ), contentDescription ="close" )
|
||||
}
|
||||
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.smi_logo), // replace with your image resource
|
||||
contentDescription = "Profile Photo",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(100.dp).clip(RoundedCornerShape(16.dp))
|
||||
modifier = Modifier
|
||||
.size(150.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Name
|
||||
Text(
|
||||
text = "John Doe",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -49,7 +68,6 @@ fun TopComponents() {
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// ID
|
||||
Text(
|
||||
text = "ID: 123456",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -59,7 +77,6 @@ fun TopComponents() {
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// Designation
|
||||
Text(
|
||||
text = "Software Engineer",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -0,0 +1,74 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import `in`.sminnovations.attendanceapp.R
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.Home
|
||||
import `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui.components.BottomComponents
|
||||
import `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.ui.components.TopComponents
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun NavigationDrawer() {
|
||||
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
fun openClose() {
|
||||
scope.launch {
|
||||
drawerState.apply {
|
||||
if (isClosed) open() else close()
|
||||
}
|
||||
}
|
||||
}
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Column(modifier = Modifier.clickable { openClose() }) {
|
||||
TopComponents(onclose = {
|
||||
scope.launch {
|
||||
openClose()
|
||||
}
|
||||
})
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
BottomComponents(appVersion = "1.0")
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = { openClose() }, modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.baseline_menu_24),
|
||||
contentDescription = "menu"
|
||||
)
|
||||
}
|
||||
}
|
||||
}) { contentPadding ->
|
||||
Column(modifier = Modifier.padding(contentPadding)) {
|
||||
Home()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.navigationDrawer.viewModel
|
||||
|
||||
class navigationViewModel {
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
|
||||
|
||||
@Composable
|
||||
fun DefaultScreen(
|
||||
qrCodeValue: String?,
|
||||
onScanClick: () -> Unit,
|
||||
onRetryClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
qrCodeValue?.let {
|
||||
Text(text = "Scanned Value: $it", modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
Button(onClick = if (qrCodeValue == null) onScanClick else onRetryClick) {
|
||||
Text(if (qrCodeValue == null) "Scan" else "Retry")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun RequestPermissions(
|
||||
cameraPermissionState: PermissionState,
|
||||
locationPermissionState: PermissionState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
locationPermissionState.launchPermissionRequest()
|
||||
}
|
||||
if (!cameraPermissionState.status.isGranted) {
|
||||
Toast.makeText(context, "Camera permission is required", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (!locationPermissionState.status.isGranted) {
|
||||
Toast.makeText(context, "Location permission is required", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@Composable
|
||||
fun QRCodeScannerScreen(
|
||||
onQRCodeScanned: (String) -> Unit,
|
||||
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val previewView = remember { PreviewView(context) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder().build().apply {
|
||||
setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
val barcodeScanner = BarcodeScanning.getClient(
|
||||
BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
)
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { analyzer ->
|
||||
analyzer.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
|
||||
processImageProxy(imageProxy, barcodeScanner) { value ->
|
||||
onQRCodeScanned(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
imageAnalyzer
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("QRCodeScanner", "Camera binding failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
factory = { previewView }
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun processImageProxy(
|
||||
imageProxy: ImageProxy,
|
||||
scanner: com.google.mlkit.vision.barcode.BarcodeScanner,
|
||||
onQRCodeScanned: (String) -> Unit
|
||||
) {
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage != null) {
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
barcodes.firstOrNull()?.rawValue?.let { qrCodeValue ->
|
||||
onQRCodeScanned(qrCodeValue)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
Log.e("QRCodeScanner", "Barcode processing failed", it)
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
imageProxy.close()
|
||||
}
|
||||
} else {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
@@ -1,8 +1,47 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.DefaultScreen
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.QRCodeScannerScreen
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.RequestPermissions
|
||||
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun Home(){
|
||||
val viewModel: HomeViewModel = viewModel()
|
||||
val isScanning by viewModel.isScanning.collectAsState()
|
||||
val qrCodeValue by viewModel.qrCodeValue.collectAsState()
|
||||
|
||||
// Request permissions
|
||||
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
// Function to check if all permissions are granted
|
||||
val allPermissionsGranted = cameraPermissionState.status.isGranted &&
|
||||
locationPermissionState.status.isGranted
|
||||
|
||||
if (!allPermissionsGranted) {
|
||||
RequestPermissions(
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
locationPermissionState = locationPermissionState
|
||||
)
|
||||
} else if (isScanning) {
|
||||
QRCodeScannerScreen(viewModel::setQRCodeValue)
|
||||
} else {
|
||||
DefaultScreen(
|
||||
qrCodeValue = qrCodeValue,
|
||||
onScanClick = { viewModel.startScanning() },
|
||||
onRetryClick = { viewModel.startScanning() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.viewModel
|
||||
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
private val _qrCodeValue = MutableStateFlow<String?>(null)
|
||||
val qrCodeValue: StateFlow<String?> = _qrCodeValue.asStateFlow()
|
||||
|
||||
private val _isScanning = MutableStateFlow(false)
|
||||
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||
|
||||
fun startScanning() {
|
||||
_isScanning.value = true
|
||||
}
|
||||
|
||||
private fun stopScanning() {
|
||||
_isScanning.value = false
|
||||
}
|
||||
|
||||
fun setQRCodeValue(value: String) {
|
||||
viewModelScope.launch {
|
||||
_qrCodeValue.emit(value)
|
||||
stopScanning()
|
||||
}
|
||||
}
|
||||
|
||||
fun getEmployeeData(){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.data
|
||||
|
||||
// File: AppModule.kt
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLoginDataStore(@ApplicationContext context: Context): LoginDataStore {
|
||||
return LoginDataStore(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.data
|
||||
// File: LoginDataStore.kt
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val DATASTORE_NAME = "user_prefs"
|
||||
private val Context.dataStore by preferencesDataStore(name = DATASTORE_NAME)
|
||||
|
||||
@Singleton
|
||||
class LoginDataStore @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val dataStore = context.dataStore
|
||||
|
||||
private val LOGIN_ID_KEY = stringPreferencesKey("login_id")
|
||||
private val PASSWORD_KEY = stringPreferencesKey("password")
|
||||
private val IS_LOGGED_IN_KEY = booleanPreferencesKey("is_logged_in")
|
||||
|
||||
val loginId: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[LOGIN_ID_KEY] ?: ""
|
||||
}
|
||||
|
||||
val password: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[PASSWORD_KEY] ?: ""
|
||||
}
|
||||
|
||||
val isLoggedIn: Flow<Boolean> = dataStore.data.map { preferences ->
|
||||
preferences[IS_LOGGED_IN_KEY] ?: false
|
||||
}
|
||||
|
||||
suspend fun saveLoginData(id: String, password: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[LOGIN_ID_KEY] = id
|
||||
preferences[PASSWORD_KEY] = password
|
||||
preferences[IS_LOGGED_IN_KEY] = true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLoginData() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(LOGIN_ID_KEY)
|
||||
preferences.remove(PASSWORD_KEY)
|
||||
preferences[IS_LOGGED_IN_KEY] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.data
|
||||
|
||||
data class LoginData(
|
||||
val id : String ="",
|
||||
val password: String = ""
|
||||
)
|
||||
@@ -1,13 +1,14 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -15,125 +16,125 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.R
|
||||
import kotlinx.coroutines.delay
|
||||
import `in`.sminnovations.attendanceapp.screens.login.viewModel.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(onLoginSuccess: () -> Unit) {
|
||||
fun LoginScreen(
|
||||
loginViewModel: LoginViewModel = hiltViewModel(),
|
||||
onLoginSuccess: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val isLoading by loginViewModel.isLoading.collectAsState()
|
||||
val isLoggedIn by loginViewModel.isLoggedIn.collectAsState()
|
||||
|
||||
var userId by remember { mutableStateOf(TextFieldValue("")) }
|
||||
var password by remember { mutableStateOf(TextFieldValue("")) }
|
||||
var showLoginButton by remember { mutableStateOf(false) }
|
||||
var isButtonLoading by remember { mutableStateOf(false) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
// Animated visibility for screen entrance
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300) // Small delay before showing content
|
||||
showLoginButton = true
|
||||
// Navigate to the main screen if the user is already logged in
|
||||
LaunchedEffect(isLoggedIn) {
|
||||
if (isLoggedIn) onLoginSuccess()
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showLoginButton,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(26.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(26.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = "Attendance App ", style = MaterialTheme.typography.headlineLarge)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
Image(painter = painterResource(id = R.drawable.smi_logo), contentDescription = "Image", alignment = Alignment.TopCenter)
|
||||
Spacer(modifier = Modifier.height(100.dp))
|
||||
Text(text = "Credentials", style = MaterialTheme.typography.headlineMedium)
|
||||
Text(text = "Attendance App", style = MaterialTheme.typography.headlineLarge)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
// Logo Image
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.smi_logo),
|
||||
contentDescription = "App Logo",
|
||||
alignment = Alignment.TopCenter
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = userId,
|
||||
onValueChange = { userId = it },
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
label = { Text("User ID") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(100.dp))
|
||||
Text(text = "Credentials", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
trailingIcon = {
|
||||
val image = if (passwordVisible) painterResource(id = R.drawable.baseline_visibility_24) else painterResource(
|
||||
id = R.drawable.baseline_visibility_off_24
|
||||
)
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(painter = image, contentDescription = "Toggle Password Visibility")
|
||||
}
|
||||
}
|
||||
)
|
||||
// User ID TextField
|
||||
OutlinedTextField(
|
||||
value = userId,
|
||||
onValueChange = { userId = it },
|
||||
label = { Text("User ID") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
isButtonLoading = true
|
||||
coroutineScope.launch {
|
||||
delay(1000) // Simulate network call
|
||||
onLoginSuccess()
|
||||
isButtonLoading = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
enabled = !isButtonLoading
|
||||
) {
|
||||
if (isButtonLoading) {
|
||||
CircularProgressIndicator(
|
||||
color = Color.White,
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
// Password TextField
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
trailingIcon = {
|
||||
val icon = if (passwordVisible) {
|
||||
painterResource(id = R.drawable.baseline_visibility_24)
|
||||
} else {
|
||||
Text(
|
||||
text = "Login",
|
||||
fontSize = 20.sp
|
||||
|
||||
)
|
||||
painterResource(id = R.drawable.baseline_visibility_off_24)
|
||||
}
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(painter = icon, contentDescription = "Toggle Password Visibility")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
|
||||
// Login Button
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
loginViewModel.login(userId.text, password.text, onLoginSuccess)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
enabled = !isLoading
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
color = Color.White,
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Login",
|
||||
fontSize = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun lofpre(){
|
||||
LoginScreen {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.viewModel
|
||||
|
||||
// File: LoginViewModel.kt
|
||||
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginData
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val loginDataStore: LoginDataStore
|
||||
) : ViewModel() {
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private val _isLoggedIn = MutableStateFlow(false)
|
||||
val isLoggedIn = _isLoggedIn.asStateFlow()
|
||||
|
||||
private val _loginData = MutableStateFlow(LoginData("", ""))
|
||||
val loginData = _loginData.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
loginDataStore.isLoggedIn.collect { loggedIn ->
|
||||
_isLoggedIn.value = loggedIn
|
||||
if (loggedIn) {
|
||||
loginDataStore.loginId.collect { id ->
|
||||
loginDataStore.password.collect { password ->
|
||||
_loginData.value = LoginData(id, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(userId: String, password: String, onLoginSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
delay(1000) // Simulate network call
|
||||
|
||||
if (userId == "1" && password == "2") {
|
||||
loginDataStore.saveLoginData(userId, password)
|
||||
_isLoggedIn.value = true
|
||||
onLoginSuccess()
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
loginDataStore.clearLoginData()
|
||||
_isLoggedIn.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.navigationDrawer.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import `in`.sminnovations.attendanceapp.screens.navigationDrawer.ui.components.BottomComponents
|
||||
import kotlinx.coroutines.launch
|
||||
import `in`.sminnovations.attendanceapp.screens.navigationDrawer.ui.components.TopComponents
|
||||
|
||||
@Composable
|
||||
fun NavigationDrawer(){
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Column (
|
||||
Modifier.fillMaxHeight(),
|
||||
|
||||
){
|
||||
TopComponents()
|
||||
BottomComponents(appVersion = "")
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text("Show drawer") },
|
||||
icon = { Icon(Icons.Filled.Add, contentDescription = "") },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
drawerState.apply {
|
||||
if (isClosed) open() else close()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { contentPadding ->
|
||||
Column (modifier = Modifier.padding(contentPadding)){
|
||||
`in`.sminnovations.attendanceapp.screens.home.ui.Home()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.navigationDrawer.viewModel
|
||||
|
||||
class navigationViewModel {
|
||||
}
|
||||
@@ -2,10 +2,10 @@ package `in`.sminnovations.attendanceapp.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Purple80 = Color(0xFFFF9800)
|
||||
val PurpleGrey80 = Color(0xFFFFCB7E)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Purple40 = Color(0xFFB86E02)
|
||||
val PurpleGrey40 = Color(0xFF865307)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -11,7 +11,7 @@ val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
|
||||
5
app/src/main/res/drawable/baseline_menu_24.xml
Normal file
5
app/src/main/res/drawable/baseline_menu_24.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
|
||||
|
||||
</vector>
|
||||
@@ -2,4 +2,5 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.jetbrains.kotlin.android) apply false
|
||||
id("com.google.dagger.hilt.android") version "2.51.1" apply false
|
||||
}
|
||||
@@ -8,6 +8,8 @@ espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.6"
|
||||
activityCompose = "1.9.2"
|
||||
composeBom = "2023.08.00"
|
||||
datastoreCoreAndroid = "1.1.1"
|
||||
datastorePreferencesCoreJvm = "1.1.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -24,6 +26,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-datastore-core-android = { group = "androidx.datastore", name = "datastore-core-android", version.ref = "datastoreCoreAndroid" }
|
||||
androidx-datastore-preferences-core-jvm = { group = "androidx.datastore", name = "datastore-preferences-core-jvm", version.ref = "datastorePreferencesCoreJvm" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user