From 74e91f20811eded48eb03ae00b9d388178944d9f Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Wed, 4 Oct 2023 21:18:05 +0530 Subject: [PATCH 1/4] Added feat: User can pick multiple image + zoom and pan across image --- app/build.gradle.kts | 6 ++ .../notify/components/note/GridNoteCard.kt | 30 ++++-- .../aritra/notify/components/note/NoteCard.kt | 33 +++++-- .../notify/data/converters/ListConverter.kt | 29 ++++++ .../com/aritra/notify/data/db/NoteDatabase.kt | 2 +- .../com/aritra/notify/domain/models/Note.kt | 5 +- .../domain/repository/BackupRepository.kt | 50 +++++----- .../usecase/SaveSelectedImageUseCase.kt | 22 +++-- .../notes/addEditScreen/AddEditScreen.kt | 99 +++++++++++-------- .../notes/addEditScreen/AddEditViewModel.kt | 22 +++-- gradle/libs.versions.toml | 6 ++ 11 files changed, 205 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca2e5bce..474424f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -123,4 +123,10 @@ dependencies { // In-App Update implementation(libs.app.update) implementation(libs.app.update.ktx) + + // Gson + implementation(libs.gson) + + implementation(libs.zoomable) + implementation(libs.zoomable.image.coil) } \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt b/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt index 00086d66..f811205f 100644 --- a/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt +++ b/app/src/main/java/com/aritra/notify/components/note/GridNoteCard.kt @@ -1,6 +1,7 @@ package com.aritra.notify.components.note import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardDefaults import androidx.compose.material3.OutlinedCard @@ -54,7 +56,6 @@ fun GridNoteCard( modifier = Modifier .padding(10.dp) .fillMaxWidth() - .clip(RoundedCornerShape(15.dp)) //make click effect rounded .clickable { navigateToUpdateNoteScreen(notesModel.id) }, shape = RoundedCornerShape(15.dp) ) { @@ -63,14 +64,25 @@ fun GridNoteCard( .padding(16.dp) .fillMaxWidth() ) { - AsyncImage( - model = ImageRequest.Builder(context) - .data(painter.value) - .build(), - contentDescription = "Image", - modifier = Modifier.fillMaxWidth() - - ) + if (painter.value.isNotEmpty()) { + Row( + modifier = Modifier + .height(80.dp) + .clip(RoundedCornerShape(8.dp)) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + painter.value.forEach { + AsyncImage( + model = ImageRequest.Builder(context) + .data(it ?: "") + .build(), + contentDescription = "Image", + modifier = Modifier.fillMaxWidth() + ) + } + } + } Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() 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 79e3db8c..133712dc 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 @@ -1,13 +1,16 @@ package com.aritra.notify.components.note import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardDefaults import androidx.compose.material3.OutlinedCard @@ -54,14 +57,26 @@ fun NotesCard( .fillMaxWidth() .padding(16.dp) ) { - AsyncImage( - model = ImageRequest.Builder(context) - .data(painter.value) - .build(), - contentDescription = "Image", - modifier = Modifier.fillMaxSize(), - ) - Spacer(modifier = Modifier.height(10.dp)) + if (painter.value.isNotEmpty()) { + Row( + modifier = Modifier + .height(80.dp) + .clip(RoundedCornerShape(8.dp)) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + painter.value.forEach { + AsyncImage( + model = ImageRequest.Builder(context) + .data(it ?: "") + .build(), + contentDescription = "Image", + modifier = Modifier.fillMaxWidth() + ) + } + } + Spacer(modifier = Modifier.height(10.dp)) + } Text( text = noteModel.title, fontSize = 22.sp, 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 new file mode 100644 index 00000000..604c43ce --- /dev/null +++ b/app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt @@ -0,0 +1,29 @@ +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() + } + } +} \ No newline at end of file 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 b67930e4..7ad7eac3 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 @@ -7,7 +7,7 @@ import androidx.room.RoomDatabase import com.aritra.notify.data.dao.NoteDao import com.aritra.notify.domain.models.Note -@Database(entities = [Note::class], version = 2) +@Database(entities = [Note::class], version = 3) 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 1ca0cc72..0d50c601 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 @@ -7,17 +7,18 @@ import androidx.room.PrimaryKey import androidx.room.TypeConverters import com.aritra.notify.data.converters.DateTypeConverter import com.aritra.notify.data.converters.UriConverter +import com.aritra.notify.data.converters.ListConverter import kotlinx.parcelize.Parcelize import java.util.Date @Parcelize @Entity(tableName = "note") -@TypeConverters(DateTypeConverter::class, UriConverter::class) +@TypeConverters(DateTypeConverter::class, UriConverter::class, ListConverter::class) data class Note( @PrimaryKey(autoGenerate = true) var id: Int = 0, var title: String, var note: String, var dateTime: Date?, - var image: Uri? + var image: List ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt b/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt index 56dea6fc..d6c7d2b1 100644 --- a/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt +++ b/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt @@ -48,8 +48,8 @@ class BackupRepository( // write the notes to the csv file provider.noteDao().getAllNotes().first().forEach { note -> // write the image to the backup directory if it exists - val image = note.image?.let { image -> - val imageName = "image_${note.id}.webp" + val images = note.image.filterNotNull().mapIndexed { index, image -> + val imageName = "image_${note.id}_($index).webp" val imageFile = File(backupDir, imageName) context.contentResolver.openInputStream(image)?.use { inputStream -> imageFile.outputStream().use { outputStream -> @@ -64,8 +64,7 @@ class BackupRepository( note.title, note.note, DateTypeConverter.toString(note.dateTime).orEmpty(), - image.orEmpty() - ) + ).plus(images) ) } // close the csv writer @@ -136,30 +135,37 @@ class BackupRepository( val title = columns[1] val text = columns[2] val dateTime = DateTypeConverter.toDate(columns[3]) - val imageName = columns[4] - val image = if (imageName.isNotEmpty()) { - val imageStore = SaveSelectedImageUseCase.image(context, id) - // copy the image from the restore directory to the cache directory - context.contentResolver.openInputStream( - Uri.fromFile(File(restoreDir, imageName)) - )?.use { inputStream -> - imageStore.outputStream().use { outputStream -> - inputStream.copyTo(outputStream) + val imageNameList = columns.slice(IntRange(4, columns.size - 1)) + val images = imageNameList.map { imageName -> + if (imageName.isNotEmpty()) { + val index = + imageName.substringBeforeLast(".webp") + .substringAfterLast('_') + .trim('(', ')') + .toInt() + val imageStore = SaveSelectedImageUseCase.image(context, id, index) + // copy the image from the restore directory to the cache directory + context.contentResolver.openInputStream( + Uri.fromFile(File(restoreDir, imageName)) + )?.use { inputStream -> + imageStore.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } } - } - // get the uri for the image in the cache directory - FileProvider.getUriForFile( - context, - "${context.packageName}.provider", - imageStore - ) - } else null + // get the uri for the image in the cache directory + FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + imageStore + ) + } else null + } val note = Note( id = id, title = title, note = text, dateTime = dateTime, - image = image + image = images ) Log.d(BackupRepository::class.simpleName, "import: $note") provider.noteDao().insertNote( diff --git a/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt b/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt index 0df2c4e2..9fb768da 100644 --- a/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt +++ b/app/src/main/java/com/aritra/notify/domain/usecase/SaveSelectedImageUseCase.kt @@ -15,22 +15,19 @@ object SaveSelectedImageUseCase { /** * Returns the image file in the cache directory */ - fun image(context: Context, id: Int) = File( + fun image(context: Context, id: Int, index: Int) = File( File(context.externalCacheDir, DIRECTORY).apply { if (!exists()) { mkdirs() } }, - "image_${id}.webp" + "image_${id}_($index).webp" ) - /** - * Saves the selected image to the cache directory and returns the uri of the saved image - */ - operator fun invoke(context: Context, uri: Uri, noteId: Int): Uri? = try { + private fun imageFileUri(context: Context, uri: Uri, noteId: Int, index: Int): Uri? = try { // copy the image to cache directory because opening the // image uri after app restart doesn't work for external storage uri on android 11 and above - val image = image(context, noteId) + val image = image(context, noteId, index) val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { ImageDecoder.decodeBitmap( ImageDecoder.createSource( @@ -59,4 +56,15 @@ object SaveSelectedImageUseCase { } catch (_: Exception) { null } + + /** + * Saves the selected image to the cache directory and returns the uri of the saved image + */ + operator fun invoke(context: Context, uris: List, noteId: Int): List { + val imageFileUris = mutableListOf() + uris.forEachIndexed { index, uri -> + imageFileUris.add(imageFileUri(context, uri, noteId, index)) + } + return imageFileUris.toList() + } } \ 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 2b5fe47d..5f3614f5 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 @@ -3,22 +3,24 @@ package com.aritra.notify.ui.screens.notes.addEditScreen import android.Manifest import android.net.Uri -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.compose.foundation.Image +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.fillMaxHeight 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.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.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -28,6 +30,7 @@ import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -46,10 +49,12 @@ 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.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -64,8 +69,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import coil.compose.AsyncImage -import coil.compose.rememberAsyncImagePainter import com.aritra.notify.R import com.aritra.notify.components.actions.BottomSheetOptions import com.aritra.notify.components.actions.SpeechRecognizerContract @@ -76,6 +79,7 @@ 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.Date @@ -94,13 +98,13 @@ fun AddEditScreen( val note = if (isNew) { null } else { - Note(noteId, "", "", Date(), null) + Note(noteId, "", "", Date(), emptyList()) } var title by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } var dateTime by remember { mutableStateOf(Calendar.getInstance().time) } - var photoUri: Uri? by remember { mutableStateOf(null) } + var photoUri by remember { mutableStateOf(emptyList()) } var characterCount by remember { mutableIntStateOf(title.length + description.length) } val cancelDialogState = remember { mutableStateOf(false) } @@ -120,8 +124,8 @@ fun AddEditScreen( val formattedCharacterCount = "${(title.length) + (description.length)} characters" val launcher = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - photoUri = uri + rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> + photoUri = uris } val permissionState = rememberPermissionState( @@ -151,7 +155,7 @@ fun AddEditScreen( if (!isNew) { title = addEditViewModel.noteModel.observeAsState().value?.title ?: "" description = addEditViewModel.noteModel.observeAsState().value?.note ?: "" - photoUri = addEditViewModel.noteModel.observeAsState().value?.image + photoUri = addEditViewModel.noteModel.observeAsState().value?.image ?: emptyList() dateTime = addEditViewModel.noteModel.observeAsState().value?.dateTime LaunchedEffect(Unit) { @@ -289,41 +293,56 @@ fun AddEditScreen( .verticalScroll(rememberScrollState()) ) { if (isNew) { - photoUri?.let { - Log.d("URI", "Photo uri $photoUri") - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End + if (photoUri.isNotEmpty()) { + LazyRow( + modifier = Modifier.height(240.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - IconButton( - onClick = { - photoUri = null - }, - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = stringResource(R.string.clear_image) - ) + items(photoUri.size) { + Box( + Modifier + .fillMaxHeight() + .width((LocalConfiguration.current.screenWidthDp * 0.8).dp) + ) { + ZoomableAsyncImage( + modifier = Modifier.fillMaxSize(), + model = photoUri[it], + contentDescription = stringResource(R.string.image), + contentScale = ContentScale.Crop + ) + FilledTonalIconButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = { + photoUri = + photoUri.filterIndexed { index, _ -> index != it } + } + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.clear_image) + ) + } + } } } - - AsyncImage( - model = photoUri, - contentDescription = stringResource(R.string.image), - modifier = Modifier.fillMaxWidth(), - contentScale = ContentScale.Crop - ) } } else { - photoUri?.let { - Image( - painter = rememberAsyncImagePainter(photoUri), - contentDescription = stringResource(R.string.image), - modifier = Modifier - .fillMaxWidth() - .size(500.dp), - contentScale = ContentScale.Fit - ) + Row( + modifier = Modifier + .height(240.dp) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + photoUri.forEach {uri -> + ZoomableAsyncImage( + modifier = Modifier + .fillMaxHeight() + .width((LocalConfiguration.current.screenWidthDp * 0.8).dp), + model = uri ?: "", + contentDescription = stringResource(R.string.image), + contentScale = ContentScale.Crop + ) + } } } 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 74a824ac..7945e8fe 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 @@ -24,7 +24,7 @@ class AddEditViewModel @Inject constructor( private val noteRepository: NoteRepository ) : AndroidViewModel(application) { - private val _noteModel = MutableLiveData(Note(0, "", "", Date(), null)) + private val _noteModel = MutableLiveData(Note(0, "", "", Date(), emptyList())) val noteModel: LiveData get() = _noteModel @@ -34,14 +34,16 @@ class AddEditViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { val id: Int = noteRepository.insertNoteToRoom(note).toInt() - if (note.image != null) { + val imageUris = note.image.filterNotNull() + + if (imageUris.isNotEmpty()) { // update the note with the new image uri noteRepository.updateNoteInRoom( note.copy( id = id, image = SaveSelectedImageUseCase( context = getApplication(), - uri = note.image!!, + uris = imageUris, noteId = id ) ) @@ -88,21 +90,23 @@ class AddEditViewModel @Inject constructor( // 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?.toFile(getApplication())?.delete() + oldNote.image.forEach {imageUri -> + imageUri?.toFile(getApplication())?.delete() + } } noteRepository.updateNoteInRoom( newNote.copy( // if the image has not been modified, use the old image uri image = if (oldNote.image == newNote.image) { oldNote.image - } else if (newNote.image != null) { + } else if (newNote.image.filterNotNull().isNotEmpty()) { // if the image has been modified, save the new image uri SaveSelectedImageUseCase( getApplication(), - newNote.image!!, + newNote.image.filterNotNull(), newNote.id ) - } else null + } else emptyList() ) ) @@ -120,7 +124,7 @@ class AddEditViewModel @Inject constructor( _noteModel.postValue(noteModel.value?.copy(note = description)) } - fun updateImage(image: Uri?) { - _noteModel.postValue(noteModel.value?.copy(image = image)) + fun updateImage(imageList: List) { + _noteModel.postValue(noteModel.value?.copy(image = imageList)) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d7b1235..1db4f620 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ core-ktx = "1.10.1" coreSplashscreen = "1.0.1" datastorePreferences = "1.0.0" espresso-core = "3.5.1" +google-gson = "2.9.0" gradle = "3.4.0" hiltAndroid = "2.44" hiltNavigationCompose = "1.0.0" @@ -29,6 +30,8 @@ android-application = "8.0.2" kotlin-android = "1.7.20" dagger-hilt-android = "2.44" devtools-ksp = "1.8.10-1.0.9" +zoomable = "0.6.2" +zoomableImageCoil = "0.6.2" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } @@ -67,9 +70,12 @@ app-update = { module = "com.google.android.play:app-update", version.ref = "app app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdate" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroid" } junit = { module = "junit:junit", version.ref = "junit" } +zoomable = { module = "me.saket.telephoto:zoomable", version.ref = "zoomable" } +zoomable-image-coil = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "zoomableImageCoil" } [plugins] android-application = { id = "com.android.application", version.ref = "android-application" } From 64bb714042f37f6a1c333d36739282cc91b228f7 Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Thu, 5 Oct 2023 09:38:03 +0530 Subject: [PATCH 2/4] Format code according to kt lint --- .idea/misc.xml | 1 - app/build.gradle.kts | 1 - .../java/com/aritra/notify/data/converters/ListConverter.kt | 6 ++---- app/src/main/java/com/aritra/notify/domain/models/Note.kt | 2 +- .../com/aritra/notify/domain/repository/BackupRepository.kt | 6 ++++-- .../notify/ui/screens/notes/addEditScreen/AddEditScreen.kt | 4 ++-- .../ui/screens/notes/addEditScreen/AddEditViewModel.kt | 6 ++++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cbd..8978d23d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8bb90c12..46b65ead 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -94,7 +94,6 @@ dependencies { implementation(libs.androidx.lifecycle.extensions) implementation(libs.androidx.room.ktx) implementation(libs.androidx.runtime.livedata) - annotationProcessor(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler) // Lifecycle 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 index 604c43ce..4057c090 100644 --- a/app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt +++ b/app/src/main/java/com/aritra/notify/data/converters/ListConverter.kt @@ -12,18 +12,16 @@ object ListConverter { @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 + val stringList = Gson().fromJson>(value) // using extension function stringList.map { Uri.parse(it) } } catch (e: Exception) { listOf() } } -} \ No newline at end of file +} 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 c512f015..95182627 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 @@ -20,5 +20,5 @@ data class Note( var title: String, var note: String, var dateTime: Date?, - var image: List + var image: List, ) : Parcelable diff --git a/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt b/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt index 9655d4d5..a4a4d588 100644 --- a/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt +++ b/app/src/main/java/com/aritra/notify/domain/repository/BackupRepository.kt @@ -63,7 +63,7 @@ class BackupRepository( note.id.toString(), note.title, note.note, - DateTypeConverter.toString(note.dateTime).orEmpty(), + DateTypeConverter.toString(note.dateTime).orEmpty() ).plus(images) ) } @@ -158,7 +158,9 @@ class BackupRepository( "${context.packageName}.provider", imageStore ) - } else null + } else { + null + } } val note = Note( id = id, 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 b2a1f22c..bbe0e50c 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 @@ -323,9 +323,9 @@ fun AddEditScreen( modifier = Modifier .height(240.dp) .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - photoUri.forEach {uri -> + photoUri.forEach { uri -> ZoomableAsyncImage( modifier = Modifier .fillMaxHeight() 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 2f9f75c6..ac09124d 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 @@ -86,7 +86,7 @@ class AddEditViewModel @Inject constructor( // 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 -> + oldNote.image.forEach { imageUri -> imageUri?.toFile(getApplication())?.delete() } } @@ -102,7 +102,9 @@ class AddEditViewModel @Inject constructor( newNote.image.filterNotNull(), newNote.id ) - } else emptyList() + } else { + emptyList() + } ) ) From 2c4f115c8d8b7d3a73d9d1bae512e3f4a9cda6f4 Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Fri, 6 Oct 2023 11:55:00 +0530 Subject: [PATCH 3/4] Update AddEditScreen UI --- .../notes/addEditScreen/AddEditScreen.kt | 394 ++++++++---------- 1 file changed, 169 insertions(+), 225 deletions(-) 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 bbe0e50c..da42d6ad 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,11 +7,9 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -21,6 +19,7 @@ import androidx.compose.foundation.layout.padding 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 @@ -32,6 +31,7 @@ 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 @@ -50,10 +50,10 @@ 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.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -118,14 +118,12 @@ fun AddEditScreen( val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = skipPartiallyExpanded ) - val formattedDateTime = - SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()).format(dateTime ?: 0) + val formattedDateTime = SimpleDateFormat(Const.DATE_TIME_FORMAT, Locale.getDefault()).format(dateTime ?: 0) val formattedCharacterCount = "${(title.length) + (description.length)} characters" - val launcher = - rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> - photoUri = uris - } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> + photoUri = uris + } val permissionState = rememberPermissionState( permission = Manifest.permission.RECORD_AUDIO @@ -137,16 +135,13 @@ fun AddEditScreen( permissionState.launchPermissionRequest() } } - val speechRecognizerLauncher = rememberLauncherForActivityResult( - contract = SpeechRecognizerContract(), - onResult = { - it?.let { - for (st in it) { - description += " $st" - } + val speechRecognizerLauncher = rememberLauncherForActivityResult(contract = SpeechRecognizerContract(), onResult = { + it?.let { + for (st in it) { + description += " $st" } } - ) + }) // edit note if (!isNew) { @@ -163,19 +158,12 @@ fun AddEditScreen( val saveEditNote: () -> Unit = if (isNew) { remember { { - addEditViewModel.insertNote( - note = Note( - id = 0, - title = title, - note = description, - dateTime = dateTime, - image = photoUri - ), - onSuccess = { - navigateBack() - Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() - } - ) + addEditViewModel.insertNote(note = Note( + id = 0, title = title, note = description, dateTime = dateTime, image = photoUri + ), onSuccess = { + navigateBack() + Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() + }) } } } else { @@ -193,89 +181,71 @@ fun AddEditScreen( } } - Scaffold( - topBar = { - AddEditTopBar( - title = title, - description = description, - onBackPress = if (isNew) { - { cancelDialogState.value = true } - } else { - navigateBack - }, - saveNote = if (isNew) { - saveEditNote - } else { - {} - }, - updateNote = if (isNew) { - {} - } else { - saveEditNote - }, - note = note - ) - }, - bottomBar = { - if (isNew) { - Column( - Modifier - .navigationBarsPadding() - .imePadding() - ) { - BottomAppBar( - content = { - IconButton(onClick = { showSheet = true }) { - Icon( - 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.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 - } + Scaffold(topBar = { + AddEditTopBar(title = title, description = description, onBackPress = if (isNew) { + { cancelDialogState.value = true } + } else { + navigateBack + }, saveNote = if (isNew) { + saveEditNote + } else { + {} + }, updateNote = if (isNew) { + {} + } else { + saveEditNote + }, note = note) + }, bottomBar = { + if (isNew) { + Column( + Modifier + .navigationBarsPadding() + .imePadding() + ) { + BottomAppBar(content = { + IconButton(onClick = { showSheet = true }) { + Icon( + 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.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 + }) } } - ) - } + } + }) } } - ) { + }) { Box( modifier = Modifier.padding(it) ) { @@ -286,50 +256,53 @@ fun AddEditScreen( ) { if (isNew) { if (photoUri.isNotEmpty()) { - LazyRow( - modifier = Modifier.height(240.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { + LazyRow { items(photoUri.size) { Box( Modifier - .fillMaxHeight() - .width((LocalConfiguration.current.screenWidthDp * 0.8).dp) + .height(180.dp) + .width(180.dp) + .padding(4.dp) + .clip(RoundedCornerShape(8.dp)) ) { - ZoomableAsyncImage( - modifier = Modifier.fillMaxSize(), - model = photoUri[it], - contentDescription = stringResource(R.string.image), - contentScale = ContentScale.Crop - ) - FilledTonalIconButton( - modifier = Modifier.align(Alignment.TopEnd), - onClick = { - photoUri = - photoUri.filterIndexed { index, _ -> index != it } - } - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = stringResource(R.string.clear_image) + ZoomableAsyncImage( + modifier = Modifier.fillMaxSize(), + model = photoUri[it], + contentDescription = stringResource(R.string.image), + contentScale = ContentScale.Crop + ) + FilledTonalIconButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = { + photoUri = photoUri.filterIndexed { index, _ -> index != it } + }, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer.copy( + alpha = 0.6f ) - } + ) + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.clear_image) + ) } } + } } } } else { Row( modifier = Modifier - .height(240.dp) .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(10.dp) ) { photoUri.forEach { uri -> ZoomableAsyncImage( modifier = Modifier - .fillMaxHeight() - .width((LocalConfiguration.current.screenWidthDp * 0.8).dp), + .height(180.dp) + .width(180.dp) + .padding(4.dp) + .clip(RoundedCornerShape(8.dp)), model = uri ?: "", contentDescription = stringResource(R.string.image), contentScale = ContentScale.Crop @@ -338,47 +311,37 @@ fun AddEditScreen( } } - TextField( - modifier = Modifier.fillMaxWidth(), - value = title, - onValueChange = { newTitle -> - if (isNew) { - title = newTitle - characterCount = title.length + description.length - } else { - addEditViewModel.updateTitle(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( + TextField(modifier = Modifier.fillMaxWidth(), value = title, onValueChange = { newTitle -> + if (isNew) { + title = newTitle + characterCount = title.length + description.length + } else { + addEditViewModel.updateTitle(newTitle) + } + }, placeholder = { + Text( + stringResource(R.string.title), fontSize = 24.sp, + fontWeight = FontWeight.W700, + color = Color.Gray, 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) - }) + ) + }, 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( @@ -386,78 +349,59 @@ fun AddEditScreen( "$currentDate, $currentTime | $characterCount characters" } else { "$formattedDateTime | $formattedCharacterCount" - }, - onValueChange = { }, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - textStyle = TextStyle( - fontSize = 15.sp, - fontFamily = FontFamily(Font(R.font.poppins_light)) - ), - colors = TextFieldDefaults.colors( + }, 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( + ), keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Text ) ) - TextField( - modifier = Modifier.fillMaxSize(), - value = description, - onValueChange = { newDescription -> - if (isNew) { - description = newDescription - characterCount = title.length + description.length - } else { - addEditViewModel.updateDescription(newDescription) - } - }, - placeholder = { - Text( - stringResource(R.string.notes), - fontSize = 20.sp, - fontWeight = FontWeight.W500, - color = Color.Gray, - fontFamily = FontFamily(Font(R.font.poppins_light)) - ) - }, - textStyle = TextStyle( - fontSize = 18.sp, + TextField(modifier = Modifier.fillMaxSize(), value = description, onValueChange = { newDescription -> + if (isNew) { + description = newDescription + characterCount = title.length + description.length + } else { + addEditViewModel.updateDescription(newDescription) + } + }, placeholder = { + Text( + stringResource(R.string.notes), + fontSize = 20.sp, + fontWeight = FontWeight.W500, + color = Color.Gray, 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( - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text - ), - maxLines = Int.MAX_VALUE + ) + }, textStyle = TextStyle( + fontSize = 18.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( + capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text + ), maxLines = Int.MAX_VALUE ) } } } - TextDialog( - title = stringResource(R.string.are_you_sure), + TextDialog(title = stringResource(R.string.are_you_sure), description = stringResource(R.string.the_text_change_will_not_be_saved), isOpened = cancelDialogState.value, onDismissCallback = { cancelDialogState.value = false }, onConfirmCallback = { navigateBack() cancelDialogState.value = false - } - ) + }) } From 2eb88a321dc67547496ab108bb3f4f069e923de4 Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Fri, 6 Oct 2023 12:04:44 +0530 Subject: [PATCH 4/4] Format code according to kt lint --- .../notes/addEditScreen/AddEditScreen.kt | 262 ++++++++++-------- 1 file changed, 154 insertions(+), 108 deletions(-) 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 da42d6ad..39d4312a 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 @@ -158,12 +158,19 @@ fun AddEditScreen( val saveEditNote: () -> Unit = if (isNew) { remember { { - addEditViewModel.insertNote(note = Note( - id = 0, title = title, note = description, dateTime = dateTime, image = photoUri - ), onSuccess = { - navigateBack() - Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() - }) + addEditViewModel.insertNote( + note = Note( + id = 0, + title = title, + note = description, + dateTime = dateTime, + image = photoUri + ), + onSuccess = { + navigateBack() + Toast.makeText(context, "Successfully Saved!", Toast.LENGTH_SHORT).show() + } + ) } } } else { @@ -182,19 +189,26 @@ fun AddEditScreen( } Scaffold(topBar = { - AddEditTopBar(title = title, description = description, onBackPress = if (isNew) { - { cancelDialogState.value = true } - } else { - navigateBack - }, saveNote = if (isNew) { - saveEditNote - } else { - {} - }, updateNote = if (isNew) { - {} - } else { - saveEditNote - }, note = note) + AddEditTopBar( + title = title, + description = description, + onBackPress = if (isNew) { + { cancelDialogState.value = true } + } else { + navigateBack + }, + saveNote = if (isNew) { + saveEditNote + } else { + {} + }, + updateNote = if (isNew) { + {} + } else { + saveEditNote + }, + note = note + ) }, bottomBar = { if (isNew) { Column( @@ -210,16 +224,19 @@ fun AddEditScreen( ) } if (showSheet) { - ModalBottomSheet(onDismissRequest = { showSheet = false }, + ModalBottomSheet( + onDismissRequest = { showSheet = false }, sheetState = bottomSheetState, - dragHandle = { BottomSheetDefaults.DragHandle() }) { + dragHandle = { BottomSheetDefaults.DragHandle() } + ) { Column( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(16.dp) ) { - BottomSheetOptions(text = stringResource(R.string.add_image), + BottomSheetOptions( + text = stringResource(R.string.add_image), icon = painterResource(id = R.drawable.gallery_icon), onClick = { launcher.launch( @@ -228,8 +245,10 @@ fun AddEditScreen( ) ) showSheet = false - }) - BottomSheetOptions(text = stringResource(R.string.speech_to_text), + } + ) + BottomSheetOptions( + text = stringResource(R.string.speech_to_text), icon = painterResource(id = R.drawable.mic_icon), onClick = { if (permissionState.status.isGranted) { @@ -238,7 +257,8 @@ fun AddEditScreen( permissionState.launchPermissionRequest() } showSheet = false - }) + } + ) } } } @@ -265,36 +285,36 @@ fun AddEditScreen( .padding(4.dp) .clip(RoundedCornerShape(8.dp)) ) { - ZoomableAsyncImage( - modifier = Modifier.fillMaxSize(), - model = photoUri[it], - contentDescription = stringResource(R.string.image), - contentScale = ContentScale.Crop - ) - FilledTonalIconButton( - modifier = Modifier.align(Alignment.TopEnd), - onClick = { - photoUri = photoUri.filterIndexed { index, _ -> index != it } - }, - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer.copy( - alpha = 0.6f - ) - ) - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = stringResource(R.string.clear_image) + ZoomableAsyncImage( + modifier = Modifier.fillMaxSize(), + model = photoUri[it], + contentDescription = stringResource(R.string.image), + contentScale = ContentScale.Crop ) + FilledTonalIconButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = { + photoUri = photoUri.filterIndexed { index, _ -> index != it } + }, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer.copy( + alpha = 0.6f + ) + ) + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.clear_image) + ) + } } } - } } } } else { Row( modifier = Modifier - .horizontalScroll(rememberScrollState()), + .horizontalScroll(rememberScrollState()) ) { photoUri.forEach { uri -> ZoomableAsyncImage( @@ -311,37 +331,44 @@ fun AddEditScreen( } } - TextField(modifier = Modifier.fillMaxWidth(), value = title, onValueChange = { newTitle -> - if (isNew) { - title = newTitle - characterCount = title.length + description.length - } else { - addEditViewModel.updateTitle(newTitle) - } - }, placeholder = { - Text( - stringResource(R.string.title), + TextField( + modifier = Modifier.fillMaxWidth(), value = title, onValueChange = { newTitle -> + if (isNew) { + title = newTitle + characterCount = title.length + description.length + } else { + addEditViewModel.updateTitle(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, - 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) - }) + ), + 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( @@ -349,59 +376,78 @@ fun AddEditScreen( "$currentDate, $currentTime | $characterCount characters" } else { "$formattedDateTime | $formattedCharacterCount" - }, onValueChange = { }, modifier = Modifier.fillMaxWidth(), readOnly = true, textStyle = TextStyle( - fontSize = 15.sp, fontFamily = FontFamily(Font(R.font.poppins_light)) - ), colors = TextFieldDefaults.colors( + }, + 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( + ), + keyboardOptions = KeyboardOptions.Default.copy( keyboardType = KeyboardType.Text ) ) - TextField(modifier = Modifier.fillMaxSize(), value = description, onValueChange = { newDescription -> - if (isNew) { - description = newDescription - characterCount = title.length + description.length - } else { - addEditViewModel.updateDescription(newDescription) - } - }, placeholder = { - Text( - stringResource(R.string.notes), - fontSize = 20.sp, - fontWeight = FontWeight.W500, - color = Color.Gray, + TextField( + modifier = Modifier.fillMaxSize(), + value = description, + onValueChange = { newDescription -> + if (isNew) { + description = newDescription + characterCount = title.length + description.length + } else { + addEditViewModel.updateDescription(newDescription) + } + }, + placeholder = { + Text( + stringResource(R.string.notes), + fontSize = 20.sp, + fontWeight = FontWeight.W500, + color = Color.Gray, + fontFamily = FontFamily(Font(R.font.poppins_light)) + ) + }, + textStyle = TextStyle( + fontSize = 18.sp, fontFamily = FontFamily(Font(R.font.poppins_light)) - ) - }, textStyle = TextStyle( - fontSize = 18.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( - capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text - ), 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 + ), + maxLines = Int.MAX_VALUE ) } } } - TextDialog(title = stringResource(R.string.are_you_sure), + TextDialog( + title = stringResource(R.string.are_you_sure), description = stringResource(R.string.the_text_change_will_not_be_saved), isOpened = cancelDialogState.value, onDismissCallback = { cancelDialogState.value = false }, onConfirmCallback = { navigateBack() cancelDialogState.value = false - }) + } + ) }