diff --git a/app/src/androidTest/java/com/github/se/travelpouch/e2e/TravelCreation.kt b/app/src/androidTest/java/com/github/se/travelpouch/e2e/TravelCreation.kt index 6fcb260c1..71aa5c004 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/e2e/TravelCreation.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/e2e/TravelCreation.kt @@ -85,9 +85,7 @@ class TravelCreation { runBlocking { firestore.terminate().await() auth.signOut() - auth - .signInWithEmailAndPassword("example1@example.com", "password1") - .await() + auth.signInWithEmailAndPassword("example1@example.com", "password1").await() val uid = auth.currentUser!!.uid auth.currentUser!!.delete().await() } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt index c0caf1fd5..d434195c5 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -25,7 +24,6 @@ import java.util.Locale * * @param calendarViewModel The ViewModel associated with the Calendar screen. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CalendarScreen(calendarViewModel: CalendarViewModel, navigationActions: NavigationActions) { // Observe the state of activities from the ViewModel @@ -44,6 +42,7 @@ fun CalendarScreen(calendarViewModel: CalendarViewModel, navigationActions: Navi CalendarView( selectedDate = calendarViewModel.selectedDate, onDateSelected = { date -> calendarViewModel.onDateSelected(date) }, + events = calendarState.map { it.date.toDate() }, modifier = Modifier.fillMaxWidth().testTag("androidCalendarView")) // Activities list diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/CalendarViewComposable.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/CalendarViewComposable.kt index 0dd325a22..c9d2e2198 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/CalendarViewComposable.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/CalendarViewComposable.kt @@ -1,39 +1,203 @@ package com.github.se.travelpouch.ui.dashboard +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag -import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date +import java.util.Locale /** - * Composable function to display an Android CalendarView. + * Custom Composable for displaying a calendar grid with event indicators. * * @param selectedDate The currently selected date. - * @param onDateSelected Callback function to handle date selection. + * @param events A list of dates with events. + * @param onDateSelected Callback when a date is selected. * @param modifier Modifier to be applied to the CalendarView. */ @Composable -fun CalendarView( +fun CustomCalendarView( selectedDate: Date, + events: List, onDateSelected: (Date) -> Unit, modifier: Modifier = Modifier ) { - val context = LocalContext.current - AndroidView( - factory = { - android.widget.CalendarView(context).apply { - // Set the initial selected date - date = selectedDate.time - - setOnDateChangeListener { _, year, month, dayOfMonth -> - val newDate = Calendar.getInstance().apply { set(year, month, dayOfMonth) }.time - onDateSelected(newDate) + val calendar = Calendar.getInstance() + calendar.time = selectedDate + + val daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH) + calendar.set(Calendar.DAY_OF_MONTH, 1) + val firstDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1 // Adjust to 0-index + + val days = + (1..daysInMonth).map { day -> + calendar.set(Calendar.DAY_OF_MONTH, day) + calendar.time + } + + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val eventDates = events.map { dateFormat.format(it) }.toSet() + + Column( + modifier = + modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) // Set a distinct background color for the Days of the week + .padding(8.dp)) { + // Days of the week titles + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { + listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat").forEach { day -> + BasicText( + text = day, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) } } - }, - modifier = modifier.testTag("androidCalendarView"), - update = { it.date = selectedDate.time }) + } + + LazyVerticalGrid( + columns = GridCells.Fixed(7), + modifier = modifier.fillMaxWidth().testTag("customCalendarGrid").background(MaterialTheme.colorScheme.onPrimary), + horizontalArrangement = Arrangement.Center) { + // Add empty spaces for days before the start of the month + items(firstDayOfWeek) { Spacer(modifier = Modifier.size(40.dp)) } + + // Add days with optional event indicators + items(days.size) { index -> + val date = days[index] + val isEventDay = eventDates.contains(dateFormat.format(date)) + val isSelected = dateFormat.format(selectedDate) == dateFormat.format(date) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(40.dp).clickable { onDateSelected(date) }) { + if (isSelected) { + Box(modifier = Modifier.size(36.dp).background(MaterialTheme.colorScheme.primary, shape = CircleShape)) + } + + BasicText( + text = (index + 1).toString(), + style = + androidx.compose.ui.text.TextStyle( + textAlign = TextAlign.Center, + fontSize = 16.sp, + color = + if (isSelected) Color.White + else Color.Black)) + + if (isEventDay) { + Box( + modifier = + Modifier.size(8.dp) + .align(Alignment.BottomCenter) + .background(Color.Red, shape = CircleShape)) + } + } + } + } +} + +/** + * Wrapper Composable for Calendar with Back and Forward navigation arrows. + * + * @param selectedDate The currently selected date. + * @param events A list of dates with events. + * @param onDateSelected Callback when a date is selected. + * @param modifier Modifier to be applied to the calendar. + */ +@Composable +fun CalendarView( + selectedDate: Date, + events: List, + onDateSelected: (Date) -> Unit, + modifier: Modifier = Modifier +) { + var currentMonth by remember { mutableStateOf(Calendar.getInstance()) } + currentMonth.time = selectedDate + + Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + // Header with navigation arrows + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically) { + // Previous month arrow + IconButton( + onClick = { + currentMonth.add(Calendar.MONTH, -1) + onDateSelected(currentMonth.time) + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Previous Month") + } + + // Month and year text + BasicText( + text = + "${ + currentMonth.getDisplayName( + Calendar.MONTH, Calendar.LONG, Locale.getDefault() + ) + } ${currentMonth.get(Calendar.YEAR)}", + style = MaterialTheme.typography.bodyLarge) + + // Next month arrow + IconButton( + onClick = { + currentMonth.add(Calendar.MONTH, 1) + onDateSelected(currentMonth.time) + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "Next Month") + } + } + + // Calendar grid + CustomCalendarView( + selectedDate = currentMonth.time, + events = + events.filter { + val eventCalendar = Calendar.getInstance().apply { time = it } + eventCalendar.get(Calendar.MONTH) == currentMonth.get(Calendar.MONTH) && + eventCalendar.get(Calendar.YEAR) == currentMonth.get(Calendar.YEAR) + }, + onDateSelected = { date -> + onDateSelected(date) + currentMonth.time = date // Update month when date selected + }, + modifier = Modifier.fillMaxWidth()) + } } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt b/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt index cf0a00c09..7baa48142 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/home/StorageDashboard.kt @@ -122,7 +122,7 @@ fun StorageDashboard( StorageLimitDialog( onDismissRequest = { storageLimitDialogOpened.value = false }, currentLimit = storageStats?.storageLimit ?: 0, - usedStorage = storageStats?.storageUsed ?: 0, + usedStorage = storageStats?.storageUsed ?: 0, onUpdate = { storageDashboardViewModel.setStorageStats( storageStats!!.copy(storageLimit = it)) @@ -308,7 +308,8 @@ fun StorageLimitDialog( val unitOptions = listOf("MiB", "GiB") val focusRequester = remember { FocusRequester() } val longOrNull = text.value.toLongOrNull() - val newValue = if (longOrNull != null) longOrNull * (1024L * 1024L shl selectedUnitIndex * 10) else 0 + val newValue = + if (longOrNull != null) longOrNull * (1024L * 1024L shl selectedUnitIndex * 10) else 0 val validInput = newValue >= usedStorage && longOrNull != null LaunchedEffect(Unit) { focusRequester.requestFocus() } Box(