diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index fe6febe1..73b6ec28 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -1,17 +1,23 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 index 4a3dd080..83cff2b8 100644 --- a/app/src/main/java/com/aritra/notify/components/appbar/AddEditBottomBar.kt +++ b/app/src/main/java/com/aritra/notify/components/appbar/AddEditBottomBar.kt @@ -44,6 +44,7 @@ fun AddEditBottomBar( showDrawingScreen: () -> Unit, showCameraSheet: () -> Unit, onReminderDateTime: () -> Unit, + addTodo: () -> Unit, ) { var showSheet by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState() @@ -145,6 +146,14 @@ fun AddEditBottomBar( showSheet = false } ) + BottomSheetOptions( + text = stringResource(R.string.add_todo), + icon = painterResource(id = R.drawable.add_box_icon), + onClick = { + addTodo() + 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 index 9be1d2ca..248d51e3 100644 --- a/app/src/main/java/com/aritra/notify/components/appbar/AddEditTopBar.kt +++ b/app/src/main/java/com/aritra/notify/components/appbar/AddEditTopBar.kt @@ -50,16 +50,14 @@ fun AddEditTopBar( { if (isNew) { onBackPress() + } else if (description.isBlank()) { + Toast.makeText( + context, + "Your note cannot be blank", + Toast.LENGTH_SHORT + ).show() } else { - if (description.isBlank()) { - Toast.makeText( - context, - "Your note cannot be blank", - Toast.LENGTH_SHORT - ).show() - } else { - saveNote() - } + saveNote() } } } diff --git a/app/src/main/java/com/aritra/notify/data/converters/CollectionConverter.kt b/app/src/main/java/com/aritra/notify/data/converters/CollectionConverter.kt new file mode 100644 index 00000000..bab0005c --- /dev/null +++ b/app/src/main/java/com/aritra/notify/data/converters/CollectionConverter.kt @@ -0,0 +1,36 @@ +package com.aritra.notify.data.converters + +import android.net.Uri +import androidx.room.TypeConverter +import com.aritra.notify.domain.models.Todo +import com.aritra.notify.utils.fromJson +import com.aritra.notify.utils.toString +import com.google.gson.Gson + +object CollectionConverter { + + @TypeConverter + fun fromUriListToString(value: List): String { + return Gson().toJson(value.map { it?.toString() ?: "" }) + } + + @TypeConverter + fun fromStringToUriList(value: String): List { + return try { + val stringList = Gson().fromJson>(value) // using extension function + stringList?.map { Uri.parse(it) } ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } + + @TypeConverter + fun fromTodoListToString(value: List?): String? { + return Gson().toString(value) + } + + @TypeConverter + fun fromStringToTodoList(value: String?): List? { + return Gson().fromJson(value) + } +} diff --git a/app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt b/app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt deleted file mode 100644 index 4057c090..00000000 --- a/app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.aritra.notify.data.converters - -import android.net.Uri -import androidx.room.TypeConverter -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken - -object ListConverter { - - private inline fun Gson.fromJson(json: String): T = - fromJson(json, object : TypeToken() {}.type) - - @TypeConverter - fun fromListToString(value: List): String { - return Gson().toJson(value.map { it?.toString() ?: "" }) - } - - @TypeConverter - fun fromStringToList(value: String): List { - return try { - val stringList = Gson().fromJson>(value) // using extension function - stringList.map { Uri.parse(it) } - } catch (e: Exception) { - listOf() - } - } -} diff --git a/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt b/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt index 6af8577b..e89b0f5e 100644 --- a/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt +++ b/app/src/main/java/com/aritra/notify/data/db/NoteDatabase.kt @@ -5,8 +5,8 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import com.aritra.notify.data.converters.CollectionConverter import com.aritra.notify.data.converters.DateTypeConverter -import com.aritra.notify.data.converters.ListConverter import com.aritra.notify.data.converters.LocalDateTimeConverter import com.aritra.notify.data.converters.UriConverter import com.aritra.notify.data.dao.NoteDao @@ -15,7 +15,12 @@ import com.aritra.notify.domain.models.Note import com.aritra.notify.domain.models.TrashNote @Database(entities = [Note::class, TrashNote::class], version = 5) -@TypeConverters(DateTypeConverter::class, UriConverter::class, ListConverter::class, LocalDateTimeConverter::class) +@TypeConverters( + DateTypeConverter::class, + UriConverter::class, + CollectionConverter::class, + LocalDateTimeConverter::class +) abstract class NoteDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao 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 13656963..e8012826 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 @@ -18,6 +18,7 @@ data class Note( var note: String = "", var dateTime: Date? = null, var image: List = emptyList(), + var checklist: List = emptyList(), @ColumnInfo(defaultValue = "false") var isMovedToTrash: Boolean = false, var reminderDateTime: LocalDateTime? = null, diff --git a/app/src/main/java/com/aritra/notify/domain/models/Todo.kt b/app/src/main/java/com/aritra/notify/domain/models/Todo.kt new file mode 100644 index 00000000..317e211e --- /dev/null +++ b/app/src/main/java/com/aritra/notify/domain/models/Todo.kt @@ -0,0 +1,14 @@ +package com.aritra.notify.domain.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * @property title the title of the todo + * @property isChecked the status of the todo + */ +@Parcelize +data class Todo( + val title: String = "", + val isChecked: Boolean = false, +) : Parcelable 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 06235b4c..b86570ae 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 @@ -12,6 +12,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController +import com.aritra.notify.domain.models.Todo import com.aritra.notify.ui.screens.notes.homeScreen.NoteScreenViewModel import java.time.LocalDateTime @@ -38,13 +39,14 @@ fun AddEditRoute( navController.popBackStack() } } - val saveNote: (String, String, List) -> Unit = remember(note, isNew) { - { title, description, images -> + val saveNote: (String, String, List, List) -> Unit = remember(note, isNew) { + { title, description, images, checklist -> if (isNew) { viewModel.insertNote( title = title, description = description, images = images, + checklist = checklist, onSuccess = { navigateBack() Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() @@ -55,12 +57,11 @@ fun AddEditRoute( title = title, description = description, images = images, + checklist = checklist, onSuccess = { updated -> + navigateBack() if (updated) { - navigateBack() Toast.makeText(context, "Successfully Updated!", Toast.LENGTH_SHORT).show() - } else { - navigateBack() } } ) 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 a8c57769..b236868c 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,7 +1,6 @@ package com.aritra.notify.ui.screens.notes.addEditScreen import android.net.Uri -import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -53,6 +52,7 @@ import com.aritra.notify.components.dialog.DateTimeDialog 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.domain.models.Todo import com.aritra.notify.ui.theme.NotifyTheme import com.aritra.notify.utils.formatReminderDateTime import java.time.LocalDateTime @@ -64,7 +64,7 @@ fun AddEditScreen( isNew: Boolean, modifier: Modifier = Modifier, navigateBack: () -> Unit, - saveNote: (String, String, List) -> Unit, + saveNote: (String, String, List, List) -> Unit, deleteNote: (() -> Unit) -> Unit, onUpdateReminderDateTime: (LocalDateTime?) -> Unit, ) { @@ -76,8 +76,14 @@ fun AddEditScreen( var description by remember { mutableStateOf(note.note) } + var showAddTodo by remember { + mutableStateOf(false) + } val images = remember { - mutableStateListOf(*note.image.filterNotNull().toTypedArray()) + mutableStateListOf() + } + val checklist = remember { + mutableStateListOf() } val cancelDialogState = remember { mutableStateOf(false) @@ -91,19 +97,25 @@ fun AddEditScreen( var openDrawingScreen by remember { mutableStateOf(false) } - var shouldShowDialogDateTime by remember { mutableStateOf(false) } + // Makes sure that the title is updated when the note is updated LaunchedEffect(note.title) { title = note.title } - - // Makes sure that the description is updated when the note is updated LaunchedEffect(note.note) { description = note.note } + LaunchedEffect(note.image) { + images.clear() + images.addAll(note.image.filterNotNull()) + } + LaunchedEffect(note.checklist) { + checklist.clear() + checklist.addAll(note.checklist.sortedBy { it.isChecked }) + } Scaffold( modifier = modifier, @@ -118,9 +130,7 @@ fun AddEditScreen( navigateBack }, saveNote = { - Log.e("AddEditScreen app bar", title) - Log.e("AddEditScreen app bar", description) - saveNote(title, description, images) + saveNote(title, description, images, checklist) }, deleteNote = deleteNote ) @@ -225,6 +235,27 @@ fun AddEditScreen( }, modifier = Modifier) } + NoteChecklist( + checklist = checklist, + showAddTodo = showAddTodo, + onAdd = { + checklist.add(Todo(title = it)) + }, + onDelete = { + checklist.removeAt(it) + }, + onUpdate = { index, newTitle -> + checklist[index] = checklist[index].copy(title = newTitle) + }, + onToggle = { index -> + checklist[index] = checklist[index].copy(isChecked = !checklist[index].isChecked) + checklist.sortBy { it.isChecked } + }, + hideAddTodo = { + showAddTodo = false + } + ) + DescriptionTextField( scrollOffset = descriptionScrollOffset, contentSize = contentSize, @@ -255,6 +286,9 @@ fun AddEditScreen( }, onReminderDateTime = { shouldShowDialogDateTime = true + }, + addTodo = { + showAddTodo = true } ) } @@ -283,6 +317,7 @@ fun AddEditScreen( } ) } + TextDialog( title = stringResource(R.string.are_you_sure), description = stringResource(R.string.the_text_change_will_not_be_saved), @@ -316,7 +351,7 @@ private fun AddEditScreenPreview() = NotifyTheme { ), isNew = true, navigateBack = {}, - saveNote = { _, _, _ -> }, + saveNote = { _, _, _, _ -> }, deleteNote = {}, onUpdateReminderDateTime = {} ) 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 61d190c7..884568c0 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 @@ -5,6 +5,7 @@ import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.aritra.notify.domain.models.Note +import com.aritra.notify.domain.models.Todo import com.aritra.notify.domain.repository.NoteRepository import com.aritra.notify.domain.usecase.SaveSelectedImageUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -70,12 +71,14 @@ class AddEditViewModel @Inject constructor( title: String, description: String, images: List, + checklist: List, onSuccess: () -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { val note = _note.value.copy( title = title, - note = description + note = description, + checklist = checklist ) val id: Int = noteRepository.insertNoteToRoom(note).toInt() @@ -101,18 +104,12 @@ class AddEditViewModel @Inject constructor( } } } - fun updateReminderDateTime(dateTime: LocalDateTime?) { - _note.update { - it.copy( - reminderDateTime = dateTime, - isReminded = false - ) - } - } + fun updateNote( title: String, description: String, images: List, + checklist: List, onSuccess: (updated: Boolean) -> Unit, ) = viewModelScope.launch(Dispatchers.IO) { val newNote = note.value @@ -120,7 +117,11 @@ class AddEditViewModel @Inject constructor( val oldNote = noteRepository.getNoteById(newNote.id) ?: return@launch // exit the method if the note has not been modified - if (oldNote.title == title && oldNote.note == description && oldNote.image == images && + if ( + oldNote.title == title && + oldNote.note == description && + oldNote.image == images && + checklist == oldNote.checklist && oldNote.reminderDateTime == newNote.reminderDateTime ) { // Note has not been modified @@ -134,7 +135,8 @@ class AddEditViewModel @Inject constructor( newNote.copy( title = title, note = description, - dateTime = Date() + dateTime = Date(), + checklist = checklist // if the image has not been modified, use the old image uri // image = if (oldNote.image == newNote.image) { // oldNote.image @@ -155,4 +157,13 @@ class AddEditViewModel @Inject constructor( onSuccess(true) } } + + fun updateReminderDateTime(dateTime: LocalDateTime?) { + _note.update { + it.copy( + reminderDateTime = dateTime, + isReminded = false + ) + } + } } diff --git a/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteChecklist.kt b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteChecklist.kt new file mode 100644 index 00000000..e656f554 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/ui/screens/notes/addEditScreen/NoteChecklist.kt @@ -0,0 +1,196 @@ +package com.aritra.notify.ui.screens.notes.addEditScreen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.aritra.notify.R +import com.aritra.notify.domain.models.Todo + +@Composable +fun NoteChecklist( + checklist: List, + showAddTodo: Boolean, + modifier: Modifier = Modifier, + onAdd: (String) -> Unit, + onDelete: (Int) -> Unit, + onUpdate: (Int, String) -> Unit, + onToggle: (Int) -> Unit, + hideAddTodo: () -> Unit, +) { + if (checklist.isNotEmpty()) { + Column( + modifier = modifier + .fillMaxWidth(), + content = { + checklist.forEachIndexed { index, todo -> + ChecklistItem( + todo = todo, + onValueChange = { + onUpdate(index, it) + }, + onCheckedChange = { + onToggle(index) + }, + onDelete = { + onDelete(index) + } + ) + if (index < checklist.lastIndex) { + Divider() + } + } + } + ) + } + + if (showAddTodo) { + AddEditTodoField( + text = "", + onUpdate = onAdd, + onDismiss = hideAddTodo + ) + } +} + +@Composable +private fun ChecklistItem( + todo: Todo, + modifier: Modifier = Modifier, + onValueChange: (String) -> Unit, + onCheckedChange: (Boolean) -> Unit, + onDelete: () -> Unit, +) { + var showEditModal by remember { + mutableStateOf(false) + } + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + content = { + Checkbox( + checked = todo.isChecked, + onCheckedChange = onCheckedChange + ) + Text( + todo.title, + modifier = Modifier.weight(1f), + style = TextStyle( + textDecoration = if (todo.isChecked) TextDecoration.LineThrough else TextDecoration.None + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + IconButton( + onClick = { + showEditModal = true + }, + content = { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit" + ) + } + ) + IconButton( + onClick = onDelete, + content = { + Icon( + painter = painterResource(id = R.drawable.ic_delete), + contentDescription = "Delete" + ) + } + ) + } + ) + + if (showEditModal) { + AddEditTodoField( + text = todo.title, + onUpdate = onValueChange, + onDismiss = { + showEditModal = false + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AddEditTodoField( + text: String, + onUpdate: (String) -> Unit, + onDismiss: () -> Unit, +) { + val focusRequester = remember { FocusRequester() } + var value by remember(text) { + mutableStateOf(text) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = Color.Transparent, + dragHandle = null, + shape = RectangleShape, + content = { + TextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = value, + onValueChange = { + value = it + }, + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { + if (value.isNotBlank()) { + onUpdate(value) + } + onDismiss() + } + ), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ) + ) + } + ) +} diff --git a/app/src/main/java/com/aritra/notify/utils/Json.kt b/app/src/main/java/com/aritra/notify/utils/Json.kt new file mode 100644 index 00000000..c644d400 --- /dev/null +++ b/app/src/main/java/com/aritra/notify/utils/Json.kt @@ -0,0 +1,18 @@ +package com.aritra.notify.utils + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +inline fun Gson.fromJson(json: String?): T? { + if (json == null) { + return null + } + return this.fromJson(json, object : TypeToken() {}.type) +} + +inline fun Gson.toString(data: T?): String? { + if (data == null) { + return null + } + return toJson(data) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbaf64c3..b094bfee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ The text change will not be saved Notes Title + Add Todo Share as Text Share as Image Cancel