From c33becdf29f0476cf55335846105191f8d058748 Mon Sep 17 00:00:00 2001 From: JustAGhost23 Date: Fri, 6 Jan 2023 15:47:22 +0530 Subject: [PATCH] Admin can now add attachments to Announcements using Firebase Storage --- .idea/deploymentTargetDropDown.xml | 17 ++ .idea/dictionaries/jasonag23.xml | 3 + .idea/gradle.xml | 1 - app/build.gradle | 12 +- .../java/com/example/cmsclonelite/Course.kt | 7 +- .../java/com/example/cmsclonelite/Screen.kt | 1 + .../cmsclonelite/graphs/RootNavGraph.kt | 7 + .../repository/CourseRepository.kt | 55 +++++- .../cmsclonelite/screens/AboutScreen.kt | 2 +- .../screens/AddAnnouncementsScreen.kt | 140 +++++++++++++++- .../screens/AnnouncementsScreen.kt | 42 ++++- .../screens/CourseDetailsScreen.kt | 5 +- .../screens/DetailedAnnouncement.kt | 158 ++++++++++++++++++ .../screens/EditCourseDetailsScreen.kt | 23 ++- .../screens/EnrolledCourseDetailsScreen.kt | 2 +- .../viewmodels/AnnouncementsViewModel.kt | 64 ++++++- app/src/main/res/values/themes.xml | 1 + build.gradle | 12 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 19 files changed, 514 insertions(+), 40 deletions(-) create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/dictionaries/jasonag23.xml create mode 100644 app/src/main/java/com/example/cmsclonelite/screens/DetailedAnnouncement.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..059878c --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/jasonag23.xml b/.idea/dictionaries/jasonag23.xml new file mode 100644 index 0000000..d0a768b --- /dev/null +++ b/.idea/dictionaries/jasonag23.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index e9969a1..7b46144 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -14,7 +14,6 @@ - diff --git a/app/build.gradle b/app/build.gradle index d92b054..0b305b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,12 +6,12 @@ plugins { } android { - compileSdk 32 + compileSdk 33 defaultConfig { applicationId "com.example.cmsclonelite" minSdk 21 - targetSdk 32 + targetSdk 33 versionCode 1 versionName "1.0" @@ -47,11 +47,19 @@ android { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } + configurations { + all { + exclude module: 'httpclient' + exclude module: 'commons-logging' + } + } } dependencies { def nav_version = "2.5.2" + implementation platform('com.google.firebase:firebase-bom:31.1.1') + implementation 'com.google.firebase:firebase-storage-ktx' implementation 'com.google.firebase:firebase-messaging-ktx:23.0.8' implementation 'com.google.firebase:firebase-firestore-ktx:24.3.1' implementation 'com.google.firebase:firebase-auth-ktx:21.0.8' diff --git a/app/src/main/java/com/example/cmsclonelite/Course.kt b/app/src/main/java/com/example/cmsclonelite/Course.kt index aef9480..8b0d888 100644 --- a/app/src/main/java/com/example/cmsclonelite/Course.kt +++ b/app/src/main/java/com/example/cmsclonelite/Course.kt @@ -1,6 +1,9 @@ package com.example.cmsclonelite +import android.net.Uri import android.os.Parcelable +import com.google.firebase.storage.StorageMetadata +import com.google.firebase.storage.ktx.storageMetadata import kotlinx.parcelize.Parcelize import kotlinx.parcelize.RawValue import java.util.* @@ -20,5 +23,7 @@ data class Course ( @Parcelize data class Announcement ( var title: String? = null, - var body: String? = null + var body: String? = null, + var fileName: String = "", + var downloadUri: Uri? = null, ): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/example/cmsclonelite/Screen.kt b/app/src/main/java/com/example/cmsclonelite/Screen.kt index dfa7f37..058c815 100644 --- a/app/src/main/java/com/example/cmsclonelite/Screen.kt +++ b/app/src/main/java/com/example/cmsclonelite/Screen.kt @@ -10,4 +10,5 @@ sealed class Screen(val route: String) { object EditCourseDetails: Screen(route = "editCourseDetailsScreen") object Announcements: Screen(route = "announcementsScreen") object AddAnnouncements: Screen(route = "addAnnouncementsScreen") + object DetailedAnnouncement: Screen(route = "detailedAnnouncement") } \ No newline at end of file diff --git a/app/src/main/java/com/example/cmsclonelite/graphs/RootNavGraph.kt b/app/src/main/java/com/example/cmsclonelite/graphs/RootNavGraph.kt index 9d43425..df479da 100644 --- a/app/src/main/java/com/example/cmsclonelite/graphs/RootNavGraph.kt +++ b/app/src/main/java/com/example/cmsclonelite/graphs/RootNavGraph.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.example.cmsclonelite.Announcement import com.example.cmsclonelite.Course import com.example.cmsclonelite.Screen import com.example.cmsclonelite.screens.* @@ -65,5 +66,11 @@ fun SetupNavGraph( AddAnnouncementsScreen(navController = navController, course = course, announcementsViewModel = announcementsViewModel) } } + composable(route = Screen.DetailedAnnouncement.route) { + val announcement = navController.previousBackStackEntry?.savedStateHandle?.get("announcement") + if (announcement != null) { + DetailedAnnouncementScreen(navController = navController, announcement = announcement) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/cmsclonelite/repository/CourseRepository.kt b/app/src/main/java/com/example/cmsclonelite/repository/CourseRepository.kt index 7e08a7c..ebf5794 100644 --- a/app/src/main/java/com/example/cmsclonelite/repository/CourseRepository.kt +++ b/app/src/main/java/com/example/cmsclonelite/repository/CourseRepository.kt @@ -3,8 +3,11 @@ package com.example.cmsclonelite.repository import android.content.ContentValues import android.content.ContentValues.TAG import android.content.Context +import android.net.Uri import android.provider.CalendarContract +import android.provider.OpenableColumns import android.util.Log +import androidx.core.net.toUri import com.example.cmsclonelite.Announcement import com.example.cmsclonelite.Course import com.google.auth.oauth2.AccessToken @@ -13,7 +16,10 @@ import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.SetOptions +import com.google.firebase.ktx.Firebase import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.storage.StorageMetadata +import com.google.firebase.storage.ktx.storage import kotlinx.coroutines.* import kotlinx.coroutines.tasks.await import okhttp3.* @@ -29,6 +35,8 @@ import kotlin.collections.HashMap class CourseRepository { private val client = OkHttpClient() private val ioCoroutineScope = CoroutineScope(Dispatchers.IO) + private val storage = Firebase.storage + var storageRef = storage.reference private suspend fun getCourses(db: FirebaseFirestore): List { val coursesRef = db.collection("courses") @@ -87,6 +95,8 @@ class CourseRepository { val announcement = Announcement() announcement.title = announcementMap[i]!!.getValue("title") announcement.body = announcementMap[i]!!.getValue("body") + announcement.fileName = announcementMap[i]!!.getValue("fileName") + announcement.downloadUri = announcementMap[i]!!.getValue("downloadUri").toUri() list.add(announcement) } } @@ -219,7 +229,7 @@ class CourseRepository { calName = calCursor.getString(nameCol) calID = calCursor.getString(idCol) - Log.d(ContentValues.TAG, "Calendar name = $calName Calendar ID = $calID") + Log.d(TAG, "Calendar name = $calName Calendar ID = $calID") calCursor.close() return calID.toLong() @@ -233,7 +243,7 @@ class CourseRepository { FirebaseMessaging.getInstance().subscribeToTopic(course.id!!) db.collection("users").document(uid) .update("enrolled", FieldValue.arrayUnion(course.id)) - .addOnSuccessListener { Log.d(ContentValues.TAG, "DocumentSnapshot successfully updated!") } + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") } .addOnFailureListener { e: Exception? -> Log.w(ContentValues.TAG, "Error updating document", e) } } } @@ -250,6 +260,12 @@ class CourseRepository { fun deleteCourse(db: FirebaseFirestore, course: Course) { ioCoroutineScope.launch { + val fileRef = storageRef.child("files/${course.id}") + fileRef.listAll().addOnCompleteListener { dir -> + for (item in dir.result.items) { + item.delete() + } + } db.collection("users").whereArrayContains("enrolled", course.id!!) .get() .addOnCompleteListener { task -> @@ -326,7 +342,9 @@ class CourseRepository { for(i in newAnnouncementList) { announcementHashMap["key${count}"] = hashMapOf( "title" to i.title!!, - "body" to i.body!! + "body" to i.body!!, + "fileName" to i.fileName, + "downloadUri" to i.downloadUri.toString() ) count -= 1 } @@ -335,6 +353,37 @@ class CourseRepository { } } + suspend fun uploadFileToFirebase(course: Course, fileUri: Uri, context: Context): Uri? { + val returnCursor = context.contentResolver.query(fileUri, null, null, null, null) + val nameIndex = returnCursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) + returnCursor?.moveToFirst() + val fileName = nameIndex?.let { returnCursor.getString(it) } + returnCursor?.close() + val fileRef = storageRef.child("files/${course.id}/${fileName}") + fileRef.putFile(fileUri).await() + return fileRef.downloadUrl.await() + } + + suspend fun getFileMetadataFromFirebase(course: Course, fileUri: Uri, context: Context): StorageMetadata { + val returnCursor = context.contentResolver.query(fileUri, null, null, null, null) + val nameIndex = returnCursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) + returnCursor?.moveToFirst() + val fileName = nameIndex?.let { returnCursor.getString(it) } + returnCursor?.close() + val fileRef = storageRef.child("files/${course.id}/${fileName}") + return fileRef.metadata.await() + } + + suspend fun deleteFileFromFirebase(course: Course, fileUri: Uri, context: Context) { + val returnCursor = context.contentResolver.query(fileUri, null, null, null, null) + val nameIndex = returnCursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) + returnCursor?.moveToFirst() + val fileName = nameIndex?.let { returnCursor.getString(it) } + returnCursor?.close() + val fileRef = storageRef.child("files/${course.id}/${fileName}") + fileRef.delete().await() + } + fun sendPushNotification(course: Course, announcement: Announcement) { ioCoroutineScope.launch { val url = "https://fcm.googleapis.com/v1/projects/cmsclonelite/messages:send" diff --git a/app/src/main/java/com/example/cmsclonelite/screens/AboutScreen.kt b/app/src/main/java/com/example/cmsclonelite/screens/AboutScreen.kt index c1e6378..e16baa2 100644 --- a/app/src/main/java/com/example/cmsclonelite/screens/AboutScreen.kt +++ b/app/src/main/java/com/example/cmsclonelite/screens/AboutScreen.kt @@ -38,7 +38,7 @@ fun AboutScreen( } ) }, - ) { + ) { padding -> Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top, diff --git a/app/src/main/java/com/example/cmsclonelite/screens/AddAnnouncementsScreen.kt b/app/src/main/java/com/example/cmsclonelite/screens/AddAnnouncementsScreen.kt index f9805f6..125621a 100644 --- a/app/src/main/java/com/example/cmsclonelite/screens/AddAnnouncementsScreen.kt +++ b/app/src/main/java/com/example/cmsclonelite/screens/AddAnnouncementsScreen.kt @@ -1,24 +1,35 @@ package com.example.cmsclonelite.screens +import android.app.Activity import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.OpenableColumns +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.rounded.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.example.cmsclonelite.Course @@ -26,9 +37,14 @@ import com.example.cmsclonelite.repository.CourseRepository import com.example.cmsclonelite.viewmodels.AnnouncementsViewModel import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.StorageMetadata +import com.google.firebase.storage.ktx.storageMetadata + private lateinit var mAuth: FirebaseAuth +//TODO: Make Progress indicator if possible + @Composable fun AddAnnouncementsScreen( navController: NavHostController, @@ -41,6 +57,20 @@ fun AddAnnouncementsScreen( val title by announcementsViewModel.title.observeAsState("") val body by announcementsViewModel.body.observeAsState("") val showAnnouncementAddDialog: Boolean by announcementsViewModel.isAddAnnouncementDialog.observeAsState(false) + val showFileDeleteDialog: Boolean by announcementsViewModel.isFileDeleteDialog.observeAsState(false) + val fileUri: Uri? by announcementsViewModel.fileUri.observeAsState(null) + val downloadUri: Uri? by announcementsViewModel.downloadUri.observeAsState(null) + val fileMetadata: StorageMetadata by announcementsViewModel.storageMetadata.observeAsState(storageMetadata {}) + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + val uriHandler = LocalUriHandler.current + val intentLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult()) { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + announcementsViewModel.setFileUri(activityResult.data?.data) + announcementsViewModel.uploadFileToFirebase(course, context) + }} announcementsViewModel.initialize() Card { if (showAnnouncementAddDialog) { @@ -53,6 +83,17 @@ fun AddAnnouncementsScreen( ) } } + Card { + if (showFileDeleteDialog) { + FileDeleteConfirmation(showDialog = showFileDeleteDialog, + onDismiss = {announcementsViewModel.removeFileDeleteDialog()}, + fileUri = fileUri!!, + course = course, + context = context, + announcementsViewModel = announcementsViewModel + ) + } + } Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background @@ -78,9 +119,10 @@ fun AddAnnouncementsScreen( Icon(Icons.Filled.Add, contentDescription = "Add Announcement (Admin Only)") } } - ) { + ) { padding -> Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize(), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ){ @@ -116,6 +158,63 @@ fun AddAnnouncementsScreen( onValueChange = { announcementsViewModel.setBody(it) }) + Spacer(modifier = Modifier.padding(top = 20.dp)) + Card( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(top = 4.dp) + .clickable( + onClick = { + intentLauncher.launch(intent) + } + ), + elevation = 24.dp + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp) + ) { + Text( + "Upload a file", + fontSize = 16.sp, + ) + Spacer(Modifier.fillMaxWidth(0.85f)) + Icon(Icons.Default.Add, contentDescription = "Upload a file") + } + } + } + Spacer(modifier = Modifier.padding(top = 40.dp)) + if(downloadUri != null && fileMetadata.name != null) { + Row( + modifier = Modifier + .fillMaxWidth(0.9f), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier + .weight(1f, fill = false) + .clickable( + onClick = { + uriHandler.openUri(downloadUri.toString()) + } + ), + text = fileMetadata.name!!, + fontSize = 20.sp + ) + IconButton( + onClick = { + announcementsViewModel.showFileDeleteDialog() + } + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Delete file" + ) + } + } + } } } } @@ -160,4 +259,37 @@ fun AnnouncementAddConfirmation( } ) } +} +@Composable +fun FileDeleteConfirmation( + showDialog: Boolean, + onDismiss: () -> Unit, + fileUri: Uri, + course: Course, + context: Context, + announcementsViewModel: AnnouncementsViewModel +) { + if (showDialog) { + AlertDialog( + title = { + Text("Delete File") + }, + text = { + Text("Are you sure you want to delete this file?") + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + announcementsViewModel.deleteFileFromFirebase(course, fileUri, context) + announcementsViewModel.removeFileDeleteDialog() + }) { + Text("OK") + } }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/cmsclonelite/screens/AnnouncementsScreen.kt b/app/src/main/java/com/example/cmsclonelite/screens/AnnouncementsScreen.kt index 1d6581e..48005a6 100644 --- a/app/src/main/java/com/example/cmsclonelite/screens/AnnouncementsScreen.kt +++ b/app/src/main/java/com/example/cmsclonelite/screens/AnnouncementsScreen.kt @@ -1,5 +1,6 @@ package com.example.cmsclonelite.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -60,17 +61,35 @@ fun AnnouncementsScreen( } } } - ) { + ) { padding -> Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - LazyColumn( - contentPadding = PaddingValues(vertical = 20.dp, horizontal = 12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(items = announcementList) { announcement -> - CustomAnnouncementCard(announcement = announcement) + if (announcementList.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "No announcements posted yet", + fontSize = MaterialTheme.typography.h5.fontSize, + fontWeight = FontWeight.Bold + ) + } + } else { + LazyColumn( + contentPadding = PaddingValues(vertical = 20.dp, horizontal = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(items = announcementList) { announcement -> + CustomAnnouncementCard( + announcement = announcement, + announcementsViewModel = announcementsViewModel, + navController = navController + ) + } } } } @@ -86,11 +105,16 @@ fun AnnouncementsScreenPreview() { AnnouncementsScreen(rememberNavController(), Course(), announcementsViewModel) } @Composable -fun CustomAnnouncementCard(announcement: Announcement) { +fun CustomAnnouncementCard(announcement: Announcement, navController: NavHostController, announcementsViewModel: AnnouncementsViewModel) { Card( modifier = Modifier .fillMaxWidth() - .padding(top = 4.dp), + .padding(top = 4.dp) + .clickable( + onClick = { + announcementsViewModel.allAnnouncementsToDetailedAnnouncement(announcement, navController) + } + ), elevation = 24.dp ) { Column( diff --git a/app/src/main/java/com/example/cmsclonelite/screens/CourseDetailsScreen.kt b/app/src/main/java/com/example/cmsclonelite/screens/CourseDetailsScreen.kt index 86b78b3..481f477 100644 --- a/app/src/main/java/com/example/cmsclonelite/screens/CourseDetailsScreen.kt +++ b/app/src/main/java/com/example/cmsclonelite/screens/CourseDetailsScreen.kt @@ -1,5 +1,6 @@ package com.example.cmsclonelite.screens +import android.content.Context import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -14,6 +15,7 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -36,6 +38,7 @@ fun CourseDetailsScreen( courseDetailsViewModel: CourseDetailsViewModel ) { mAuth = FirebaseAuth.getInstance() + val context = LocalContext.current val showDeleteDialog: Boolean by courseDetailsViewModel.isDeleteDialog.observeAsState(false) val showEnrollDialog: Boolean by courseDetailsViewModel.isEnrollDialog.observeAsState(false) val userEnrolledCourseList: List by courseDetailsViewModel.userEnrolledCourseList.observeAsState(listOf()) @@ -82,7 +85,7 @@ fun CourseDetailsScreen( } ) }, - ) { + ) { padding -> Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally diff --git a/app/src/main/java/com/example/cmsclonelite/screens/DetailedAnnouncement.kt b/app/src/main/java/com/example/cmsclonelite/screens/DetailedAnnouncement.kt new file mode 100644 index 0000000..d92243e --- /dev/null +++ b/app/src/main/java/com/example/cmsclonelite/screens/DetailedAnnouncement.kt @@ -0,0 +1,158 @@ +package com.example.cmsclonelite.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +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.sp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.example.cmsclonelite.Announcement +import com.example.cmsclonelite.repository.CourseRepository +import com.example.cmsclonelite.viewmodels.AnnouncementsViewModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore + +private lateinit var mAuth: FirebaseAuth + +@Composable +fun DetailedAnnouncementScreen( + navController: NavHostController, + announcement: Announcement +) { + mAuth = FirebaseAuth.getInstance() + val uriHandler = LocalUriHandler.current + Scaffold( + topBar = { + TopAppBar( + title = { Text( + "${announcement.title}" + ) }, + navigationIcon = { + IconButton(onClick = { + navController.navigateUp() + }) { + Icon(Icons.Rounded.ArrowBack, "") + } + }, + ) + } + ) { padding -> + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.padding(top = 8.dp)) + Card( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(top = 12.dp), + elevation = 24.dp + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp) + ) { + Text( + "${announcement.title}", + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + } + Divider( + color = Color.LightGray, modifier = Modifier + .fillMaxWidth() + .width(1.dp) + ) + Row( + modifier = Modifier.padding( + start = 8.dp, + end = 8.dp, + top = 8.dp, + bottom = 8.dp + ) + ) { + Text( + "${announcement.body}", + fontSize = 16.sp + ) + } + } + } + Spacer(modifier = Modifier.padding(top = 12.dp)) + if(announcement.downloadUri.toString() != "null" && announcement.fileName != "null") { + Card( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(top = 12.dp), + elevation = 24.dp + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding( + start = 8.dp, + end = 8.dp, + top = 8.dp, + bottom = 8.dp + ) + ) { + Text( + "Attachments:", + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + } + Divider( + color = Color.LightGray, modifier = Modifier + .fillMaxWidth() + .width(1.dp) + ) + Row( + modifier = Modifier.padding( + start = 8.dp, + end = 8.dp, + top = 16.dp, + bottom = 16.dp + ).clickable( + onClick = { + uriHandler.openUri(announcement.downloadUri.toString()) + } + ) + ) { + Text( + modifier = Modifier.weight(0.9f), + text = announcement.fileName, + fontSize = 16.sp + ) + Icon( + modifier = Modifier.weight(0.1f), + imageVector = Icons.Filled.Download, + contentDescription = "Download file" + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun DetailedAnnouncementScreenPreview() { + DetailedAnnouncementScreen(rememberNavController(), Announcement()) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/cmsclonelite/screens/EditCourseDetailsScreen.kt b/app/src/main/java/com/example/cmsclonelite/screens/EditCourseDetailsScreen.kt index 33a501f..86024ee 100644 --- a/app/src/main/java/com/example/cmsclonelite/screens/EditCourseDetailsScreen.kt +++ b/app/src/main/java/com/example/cmsclonelite/screens/EditCourseDetailsScreen.kt @@ -4,7 +4,9 @@ import android.app.DatePickerDialog import android.app.TimePickerDialog import android.content.Context import android.widget.DatePicker +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* @@ -17,6 +19,8 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction @@ -156,7 +160,7 @@ fun EditCourseDetailsScreen( } } } - ) { + ) { padding -> Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Top, @@ -197,7 +201,7 @@ fun EditCourseDetailsScreen( editCourseDetailsViewModel.setInstructor(course, it) }) Spacer(modifier = Modifier.padding(top = 20.dp)) - GroupedCheckbox(mItemsList = listOf("MO", "TU", "WE", "TH", "FR", "SA"), course) + GroupedCheckbox(mItemsList = listOf("MO", "TU", "WE", "TH", "FR", "SA"), course, padding) Spacer(modifier = Modifier.padding(top = 40.dp)) Row( modifier = Modifier.fillMaxWidth(0.9f), @@ -226,7 +230,7 @@ fun EditCourseDetailsScreenPreview() { EditCourseDetailsScreen(rememberNavController(), Course(), editCourseDetailsViewModel) } @Composable -fun GroupedCheckbox(mItemsList: List, course: Course) { +fun GroupedCheckbox(mItemsList: List, course: Course, paddingValues: PaddingValues) { var stringList = arrayListOf() var length = if(course.days == null) 0 else course.days?.length!! val list = arrayListOf() @@ -236,17 +240,21 @@ fun GroupedCheckbox(mItemsList: List, course: Course) { length-=3 } Row( - modifier = Modifier.fillMaxWidth(0.9f), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(0.9f).padding(paddingValues), ) { mItemsList.forEach { item -> val isChecked = remember { mutableStateOf(item in stringList) } if(isChecked.value) { list.add(item) } - Column { - Row { + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.padding(start = 4.dp), + text = item, + ) Checkbox( + modifier = Modifier.padding(end = 4.dp), checked = isChecked.value, onCheckedChange = { isChecked.value = it @@ -266,7 +274,6 @@ fun GroupedCheckbox(mItemsList: List, course: Course) { }, enabled = true ) - Text(text = item) } } } diff --git a/app/src/main/java/com/example/cmsclonelite/screens/EnrolledCourseDetailsScreen.kt b/app/src/main/java/com/example/cmsclonelite/screens/EnrolledCourseDetailsScreen.kt index 457077d..d8873c1 100644 --- a/app/src/main/java/com/example/cmsclonelite/screens/EnrolledCourseDetailsScreen.kt +++ b/app/src/main/java/com/example/cmsclonelite/screens/EnrolledCourseDetailsScreen.kt @@ -79,7 +79,7 @@ fun EnrolledCourseDetailsScreen( } ) }, - ) { + ) { padding -> Card { if (showCalendarDialog) { CalendarExportConfirmation( diff --git a/app/src/main/java/com/example/cmsclonelite/viewmodels/AnnouncementsViewModel.kt b/app/src/main/java/com/example/cmsclonelite/viewmodels/AnnouncementsViewModel.kt index e3a82d7..49c9a6c 100644 --- a/app/src/main/java/com/example/cmsclonelite/viewmodels/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/example/cmsclonelite/viewmodels/AnnouncementsViewModel.kt @@ -1,6 +1,7 @@ package com.example.cmsclonelite.viewmodels import android.content.Context +import android.net.Uri import android.widget.Toast import androidx.lifecycle.* import androidx.navigation.NavHostController @@ -10,6 +11,10 @@ import com.example.cmsclonelite.Screen import com.example.cmsclonelite.repository.CourseRepository import com.example.cmsclonelite.screens.findActivity import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.storage.StorageMetadata +import com.google.firebase.storage.ktx.storageMetadata +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class AnnouncementsViewModel( @@ -24,6 +29,14 @@ class AnnouncementsViewModel( val isAddAnnouncementDialog: LiveData get() = _isAddAnnouncementDialog + private val _isFileDeleteDialog = MutableLiveData() + val isFileDeleteDialog: LiveData + get() = _isFileDeleteDialog + + private val _storageMetadata = MutableLiveData() + val storageMetadata: LiveData + get() = _storageMetadata + private val _title = MutableLiveData() val title: LiveData get() = _title @@ -31,11 +44,23 @@ class AnnouncementsViewModel( private val _body = MutableLiveData() val body: LiveData get() = _body + + private val _fileUri = MutableLiveData() + val fileUri: LiveData + get() = _fileUri + + private val _downloadUri = MutableLiveData() + val downloadUri: LiveData + get() = _downloadUri fun initialize() { _title.value = "" _body.value = "" + _fileUri.value = null + _downloadUri.value = null + _storageMetadata.value = storageMetadata {} _isAddAnnouncementDialog.value = false + _isFileDeleteDialog.value = false } fun setTitle(title: String) { @@ -46,6 +71,10 @@ class AnnouncementsViewModel( _body.value = body } + fun setFileUri(fileUri: Uri?) { + _fileUri.value = fileUri + } + fun showAddAnnouncementDialog() { _isAddAnnouncementDialog.value = true } @@ -54,12 +83,28 @@ class AnnouncementsViewModel( _isAddAnnouncementDialog.value = false } + fun showFileDeleteDialog() { + _isFileDeleteDialog.value = true + } + + fun removeFileDeleteDialog() { + _isFileDeleteDialog.value = false + } + fun getAllAnnouncementsList(courseId: String) { viewModelScope.launch { _allAnnouncementsList.value = courseRepository.getAnnouncements(db, courseId) } } + fun allAnnouncementsToDetailedAnnouncement(announcement: Announcement, navController:NavHostController) { + navController.currentBackStackEntry?.savedStateHandle?.set( + key = "announcement", + value = announcement + ) + navController.navigate(Screen.DetailedAnnouncement.route) + } + fun allAnnouncementsToAddCourseDetails(navController: NavHostController, course: Course) { navController.currentBackStackEntry?.savedStateHandle?.set( key = "courseAnnouncements", @@ -68,6 +113,21 @@ class AnnouncementsViewModel( navController.navigate(Screen.AddAnnouncements.route) } + fun uploadFileToFirebase(course: Course, context: Context) { + CoroutineScope(Dispatchers.IO).launch { + _downloadUri.postValue(courseRepository.uploadFileToFirebase(course, _fileUri.value!!, context)) + _storageMetadata.postValue(courseRepository.getFileMetadataFromFirebase(course, _fileUri.value!!, context)) + } + } + + fun deleteFileFromFirebase(course: Course, fileUri: Uri, context: Context) { + CoroutineScope(Dispatchers.IO).launch { + courseRepository.deleteFileFromFirebase(course, fileUri, context) + } + _fileUri.value = null + _downloadUri.value = null + } + fun postAnnouncement(context: Context, navController: NavHostController, course: Course) { if(_title.value == "") { Toast.makeText(context.findActivity(), "Please enter the title of the announcement", @@ -80,12 +140,12 @@ class AnnouncementsViewModel( else { courseRepository.sendPushNotification( course, - Announcement(_title.value, _body.value) + Announcement(_title.value, _body.value, _storageMetadata.value?.name.toString(), _downloadUri.value) ) courseRepository.addAnnouncement( db, course.id!!, - Announcement(_title.value, _body.value) + Announcement(_title.value, _body.value, _storageMetadata.value?.name.toString(), _downloadUri.value) ) navController.navigate(Screen.MainScreen.route) { popUpTo(Screen.MainScreen.route) { diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7009456..2a3eeec 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/build.gradle b/build.gradle index d50c8f2..071bbc0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,17 @@ buildscript { ext { - compose_version = '1.0.1' + compose_version = '1.3.1' } dependencies { classpath 'com.google.gms:google-services:4.3.14' - classpath 'com.android.tools.build:gradle:4.1.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20" + classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21' } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.1.2' apply false - id 'com.android.library' version '7.1.2' apply false - id 'org.jetbrains.kotlin.android' version '1.5.21' apply false + id 'com.android.application' version '7.3.1' apply false + id 'com.android.library' version '7.3.1' apply false + id 'org.jetbrains.kotlin.android' version '1.7.10' apply false } task clean(type: Delete) { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f22995a..f03e358 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Sep 16 18:49:00 IST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME