diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/ChatHistory_schema_v2.xml b/.idea/ChatHistory_schema_v2.xml new file mode 100644 index 0000000..ed00cfe --- /dev/null +++ b/.idea/ChatHistory_schema_v2.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 0897082..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,6 +4,7 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 8978d23..b2c751a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index b1ceb26..03ffe14 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,8 @@ Event Tracker is a user-friendly app designed to help individuals discover, mana ## Technologies Used - **Kotlin:** For building a robust Android application with excellent performance and stability. - **Jetpack Compose:** To create modern, responsive, and visually appealing user interfaces. -- **Firebase:** For backend services such as authentication, real-time database management, and cloud storage. - - -## Technologies Used -- **Kotlin:** For robust Android application development, enhancing app stability and performance. -- **Jetpack Compose:** To design modern, reactive UIs that are responsive and intuitive. -- **Firebase:** For backend services such as authentication, database management, and cloud storage. - +- **SpringBoot:** For backend services such as authentication, real-time database management, and cloud storage. + link - https://github.com/yuvraj-coder1/EvenetTracker-Backend ## Demo-Video [https://youtu.be/i6nAK03j8w8?si=yLfDeuTRFHS_z03o](https://github.com/yuvraj-coder1/EventTracker/assets/142040957/377dbac7-74c4-4059-8006-6b2ed5f32989) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89a6f2d..7ccdcff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,8 @@ android { } dependencies { - + implementation(libs.retrofit) + implementation(libs.retrofit.gson) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -78,6 +79,12 @@ dependencies { implementation(libs.firebase.auth) implementation(libs.firebase.database) implementation(libs.firebase.firestore) + // Import the BoM for the Firebase platform + implementation(platform(libs.firebase.bom)) + + // Add the dependencies for the App Check libraries + // When using the BoM, you don't specify versions in Firebase library dependencies + implementation(libs.firebase.appcheck.playintegrity) //coil implementation(libs.coil.compose) @@ -90,5 +97,12 @@ dependencies { //serialization implementation(libs.jetbrains.kotlinx.serialization.json) + implementation ("com.google.code.gson:gson:2.10.1") + + + //encrypted Shared Prefernces + implementation (libs.androidx.security.crypto) + //http interceptor + implementation ("com.squareup.okhttp3:logging-interceptor:4.9.3") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 25c7b53..437bf51 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,10 @@ + + =3) { + utils.logOutUser.value = true + return null; + } + return TODO("Provide the return value") + } + fun responseCount(response:Response):Int { + var prev = response.priorResponse + var count=0; + while(prev!=null){ + count++; + prev = prev.priorResponse + if(count>3) + break; + } + return count; + } +} diff --git a/app/src/main/java/com/example/eventtracker/model/EventData.kt b/app/src/main/java/com/example/eventtracker/model/EventData.kt index d9da9f3..713f462 100644 --- a/app/src/main/java/com/example/eventtracker/model/EventData.kt +++ b/app/src/main/java/com/example/eventtracker/model/EventData.kt @@ -9,7 +9,9 @@ data class EventData( val description:String = "THis is the description of the event", val category:String = "Technical", val userId:String = "", - val eventId:String = "" + val eventId:String = "", + val eventLink:String ="", + val eventImageUrl:String ="" ) { fun doesMatchSearchQuery(query: String):Boolean { return name.contains(query, ignoreCase = true) diff --git a/app/src/main/java/com/example/eventtracker/model/GetEventResponse.kt b/app/src/main/java/com/example/eventtracker/model/GetEventResponse.kt new file mode 100644 index 0000000..117443d --- /dev/null +++ b/app/src/main/java/com/example/eventtracker/model/GetEventResponse.kt @@ -0,0 +1,9 @@ +package com.example.eventtracker.model + +import com.example.eventtracker.dto.EventDto + +data class GetEventResponse( + val success: Boolean, + val message: String, + val data: List? +) diff --git a/app/src/main/java/com/example/eventtracker/model/SignUpResponse.kt b/app/src/main/java/com/example/eventtracker/model/SignUpResponse.kt new file mode 100644 index 0000000..381360d --- /dev/null +++ b/app/src/main/java/com/example/eventtracker/model/SignUpResponse.kt @@ -0,0 +1,6 @@ +package com.example.eventtracker.model + +data class SignUpResponse( + val success: Boolean, + val message: String, +) diff --git a/app/src/main/java/com/example/eventtracker/model/UserLogInRequest.kt b/app/src/main/java/com/example/eventtracker/model/UserLogInRequest.kt new file mode 100644 index 0000000..8422bba --- /dev/null +++ b/app/src/main/java/com/example/eventtracker/model/UserLogInRequest.kt @@ -0,0 +1,6 @@ +package com.example.eventtracker.model + +data class UserLogInRequest ( + val username: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/eventtracker/model/UserLogInResponse.kt b/app/src/main/java/com/example/eventtracker/model/UserLogInResponse.kt new file mode 100644 index 0000000..4c2b09a --- /dev/null +++ b/app/src/main/java/com/example/eventtracker/model/UserLogInResponse.kt @@ -0,0 +1,17 @@ +package com.example.eventtracker.model + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializer + +data class UserLogInResponse( + val success: Boolean, + val message: String, + val data: LogInResponseData +) +data class LogInResponseData( + @SerializedName("access_token") + val jwt: String, + @SerializedName("refresh_token") + val refreshToken: String, + val userId:String +) diff --git a/app/src/main/java/com/example/eventtracker/model/UserSIgnUpRequest.kt b/app/src/main/java/com/example/eventtracker/model/UserSIgnUpRequest.kt new file mode 100644 index 0000000..751b099 --- /dev/null +++ b/app/src/main/java/com/example/eventtracker/model/UserSIgnUpRequest.kt @@ -0,0 +1,8 @@ +package com.example.eventtracker.model + +data class UserSIgnUpRequest( + val username: String, + val password: String, + val email: String, + val collegeId: String +) diff --git a/app/src/main/java/com/example/eventtracker/ui/home/EventDetailScreen.kt b/app/src/main/java/com/example/eventtracker/ui/home/EventDetailScreen.kt index 1795ce9..82d62ca 100644 --- a/app/src/main/java/com/example/eventtracker/ui/home/EventDetailScreen.kt +++ b/app/src/main/java/com/example/eventtracker/ui/home/EventDetailScreen.kt @@ -1,5 +1,8 @@ package com.example.eventtracker.ui.home +import android.content.Intent +import android.net.Uri +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -18,6 +21,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material3.Button @@ -29,25 +33,44 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text 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.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.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max +import androidx.core.content.ContextCompat.startActivity +import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import com.example.eventtracker.R import com.example.eventtracker.model.EventData import com.example.eventtracker.ui.theme.EventTrackerTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import androidx.core.net.toUri @Composable -fun EventDetailScreen(modifier: Modifier = Modifier, event: EventData) { +fun EventDetailScreen( + modifier: Modifier = Modifier, + event: EventData, + viewModel: HomeScreenViewModel = hiltViewModel() +) { - EventDetailScreenContent(modifier = Modifier.padding(), event = event) + EventDetailScreenContent(modifier = Modifier.padding(), event = event, viewModel = viewModel) } @OptIn(ExperimentalMaterial3Api::class) @@ -72,87 +95,139 @@ fun EventDetailScreenTopBar(modifier: Modifier = Modifier) { } @Composable -fun EventDetailScreenContent(modifier: Modifier = Modifier, event: EventData) { +fun EventDetailScreenContent( + modifier: Modifier = Modifier, + event: EventData, + viewModel: HomeScreenViewModel +) { + val context = LocalContext.current Column( modifier = modifier .verticalScroll(rememberScrollState()) ) { - AsyncImage( - model = event.image, - error = painterResource(id = R.drawable.default_image), - contentDescription = null, - modifier = Modifier.fillMaxWidth(), + AsyncImage( + model = event.eventImageUrl, + error = painterResource(id = R.drawable.default_image), + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + val bookmarked = viewModel.bookmarkedEvents.collectAsState() + val isBookmarked = bookmarked.value.any { it.eventId == event.eventId } + Text( + text = event.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge ) - Column( + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "${event.date}, ${event.time}", modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = event.name, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "${event.date}, ${event.time}", + ) + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + imageVector = Icons.Outlined.LocationOn, + contentDescription = "Event Location", + tint = Color.Black, modifier = Modifier - ) - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Icon( - imageVector = Icons.Outlined.LocationOn, - contentDescription = "Event Location", - tint = Color.Black, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color(176, 183, 192, 70)) - .padding(4.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(176, 183, 192, 70)) + .padding(4.dp) - ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = event.location, modifier = Modifier.weight(1f)) + } + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Button( + onClick = { + CoroutineScope(Dispatchers.IO).launch { + viewModel.onInterestedClicked(event, onSuccess = { + Toast.makeText( + context, + "Event Bookmarked", + Toast.LENGTH_SHORT + ).show() + }, onFailure = { + Toast.makeText( + context, + "Failed try again later", + Toast.LENGTH_SHORT + ).show() + }) + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors(Color(176, 183, 192, 70)), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = if (!isBookmarked) "Save" else "Saved", + color = Color.Black, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.width(8.dp)) + if (isBookmarked) + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Event Detail", + tint = Color.Black + ) + } - Spacer(modifier = Modifier.width(8.dp)) - Text(text = event.location, modifier = Modifier.weight(1f)) } - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(10.dp) + Button( + onClick = { + if (event.eventLink.isNotEmpty()) { + val link = if (event.eventLink.startsWith("http")) { + event.eventLink + } else { + "https://${event.eventlink}/" + } + val intent = Intent(Intent.ACTION_VIEW, link.toUri()) + startActivity( + context, + intent, + null + ) + } else { + Toast.makeText(context, "No link found", Toast.LENGTH_SHORT).show() + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors(Color(13, 125, 242)) ) { - Button( - onClick = { /*TODO*/ }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors(Color(176, 183, 192, 70)), - ) { - Text(text = "Save", color = Color.Black, fontWeight = FontWeight.SemiBold) - } - Button( - onClick = { /*TODO*/ }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors(Color(13, 125, 242)) - ) { - Text(text = "Attend", fontWeight = FontWeight.SemiBold) - } + Text(text = "Attend", fontWeight = FontWeight.SemiBold) } - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "About the Event", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = event.description) } + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "About the Event", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = event.description) } } +} - @Preview(showBackground = true, showSystemUi = true) - @Composable - fun EventDetailScreenPreview(modifier: Modifier = Modifier) { - EventTrackerTheme { - EventDetailScreen(event = EventData()) - } +@Preview(showBackground = true, showSystemUi = true) +@Composable +fun EventDetailScreenPreview(modifier: Modifier = Modifier) { + EventTrackerTheme { + EventDetailScreen(event = EventData()) } +} diff --git a/app/src/main/java/com/example/eventtracker/ui/home/HomeScreen.kt b/app/src/main/java/com/example/eventtracker/ui/home/HomeScreen.kt index 24b2741..88f1ddc 100644 --- a/app/src/main/java/com/example/eventtracker/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/example/eventtracker/ui/home/HomeScreen.kt @@ -1,6 +1,7 @@ package com.example.eventtracker.ui.home import android.util.Log +import android.widget.Toast import androidx.compose.animation.VectorConverter import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -80,11 +81,12 @@ fun HomeScreenTopBar( if (isSearchOn.value) { CenterAlignedTopAppBar( title = { - TextField(value = searchQuery.value, onValueChange = { - viewModel.updateSearchQuery( - it - ) - }, modifier = Modifier.fillMaxWidth(), + TextField( + value = searchQuery.value, onValueChange = { + viewModel.updateSearchQuery( + it + ) + }, modifier = Modifier.fillMaxWidth(), placeholder = { Text(text = "Search events") }, singleLine = true, trailingIcon = { @@ -127,6 +129,7 @@ fun HomeBody( viewModel: HomeScreenViewModel, onEventClick: (EventData) -> Unit ) { + Column(modifier = modifier.padding(horizontal = 16.dp)) { if (viewModel.isSearchOn.collectAsState().value) @@ -150,7 +153,8 @@ fun HomeBody( eventList = viewModel.eventList.collectAsState().value, onEventClick = onEventClick, onInterestedAction = viewModel::onInterestedClicked, - checkIfBookmarked = viewModel::checkIfBookmarked + checkIfBookmarked = viewModel::checkIfBookmarked, + viewModel = viewModel ) } @@ -162,8 +166,9 @@ fun EventList( modifier: Modifier = Modifier, eventList: List, onEventClick: (EventData) -> Unit, - onInterestedAction: suspend (EventData, Boolean) -> Boolean, - checkIfBookmarked: (event: EventData) -> Boolean + onInterestedAction: suspend (EventData, onSuccess: () -> Unit, onFailure: () -> Unit) -> Unit, + checkIfBookmarked: (event: EventData) -> Boolean, + viewModel: HomeScreenViewModel ) { LazyColumn(modifier = modifier) { itemsIndexed(eventList) { index, item -> @@ -180,7 +185,8 @@ fun EventList( onClickAction = onEventClick, event = item, onInterestedAction = onInterestedAction, - checkIfBookmarked = checkIfBookmarked + checkIfBookmarked = checkIfBookmarked, + viewModel = viewModel ) } } @@ -196,15 +202,19 @@ fun EventListItem( onClickAction: (EventData) -> Unit = {}, eventTime: String, eventLocation: String, - onInterestedAction: suspend (EventData, Boolean) -> Boolean, + onInterestedAction: suspend (EventData,() -> Unit, () -> Unit) -> Unit, event: EventData, + viewModel: HomeScreenViewModel, + checkIfBookmarked: (event: EventData) -> Boolean = { false } ) { - var isBookmarked by rememberSaveable { mutableStateOf(checkIfBookmarked(event)) } + val bookmarked = viewModel.bookmarkedEvents.collectAsState() + val isBookmarked = bookmarked.value.any { it.eventId == event.eventId } + val context = LocalContext.current // isBookmarked = checkIfBookmarked(event) Column(modifier = modifier.clickable { onClickAction(event) }) { AsyncImage( - model = eventImage, + model = event.eventImageUrl, error = painterResource(id = R.drawable.default_image), contentDescription = "Event Image", contentScale = ContentScale.Crop, @@ -224,17 +234,32 @@ fun EventListItem( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(text = eventLocation) + Text(text = eventLocation, modifier = Modifier.weight(0.7f)) Button( onClick = - { - CoroutineScope(Dispatchers.IO).launch { - isBookmarked = onInterestedAction(event, isBookmarked) - } + { + CoroutineScope(Dispatchers.IO).launch { + viewModel.onInterestedClicked( + event, + onSuccess = { + Toast.makeText( + context, + "Event Bookmarked", + Toast.LENGTH_SHORT + ).show() + }, onFailure = { + Toast.makeText( + context, + "Event UnBookmarked", + Toast.LENGTH_SHORT + ).show() + } + ) + } // Update state after Firebase task} // isBookmarked = onInterestedAction(event, isBookmarked) // Log.d("isBookmarked", isBookmarked.toString()) - }, + }, colors = ButtonDefaults.buttonColors( if (isBookmarked) @@ -244,8 +269,9 @@ fun EventListItem( 192, 70 ) else Color.Unspecified - ) - ) { + ), + + ) { Text( text = if (isBookmarked) "Unbookmark" else "Bookmark", diff --git a/app/src/main/java/com/example/eventtracker/ui/home/HomeScreenViewModel.kt b/app/src/main/java/com/example/eventtracker/ui/home/HomeScreenViewModel.kt index 68a6014..141f313 100644 --- a/app/src/main/java/com/example/eventtracker/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/com/example/eventtracker/ui/home/HomeScreenViewModel.kt @@ -1,18 +1,26 @@ package com.example.eventtracker.ui.home +import android.content.Context import android.util.Log +import android.widget.Toast import androidx.collection.emptyLongSet import androidx.compose.runtime.MutableState import androidx.compose.runtime.saveable.rememberSaveable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.eventtracker.data.event.EventApiService +import com.example.eventtracker.data.event.NetworkEventRepository +import com.example.eventtracker.dto.toEventData import com.example.eventtracker.model.EventData +import com.example.eventtracker.model.GetEventResponse import com.google.apphosting.datastore.testing.DatastoreTestTrace.FirestoreV1Action.Listen import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.ListenerRegistration import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -23,13 +31,17 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class HomeScreenViewModel @Inject constructor( private val db: FirebaseFirestore, - private val auth: FirebaseAuth + private val auth: FirebaseAuth, + private val eventRepository: NetworkEventRepository, + @ApplicationContext private val context: Context ) : ViewModel() { private val _isSearchOn = MutableStateFlow(false) val isSearchOn = _isSearchOn.asStateFlow() @@ -37,10 +49,12 @@ class HomeScreenViewModel @Inject constructor( val searchQuery = _searchQuery.asStateFlow() private val _isSearching = MutableStateFlow(false) val isSearching = _isSearching.asStateFlow() - var eventListener: ListenerRegistration? = null - var bookmarkEventListener: ListenerRegistration? = null - private val _bookmarkedEvents = MutableStateFlow(emptyList()) + private val _bookmarkedEventsId = MutableStateFlow>(emptyList()) + val bookmarkedEventsId = _bookmarkedEventsId.asStateFlow() + private val _bookmarkedEvents = MutableStateFlow>(emptyList()) val bookmarkedEvents = _bookmarkedEvents.asStateFlow() + private val _hostedEvents = MutableStateFlow>(emptyList()) + val hostedEvents = _hostedEvents.asStateFlow() fun updateSearchQuery(query: String) { _searchQuery.value = query @@ -72,147 +86,124 @@ class HomeScreenViewModel @Inject constructor( ) fun getEvents() { - eventListener = db.collection("events").addSnapshotListener { value, error -> - if (error != null) { - Log.d("TAG", "populateMessages: $error") - return@addSnapshotListener - } - val events = value?.toObjects(EventData::class.java) - Log.d("events fetched", events.toString()) - if (events != null) { - _eventList.value = events + viewModelScope.launch { + try { + val events = eventRepository.getEvents() + _eventList.value = events.data?.map { + it.toEventData() + }?:emptyList() +// _eventList.value = _allEvents.value + } catch (e: Exception) { + Toast.makeText(context, "Error getting events", Toast.LENGTH_SHORT).show() + Log.d("TAG", "getEvents: $e") } + } - bookmarkEventListener = auth.currentUser?.let { - db.collection("users") - .document(it.uid) - .collection("Bookmarked Events") - .addSnapshotListener { value, error -> - if (error != null) { - Log.d("TAG", "populateMessages: $error") - return@addSnapshotListener - } - val events = value?.toObjects(EventData::class.java) - if (events != null) { - _bookmarkedEvents.value = events - } + getBookmarkedEvents() + getUserEvents() - } + } + + fun getBookmarkedEvents() { + viewModelScope.launch(Dispatchers.IO) { + try { + val events = eventRepository.getBookmarkedEvents() + _bookmarkedEvents.value = events.data?.map { + it.toEventData() + }?:emptyList() + + } catch (e: Exception) { +// Toast.makeText(context, "Error getting bookmarked events", Toast.LENGTH_SHORT) +// .show() + Log.e("getbookmarkedEventsId", "getbookmarkedEventsId: $e") + } } } - override fun onCleared() { - super.onCleared() - eventListener?.remove() - bookmarkEventListener?.remove() + fun getUserEvents() { + viewModelScope.launch(Dispatchers.IO) { + try { + val events = eventRepository.getUserEvents() + _hostedEvents.value = events.data?.map { + it.toEventData() + }?:emptyList() + + } catch (e: Exception) { +// Toast.makeText(context, "Error getting events", Toast.LENGTH_SHORT).show() + Log.e("getEvents", "getEvents: $e") + } + } } - init { - getEvents() + fun onInterestedClicked(event: EventData, onSuccess: () -> Unit, onFailure: () -> Unit): Unit { + Log.d("event", event.eventId.toString()) + val isBookmarked = bookmarkedEventsId.value.any { it == event.eventId } + Log.d("isBookmarked", isBookmarked.toString()) + try { + if (isBookmarked) + unBookmarkEvent(event.eventId, { + Toast.makeText(context, "Event UnBookmarked", Toast.LENGTH_SHORT).show() + }, onFailure) + else + bookmarkEvent(event.eventId, onSuccess, onFailure) + } catch ( + e: Exception + ) { + Log.d("onInterestedClicked", "onInterestedClicked: $e") + onFailure() + } + Log.d("Bookmark List","${bookmarkedEventsId.value}") } -// suspend fun onInterestedClicked(event: EventData, isBookmarked: Boolean):Boolean { -// val currentUserId = auth.currentUser?.uid -// var wasSuccess:Boolean = false -// if (currentUserId != null) { -// if (!isBookmarked) { -// db.collection("users") -// .document(currentUserId) -// .collection("Bookmarked Events") -// .add(event) -// .addOnSuccessListener { -// Log.d("TAG", "onInterestedClicked: Success") -// wasSuccess = true -//// Log.d("TAG", "onInterestedClicked: $wasSuccess") -// } -// .addOnFailureListener { -// Log.d("TAG", "onInterestedClicked: Failure") -// } -// } else { -// db.collection("users") -// .document(currentUserId) -// .collection("Bookmarked Events").whereEqualTo("eventId", event.eventId) -// .get() -// .addOnSuccessListener { querySnapshot -> -// if (querySnapshot.documents.isNotEmpty()) { -// val documentId = querySnapshot.documents[0].id -// db.collection("users") -// .document(currentUserId) -// .collection("Bookmarked Events") -// .document(documentId) -// .delete() -// .addOnSuccessListener { -// Log.d("TAG", "Event Deleted Successful") -// wasSuccess = true -// } -// .addOnFailureListener { -// Log.d("TAG", "Event Deletion Failed") -// } -// } -// else { -// Log.d("TAG", "Could not find event") -// } -// } -// .addOnFailureListener { -// Log.d("TAG", "Could not find event") -// } -// } -// } -// Log.d("TAG", "wasSuccess: $wasSuccess") -// return if(wasSuccess) !isBookmarked else isBookmarked -// } - - suspend fun onInterestedClicked(event: EventData, isBookmarked: Boolean): Boolean { - val currentUserId = auth.currentUser?.uid - return if (currentUserId != null) { - if (!isBookmarked) { - try { - db.collection("users") - .document(currentUserId) - .collection("Bookmarked Events") - .add(event) - .await() // Use await() to suspend until the operation completes - Log.d("TAG", "onInterestedClicked: Success") - true // Return true if successful - } catch (e: Exception) { - Log.d("TAG", "onInterestedClicked: Failure", e) - false // Return false on failure + fun bookmarkEvent(eventId: String, onSuccess: () -> Unit, onFailure: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + val resp = eventRepository.bookmarkEvent(eventId) + withContext(Dispatchers.Main) { + if (resp.success) { + _bookmarkedEventsId.update { + it + eventId + } + onSuccess() + } else + onFailure() } - } else { - try { - val querySnapshot = db.collection("users") - .document(currentUserId) - .collection("Bookmarked Events") - .whereEqualTo("eventId", event.eventId) - .get() - .await() - if (querySnapshot.documents.isNotEmpty()) { - val documentId = querySnapshot.documents[0].id - db.collection("users") - .document(currentUserId) - .collection("Bookmarked Events") - .document(documentId) - .delete() - .await() - Log.d("TAG", "Event Deleted Successful") - false - } else { - Log.d("TAG", "Could not find event") - true} - } catch (e: Exception) { - Log.d("TAG", "Could not find event", e) - true + } catch (e: Exception) { + withContext(Dispatchers.Main) { + Log.e("bookmarkEvent", "Error: ${e.message}", e) + onFailure() } } - } else { - false // Return false if no user is logged in + } } + fun unBookmarkEvent(eventId: String, onSuccess: () -> Unit, onFailure: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + val resp = eventRepository.unBookmarkEvent(eventId) + withContext(Dispatchers.Main) { + if (resp.success) { + _bookmarkedEventsId.update { + it - eventId + } + onSuccess() + } else + onFailure() + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + Log.e("unBookmarkEvent", "Error: ${e.message}", e) + onFailure() + } + } + + } + } fun checkIfBookmarked(event: EventData): Boolean { - if(bookmarkedEvents.value.isEmpty()) + if (bookmarkedEventsId.value.isEmpty()) return false - return bookmarkedEvents.value.contains(event) + return bookmarkedEventsId.value.any { it == event.eventId } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/eventtracker/ui/navigation/EventTrackerApp.kt b/app/src/main/java/com/example/eventtracker/ui/navigation/EventTrackerApp.kt index 7ddf365..7fdc4e0 100644 --- a/app/src/main/java/com/example/eventtracker/ui/navigation/EventTrackerApp.kt +++ b/app/src/main/java/com/example/eventtracker/ui/navigation/EventTrackerApp.kt @@ -1,6 +1,10 @@ package com.example.eventtracker.ui.navigation +import android.content.SharedPreferences +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.navigation.NavController @@ -17,7 +21,10 @@ import com.example.eventtracker.ui.profile.ProfileScreen import com.example.eventtracker.ui.signIn.SignInScreen import com.example.eventtracker.ui.signIn.SignInViewModel import com.example.eventtracker.ui.userEventsScreen.UserEventsScreen +import com.example.eventtracker.utils +import retrofit2.http.Field +@RequiresApi(Build.VERSION_CODES.O) @Composable fun EventTrackerApp( modifier: Modifier = Modifier, @@ -26,10 +33,16 @@ fun EventTrackerApp( homeScreenViewModel: HomeScreenViewModel, signInViewModel: SignInViewModel, ) { + utils.logOutUser.observeForever { + if(it) { + signInViewModel.signOut() + navController.navigate(LogInScreen) + } + } val scope = rememberCoroutineScope() NavHost( navController = navController, - startDestination = LogInScreen, + startDestination = startScreen(signInViewModel), modifier = modifier ) { composable { @@ -46,9 +59,11 @@ fun EventTrackerApp( } composable { + homeScreenViewModel.getEvents() +// homeScreenViewModel.getBookmarkedEvents() +// homeScreenViewModel.getUserEvents() onBottomBarVisibilityChanged(true) com.example.eventtracker.ui.home.HomeScreen( - onEventClick = { navController.navigate( EventDetailsScreen( @@ -58,7 +73,10 @@ fun EventTrackerApp( location = it.location, time = it.time, image = it.image, - category = it.category + category = it.category, + eventLink = it.eventLink, + eventId = it.eventId, + eventImageUrl = it.eventImageUrl ) ) }, @@ -75,10 +93,14 @@ fun EventTrackerApp( location = args.location, time = args.time, image = args.image, - category = args.category + category = args.category, + eventLink = args.eventLink, + eventId = args.eventId, + eventImageUrl = args.eventImageUrl ) EventDetailScreen( event = event, + viewModel = homeScreenViewModel ) } composable { @@ -105,12 +127,22 @@ fun EventTrackerApp( location = it.location, time = it.time, image = it.image, - category = it.category + category = it.category, + eventLink = it.eventLink, + eventId = it.eventId ) ) }, + bookmarkedEvents = homeScreenViewModel.bookmarkedEvents.collectAsState().value, + hostedEvents = homeScreenViewModel.hostedEvents.collectAsState().value ) } } +} +fun startScreen(signInViewModel: SignInViewModel): Any { + if(signInViewModel.isUserLoggedIn()) + return HomeScreen + else + return LogInScreen } \ No newline at end of file diff --git a/app/src/main/java/com/example/eventtracker/ui/navigation/Routes.kt b/app/src/main/java/com/example/eventtracker/ui/navigation/Routes.kt index 9a973c0..45f4b78 100644 --- a/app/src/main/java/com/example/eventtracker/ui/navigation/Routes.kt +++ b/app/src/main/java/com/example/eventtracker/ui/navigation/Routes.kt @@ -1,5 +1,6 @@ package com.example.eventtracker.ui.navigation +import android.annotation.SuppressLint import kotlinx.serialization.Serializable import com.example.eventtracker.model.EventData import kotlinx.serialization.Contextual @@ -7,6 +8,7 @@ import kotlinx.serialization.Contextual @Serializable data object HomeScreen +@SuppressLint("UnsafeOptInUsageError") @Serializable data class EventDetailsScreen( val name:String="", @@ -16,6 +18,9 @@ data class EventDetailsScreen( val location:String = "", val description:String = "", val category:String = "", + val eventLink:String = "", + val eventId:String = "", + val eventImageUrl:String = "" ) @Serializable diff --git a/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventScreen.kt b/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventScreen.kt index 5ea44c7..bd9c641 100644 --- a/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventScreen.kt +++ b/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventScreen.kt @@ -81,7 +81,6 @@ fun PostNewEventScreen(modifier: Modifier = Modifier, getEvents: () -> Unit) { modifier = Modifier.padding(), uiState = uiState, viewModel = viewModel, - getEvents = getEvents ) } @@ -104,7 +103,6 @@ fun PostNewEventBody( modifier: Modifier = Modifier, viewModel: PostNewEventViewModel = viewModel(), uiState: PostNewEventUiState, - getEvents: () -> Unit = {} ) { var selectedImageUri by remember { mutableStateOf(null) } @@ -173,6 +171,24 @@ fun PostNewEventBody( singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next) ) + OutlinedTextField( + value = uiState.eventLink, + onValueChange = { viewModel.updateEventLink(it) }, + shape = RoundedCornerShape(8.dp), + label = { Text("Event Register Link", color = Color.Gray) }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.Black, + unfocusedBorderColor = Color.Transparent, + unfocusedContainerColor = Color(176, 183, 192, 70), + focusedContainerColor = Color(176, 183, 192, 70), + + ), + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next) + ) var date by rememberSaveable { mutableStateOf("") } date = pickDate() viewModel.updateEventDate(date) @@ -233,7 +249,15 @@ fun PostNewEventBody( "Event added successfully", Toast.LENGTH_SHORT ).show() - }) + }, + onFail = { + Toast.makeText( + context, + "Event not added Please try again", + Toast.LENGTH_SHORT + ).show() + } + ) }, modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.small, diff --git a/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventViewModel.kt b/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventViewModel.kt index 9e8aa9d..d19e5ff 100644 --- a/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventViewModel.kt +++ b/app/src/main/java/com/example/eventtracker/ui/postNewEvent/PostNewEventViewModel.kt @@ -1,6 +1,7 @@ package com.example.eventtracker.ui.postNewEvent import android.content.ContentValues.TAG +import android.content.Context import android.net.Uri import android.util.Log import androidx.compose.runtime.getValue @@ -8,6 +9,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.eventtracker.data.event.NetworkEventRepository +import com.example.eventtracker.dto.CreateEventRequest +import com.example.eventtracker.dto.EventDto import com.example.eventtracker.model.EventData import com.google.android.gms.tasks.Tasks import com.google.firebase.Firebase @@ -15,6 +20,7 @@ import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.storage.FirebaseStorage import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -22,18 +28,25 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody import javax.inject.Inject @HiltViewModel class PostNewEventViewModel @Inject constructor( private val db: FirebaseFirestore, private val storage: FirebaseStorage, - private val auth: FirebaseAuth + private val auth: FirebaseAuth, + private val eventRepository: NetworkEventRepository, + @ApplicationContext private val context: Context ) : ViewModel() { private val _uiState = MutableStateFlow(PostNewEventUiState()) val uiState: StateFlow = _uiState.asStateFlow() var uri by mutableStateOf(null) - + val initialUiState = PostNewEventUiState() fun updateEventName(eventName: String) { _uiState.value = _uiState.value.copy(eventName = eventName) } @@ -62,6 +75,10 @@ class PostNewEventViewModel @Inject constructor( _uiState.value = _uiState.value.copy(id = id) } + fun updateEventLink(eventLink: String) { + _uiState.value = _uiState.value.copy(eventLink = eventLink) + } + private suspend fun uploadEventImage(eventImageUri: Uri?): String { val storageRef = storage.reference.child("images/${_uiState.value.id}") if (eventImageUri != null) { @@ -78,50 +95,66 @@ class PostNewEventViewModel @Inject constructor( return "" } - fun addEventToDatabase(onSuccess: () -> Unit) { - val id = db.collection("events").document().id - updateId(id) - CoroutineScope(Dispatchers.Main).launch { - val url = uploadEventImage(uri) - _uiState.value = _uiState.value.copy(eventImage = url) - val event = EventData( - name = _uiState.value.eventName, - description = _uiState.value.eventDescription, - category = _uiState.value.eventCategory, - date = _uiState.value.eventDate, - time = _uiState.value.eventTime, - location = _uiState.value.location, - image = _uiState.value.eventImage, - userId = auth.currentUser?.uid.toString(), - eventId = id - ) - Log.d("url update", "addEventToDatabase: $url") - db.collection("events").document(id).set(event).addOnSuccessListener { - onSuccess() - Log.d(TAG, "addEventToDatabase: Suceess") - updateEventName("") - updateEventDescription("") - updateEventCategory("") - updateEventDate("") - updateEventTime("") - updateLocation("") - uri = null - } - .addOnFailureListener { - Log.d(TAG, "addEventToDatabase: Failed To Add Event`") + fun addEventToDatabase(onSuccess: () -> Unit, onFail: () -> Unit = {}) { + val namePart = _uiState.value.eventName.toString().toPart() + val descriptionPart = _uiState.value.eventDescription.toString().toPart() + val categoryPart = _uiState.value.eventCategory.toString().toPart() + val datePart = _uiState.value.eventDate.toString().toPart() + val timePart = _uiState.value.eventTime.toString().toPart() + val locationPart = _uiState.value.location.toString().toPart() + val imageLinkPart = _uiState.value.eventLink.toString().toPart() + val image: MultipartBody.Part? = uri?.let {uriToMultiPartFile(context = context, uri = uri!!, partname = "image")} + viewModelScope.launch(Dispatchers.IO) { + try { + val resp = eventRepository.createEvent( + namePart = namePart, + descriptionPart = descriptionPart, + datePart = datePart, + timePart = timePart, + locationPart = locationPart, + imagePart = image, + categoryPart = categoryPart, + eventLinkPart = imageLinkPart + ) + withContext(context = Dispatchers.Main) { + if (resp.success) { + onSuccess() + _uiState.value = initialUiState + uri = null + } else { + onFail() + } } - val userCollection = db.collection("users") - val userId = auth.currentUser?.uid - val userDocument = userId?.let { userCollection.document(it) } - val nestedCollection = userDocument?.collection("Hosted Events") - nestedCollection?.add(event)?.addOnSuccessListener { documentReference -> - Log.d(TAG, "addEventToDatabase: Hosted Event Added ${documentReference}") - } - ?.addOnFailureListener { - Log.d(TAG, "addEventToDatabase: Failed To Add Hosted Event") + } catch (exception: Exception) { + withContext(context = Dispatchers.Main) { + onFail() } + Log.e("addEventToDatabase", "addEventToDatabase: $exception") + } } } + + fun uriToMultiPartFile( + context: Context, + uri: Uri, + partname: String + ): MultipartBody.Part { + val inputStream = context.contentResolver.openInputStream(uri) + ?: throw IllegalArgumentException("Unable to open URI:$uri") + val bytes = inputStream.readBytes() + inputStream.close() + val mimeType = context.contentResolver.getType(uri) + ?: "application/octet-stream" + val requestFile = bytes.toRequestBody(mimeType.toMediaTypeOrNull()) + val filename = "${System.currentTimeMillis()}.jpg" + return MultipartBody.Part.createFormData( + partname, + filename, + requestFile + ) + } + fun String.toPart() = + toRequestBody("text/plain".toMediaType()) } data class PostNewEventUiState( @@ -133,4 +166,5 @@ data class PostNewEventUiState( val eventTime: String = "", val location: String = "", val eventImage: String = "", + val eventLink: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreen.kt b/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreen.kt index 6a943f0..76d5c17 100644 --- a/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreen.kt +++ b/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreen.kt @@ -65,7 +65,6 @@ fun ProfileScreen(modifier: Modifier = Modifier,navigateToLogin: () -> Unit) { fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleLarge ) - Spacer(modifier = Modifier.height(8.dp)) Text( text = "USN - ${viewModel.userUsn.collectAsState().value}", @@ -74,13 +73,7 @@ fun ProfileScreen(modifier: Modifier = Modifier,navigateToLogin: () -> Unit) { ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Branch - ISE", - style = MaterialTheme.typography.bodyLarge, - color = Color.Gray - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "Section - E", + text = "email - ${viewModel.userEmail.collectAsState().value}", style = MaterialTheme.typography.bodyLarge, color = Color.Gray ) @@ -116,12 +109,6 @@ fun EventList(modifier: Modifier = Modifier, events: List) { EventItem(event = event) } } -// EventItem(eventName = "Introduction to Android Development") -// EventItem(eventName = "Introduction to Android Development") -// EventItem(eventName = "Introduction to Android Development") -// EventItem(eventName = "Introduction to Android Development") -// EventItem(eventName = "Introduction to Android Development") - } @Composable diff --git a/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreenViewModel.kt b/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreenViewModel.kt index 9a64f71..75f0377 100644 --- a/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreenViewModel.kt +++ b/app/src/main/java/com/example/eventtracker/ui/profile/ProfileScreenViewModel.kt @@ -1,22 +1,27 @@ package com.example.eventtracker.ui.profile +import android.content.SharedPreferences import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.eventtracker.data.login.NetworkLogInRepository import com.example.eventtracker.model.EventData import com.example.eventtracker.model.UserData import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.ListenerRegistration import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import javax.inject.Inject @HiltViewModel class ProfileScreenViewModel @Inject constructor( - private val auth: FirebaseAuth, - private val db: FirebaseFirestore + private val loginRepository: NetworkLogInRepository, + private val sharedPreferences: SharedPreferences ) : ViewModel() { val currentUser = UserData() private val _userName = MutableStateFlow(currentUser.name) @@ -27,36 +32,22 @@ class ProfileScreenViewModel @Inject constructor( val userUsn = _userUsn.asStateFlow() private val _userEvents = MutableStateFlow(listOf()) val userEvents = _userEvents.asStateFlow() - var eventListener: ListenerRegistration? = null init { - db.collection("users") - .document(auth.currentUser!!.uid) - .get() - .addOnSuccessListener { - _userName.value = it.get("name") as String - _userEmail.value = it.get("email") as String - _userUsn.value = it.get("collegeId") as String - } - val user = auth.currentUser - if (user != null) { - eventListener = db.collection("users") - .document(user.uid) - .collection("Hosted Events") - .addSnapshotListener { value, error -> - if (error != null) { - return@addSnapshotListener - } - if (value != null) { - val events = value.toObjects(EventData::class.java) - _userEvents.value = events - } + viewModelScope.launch(Dispatchers.IO) { + try { + val userid = sharedPreferences.getString("userId", null) + ?: throw IllegalStateException("User ID not found in SharedPreferences") + Log.d("ProfileScreenViewModel", "User ID: $userid") + loginRepository.getUserById(userid).let { + _userName.value = it.username + _userEmail.value = it.email + _userUsn.value = it.collegeId } - + } catch (e: Exception) { + Log.e("ProfileScreenViewModel", "Error getting user data: ${e.message}", e) + } } + } - fun logOut() { - auth.signOut() - Log.d("TAG", auth.currentUser.toString()) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/eventtracker/ui/signIn/SignInScreen.kt b/app/src/main/java/com/example/eventtracker/ui/signIn/SignInScreen.kt index 6727553..12481ae 100644 --- a/app/src/main/java/com/example/eventtracker/ui/signIn/SignInScreen.kt +++ b/app/src/main/java/com/example/eventtracker/ui/signIn/SignInScreen.kt @@ -1,7 +1,9 @@ package com.example.eventtracker.ui.signIn +import android.os.Build import android.util.Log import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -55,6 +57,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.example.eventtracker.R import com.example.eventtracker.ui.theme.EventTrackerTheme +@RequiresApi(Build.VERSION_CODES.O) @Composable fun SignInScreen( modifier: Modifier = Modifier, @@ -64,10 +67,10 @@ fun SignInScreen( navigateToSignIn: () -> Unit = {}, ) { LaunchedEffect(Unit) { - if(viewModel.checkIfLoggedIn()){ - navigateToHome() - Log.d("TAG", "SignInScreen: ${viewModel.checkIfLoggedIn()}") - } +// if(viewModel.checkIfLoggedIn()){ +// navigateToHome() +// Log.d("TAG", "SignInScreen: ${viewModel.checkIfLoggedIn()}") +// } } val context = LocalContext.current @@ -77,8 +80,8 @@ fun SignInScreen( modifier = Modifier.padding(16.dp), onSignIn = { - if (uiState.email.isEmpty() || uiState.password.isEmpty()) { - Toast.makeText(context, "Please enter email and password", Toast.LENGTH_SHORT) + if (uiState.username.isEmpty() || uiState.password.isEmpty()) { + Toast.makeText(context, "Please enter username and password", Toast.LENGTH_SHORT) .show() } else { viewModel.signIn( @@ -210,13 +213,13 @@ fun SignInScreenContent( Spacer(modifier = Modifier.height(10.dp)) } OutlinedTextField( - value = uiState.email, - onValueChange = { viewModel.updateEmail(it) }, - label = { Text(text = "Email") }, + value = uiState.username, + onValueChange = { viewModel.updateName(it) }, + label = { Text(text = "Username") }, leadingIcon = { Icon( - imageVector = Icons.Outlined.Mail, - contentDescription = "email" + imageVector = Icons.Outlined.Person, + contentDescription = "username" ) }, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/example/eventtracker/ui/signIn/SignInViewModel.kt b/app/src/main/java/com/example/eventtracker/ui/signIn/SignInViewModel.kt index ce8d6f6..57c0602 100644 --- a/app/src/main/java/com/example/eventtracker/ui/signIn/SignInViewModel.kt +++ b/app/src/main/java/com/example/eventtracker/ui/signIn/SignInViewModel.kt @@ -1,11 +1,20 @@ package com.example.eventtracker.ui.signIn +import android.content.SharedPreferences +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.security.crypto.EncryptedSharedPreferences +import com.example.eventtracker.data.login.NetworkLogInRepository +import com.example.eventtracker.model.SignUpResponse import com.example.eventtracker.model.UserData +import com.example.eventtracker.model.UserLogInResponse import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore import dagger.hilt.android.lifecycle.HiltViewModel @@ -13,94 +22,109 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject +import androidx.core.content.edit @HiltViewModel class SignInViewModel @Inject constructor( - private val auth: FirebaseAuth, - private val db: FirebaseFirestore -) :ViewModel() { + private val loginRepository: NetworkLogInRepository, + private val encryptedSharedPreferences: SharedPreferences +) : ViewModel() { private val _uiState = MutableStateFlow(SignInUiState()) - val uiState:StateFlow = _uiState.asStateFlow() + val uiState: StateFlow = _uiState.asStateFlow() var inProcess by mutableStateOf(false) - - var currentUser = auth.currentUser - fun updateEmail(email:String){ + fun updateEmail(email: String) { _uiState.value = _uiState.value.copy(email = email) } - fun updatePassword(password:String){ + + fun updatePassword(password: String) { _uiState.value = _uiState.value.copy(password = password) } - fun updateConfirmPassword(confirmPassword:String){ + + fun updateConfirmPassword(confirmPassword: String) { _uiState.value = _uiState.value.copy(confirmPassword = confirmPassword) } - fun updateName(name:String){ + + fun updateName(name: String) { _uiState.value = _uiState.value.copy(username = name) } - fun updateIsSignIn(isSignIn:Boolean){ + + fun updateIsSignIn(isSignIn: Boolean) { _uiState.value = _uiState.value.copy(isSignIn = isSignIn) } - fun updateCollegeId(collegeId:String){ + + fun updateCollegeId(collegeId: String) { _uiState.value = _uiState.value.copy(collegeId = collegeId) } - fun signIn( - onSuccess:()->Unit, - onFailure:()->Unit - ){ - inProcess = true - auth.signInWithEmailAndPassword(uiState.value.email,uiState.value.password) - .addOnSuccessListener { - currentUser = auth.currentUser - inProcess = false - onSuccess() - } - .addOnFailureListener{ - inProcess = false + fun signIn( + onSuccess: () -> Unit, + onFailure: () -> Unit + ) { + viewModelScope.launch { + inProcess = true + try { + val response: UserLogInResponse = loginRepository.signInUser( + username = uiState.value.username, + password = uiState.value.password + ) + if (response.success) { + encryptedSharedPreferences.edit() { putString("jwt", response.data.jwt) } + encryptedSharedPreferences.edit() { putString("refreshToken", response.data.refreshToken) } + encryptedSharedPreferences.edit() { putString("userId", response.data.userId) } + Log.d("jwt", "signIn: ${response.data.jwt}") + onSuccess() + } else { + onFailure() + } + } catch ( + e: Exception + ) { + Log.e("SignIn", e.message.toString()) onFailure() + } finally { + inProcess = false } + } } + + @RequiresApi(Build.VERSION_CODES.O) fun signUp( - onSuccess:()->Unit, - onFailure:()->Unit - ){ - if(uiState.value.password != uiState.value.confirmPassword) + onSuccess: () -> Unit, + onFailure: () -> Unit + ) { + if (uiState.value.password != uiState.value.confirmPassword) return - auth.createUserWithEmailAndPassword(uiState.value.email,uiState.value.password) - .addOnSuccessListener {result -> - val user: UserData? = result.user?.uid?.let { - UserData( - name = uiState.value.username, - email = uiState.value.email, - collegeId = uiState.value.collegeId, - password = uiState.value.password, - id = it, - ) - } - if (user != null) { - db.collection("users").document(user.id).set(user) - .addOnSuccessListener { - onSuccess() - Log.d("Sign", "onSignUp: Success") - _uiState.update { it.copy(isSignIn = true) } - } - .addOnFailureListener { - onFailure() - Log.d("Sign", "onSignUp: ${it.message}") - } + viewModelScope.launch { + inProcess = true + try { + val response: SignUpResponse = loginRepository.signUpUser( + username = uiState.value.username, + password = uiState.value.password, + collegeId = uiState.value.collegeId, + email = uiState.value.email + ) + if (response.success) { + onSuccess() + } else { + onFailure() } - } - .addOnFailureListener { + } catch (e: Exception) { + Log.e("SignUp", e.message.toString()) onFailure() - Log.d("Sign", "onSignUp: ${it.message}") + } finally { + inProcess = false } + + } } - fun checkIfLoggedIn():Boolean { - return currentUser != null + fun signOut() { + encryptedSharedPreferences.edit() { putString("jwt", null) } + encryptedSharedPreferences.edit() { putString("refreshToken", null) } + encryptedSharedPreferences.edit() { putString("userId", null) } } - - fun signOut(){ - auth.signOut() - currentUser = null + fun isUserLoggedIn(): Boolean { + return !encryptedSharedPreferences.getString("refreshToken",null).isNullOrEmpty() } } diff --git a/app/src/main/java/com/example/eventtracker/ui/theme/Theme.kt b/app/src/main/java/com/example/eventtracker/ui/theme/Theme.kt index 06ada97..1a1ff82 100644 --- a/app/src/main/java/com/example/eventtracker/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/eventtracker/ui/theme/Theme.kt @@ -35,7 +35,7 @@ private val LightColorScheme = lightColorScheme( @Composable fun EventTrackerTheme( - darkTheme: Boolean = isSystemInDarkTheme(), + darkTheme: Boolean = false, // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit diff --git a/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreen.kt b/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreen.kt index 6062786..caa6e5b 100644 --- a/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreen.kt +++ b/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreen.kt @@ -43,30 +43,38 @@ import com.example.eventtracker.R import com.example.eventtracker.model.EventData @Composable -fun UserEventsScreen(modifier: Modifier = Modifier,onEventClick: (EventData) -> Unit) { +fun UserEventsScreen( + modifier: Modifier = Modifier, + onEventClick: (EventData) -> Unit, + bookmarkedEvents: List = emptyList(), + hostedEvents: List = emptyList() +) { val viewModel: UserEventsScreenViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsState() Column( modifier = modifier, - ) { + ) { EventsToShow( isBookmarksSelected = uiState.isBookmarksSelected, onClick = { viewModel.updateIsBookmarkedSelected(it) }) Spacer(modifier = Modifier.height(16.dp)) EventList( eventList = if (uiState.isBookmarksSelected) - uiState.bookmarkedEvents else - uiState.hostedEvents, + bookmarkedEvents else + hostedEvents, onEventClick = onEventClick ) } } @Composable -fun EventList(modifier: Modifier = Modifier, eventList: List = emptyList(),onEventClick: (EventData) -> Unit) { +fun EventList( + modifier: Modifier = Modifier, + eventList: List = emptyList(), + onEventClick: (EventData) -> Unit +) { LazyColumn { - itemsIndexed(eventList) { - _, item -> + itemsIndexed(eventList) { _, item -> EventListItem( eventTitle = item.name, eventImage = item.image, @@ -89,7 +97,12 @@ fun EventListItem( onEventClick: (EventData) -> Unit = {}, event: EventData ) { - Row(modifier = modifier.padding(12.dp).clickable { onEventClick(event) }, verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = modifier + .padding(12.dp) + .clickable { onEventClick(event) }, + verticalAlignment = Alignment.CenterVertically + ) { AsyncImage( model = eventImage, error = painterResource(id = R.drawable.default_image), @@ -125,8 +138,20 @@ fun EventsToShow( ) { val bookmarkWeight by animateFloatAsState(if (isBookmarksSelected) 1.4f else 1f) val hostedWeight by animateFloatAsState(if (isBookmarksSelected) 1f else 1.4f) - val bookmarkColor by animateColorAsState(if (isBookmarksSelected) Color.White else Color(240, 242, 245)) - val hostedColor by animateColorAsState(if (!isBookmarksSelected) Color.White else Color(240, 242, 245)) + val bookmarkColor by animateColorAsState( + if (isBookmarksSelected) Color.White else Color( + 240, + 242, + 245 + ) + ) + val hostedColor by animateColorAsState( + if (!isBookmarksSelected) Color.White else Color( + 240, + 242, + 245 + ) + ) val bookmarkElevation by animateDpAsState(if (isBookmarksSelected) 4.dp else 0.dp) val hostedElevation by animateDpAsState(if (!isBookmarksSelected) 4.dp else 0.dp) val bookmarkTextColor by animateColorAsState(if (isBookmarksSelected) Color.Black else Color.Gray) diff --git a/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreenViewModel.kt b/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreenViewModel.kt index 8e0b446..006e3c4 100644 --- a/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreenViewModel.kt +++ b/app/src/main/java/com/example/eventtracker/ui/userEventsScreen/UserEventsScreenViewModel.kt @@ -20,58 +20,7 @@ class UserEventsScreenViewModel @Inject constructor( ) : ViewModel() { private val _uiState = MutableStateFlow(UserEventsScreenUiState()) val uiState: StateFlow = _uiState.asStateFlow() - private var hostedEventListener: ListenerRegistration?=null - private var bookmarkedEventListener: ListenerRegistration?=null fun updateIsBookmarkedSelected(isBookmarkedSelected:Boolean) { _uiState.value = _uiState.value.copy(isBookmarksSelected = isBookmarkedSelected) } - private fun fetchEvents() { - val user = auth.currentUser - if (user != null) { - val userId = user.uid - hostedEventListener = db.collection("users") - .document(userId) - .collection("Hosted Events") - .addSnapshotListener{ - snapshot, error -> - if (error != null) { - // Handle error - Log.d("TAG", "populateMessages: $error") - return@addSnapshotListener - } - if (snapshot != null && !snapshot.isEmpty) { - val hostedEvents = snapshot.toObjects(EventData::class.java) - _uiState.value = _uiState.value.copy(hostedEvents = hostedEvents) - } - - } - bookmarkedEventListener = db.collection("users") - .document(userId) - .collection("Bookmarked Events") - .addSnapshotListener { - snapshot, error -> - if (error != null) { - // Handle error - Log.d("TAG", "populateMessages: $error") - return@addSnapshotListener - } - if (snapshot != null && !snapshot.isEmpty) { - val bookmarkedEvents = snapshot.toObjects(EventData::class.java) - _uiState.value = _uiState.value.copy(bookmarkedEvents = bookmarkedEvents) - } - else { - _uiState.value = _uiState.value.copy(bookmarkedEvents = emptyList()) - } - } - } - } - override fun onCleared() { - super.onCleared() - hostedEventListener?.remove() - bookmarkedEventListener?.remove() - } - init { - fetchEvents() - } - } \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..4ae7d12 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..4ae7d12 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..84c01bc 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp new file mode 100644 index 0000000..1ff6a35 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..9858321 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..3796629 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..253c471 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp new file mode 100644 index 0000000..2c627e1 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..74222d1 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..5308606 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..e1612dd 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp new file mode 100644 index 0000000..6634b11 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a536412 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..c0691ca 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..bec15cc 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000..b470432 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..875f565 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..8fa657d 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..a33241f 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp new file mode 100644 index 0000000..7c55a19 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..78f6c03 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..ca250ce 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a373675..fbfd9d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.5.0" coilCompose = "2.5.0" +firebaseBom = "33.1.1" generativeai = "0.7.0" kotlin = "1.9.0" coreKtx = "1.13.1" @@ -25,12 +26,18 @@ lifecycleViewmodelCompose = "2.8.1" firebaseStorageKtx = "21.0.0" secretsGradlePlugin = "2.0.1" composePreviewRenderer = "0.0.1-alpha01" +securityCrypto = "1.1.0-alpha07" uiTextGoogleFonts = "1.6.7" - +retrofit="2.9.0" +retrofit-gson="2.9.0" +retrofit2KotlinxSerializationConverter = "1.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-material-icons-extended-android = { module = "androidx.compose.material:material-icons-extended-android", version.ref = "materialIconsExtendedAndroid" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } +firebase-appcheck-playintegrity = { module = "com.google.firebase:firebase-appcheck-playintegrity" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } generativeai = { module = "com.google.ai.client.generativeai:generativeai", version.ref = "generativeai" } jetbrains-kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -60,6 +67,9 @@ firebase-storage-ktx = { group = "com.google.firebase", name = "firebase-storage secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } compose-preview-renderer = { group = "com.android.tools.compose", name = "compose-preview-renderer", version.ref = "composePreviewRenderer" } androidx-ui-text-google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts", version.ref = "uiTextGoogleFonts" } +retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit-gson" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }