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