From 338fc3d4aff664ff66a2c95b90794d39f96a6cf4 Mon Sep 17 00:00:00 2001 From: salmanA169 Date: Wed, 25 Oct 2023 22:00:02 +0300 Subject: [PATCH 01/23] add icon alert reminder on addEditScreen modified Note model add reminderDateTime and isReminded Improve CardNote to show reminder label Add ReminderDialog on addEditScreen --- .../notify/components/dialog/TextDialog.kt | 193 ++++++++++++++++++ .../aritra/notify/components/note/NoteCard.kt | 44 +++- .../com/aritra/notify/domain/models/Note.kt | 8 + .../domain/models/ReminderDateTimeModel.kt | 14 ++ .../notes/addEditScreen/AddEditScreen.kt | 162 +++++++++------ .../aritra/notify/utils/AnnotationPreview.kt | 0 .../aritra/notify/utils/FormatLocalDate.kt | 44 ++++ app/src/main/res/drawable/add_alert.xml | 10 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 411 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/com/aritra/notify/domain/models/ReminderDateTimeModel.kt create mode 100644 app/src/main/java/com/aritra/notify/utils/AnnotationPreview.kt create mode 100644 app/src/main/java/com/aritra/notify/utils/FormatLocalDate.kt create mode 100644 app/src/main/res/drawable/add_alert.xml diff --git a/app/src/main/java/com/aritra/notify/components/dialog/TextDialog.kt b/app/src/main/java/com/aritra/notify/components/dialog/TextDialog.kt index 73375ded..24a08905 100644 --- a/app/src/main/java/com/aritra/notify/components/dialog/TextDialog.kt +++ b/app/src/main/java/com/aritra/notify/components/dialog/TextDialog.kt @@ -1,20 +1,64 @@ package com.aritra.notify.components.dialog +import android.util.Log +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.DateRange import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Card +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChip import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties import com.aritra.notify.R +import com.aritra.notify.components.actions.BackPressHandler +import com.aritra.notify.domain.models.ReminderDateTimeModel import com.aritra.notify.utils.Const +import com.aritra.notify.utils.checkDateIsNotOld +import com.aritra.notify.utils.checkTimeIsNotOld +import com.aritra.notify.utils.formatReminderDateTime +import com.aritra.notify.utils.toast +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId @Composable fun TextDialog( @@ -74,3 +118,152 @@ fun TextDialog( ) } } + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun DateTimeDialog( + modifier: Modifier = Modifier, + isOpen: Boolean = false, + onDateTimeUpdated: (LocalDateTime) -> Unit, + onConfirmCallback: () -> Unit, + onDismissCallback: () -> Unit +) { + var shouldShowDatePicker by remember { + mutableStateOf(false) + } + var shouldShowTimePicker by remember { + mutableStateOf(false) + } + + val context = LocalContext.current + val datePickerState = rememberDatePickerState() + val timePickerState = rememberTimePickerState() + + + if (shouldShowDatePicker) { + DatePickerDialog(onDismissRequest = { + shouldShowDatePicker = false + + }, confirmButton = { + TextButton(onClick = { + if (datePickerState.selectedDateMillis == null) { + context.toast("Please Select Date") + } else { + if (LocalDateTime.of( + LocalDate.ofInstant( + Instant.ofEpochMilli(datePickerState.selectedDateMillis!!), + ZoneId.systemDefault() + ), LocalTime.now() + ).checkDateIsNotOld() + ) { + shouldShowDatePicker = false + shouldShowTimePicker = true + } else { + context.toast("Can not choose old Date") + } + } + }) { + Text(text = "Confirm") + } + }) { + DatePicker(state = datePickerState) + } + } + + if (shouldShowTimePicker) { + DatePickerDialog(onDismissRequest = { + shouldShowTimePicker = false + shouldShowDatePicker = true + }, confirmButton = { + TextButton(onClick = { + val dateTime = LocalDateTime.of( + LocalDate.ofInstant( + Instant.ofEpochMilli(datePickerState.selectedDateMillis!!), + ZoneId.systemDefault() + ), LocalTime.of(timePickerState.hour, timePickerState.minute) + ) + if (dateTime.checkTimeIsNotOld()){ + onDateTimeUpdated( + dateTime + ) + shouldShowTimePicker = false + }else{ + context.toast("Can not choose old Time") + } + + }) { + Text(text = "Confirm") + } + + }) { + TimePicker(state = timePickerState, modifier = Modifier + .align(CenterHorizontally) + .padding(top = 16.dp)) + } + } + if (isOpen) { + AlertDialog(onDismissRequest = onDismissCallback) { + Surface( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(8.dp), + modifier = modifier, + tonalElevation = 6.dp + ) { + Column(modifier = Modifier.padding(24.dp)) { + Icon( + imageVector = Icons.Default.DateRange, + contentDescription = "Date", + modifier = Modifier + .align(CenterHorizontally) + .padding(bottom = 24.dp) + ) + Text( + text = stringResource(id = R.string.set_reminder), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 24.dp) + ) + FlowRow( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(bottom = 24.dp), + maxItemsInEachRow = 1 + ) { + + ReminderDateTimeModel.values().forEach { + AssistChip(leadingIcon = { + Icon(imageVector = Icons.Default.AccessTime, contentDescription = "") + }, onClick = { + if (it == ReminderDateTimeModel.CUSTOM) { + shouldShowDatePicker = true + }else{ + onDateTimeUpdated(it.dateTime) + } + + }, label = { + Text( + text = if (it != ReminderDateTimeModel.CUSTOM) it.dateTime.formatReminderDateTime() else "Custom", + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurface + ) + }) + + } + } +// TextButton( +// onClick = { onConfirmCallback() }, +// shape = RoundedCornerShape(8.dp), +// modifier = Modifier.align(End) +// ) { +// Text( +// text = stringResource(R.string.confirm), +// fontSize = 16.sp, +// fontWeight = FontWeight.SemiBold +// ) +// } + } + } + } + } +} diff --git a/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt b/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt index 461374f9..a29b6b49 100644 --- a/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt +++ b/app/src/main/java/com/aritra/notify/components/note/NoteCard.kt @@ -18,8 +18,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedAssistChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard @@ -35,6 +38,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -43,7 +48,9 @@ import coil.request.ImageRequest import com.aritra.notify.R import com.aritra.notify.domain.models.Note import com.aritra.notify.utils.Const +import com.aritra.notify.utils.formatReminderDateTime import java.text.SimpleDateFormat +import java.time.LocalDateTime import java.util.Locale @OptIn(ExperimentalFoundationApi::class) @@ -137,13 +144,19 @@ fun NotesCard( ).format(it) } Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - formattedDateTime?.let { - Text( - text = formattedDateTime, - fontSize = 16.sp, - fontFamily = FontFamily(Font(R.font.poppins_light)), - color = Color.Gray - ) + Column { + formattedDateTime?.let { + + Text( + text = formattedDateTime, + fontSize = 16.sp, + fontFamily = FontFamily(Font(R.font.poppins_light)), + color = Color.Gray + ) + } + noteModel.reminderDateTime?.let { + ReminderSection(it) + } } dateTimeDeleted?.let { Text( @@ -158,3 +171,20 @@ fun NotesCard( } } } + +@Composable +fun ReminderSection( + dateTime: LocalDateTime, + isReminded: Boolean = false +) { + ElevatedAssistChip(elevation = AssistChipDefaults.elevatedAssistChipElevation(4.dp), leadingIcon = { + Icon(imageVector = Icons.Default.AccessTime, contentDescription = "") + }, onClick = { /*TODO*/ }, label = { + Text( + text = dateTime.formatReminderDateTime(), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + textDecoration = if (isReminded) TextDecoration.LineThrough else null + ) + }, modifier = Modifier) +} diff --git a/app/src/main/java/com/aritra/notify/domain/models/Note.kt b/app/src/main/java/com/aritra/notify/domain/models/Note.kt index 38cdfac9..fa02ab3f 100644 --- a/app/src/main/java/com/aritra/notify/domain/models/Note.kt +++ b/app/src/main/java/com/aritra/notify/domain/models/Note.kt @@ -6,6 +6,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import kotlinx.parcelize.Parcelize +import java.time.LocalDateTime import java.util.Date @Parcelize @@ -19,4 +20,11 @@ data class Note( var image: List, @ColumnInfo(defaultValue = "false") var isMovedToTrash: Boolean = false, + var reminderDateTime:LocalDateTime? = null, + var isReminded:Boolean = false ) : Parcelable + +data class ReminderDateTimeInfo( + val dateTime:LocalDateTime, + val isReminded: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/domain/models/ReminderDateTimeModel.kt b/app/src/main/java/com/aritra/notify/domain/models/ReminderDateTimeModel.kt new file mode 100644 index 00000000..881b3302 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/domain/models/ReminderDateTimeModel.kt @@ -0,0 +1,14 @@ +package com.aritra.notify.domain.models + +import java.time.LocalDateTime + +enum class ReminderDateTimeModel(val dateTime: LocalDateTime = LocalDateTime.now()) { + AFTER_30_MINUTES( + dateTime = LocalDateTime.now().plusMinutes(30) + ), + AFTER_1_HOUR(dateTime = LocalDateTime.now().plusHours(1)), + TOMORROW_MORNING_7(dateTime = LocalDateTime.now().withHour(7).plusDays(1)), + CUSTOM ; + + +} \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt index 2ce19b0c..aab8dd8f 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt @@ -7,6 +7,7 @@ import android.graphics.Bitmap import android.graphics.Matrix import android.net.Uri import android.provider.MediaStore +import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -17,6 +18,7 @@ import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageProxy import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -38,13 +40,16 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Cameraswitch +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ElevatedAssistChip import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon @@ -94,15 +99,18 @@ import com.aritra.notify.R import com.aritra.notify.components.actions.BottomSheetOptions import com.aritra.notify.components.actions.SpeechRecognizerContract import com.aritra.notify.components.camPreview.CameraPreview +import com.aritra.notify.components.dialog.DateTimeDialog import com.aritra.notify.components.dialog.TextDialog import com.aritra.notify.components.topbar.AddEditTopBar import com.aritra.notify.domain.models.Note import com.aritra.notify.utils.Const +import com.aritra.notify.utils.formatReminderDateTime import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage import java.text.SimpleDateFormat +import java.time.LocalDateTime import java.util.Calendar import java.util.Date import java.util.Locale @@ -124,11 +132,14 @@ fun AddEditScreen( Note(noteId, "", "", Date(), emptyList()) } + var shouldShowDialogDateTime by remember { + mutableStateOf(false) + } var title by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } var dateTime by remember { mutableStateOf(Calendar.getInstance().time) } var photoUri by remember { mutableStateOf(emptyList()) } - + var reminderDateTime by remember { mutableStateOf(null) } var characterCount by remember { mutableIntStateOf(title.length + description.length) } val cancelDialogState = remember { mutableStateOf(false) } var showSheet by remember { mutableStateOf(false) } @@ -206,8 +217,8 @@ fun AddEditScreen( description = addEditViewModel.noteModel.observeAsState().value?.note ?: "" photoUri = addEditViewModel.noteModel.observeAsState().value?.image ?: emptyList() dateTime = addEditViewModel.noteModel.observeAsState().value?.dateTime - - note = note?.copy(title = title, note = description, dateTime = dateTime, image = photoUri) + reminderDateTime = addEditViewModel.noteModel.observeAsState().value?.reminderDateTime + note = note?.copy(title = title, note = description, dateTime = dateTime, image = photoUri, reminderDateTime = reminderDateTime) LaunchedEffect(Unit) { addEditViewModel.getNoteById(noteId) } @@ -222,7 +233,9 @@ fun AddEditScreen( title = title, note = description, dateTime = dateTime, - image = photoUri + image = photoUri, + reminderDateTime = reminderDateTime, + ), onSuccess = { navigateBack() @@ -276,65 +289,73 @@ fun AddEditScreen( ) { BottomAppBar(containerColor = Color.Transparent, content = { - IconButton(onClick = { showSheet = true }) { - Icon( - modifier = Modifier.size(25.dp), - painter = painterResource(id = R.drawable.add_box_icon), - contentDescription = stringResource(R.string.add_box) - ) - } - if (showSheet) { - ModalBottomSheet( - onDismissRequest = { showSheet = false }, - sheetState = bottomSheetState, - dragHandle = { BottomSheetDefaults.DragHandle() } - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(16.dp) + + IconButton(onClick = { showSheet = true }) { + Icon( + modifier = Modifier.size(25.dp), + painter = painterResource(id = R.drawable.add_box_icon), + contentDescription = stringResource(R.string.add_box) + ) + } + IconButton(onClick = { shouldShowDialogDateTime = true }) { + Icon( + modifier = Modifier.size(25.dp), + painter = painterResource(id = R.drawable.add_alert), + contentDescription = stringResource(R.string.add_box) + ) + } + if (showSheet) { + ModalBottomSheet( + onDismissRequest = { showSheet = false }, + sheetState = bottomSheetState, + dragHandle = { BottomSheetDefaults.DragHandle() } ) { - BottomSheetOptions( - text = stringResource(R.string.take_image), - icon = painterResource(id = R.drawable.camera_icon), - onClick = { - if (camPermissionState.status.isGranted) { - openCameraBottomSheet = true - } else { - camPermissionState.launchPermissionRequest() + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(16.dp) + ) { + BottomSheetOptions( + text = stringResource(R.string.take_image), + icon = painterResource(id = R.drawable.camera_icon), + onClick = { + if (camPermissionState.status.isGranted) { + openCameraBottomSheet = true + } else { + camPermissionState.launchPermissionRequest() + } + showSheet = false } - showSheet = false - } - ) - BottomSheetOptions( - text = stringResource(R.string.add_image), - icon = painterResource(id = R.drawable.gallery_icon), - onClick = { - launcher.launch( - PickVisualMediaRequest( - mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + ) + BottomSheetOptions( + text = stringResource(R.string.add_image), + icon = painterResource(id = R.drawable.gallery_icon), + onClick = { + launcher.launch( + PickVisualMediaRequest( + mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + ) ) - ) - showSheet = false - } - ) - BottomSheetOptions( - text = stringResource(R.string.speech_to_text), - icon = painterResource(id = R.drawable.mic_icon), - onClick = { - if (permissionState.status.isGranted) { - speechRecognizerLauncher.launch(Unit) - } else { - permissionState.launchPermissionRequest() + showSheet = false } - showSheet = false - } - ) + ) + BottomSheetOptions( + text = stringResource(R.string.speech_to_text), + icon = painterResource(id = R.drawable.mic_icon), + onClick = { + if (permissionState.status.isGranted) { + speechRecognizerLauncher.launch(Unit) + } else { + permissionState.launchPermissionRequest() + } + showSheet = false + } + ) + } } } - } - }) + }) } } }) { contentPadding -> @@ -482,6 +503,7 @@ fun AddEditScreen( keyboardType = KeyboardType.Text ) ) + TextField( value = if (isNew) { "$characterCount characters | $totalWords words" @@ -489,7 +511,7 @@ fun AddEditScreen( "$formattedCharacterCount | $formattedWordCount" }, onValueChange = { }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier, readOnly = true, textStyle = TextStyle( fontSize = 15.sp, @@ -507,6 +529,22 @@ fun AddEditScreen( keyboardType = KeyboardType.Text ) ) + reminderDateTime?.let { + ElevatedAssistChip(leadingIcon = { + Icon(imageVector = Icons.Default.AccessTime, contentDescription = "") + }, onClick = { /*TODO*/ }, label = { + Text( + text = it.formatReminderDateTime(), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold + ) + }, trailingIcon = { + Icon(imageVector = Icons.Default.Close, contentDescription = "",modifier = Modifier.clickable { + reminderDateTime = null + }) + }, modifier = Modifier) + } + } DescriptionTextField( @@ -540,6 +578,14 @@ fun AddEditScreen( } ) + DateTimeDialog(isOpen = shouldShowDialogDateTime, onDateTimeUpdated = { + reminderDateTime = it + shouldShowDialogDateTime = false + }, onConfirmCallback = { + + }) { + shouldShowDialogDateTime = false + } if (openCameraBottomSheet) { BottomSheetScaffold(scaffoldState = scaffoldState, sheetPeekHeight = 0.dp, sheetContent = {}) { Box( diff --git a/app/src/main/java/com/aritra/notify/utils/AnnotationPreview.kt b/app/src/main/java/com/aritra/notify/utils/AnnotationPreview.kt new file mode 100644 index 00000000..e69de29b diff --git a/app/src/main/java/com/aritra/notify/utils/FormatLocalDate.kt b/app/src/main/java/com/aritra/notify/utils/FormatLocalDate.kt new file mode 100644 index 00000000..5a239613 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/utils/FormatLocalDate.kt @@ -0,0 +1,44 @@ +package com.aritra.notify.utils + +import java.lang.StringBuilder +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.time.temporal.ChronoField +import java.time.temporal.ChronoUnit + +fun LocalDateTime.formatReminderDateTime(): String { + val currentDateTime = LocalDateTime.now() + val calcDays = dayOfMonth - currentDateTime.dayOfMonth + val buildString = StringBuilder() + when { + calcDays == 1 -> { + + buildString.append("Tomorrow , ") + buildString.append(format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))) + } + + calcDays == 0 -> { + buildString.append("Today , ") + buildString.append(format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))) + } + + else -> { + buildString.append(format(DateTimeFormatter.ofPattern("dd MMM"))) + .append(" , ") + .append(format(DateTimeFormatter.ofPattern("hh:mm a"))) + } + } + return buildString.toString() +} + +fun LocalDateTime.checkDateIsNotOld():Boolean{ + val currentDateTime = LocalDateTime.now() + return (currentDateTime.dayOfMonth - dayOfMonth) <= 0 +} + +fun LocalDateTime.checkTimeIsNotOld():Boolean{ + val currentDateTime = LocalTime.now() + return currentDateTime.until(this.toLocalTime(),ChronoUnit.MILLIS) >= 0 +} \ No newline at end of file diff --git a/app/src/main/res/drawable/add_alert.xml b/app/src/main/res/drawable/add_alert.xml new file mode 100644 index 00000000..8d343116 --- /dev/null +++ b/app/src/main/res/drawable/add_alert.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4a0ad3a8..01b09f78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,5 @@ Secure Screen Trash %s left + Set Reminder \ No newline at end of file From b178689e38aa6d4952a73935ae6f7b5aa4d1d0bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:06:38 +0000 Subject: [PATCH 02/23] fix(deps): update dependency androidx.compose.material:material-icons-extended to v1.5.4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b51dfc4..cb5869e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ lifecycle-runtime-ktx = "2.6.2" lifecycleExtensions = "2.2.0" lifecycleVersion = "2.6.2" material3 = "1.1.2" -materialIconsExtended = "1.5.3" +materialIconsExtended = "1.5.4" navVersion = "2.7.4" roomVersion = "2.5.2" runtimeLivedata = "1.5.3" From 6721fd7cb189706370057093585367dc3b3aa945 Mon Sep 17 00:00:00 2001 From: Jeffery Orazulike Date: Thu, 26 Oct 2023 15:44:58 +0100 Subject: [PATCH 03/23] chore(add-edit): refactored code --- .idea/deploymentTargetDropDown.xml | 15 +- .../components/appbar/AddEditBottomBar.kt | 147 ++++ .../notify/components/appbar/AddEditTopBar.kt | 202 +++++ .../SelectionModeTopAppBar.kt | 2 +- .../components/camPreview/CameraPreview.kt | 191 ++++- .../notify/components/topbar/AddEditTopBar.kt | 182 ----- .../com/aritra/notify/navigation/NotifyApp.kt | 27 +- .../notes/addEditScreen/AddEditRoute.kt | 104 +++ .../notes/addEditScreen/AddEditScreen.kt | 704 ++++-------------- .../notes/addEditScreen/AddEditViewModel.kt | 137 ++-- .../screens/notes/addEditScreen/NoteImages.kt | 93 +++ .../screens/notes/addEditScreen/NoteStats.kt | 110 +++ .../ui/screens/notes/homeScreen/NoteScreen.kt | 2 +- .../notes/homeScreen/NoteScreenViewModel.kt | 20 +- 14 files changed, 1093 insertions(+), 843 deletions(-) create mode 100644 app/src/main/java/com/aritra/notify/components/appbar/AddEditBottomBar.kt create mode 100644 app/src/main/java/com/aritra/notify/components/appbar/AddEditTopBar.kt rename app/src/main/java/com/aritra/notify/components/{topbar => appbar}/SelectionModeTopAppBar.kt (97%) delete mode 100644 app/src/main/java/com/aritra/notify/components/topbar/AddEditTopBar.kt create mode 100644 app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt create mode 100644 app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteImages.kt create mode 100644 app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteStats.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 4134ad40..0c0c3383 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,20 +3,7 @@ - - - - - - - - - - - - - - + diff --git a/app/src/main/java/com/aritra/notify/components/appbar/AddEditBottomBar.kt b/app/src/main/java/com/aritra/notify/components/appbar/AddEditBottomBar.kt new file mode 100644 index 00000000..c1997271 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/components/appbar/AddEditBottomBar.kt @@ -0,0 +1,147 @@ +package com.aritra.notify.components.appbar + +import android.Manifest +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +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.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.aritra.notify.R +import com.aritra.notify.components.actions.BottomSheetOptions +import com.aritra.notify.components.actions.SpeechRecognizerContract +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun AddEditBottomBar( + modifier: Modifier = Modifier, + onImagesSelected: (List) -> Unit, + onSpeechRecognized: (String) -> Unit, + showDrawingScreen: () -> Unit, + showCameraSheet: () -> Unit, +) { + var showSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState() + + val microphonePermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO) + val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) + + val imageLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(), + onResult = { uris -> + onImagesSelected(uris) + } + ) + val speechLauncher = rememberLauncherForActivityResult( + contract = SpeechRecognizerContract(), + onResult = { words -> + if (words.isNullOrEmpty()) { + return@rememberLauncherForActivityResult + } + onSpeechRecognized(words.joinToString(separator = " ")) + } + ) + + BottomAppBar( + modifier = modifier + .navigationBarsPadding() + .imePadding(), + containerColor = Color.Transparent, + content = { + IconButton( + onClick = { showSheet = true }, + content = { + Icon( + modifier = Modifier.size(25.dp), + painter = painterResource(id = R.drawable.add_box_icon), + contentDescription = stringResource(R.string.add_box) + ) + } + ) + if (showSheet) { + ModalBottomSheet( + onDismissRequest = { showSheet = false }, + sheetState = sheetState, + dragHandle = { BottomSheetDefaults.DragHandle() }, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + content = { + BottomSheetOptions( + text = stringResource(R.string.take_image), + icon = painterResource(id = R.drawable.camera_icon), + onClick = { + if (cameraPermissionState.status.isGranted) { + showCameraSheet() + } else { + cameraPermissionState.launchPermissionRequest() + } + showSheet = false + } + ) + BottomSheetOptions( + text = stringResource(R.string.add_image), + icon = painterResource(id = R.drawable.gallery_icon), + onClick = { + imageLauncher.launch( + PickVisualMediaRequest( + mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) + showSheet = false + } + ) + BottomSheetOptions( + text = stringResource(R.string.drawing), + icon = painterResource(id = R.drawable.gallery_icon), + onClick = { + showDrawingScreen() + showSheet = false + } + ) + BottomSheetOptions( + text = stringResource(R.string.speech_to_text), + icon = painterResource(id = R.drawable.mic_icon), + onClick = { + if (microphonePermissionState.status.isGranted) { + speechLauncher.launch(Unit) + } else { + microphonePermissionState.launchPermissionRequest() + } + showSheet = false + } + ) + } + ) + } + ) + } + } + ) +} diff --git a/app/src/main/java/com/aritra/notify/components/appbar/AddEditTopBar.kt b/app/src/main/java/com/aritra/notify/components/appbar/AddEditTopBar.kt new file mode 100644 index 00000000..9be1d2ca --- /dev/null +++ b/app/src/main/java/com/aritra/notify/components/appbar/AddEditTopBar.kt @@ -0,0 +1,202 @@ +package com.aritra.notify.components.appbar + +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.TopAppBarDefaults +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.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.aritra.notify.R +import com.aritra.notify.components.actions.ShareOption +import com.aritra.notify.components.dialog.TextDialog +import com.aritra.notify.utils.shareAsImage +import com.aritra.notify.utils.shareAsPdf +import com.aritra.notify.utils.shareNoteAsText + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddEditTopBar( + title: String, + description: String, + isNew: Boolean, + onBackPress: () -> Unit, + saveNote: () -> Unit, + deleteNote: (() -> Unit) -> Unit, +) { + val context = LocalContext.current + val deleteDialogVisible = remember { mutableStateOf(false) } + + val onBack = remember(description) { + { + if (isNew) { + onBackPress() + } else { + if (description.isBlank()) { + Toast.makeText( + context, + "Your note cannot be blank", + Toast.LENGTH_SHORT + ).show() + } else { + saveNote() + } + } + } + } + + BackHandler(onBack = onBack) + + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ), + title = {}, + navigationIcon = { + IconButton( + onClick = onBack, + content = { + Icon( + painterResource(R.drawable.back), + contentDescription = stringResource(R.string.back) + ) + } + ) + }, + actions = { + if (!isNew) { + IconButton( + onClick = { + deleteDialogVisible.value = true + }, + content = { + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = "Delete" + ) + } + ) + + if (deleteDialogVisible.value) { + TextDialog( + title = stringResource(R.string.warning), + description = stringResource( + R.string.are_you_sure_want_to_delete_these_items_it_cannot_be_recovered + ), + isOpened = deleteDialogVisible.value, + onDismissCallback = { + deleteDialogVisible.value = false + }, + onConfirmCallback = { + deleteNote { + deleteDialogVisible.value = false + onBackPress() + } + } + ) + } + } + if (description.isNotBlank()) { + ShareNote(title, description) + IconButton( + onClick = saveNote, + content = { + Icon( + painterResource(R.drawable.save), + contentDescription = stringResource(R.string.save) + ) + } + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShareNote( + title: String, + description: String, + modifier: Modifier = Modifier, +) { + val view = LocalView.current + val context = LocalContext.current + + var showSheet by remember { mutableStateOf(false) } + + IconButton( + modifier = modifier, + onClick = { + showSheet = true + }, + content = { + Icon( + painterResource(R.drawable.ic_share), + contentDescription = stringResource(R.string.share) + ) + } + ) + if (showSheet) { + ModalBottomSheet( + onDismissRequest = { + showSheet = false + }, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(16.dp), + content = { + Spacer(modifier = Modifier.height(5.dp)) + ShareOption( + text = stringResource(R.string.share_note_as_text), + icon = painterResource(id = R.drawable.text_icon), + onClick = { + shareNoteAsText(context, title, description) + showSheet = false + } + ) + Spacer(modifier = Modifier.height(15.dp)) + ShareOption( + text = stringResource(R.string.share_note_as_picture), + icon = painterResource(id = R.drawable.image_icon), + onClick = { + shareAsImage(view, view.width to view.height) + showSheet = false + } + ) + Spacer(modifier = Modifier.height(15.dp)) + ShareOption( + text = stringResource(R.string.share_as_pdf), + icon = painterResource(id = R.drawable.pdf_icon), + onClick = { + shareAsPdf(view, "Notify") + showSheet = false + } + ) + } + ) + } + ) + } +} diff --git a/app/src/main/java/com/aritra/notify/components/topbar/SelectionModeTopAppBar.kt b/app/src/main/java/com/aritra/notify/components/appbar/SelectionModeTopAppBar.kt similarity index 97% rename from app/src/main/java/com/aritra/notify/components/topbar/SelectionModeTopAppBar.kt rename to app/src/main/java/com/aritra/notify/components/appbar/SelectionModeTopAppBar.kt index 8b1ab22b..d3c53b32 100644 --- a/app/src/main/java/com/aritra/notify/components/topbar/SelectionModeTopAppBar.kt +++ b/app/src/main/java/com/aritra/notify/components/appbar/SelectionModeTopAppBar.kt @@ -1,4 +1,4 @@ -package com.aritra.notify.components.topbar +package com.aritra.notify.components.appbar import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons diff --git a/app/src/main/java/com/aritra/notify/components/camPreview/CameraPreview.kt b/app/src/main/java/com/aritra/notify/components/camPreview/CameraPreview.kt index dbdd7144..571483e8 100644 --- a/app/src/main/java/com/aritra/notify/components/camPreview/CameraPreview.kt +++ b/app/src/main/java/com/aritra/notify/components/camPreview/CameraPreview.kt @@ -1,18 +1,197 @@ package com.aritra.notify.components.camPreview + +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.net.Uri +import android.provider.MediaStore +import android.widget.Toast +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.ImageProxy +import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Cameraswitch +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import java.util.Locale @Composable -fun CameraPreview(controller: LifecycleCameraController, modifier: Modifier) { +fun CameraPreview( + controller: LifecycleCameraController, + modifier: Modifier = Modifier, +) { val lifecycleOwner = LocalLifecycleOwner.current - AndroidView(factory = { - PreviewView(it).apply { - this.controller = controller - controller.bindToLifecycle(lifecycleOwner) + AndroidView( + modifier = modifier, + factory = { + PreviewView(it).apply { + this.controller = controller + controller.bindToLifecycle(lifecycleOwner) + } + } + ) +} + +@Composable +fun CameraPreview( + modifier: Modifier = Modifier, + close: () -> Unit, + onImageCaptured: (Uri) -> Unit, +) { + val context = LocalContext.current + val controller = remember { + LifecycleCameraController(context).apply { + setEnabledUseCases(CameraController.IMAGE_CAPTURE) + } + } + + val iconModifier = Modifier.background( + color = MaterialTheme.colorScheme.surface, + shape = CircleShape + ) + + Box( + modifier = modifier.fillMaxSize(), + content = { + CameraPreview( + modifier = Modifier.fillMaxSize(), + controller = controller + ) + + IconButton( + modifier = Modifier + .offset(16.dp, 16.dp) + .then(iconModifier), + onClick = close, + content = { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Navigate back" + ) + } + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceAround, + content = { + IconButton( + modifier = iconModifier, + onClick = { + controller.cameraSelector = + if (controller.cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } + }, + content = { + Icon( + imageVector = Icons.Filled.Cameraswitch, + contentDescription = "Switch Camera" + ) + } + ) + + IconButton( + modifier = iconModifier, + onClick = { + takePhoto( + controller, + context, + onPhotoCaptured = { uri -> + if (uri == null) return@takePhoto + onImageCaptured(uri) + } + ) + }, + content = { + Icon( + imageVector = Icons.Filled.PhotoCamera, + contentDescription = "Click To Capture" + ) + } + ) + } + ) } - }, modifier = modifier) + ) +} + +private fun takePhoto( + controller: LifecycleCameraController, + context: Context, + onPhotoCaptured: (Uri?) -> Unit, +) { + controller.takePicture( + ContextCompat.getMainExecutor(context), + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + super.onCaptureSuccess(image) + val matrix = Matrix().apply { + postRotate(image.imageInfo.rotationDegrees.toFloat()) + if (controller.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) { + postScale(-1f, 1f) + } + } + val bitmap = Bitmap.createBitmap(image.toBitmap(), 0, 0, image.width, image.height, matrix, true) + onPhotoCaptured(bitmap.toUri(context = context)) + Toast.makeText(context, "Photo Attached Successfully", Toast.LENGTH_SHORT).show() + } + + override fun onError(exception: ImageCaptureException) { + super.onError(exception) + Toast.makeText(context, "Something Went Wrong ! Try Again", Toast.LENGTH_SHORT).show() + } + } + ) +} + +private fun Bitmap.toUri( + context: Context, + format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, +): Uri? { + val uri = context.contentResolver.insert( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + ContentValues().apply { + put(MediaStore.Images.Media.MIME_TYPE, "image/${format.name.lowercase(Locale.ROOT)}") + } + ) ?: return null + + context.contentResolver.openOutputStream(uri)?.use { outputStream -> + if (compress(format, 100, outputStream)) { + return uri + } + } + + return null } diff --git a/app/src/main/java/com/aritra/notify/components/topbar/AddEditTopBar.kt b/app/src/main/java/com/aritra/notify/components/topbar/AddEditTopBar.kt deleted file mode 100644 index 0a45487c..00000000 --- a/app/src/main/java/com/aritra/notify/components/topbar/AddEditTopBar.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.aritra.notify.components.topbar - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberModalBottomSheetState -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.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.aritra.notify.R -import com.aritra.notify.components.actions.ShareOption -import com.aritra.notify.components.dialog.TextDialog -import com.aritra.notify.domain.models.Note -import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel -import com.aritra.notify.utils.shareAsImage -import com.aritra.notify.utils.shareAsPdf -import com.aritra.notify.utils.shareNoteAsText - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddEditTopBar( - note: Note, - isNew: Boolean, - onBackPress: () -> Unit, - saveNote: () -> Unit, - updateNote: () -> Unit, -) { - val noteScreenViewModel = hiltViewModel() - var showSheet by remember { mutableStateOf(false) } - val context = LocalContext.current - val skipPartiallyExpanded by remember { mutableStateOf(false) } - val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = skipPartiallyExpanded - ) - val view = LocalView.current - val bitmapSize = view.width to view.height - val deleteDialogVisible = remember { mutableStateOf(false) } - - BackHandler(onBack = { - if (isNew) { - onBackPress() - } else { - updateNote() - } - }) - - CenterAlignedTopAppBar( - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ), - title = { - Text( - text = "", - fontFamily = FontFamily(Font(R.font.poppins_semibold)) - ) - }, - navigationIcon = { - IconButton(onClick = { - if (isNew) { - onBackPress() - } else { - updateNote() - } - }) { - Icon( - painterResource(R.drawable.back), - contentDescription = stringResource(R.string.back) - ) - } - }, - actions = { - if (!isNew) { - IconButton(onClick = { deleteDialogVisible.value = true }) { - Icon( - painter = painterResource(R.drawable.ic_delete), - contentDescription = "Delete" - ) - } - - if (deleteDialogVisible.value) { - TextDialog( - title = stringResource(R.string.warning), - description = stringResource( - R.string.are_you_sure_want_to_delete_these_items_it_cannot_be_recovered - ), - isOpened = deleteDialogVisible.value, - onDismissCallback = { deleteDialogVisible.value = false }, - onConfirmCallback = { - noteScreenViewModel.deleteNote(note) - deleteDialogVisible.value = false - onBackPress() - } - ) - } - } - if (note.title.isNotEmpty() && note.note.isNotEmpty()) { - IconButton(onClick = { showSheet = true }) { - Icon( - painterResource(R.drawable.ic_share), - contentDescription = stringResource(R.string.share) - ) - } - if (showSheet) { - ModalBottomSheet( - onDismissRequest = { showSheet = false }, - sheetState = bottomSheetState - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(16.dp) - ) { - Spacer(modifier = Modifier.height(5.dp)) - ShareOption( - text = stringResource(R.string.share_note_as_text), - icon = painterResource(id = R.drawable.text_icon), - onClick = { - shareNoteAsText(context, note.title, note.note) - showSheet = false - } - ) - Spacer(modifier = Modifier.height(15.dp)) - ShareOption( - text = stringResource(R.string.share_note_as_picture), - icon = painterResource(id = R.drawable.image_icon), - onClick = { - shareAsImage(view, bitmapSize) - showSheet = false - } - ) - Spacer(modifier = Modifier.height(15.dp)) - ShareOption( - text = stringResource(R.string.share_as_pdf), - icon = painterResource(id = R.drawable.pdf_icon), - onClick = { - shareAsPdf(view, "Notify") - showSheet = false - } - ) - } - } - } - IconButton(onClick = { - if (isNew) { - saveNote() - } else { - updateNote() - } - }) { - Icon( - painterResource(R.drawable.save), - contentDescription = stringResource(R.string.save) - ) - } - } - } - ) -} diff --git a/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt b/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt index 0083a9f5..641fb8de 100644 --- a/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt +++ b/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt @@ -1,6 +1,5 @@ package com.aritra.notify.navigation -import android.net.Uri import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween @@ -16,7 +15,6 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,7 +25,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import androidx.navigation.NavType.Companion.IntType @@ -38,8 +35,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.aritra.notify.R import com.aritra.notify.components.drawing.DrawingScreen -import com.aritra.notify.ui.screens.notes.addEditScreen.AddEditScreen -import com.aritra.notify.ui.screens.notes.addEditScreen.AddEditViewModel +import com.aritra.notify.ui.screens.notes.addEditScreen.AddEditRoute import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreen import com.aritra.notify.ui.screens.notes.trash.trashNoteDest import com.aritra.notify.ui.screens.settingsScreen.SettingsScreen @@ -73,11 +69,11 @@ fun NotifyApp(navController: NavHostController = rememberNavController()) { ) } } - ) { + ) { scaffoldPadding -> NavHost( navController = navController, startDestination = NotifyScreens.Notes.name, - modifier = Modifier.padding(it), + modifier = Modifier.padding(scaffoldPadding), enterTransition = { fadeIn( animationSpec = tween(220, delayMillis = 90) @@ -118,20 +114,9 @@ fun NotifyApp(navController: NavHostController = rememberNavController()) { route = "${NotifyScreens.AddEditNotes.name}/{noteId}", arguments = listOf(navArgument("noteId") { type = IntType }) ) { backStack -> - val noteId = backStack.arguments?.getInt("noteId") ?: 0 - val viewModel = hiltViewModel() - val drawing = backStack.savedStateHandle.get("drawing") - - LaunchedEffect(drawing) { - if (drawing != null) { - viewModel.addImages(drawing) - } - } - - AddEditScreen( - noteId = if (noteId < 0) null else noteId, - navigateBack = { navController.popBackStack() }, - showDrawingScreen = { navController.navigate(NotifyScreens.Drawing.name) } + AddEditRoute( + navController = navController, + backStack = backStack ) } diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt new file mode 100644 index 00000000..9ae9df14 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt @@ -0,0 +1,104 @@ +package com.aritra.notify.ui.screens.notes.addEditScreen + +import android.net.Uri +import android.util.Log +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import com.aritra.notify.navigation.NotifyScreens +import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel + +@Composable +fun AddEditRoute( + navController: NavController, + backStack: NavBackStackEntry, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val noteId = (backStack.arguments?.getInt("noteId") ?: 0).let { + if (it < 0) null else it + } + val viewModel = hiltViewModel() + val noteViewModel = hiltViewModel() + val drawing = backStack.savedStateHandle.get("drawing") + + val note by viewModel.note.collectAsState() + + val isNew = remember(noteId) { + noteId == null + } + val navigateBack: () -> Unit = remember { + { + navController.popBackStack() + } + } + val saveNote: (String, String, List) -> Unit = remember(note, isNew) { + { title, description, images -> + Log.e("AddEditRoute", title) + Log.e("AddEditRoute", description) + if (isNew) { + viewModel.insertNote( + title = title, + description = description, + images = images, + onSuccess = { + navigateBack() + Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() + } + ) + } else { + viewModel.updateNote( + title = title, + description = description, + images = images, + onSuccess = { updated -> + if (updated) { + navigateBack() + Toast.makeText(context, "Successfully Updated!", Toast.LENGTH_SHORT).show() + } else { + navigateBack() + } + } + ) + } + } + } + val deleteNote: (() -> Unit) -> Unit = remember(noteId) { + { + if (noteId != null) { + noteViewModel.deleteNote( + noteId = noteId, + onSuccess = it + ) + } + } + } + + LaunchedEffect(drawing) { + if (drawing != null) { + viewModel.addImages(drawing) + } + } + + LaunchedEffect(noteId) { + viewModel.getNoteById(noteId) + } + + AddEditScreen( + modifier = modifier, + note = note, + isNew = isNew, + navigateBack = navigateBack, + showDrawingScreen = { navController.navigate(NotifyScreens.Drawing.name) }, + saveNote = saveNote, + deleteNote = deleteNote + ) +} diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt index 27416682..24b5d9ad 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt @@ -1,84 +1,33 @@ package com.aritra.notify.ui.screens.notes.addEditScreen -import android.Manifest -import android.content.ContentValues -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Matrix import android.net.Uri -import android.provider.MediaStore import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.camera.core.CameraSelector -import androidx.camera.core.ImageCapture.OnImageCapturedCallback -import androidx.camera.core.ImageCaptureException -import androidx.camera.core.ImageProxy -import androidx.camera.view.CameraController -import androidx.camera.view.LifecycleCameraController -import androidx.compose.foundation.horizontalScroll -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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Cameraswitch -import androidx.compose.material.icons.filled.PhotoCamera -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalIconButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf 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.draw.clip import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font @@ -87,440 +36,194 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat -import androidx.hilt.navigation.compose.hiltViewModel import com.aritra.notify.R -import com.aritra.notify.components.actions.BottomSheetOptions -import com.aritra.notify.components.actions.SpeechRecognizerContract +import com.aritra.notify.components.appbar.AddEditBottomBar +import com.aritra.notify.components.appbar.AddEditTopBar import com.aritra.notify.components.camPreview.CameraPreview import com.aritra.notify.components.dialog.TextDialog -import com.aritra.notify.components.topbar.AddEditTopBar -import com.aritra.notify.utils.Const -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale -import kotlin.math.ceil +import com.aritra.notify.domain.models.Note +import com.aritra.notify.ui.theme.NotifyTheme +import java.util.Date -@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun AddEditScreen( - noteId: Int?, + note: Note, + isNew: Boolean, + modifier: Modifier = Modifier, navigateBack: () -> Unit, showDrawingScreen: () -> Unit, + saveNote: (String, String, List) -> Unit, + deleteNote: (() -> Unit) -> Unit, ) { - val viewModel = hiltViewModel() - val context = LocalContext.current - val isNew = noteId == null - - LaunchedEffect(noteId) { - viewModel.getNoteById(noteId) - } - - val note by viewModel.note.collectAsState() - - LaunchedEffect(note) { - Log.i("Screen", note.toString()) - } - - val dateTime by remember { mutableStateOf(Calendar.getInstance().time) } - - var characterCount by remember(note) { mutableIntStateOf(note.title.length + note.note.length) } - val cancelDialogState = remember { mutableStateOf(false) } - var showSheet by remember { mutableStateOf(false) } - val dateFormat = SimpleDateFormat(Const.DATE_FORMAT, Locale.getDefault()) - val timeFormat = SimpleDateFormat(Const.TIME_FORMAT, Locale.getDefault()) - timeFormat.isLenient = false - val currentDate = dateFormat.format(Calendar.getInstance().time) - val currentTime = timeFormat.format(Calendar.getInstance().time).uppercase(Locale.getDefault()) val focus = LocalFocusManager.current - val skipPartiallyExpanded by remember { mutableStateOf(false) } - val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = skipPartiallyExpanded - ) - val wordsPerMinute = 238 // words per minute - val wordCount = remember(note) { derivedStateOf { countWords(note.note) } } - var totalWords by remember(wordCount) { - mutableIntStateOf(wordCount.value) + var title by remember { + mutableStateOf(note.title) } - val readTimeProcess = remember { derivedStateOf { calculateReadTime(totalWords, wordsPerMinute) } } - var readTime by remember { - mutableIntStateOf(readTimeProcess.value) + var description by remember { + mutableStateOf(note.note) } - - val scaffoldState = rememberBottomSheetScaffoldState() - var openCameraBottomSheet by remember { - mutableStateOf(false) + val images = remember { + mutableStateListOf(*note.image.filterNotNull().toTypedArray()) } - - val formattedDateTime = SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()).format(dateTime ?: 0) - val formattedCharacterCount = remember(note) { "${(note.title.length) + (note.note.length)} characters" } - val formattedWordCount = remember(note) { "${countWords(note.note)} words" } - val formattedReadTime = remember(note) { "${calculateReadTime(countWords(note.note), wordsPerMinute)} sec read" } - - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> - viewModel.addImages(*uris.toTypedArray()) + val cancelDialogState = remember { + mutableStateOf(false) } - val controller = remember { - LifecycleCameraController(context).apply { - setEnabledUseCases(CameraController.IMAGE_CAPTURE) - } + var openCameraPreview by remember { + mutableStateOf(false) } - val permissionState = rememberPermissionState( - permission = Manifest.permission.RECORD_AUDIO - ) - val camPermissionState = rememberPermissionState( - permission = Manifest.permission.CAMERA - ) - - // add note - if (isNew) { - SideEffect { - permissionState.launchPermissionRequest() - } - if ((permissionState.status.isGranted || !permissionState.status.isGranted) && - !camPermissionState.status.isGranted - ) { - SideEffect { - camPermissionState.launchPermissionRequest() - } - } + // Makes sure that the title is updated when the note is updated + LaunchedEffect(note.title) { + title = note.title } - val speechRecognizerLauncher = rememberLauncherForActivityResult(contract = SpeechRecognizerContract(), onResult = { - if (it.isNullOrEmpty()) { - return@rememberLauncherForActivityResult - } - for (st in it) { - viewModel.updateDescription("${note.note} $st") - } - }) - val saveEditNote: () -> Unit = if (isNew) { - remember { - { - viewModel.insertNote( - note = note.copy( - dateTime = dateTime - ), - onSuccess = { - navigateBack() - Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() - } - ) - } - } - } else { - remember { - { - viewModel.updateNotes { updated -> - if (updated) { - navigateBack() - Toast.makeText(context, "Successfully Updated!", Toast.LENGTH_SHORT).show() - } else { - navigateBack() - } - } - } - } + // Makes sure that the description is updated when the note is updated + LaunchedEffect(note.note) { + description = note.note } - Scaffold(topBar = { - AddEditTopBar( - note = note, - isNew = isNew, - onBackPress = if (isNew) { - { cancelDialogState.value = true } - } else { - navigateBack - }, - saveNote = if (isNew) { - saveEditNote - } else { - {} - }, - updateNote = if (isNew) { - {} - } else { - saveEditNote - } - ) - }, bottomBar = { - if (isNew) { - Column( - Modifier - .navigationBarsPadding() - .imePadding() - ) { - BottomAppBar( - containerColor = Color.Transparent, - content = { - IconButton(onClick = { showSheet = true }) { - Icon( - modifier = Modifier.size(25.dp), - painter = painterResource(id = R.drawable.add_box_icon), - contentDescription = stringResource(R.string.add_box) - ) - } - if (showSheet) { - ModalBottomSheet( - onDismissRequest = { showSheet = false }, - sheetState = bottomSheetState, - dragHandle = { BottomSheetDefaults.DragHandle() } - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(16.dp) - ) { - BottomSheetOptions( - text = stringResource(R.string.take_image), - icon = painterResource(id = R.drawable.camera_icon), - onClick = { - if (camPermissionState.status.isGranted) { - openCameraBottomSheet = true - } else { - camPermissionState.launchPermissionRequest() - } - showSheet = false - } - ) - BottomSheetOptions( - text = stringResource(R.string.add_image), - icon = painterResource(id = R.drawable.gallery_icon), - onClick = { - launcher.launch( - PickVisualMediaRequest( - mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly - ) - ) - showSheet = false - } - ) - BottomSheetOptions( - text = stringResource(R.string.drawing), - icon = painterResource(id = R.drawable.gallery_icon), - onClick = { - showDrawingScreen() - showSheet = false - } - ) - BottomSheetOptions( - text = stringResource(R.string.speech_to_text), - icon = painterResource(id = R.drawable.mic_icon), - onClick = { - if (permissionState.status.isGranted) { - speechRecognizerLauncher.launch(Unit) - } else { - permissionState.launchPermissionRequest() - } - showSheet = false - } - ) - } - } - } - } - ) - } - } - }) { contentPadding -> - - val scrollState = rememberScrollState() - var descriptionScrollOffset by remember { mutableIntStateOf(0) } - var contentSize by remember { mutableIntStateOf(0) } + Scaffold( + modifier = modifier, + topBar = { + AddEditTopBar( + title = title, + description = description, + isNew = isNew, + onBackPress = if (isNew) { + { cancelDialogState.value = true } + } else { + navigateBack + }, + saveNote = { + Log.e("AddEditScreen app bar", title) + Log.e("AddEditScreen app bar", description) + saveNote(title, description, images) + }, + deleteNote = deleteNote + ) + }, + content = { scaffoldPadding -> + val scrollState = rememberScrollState() + var descriptionScrollOffset by remember { mutableIntStateOf(0) } + var contentSize by remember { mutableIntStateOf(0) } - Box( - modifier = Modifier - .padding(contentPadding) - .onGloballyPositioned { - contentSize = it.size.height - } - ) { Column( modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - ) { - Column( - modifier = Modifier.onGloballyPositioned { layoutCoordinates -> - descriptionScrollOffset = layoutCoordinates.size.height + .padding(scaffoldPadding) + .onGloballyPositioned { + contentSize = it.size.height } - ) { - if (isNew) { - if (note.image.isNotEmpty()) { - LazyRow { - items(note.image.size) { index -> - Box( - Modifier - .height(180.dp) - .width(180.dp) - .padding(4.dp) - .clip(RoundedCornerShape(8.dp)) - ) { - ZoomableAsyncImage( - modifier = Modifier.fillMaxSize(), - model = note.image[index], - contentDescription = stringResource(R.string.image), - contentScale = ContentScale.Crop - ) - FilledTonalIconButton( - modifier = Modifier - .align(Alignment.TopEnd) - .size(25.dp), - onClick = { - viewModel.removeImage(index) - }, - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer.copy( - alpha = 0.6f - ) - ) - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = stringResource(R.string.clear_image) - ) - } + .fillMaxSize() + .verticalScroll(scrollState), + content = { + Column( + modifier = Modifier.onGloballyPositioned { layoutCoordinates -> + descriptionScrollOffset = layoutCoordinates.size.height + }, + content = { + NoteImages( + images = images, + isNew = isNew, + onRemoveImage = { index -> + if (index >= 0 && index < images.lastIndex) { + images.removeAt(index) } } - } - } - } else { - Row( - modifier = Modifier - .horizontalScroll(rememberScrollState()) - ) { - note.image.forEach { uri -> - ZoomableAsyncImage( - modifier = Modifier - .height(180.dp) - .width(180.dp) - .padding(4.dp) - .clip(RoundedCornerShape(8.dp)), - model = uri ?: "", - contentDescription = stringResource(R.string.image), - contentScale = ContentScale.Crop + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = title, + onValueChange = { newTitle -> + title = newTitle + }, + placeholder = { + Text( + stringResource(R.string.title), + fontSize = 24.sp, + fontWeight = FontWeight.W700, + color = Color.Gray, + fontFamily = FontFamily(Font(R.font.poppins_medium)) + ) + }, + textStyle = TextStyle( + fontSize = 24.sp, + fontFamily = FontFamily(Font(R.font.poppins_medium)) + ), + maxLines = Int.MAX_VALUE, + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { + focus.moveFocus(FocusDirection.Down) + } ) - } - } - } - TextField( - modifier = Modifier.fillMaxWidth(), - value = note.title, - onValueChange = { newTitle -> - viewModel.updateTitle(newTitle) - if (isNew) { - characterCount = newTitle.length + note.note.length - } - }, placeholder = { - Text( - stringResource(R.string.title), - fontSize = 24.sp, - fontWeight = FontWeight.W700, - color = Color.Gray, - fontFamily = FontFamily(Font(R.font.poppins_medium)) ) - }, - textStyle = TextStyle( - fontSize = 24.sp, - fontFamily = FontFamily(Font(R.font.poppins_medium)) - ), - maxLines = Int.MAX_VALUE, - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions(onNext = { - focus.moveFocus(FocusDirection.Down) - }) - ) - TextField( - value = if (isNew) { - "$currentDate, $currentTime | $readTime sec read" - } else { - "$formattedDateTime | $formattedReadTime" - }, - onValueChange = { }, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - textStyle = TextStyle( - fontSize = 15.sp, - fontFamily = FontFamily(Font(R.font.poppins_light)) - ), - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text - ) + } ) - TextField( - value = if (isNew) { - "$characterCount characters | $totalWords words" - } else { - "$formattedCharacterCount | $formattedWordCount" - }, - onValueChange = { }, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - textStyle = TextStyle( - fontSize = 15.sp, - fontFamily = FontFamily(Font(R.font.poppins_light)) - ), - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent - ), - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text - ) + + NoteStats( + title = title, + description = description, + dateTime = note.dateTime ) - } - DescriptionTextField( - scrollOffset = descriptionScrollOffset, - contentSize = contentSize, - description = note.note, - parentScrollState = scrollState, - isNewNote = isNew, - onDescriptionChange = { newDescription -> - viewModel.updateDescription(newDescription) - if (isNew) { - characterCount = note.title.length + newDescription.length - totalWords = wordCount.value - readTime = readTimeProcess.value + DescriptionTextField( + scrollOffset = descriptionScrollOffset, + contentSize = contentSize, + description = description, + parentScrollState = scrollState, + isNewNote = isNew, + onDescriptionChange = { newDescription -> + description = newDescription } + ) + } + ) + }, + bottomBar = { + if (isNew) { + AddEditBottomBar( + showDrawingScreen = showDrawingScreen, + showCameraSheet = { + openCameraPreview = true + }, + onImagesSelected = { + images += it + }, + onSpeechRecognized = { + description += " $it" } ) } } + ) + + if (openCameraPreview) { + CameraPreview( + close = { + openCameraPreview = false + }, + onImageCaptured = { image -> + images += image + } + ) } + TextDialog( title = stringResource(R.string.are_you_sure), description = stringResource(R.string.the_text_change_will_not_be_saved), @@ -531,99 +234,22 @@ fun AddEditScreen( cancelDialogState.value = false } ) - - if (openCameraBottomSheet) { - BottomSheetScaffold(scaffoldState = scaffoldState, sheetPeekHeight = 0.dp, sheetContent = {}) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(it) - ) { - CameraPreview(controller = controller, modifier = Modifier.fillMaxSize()) - - IconButton(onClick = { - openCameraBottomSheet = false - }, modifier = Modifier.offset(16.dp, 16.dp)) { - Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "navigate back") - } - Row( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceAround - ) { - IconButton(onClick = { - controller.cameraSelector = - if (controller.cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { - CameraSelector.DEFAULT_FRONT_CAMERA - } else { - CameraSelector.DEFAULT_BACK_CAMERA - } - }) { - Icon(imageVector = Icons.Filled.Cameraswitch, contentDescription = "camera Switch") - } - IconButton(onClick = { - takePhoto(controller, context, onPhotoCaptured = { uri -> - if (uri == null) return@takePhoto - viewModel.addImages(uri) - }) - }) { - Icon(imageVector = Icons.Filled.PhotoCamera, contentDescription = "Click To Capture") - } - } - } - } - } } -fun takePhoto(controller: LifecycleCameraController, context: Context, onPhotoCaptured: (Uri?) -> Unit) { - controller.takePicture( - ContextCompat.getMainExecutor(context), - object : OnImageCapturedCallback() { - override fun onCaptureSuccess(image: ImageProxy) { - super.onCaptureSuccess(image) - val matrix = Matrix().apply { - postRotate(image.imageInfo.rotationDegrees.toFloat()) - if (controller.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) { - postScale(-1f, 1f) - } - } - val bitmap = Bitmap.createBitmap(image.toBitmap(), 0, 0, image.width, image.height, matrix, true) - Toast.makeText(context, "Photo Attached Successfully", Toast.LENGTH_SHORT).show() - onPhotoCaptured(bitmap.toUri(context = context)) - } - - override fun onError(exception: ImageCaptureException) { - super.onError(exception) - Toast.makeText(context, "Something Went Wrong ! Try Again", Toast.LENGTH_SHORT).show() - } - } +@Preview(showBackground = true) +@Composable +private fun AddEditScreenPreview() = NotifyTheme { + AddEditScreen( + note = Note( + title = "Title", + note = "Description", + image = listOf(), + dateTime = Date() + ), + isNew = true, + navigateBack = {}, + showDrawingScreen = {}, + saveNote = { _, _, _ -> }, + deleteNote = {} ) } - -fun Bitmap.toUri(context: Context, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG): Uri? { - val values = ContentValues() - values.put(MediaStore.Images.Media.MIME_TYPE, "image/${format.name.lowercase(Locale.ROOT)}") - val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) - - uri?.let { imageUri -> - context.contentResolver.openOutputStream(imageUri)?.use { outputStream -> - if (compress(format, 100, outputStream)) { - return imageUri - } - } - } - - return null -} - -fun countWords(text: String): Int { - val words = text.split(Regex("\\s+")) - return words.count { it.isNotEmpty() } -} - -private fun calculateReadTime(words: Int, wordsPerMinute: Int): Int { - val minutes = words / wordsPerMinute.toDouble() - return ceil(minutes * 60).toInt() // Convert to seconds -} diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt index 207e682e..9b828319 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope import com.aritra.notify.domain.models.Note import com.aritra.notify.domain.repository.NoteRepository import com.aritra.notify.domain.usecase.SaveSelectedImageUseCase -import com.aritra.notify.utils.toFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -24,35 +23,11 @@ class AddEditViewModel @Inject constructor( private val noteRepository: NoteRepository, ) : AndroidViewModel(application) { + private var noteId: Int? = null + private val _note = MutableStateFlow(Note(dateTime = Date())) val note: StateFlow = _note - fun insertNote(note: Note, onSuccess: () -> Unit) { - viewModelScope.launch(Dispatchers.IO) { - val id: Int = noteRepository.insertNoteToRoom(note).toInt() - - val images = _note.value.image.filterNotNull() - - if (images.isNotEmpty()) { - // update the note with the new image uri - noteRepository.updateNoteInRoom( - note.copy( - id = id, - image = SaveSelectedImageUseCase( - context = getApplication(), - uris = images, - noteId = id - ) - ) - ) - } - - withContext(Dispatchers.Main) { - onSuccess() - } - } - } - fun insertListOfNote(notes: List, onSuccess: () -> Unit) { viewModelScope.launch(Dispatchers.IO) { noteRepository.insertListOfNotesToRoom(notes) @@ -81,6 +56,8 @@ class AddEditViewModel @Inject constructor( } fun getNoteById(noteId: Int?) = viewModelScope.launch(Dispatchers.IO) { + this@AddEditViewModel.noteId = noteId + var note = note.value if (noteId != null) { note = noteRepository.getNoteById(noteId) ?: note @@ -88,41 +65,79 @@ class AddEditViewModel @Inject constructor( _note.update { note } } - fun updateNotes(onSuccess: (updated: Boolean) -> Unit) = viewModelScope.launch(Dispatchers.IO) { + fun insertNote( + title: String, + description: String, + images: List, + onSuccess: () -> Unit, + ) { + viewModelScope.launch(Dispatchers.IO) { + val note = _note.value.copy( + title = title, + note = description + ) + + val id: Int = noteRepository.insertNoteToRoom(note).toInt() + + if (images.isNotEmpty()) { + noteRepository.updateNoteInRoom( + note.copy( + id = id, + // update the note with the new image uri + image = SaveSelectedImageUseCase( + context = getApplication(), + uris = images, + noteId = id + ), + // update the note with the new date time + dateTime = Date() + ) + ) + } + + withContext(Dispatchers.Main) { + onSuccess() + } + } + } + + fun updateNote( + title: String, + description: String, + images: List, + onSuccess: (updated: Boolean) -> Unit, + ) = viewModelScope.launch(Dispatchers.IO) { val newNote = note.value // retrieve the note from the database to check if the image has been modified val oldNote = noteRepository.getNoteById(newNote.id) ?: return@launch - // exit the method if the note has not been modified - if (oldNote.title == newNote.title && oldNote.note == newNote.note && oldNote.image == newNote.image) { + // exit the method if the note has not been modified + if (oldNote.title == title && oldNote.note == description && oldNote.image == images) { // Note has not been modified withContext(Dispatchers.Main) { onSuccess(false) } return@launch } -// if (oldNote.title == newNote.title && oldNote.note == newNote.note && oldNote.image == newNote.image) return@launch - // if the image has been modified, delete the old image - if (oldNote.image != newNote.image) { - oldNote.image.forEach { imageUri -> - imageUri?.toFile(getApplication())?.delete() - } - } + noteRepository.updateNoteInRoom( newNote.copy( + title = title, + note = description, + dateTime = Date() // if the image has not been modified, use the old image uri - image = if (oldNote.image == newNote.image) { - oldNote.image - } else if (newNote.image.filterNotNull().isNotEmpty()) { - // if the image has been modified, save the new image uri - SaveSelectedImageUseCase( - getApplication(), - newNote.image.filterNotNull(), - newNote.id - ) - } else { - emptyList() - } +// image = if (oldNote.image == newNote.image) { +// oldNote.image +// } else if (newNote.image.filterNotNull().isNotEmpty()) { +// // if the image has been modified, save the new image uri +// SaveSelectedImageUseCase( +// getApplication(), +// newNote.image.filterNotNull(), +// newNote.id +// ) +// } else { +// emptyList() +// } ) ) @@ -131,33 +146,9 @@ class AddEditViewModel @Inject constructor( } } - fun updateTitle(title: String) { - _note.update { - it.copy(title = title) - } - } - - fun updateDescription(description: String) { - _note.update { - it.copy(note = description) - } - } - fun addImages(vararg image: Uri?) { _note.update { it.copy(image = it.image + image) } } - - fun removeImage(index: Int) { - _note.update { - it.copy( - image = it.image.toMutableList().apply { - if (index in 0.., + isNew: Boolean, + modifier: Modifier = Modifier, + onRemoveImage: (Int) -> Unit, +) { + if (images.isNotEmpty()) { + LazyRow( + modifier = modifier.fillMaxWidth(), + content = { + items( + count = images.size, + itemContent = { index -> + Box( + Modifier + .size(180.dp) + .padding(4.dp) + .clip(RoundedCornerShape(8.dp)), + content = { + ZoomableAsyncImage( + modifier = Modifier.fillMaxSize(), + model = images[index], + contentDescription = stringResource(R.string.image), + contentScale = ContentScale.Crop + ) + if (isNew) { + FilledTonalIconButton( + modifier = Modifier + .align(Alignment.TopEnd) + .size(25.dp), + onClick = { + onRemoveImage(index) + }, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer.copy( + alpha = 0.6f + ) + ), + content = { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.clear_image) + ) + } + ) + } + } + ) + } + ) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun NoteImagesPreview() = NotifyTheme { + NoteImages( + images = listOf(), + onRemoveImage = {}, + isNew = false + ) +} diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteStats.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteStats.kt new file mode 100644 index 00000000..5c8cf1d3 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteStats.kt @@ -0,0 +1,110 @@ +package com.aritra.notify.ui.screens.notes.addEditScreen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import com.aritra.notify.R +import com.aritra.notify.ui.theme.NotifyTheme +import com.aritra.notify.utils.Const +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.math.ceil + +@Composable +fun NoteStats( + title: String, + description: String, + dateTime: Date?, + modifier: Modifier = Modifier, +) { + val textStyle = TextStyle( + fontSize = 15.sp, + fontFamily = FontFamily(Font(R.font.poppins_light)) + ) + val colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + + val formattedDateTime = remember(dateTime) { + SimpleDateFormat( + Const.DATE_TIME_FORMAT, + Locale.getDefault() + ).format(dateTime ?: Date()) + } + val formattedCharacterCount = remember(title, description) { + "${(title.trim().length) + (description.trim().length)} characters" + } + val formattedWordCount = remember(description) { + "${countWords(description)} words" + } + val formattedReadTime = remember(description) { + "${calculateReadTime(countWords(description))} sec read" + } + + Column( + modifier = modifier, + content = { + TextField( + value = "$formattedDateTime | $formattedReadTime", + onValueChange = { }, + modifier = Modifier.fillMaxWidth(), + readOnly = true, + textStyle = textStyle, + colors = colors, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text + ) + ) + TextField( + modifier = Modifier.fillMaxWidth(), + value = "$formattedCharacterCount | $formattedWordCount", + onValueChange = { }, + readOnly = true, + textStyle = textStyle, + colors = colors, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text + ) + ) + } + ) +} + +private fun countWords(text: String): Int { + val words = text.split(Regex("\\s+")) + return words.count { it.isNotBlank() } +} + +private fun calculateReadTime(words: Int, wordsPerMinute: Int = 238): Int { + val minutes = words / wordsPerMinute.toDouble() + return ceil(minutes * 60).toInt() // Convert to seconds +} + +@Preview(showBackground = true) +@Composable +fun NoteStatsPreview() = NotifyTheme { + NoteStats( + title = "Title", + description = "Description", + dateTime = Date() + ) +} diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt index c3695f94..957dc1a6 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreen.kt @@ -69,7 +69,7 @@ import com.aritra.notify.components.actions.BackPressHandler import com.aritra.notify.components.actions.LayoutToggleButton import com.aritra.notify.components.note.GridNoteCard import com.aritra.notify.components.note.NotesCard -import com.aritra.notify.components.topbar.SelectionModeTopAppBar +import com.aritra.notify.components.appbar.SelectionModeTopAppBar import com.aritra.notify.domain.models.Note import com.aritra.notify.ui.screens.notes.addEditScreen.AddEditViewModel import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt index cfa022a5..f7dab34f 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/homeScreen/NoteScreenViewModel.kt @@ -12,6 +12,7 @@ import com.aritra.notify.domain.repository.NoteRepository import com.aritra.notify.domain.repository.trash.TrashNoteRepo import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.time.LocalDateTime import javax.inject.Inject @@ -23,22 +24,29 @@ class NoteScreenViewModel @Inject constructor( private val dispatcherProvider: DispatcherProvider, ) : AndroidViewModel(application) { - var listOfNotes = homeRepository.getAllNotesFromRoom().asLiveData().map { it.filter { !it.isMovedToTrash } } + var listOfNotes = homeRepository.getAllNotesFromRoom().asLiveData().map { notes -> + notes.filterNot { it.isMovedToTrash } + } - fun deleteNote(note: Note) { + fun deleteNote(noteId: Int, onSuccess: () -> Unit) { viewModelScope.launch(dispatcherProvider.io) { - moveToTrash(note) + val note = homeRepository.getNoteById(noteId) ?: return@launch + moveToTrash(noteId) homeRepository.updateNoteInRoom(note.copy(isMovedToTrash = true)) + withContext(dispatcherProvider.main) { + onSuccess() + } } } - private suspend fun moveToTrash(note: Note) { - trashNote.upsertTrashNote(TrashNote(note.id, LocalDateTime.now())) + private suspend fun moveToTrash(noteId: Int) { + trashNote.upsertTrashNote(TrashNote(noteId, LocalDateTime.now())) } + fun deleteListOfNote(noteList: List) { viewModelScope.launch(dispatcherProvider.io) { noteList.forEach { - moveToTrash(it) + moveToTrash(it.id) homeRepository.updateNoteInRoom(it.copy(isMovedToTrash = true)) } } From 2eac9922b2b5cdb90dc4c6c979336bc362602fc9 Mon Sep 17 00:00:00 2001 From: Jeffery Orazulike Date: Thu, 26 Oct 2023 15:58:27 +0100 Subject: [PATCH 04/23] chore(drawing): changed DrawingScreen to a component --- .../components/drawing/DrawingScreen.kt | 5 +++- .../com/aritra/notify/navigation/NotifyApp.kt | 18 +------------- .../aritra/notify/navigation/NotifyScreens.kt | 2 -- .../notes/addEditScreen/AddEditRoute.kt | 10 -------- .../notes/addEditScreen/AddEditScreen.kt | 24 +++++++++++++++---- .../notes/addEditScreen/AddEditViewModel.kt | 6 ----- .../screens/notes/addEditScreen/NoteImages.kt | 1 + 7 files changed, 26 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/aritra/notify/components/drawing/DrawingScreen.kt b/app/src/main/java/com/aritra/notify/components/drawing/DrawingScreen.kt index c66506e1..cdb989de 100644 --- a/app/src/main/java/com/aritra/notify/components/drawing/DrawingScreen.kt +++ b/app/src/main/java/com/aritra/notify/components/drawing/DrawingScreen.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Picture import android.net.Uri +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -42,8 +43,10 @@ fun DrawingScreen( val picture = remember { Picture() } val context = LocalContext.current + BackHandler(onBack = onBack) + Scaffold( - modifier = modifier, + modifier = modifier.fillMaxSize(), topBar = { Row( content = { diff --git a/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt b/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt index 641fb8de..dc671952 100644 --- a/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt +++ b/app/src/main/java/com/aritra/notify/navigation/NotifyApp.kt @@ -34,7 +34,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.aritra.notify.R -import com.aritra.notify.components.drawing.DrawingScreen import com.aritra.notify.ui.screens.notes.addEditScreen.AddEditRoute import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreen import com.aritra.notify.ui.screens.notes.trash.trashNoteDest @@ -45,8 +44,7 @@ fun NotifyApp(navController: NavHostController = rememberNavController()) { val bottomNavItem = getBottomNavItems() val screensWithHiddenNavBar = listOf( "${NotifyScreens.AddEditNotes.name}/{noteId}", - NotifyScreens.TrashNoteScreen.name, - NotifyScreens.Drawing.name + NotifyScreens.TrashNoteScreen.name ) val backStackEntry = navController.currentBackStackEntryAsState() @@ -126,20 +124,6 @@ fun NotifyApp(navController: NavHostController = rememberNavController()) { SettingsScreen(controller = navController) } trashNoteDest(navController) - - composable( - route = NotifyScreens.Drawing.name - ) { - DrawingScreen( - onBack = { - navController.popBackStack() - }, - onSave = { drawing -> - navController.popBackStack() - navController.currentBackStackEntry?.savedStateHandle?.set("drawing", drawing) - } - ) - } } } } diff --git a/app/src/main/java/com/aritra/notify/navigation/NotifyScreens.kt b/app/src/main/java/com/aritra/notify/navigation/NotifyScreens.kt index 1fb42789..520d6d61 100644 --- a/app/src/main/java/com/aritra/notify/navigation/NotifyScreens.kt +++ b/app/src/main/java/com/aritra/notify/navigation/NotifyScreens.kt @@ -7,6 +7,4 @@ sealed class NotifyScreens(val name: String) { data object TodoHome : NotifyScreens("todo_home") data object Settings : NotifyScreens("setting") data object TrashNoteScreen : NotifyScreens("trash_note_route") - - data object Drawing : NotifyScreens("drawing") } diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt index 9ae9df14..e2d8ce33 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt @@ -28,7 +28,6 @@ fun AddEditRoute( } val viewModel = hiltViewModel() val noteViewModel = hiltViewModel() - val drawing = backStack.savedStateHandle.get("drawing") val note by viewModel.note.collectAsState() @@ -42,8 +41,6 @@ fun AddEditRoute( } val saveNote: (String, String, List) -> Unit = remember(note, isNew) { { title, description, images -> - Log.e("AddEditRoute", title) - Log.e("AddEditRoute", description) if (isNew) { viewModel.insertNote( title = title, @@ -82,12 +79,6 @@ fun AddEditRoute( } } - LaunchedEffect(drawing) { - if (drawing != null) { - viewModel.addImages(drawing) - } - } - LaunchedEffect(noteId) { viewModel.getNoteById(noteId) } @@ -97,7 +88,6 @@ fun AddEditRoute( note = note, isNew = isNew, navigateBack = navigateBack, - showDrawingScreen = { navController.navigate(NotifyScreens.Drawing.name) }, saveNote = saveNote, deleteNote = deleteNote ) diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt index 24b5d9ad..b010d76e 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditScreen.kt @@ -43,6 +43,7 @@ import com.aritra.notify.components.appbar.AddEditBottomBar import com.aritra.notify.components.appbar.AddEditTopBar import com.aritra.notify.components.camPreview.CameraPreview import com.aritra.notify.components.dialog.TextDialog +import com.aritra.notify.components.drawing.DrawingScreen import com.aritra.notify.domain.models.Note import com.aritra.notify.ui.theme.NotifyTheme import java.util.Date @@ -53,7 +54,6 @@ fun AddEditScreen( isNew: Boolean, modifier: Modifier = Modifier, navigateBack: () -> Unit, - showDrawingScreen: () -> Unit, saveNote: (String, String, List) -> Unit, deleteNote: (() -> Unit) -> Unit, ) { @@ -74,6 +74,9 @@ fun AddEditScreen( var openCameraPreview by remember { mutableStateOf(false) } + var openDrawingScreen by remember { + mutableStateOf(false) + } // Makes sure that the title is updated when the note is updated LaunchedEffect(note.title) { @@ -128,7 +131,7 @@ fun AddEditScreen( images = images, isNew = isNew, onRemoveImage = { index -> - if (index >= 0 && index < images.lastIndex) { + if (index >= 0 && index <= images.lastIndex) { images.removeAt(index) } } @@ -198,7 +201,9 @@ fun AddEditScreen( bottomBar = { if (isNew) { AddEditBottomBar( - showDrawingScreen = showDrawingScreen, + showDrawingScreen = { + openDrawingScreen = true + }, showCameraSheet = { openCameraPreview = true }, @@ -224,6 +229,18 @@ fun AddEditScreen( ) } + if (openDrawingScreen) { + DrawingScreen( + onBack = { + openDrawingScreen = false + }, + onSave = { + images += it + openDrawingScreen = false + } + ) + } + TextDialog( title = stringResource(R.string.are_you_sure), description = stringResource(R.string.the_text_change_will_not_be_saved), @@ -248,7 +265,6 @@ private fun AddEditScreenPreview() = NotifyTheme { ), isNew = true, navigateBack = {}, - showDrawingScreen = {}, saveNote = { _, _, _ -> }, deleteNote = {} ) diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt index 9b828319..270fea82 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditViewModel.kt @@ -145,10 +145,4 @@ class AddEditViewModel @Inject constructor( onSuccess(true) } } - - fun addImages(vararg image: Uri?) { - _note.update { - it.copy(image = it.image + image) - } - } } diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteImages.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteImages.kt index 4cb567e0..6c2882a8 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteImages.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteImages.kt @@ -39,6 +39,7 @@ fun NoteImages( content = { items( count = images.size, + key = { index -> images[index] }, itemContent = { index -> Box( Modifier From 9157c02c13a6c59ce35ae584d4c8a81b39ca06fb Mon Sep 17 00:00:00 2001 From: Jeffery Orazulike Date: Thu, 26 Oct 2023 16:01:45 +0100 Subject: [PATCH 05/23] chore(lint): remove unused import --- .../notify/ui/screens/notes/addEditScreen/AddEditRoute.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt index e2d8ce33..9e0fe9dd 100644 --- a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/AddEditRoute.kt @@ -1,7 +1,6 @@ package com.aritra.notify.ui.screens.notes.addEditScreen import android.net.Uri -import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -13,7 +12,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import com.aritra.notify.navigation.NotifyScreens import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel @Composable From c62c22619ac10f87eed8c33a173566f941b78548 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:04:12 +0000 Subject: [PATCH 06/23] fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.32.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b51dfc4..946fe0a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -accompanistPermissions = "0.28.0" +accompanistPermissions = "0.32.0" accompanistSystemuicontroller = "0.32.0" activity-compose = "1.8.0" androidx-junit = "1.1.5" From ab1425e10a51da6be7b27fb45da264ef99382d14 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:06:02 +0000 Subject: [PATCH 07/23] chore(deps): update dependency org.jlleitschuh.gradle.ktlint to v11.6.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b51dfc4..1ccde894 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ dagger-hilt-android = "2.48.1" devtools-ksp = "1.9.0-1.0.12" zoomable = "0.6.2" zoomableImageCoil = "0.6.2" -ktlint = "11.6.0" +ktlint = "11.6.1" camerax_version = "1.4.0-alpha01" [libraries] From 2e4e9944631be04c87259a1b5286ee602e4645e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:07:52 +0000 Subject: [PATCH 08/23] chore(deps): update actions/checkout action to v4 --- .github/workflows/ci_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index ce8554d9..facdcef7 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout the code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v2 From e7105061f3b4dd244823609928af21eca5051ef9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:07:55 +0000 Subject: [PATCH 09/23] chore(deps): update actions/setup-java action to v3 --- .github/workflows/ci_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index ce8554d9..b182d520 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v2 - name: Set up JDK 17 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17' From 2d415dbb0d872d2c7f61d2e6eedcb3f895c84fb8 Mon Sep 17 00:00:00 2001 From: salmanA169 Date: Thu, 26 Oct 2023 21:14:11 +0300 Subject: [PATCH 10/23] fix when edit note not updated removed reminderDateTime - create alarmScheduler to schedule alarm at specific time - added ReminderNoteNotificationBroadcast.kt to send notification when alarm fire at specific dateTime - provide notificationManager , AlarmScheduler dependency - added new utils for LocalDateTime for triggerDateTime easily - Implement NotificationChannel - added new icon for notification --- .idea/gradle.xml | 1 + app/src/main/AndroidManifest.xml | 3 + .../com/aritra/notify/NotifyApplication.kt | 8 ++- .../notify/core/alarm/AlarmScheduler.kt | 13 ++++ .../notify/core/alarm/AlarmSchedulerImpl.kt | 43 ++++++++++++ .../core/notification/NotificationChannel.kt | 22 +++++++ .../java/com/aritra/notify/di/AppModule.kt | 11 ++-- .../com/aritra/notify/di/DispatcherModule.kt | 7 ++ .../domain/repository/NoteRepository.kt | 16 ++++- .../ReminderNoteNotificationBroadcast.kt | 24 +++++++ .../notes/addEditScreen/AddEditScreen.kt | 62 ++++++++++-------- .../notes/addEditScreen/AddEditViewModel.kt | 12 +++- .../aritra/notify/utils/FormatLocalDate.kt | 5 ++ .../aritra/notify/utils/NotificationUtil.kt | 17 +++++ .../aritra/notify/utils/PendingIntentUtil.kt | 22 +++++++ .../main/res/drawable-anydpi/notify_icon.xml | 23 +++++++ .../main/res/drawable-hdpi/notify_icon.png | Bin 0 -> 388 bytes .../main/res/drawable-mdpi/notify_icon.png | Bin 0 -> 276 bytes .../main/res/drawable-xhdpi/notify_icon.png | Bin 0 -> 516 bytes .../main/res/drawable-xxhdpi/notify_icon.png | Bin 0 -> 791 bytes .../java/com/aritra/notify/ExampleUnitTest.kt | 5 ++ 21 files changed, 258 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/aritra/notify/core/alarm/AlarmScheduler.kt create mode 100644 app/src/main/java/com/aritra/notify/core/alarm/AlarmSchedulerImpl.kt create mode 100644 app/src/main/java/com/aritra/notify/core/notification/NotificationChannel.kt create mode 100644 app/src/main/java/com/aritra/notify/reciever/ReminderNoteNotificationBroadcast.kt create mode 100644 app/src/main/java/com/aritra/notify/utils/NotificationUtil.kt create mode 100644 app/src/main/java/com/aritra/notify/utils/PendingIntentUtil.kt create mode 100644 app/src/main/res/drawable-anydpi/notify_icon.xml create mode 100644 app/src/main/res/drawable-hdpi/notify_icon.png create mode 100644 app/src/main/res/drawable-mdpi/notify_icon.png create mode 100644 app/src/main/res/drawable-xhdpi/notify_icon.png create mode 100644 app/src/main/res/drawable-xxhdpi/notify_icon.png diff --git a/.idea/gradle.xml b/.idea/gradle.xml index cb865f69..9f47dfb4 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,6 +4,7 @@