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