diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 29abbe7..47c17c0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,8 +39,8 @@ android { applicationId = "us.huseli.retain" minSdk = 26 targetSdk = targetSdk - versionCode = 4 - versionName = "1.0.0-beta.4" + versionCode = 5 + versionName = "1.0.0-beta.5" vectorDrawables.useSupportLibrary = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // buildConfigField("String", "dropboxAppKey", "\"${dropboxAppKey}\"") @@ -157,13 +157,13 @@ dependencies { implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0") // HTML parsing: - implementation("org.jsoup:jsoup:1.16.1") + implementation("org.jsoup:jsoup:1.16.2") // SFTP: - implementation(group = "com.github.mwiede", name = "jsch", version = "0.2.9") + implementation(group = "com.github.mwiede", name = "jsch", version = "0.2.12") // Dropbox: - implementation("com.dropbox.core:dropbox-core-sdk:5.4.5") + implementation("com.dropbox.core:dropbox-core-sdk:5.4.6") // Theme: implementation("com.github.Eboreg:RetainTheme:2.2.1") diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 962e8ca..6d03462 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,9 +11,9 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 4, - "versionName": "1.0.0-beta.4", - "outputFile": "retain_1.0.0-beta.4-release.apk" + "versionCode": 5, + "versionName": "1.0.0-beta.5", + "outputFile": "retain_1.0.0-beta.5-release.apk" } ], "elementType": "File" diff --git a/app/release/retain_1.0.0-beta.4-release.apk b/app/release/retain_1.0.0-beta.5-release.apk similarity index 76% rename from app/release/retain_1.0.0-beta.4-release.apk rename to app/release/retain_1.0.0-beta.5-release.apk index 4ebef37..7090813 100644 Binary files a/app/release/retain_1.0.0-beta.4-release.apk and b/app/release/retain_1.0.0-beta.5-release.apk differ diff --git a/app/src/main/java/us/huseli/retain/compose/notescreen/ChecklistNoteChecklist.kt b/app/src/main/java/us/huseli/retain/compose/notescreen/ChecklistNoteChecklist.kt index dd00e7d..e6cca96 100644 --- a/app/src/main/java/us/huseli/retain/compose/notescreen/ChecklistNoteChecklist.kt +++ b/app/src/main/java/us/huseli/retain/compose/notescreen/ChecklistNoteChecklist.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.Add import androidx.compose.material.icons.sharp.ExpandMore import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -23,6 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import org.burnoutcrew.reorderable.ReorderableItem @@ -45,6 +47,7 @@ fun LazyListScope.ChecklistNoteChecklist( onItemFocus: (ChecklistItem) -> Unit, onShowCheckedClick: () -> Unit, backgroundColor: Color, + onAddItemClick: () -> Unit, ) { items(uncheckedItems, key = { it.id }) { item -> ReorderableItem(state, key = item.id) { isDragging -> @@ -72,9 +75,9 @@ fun LazyListScope.ChecklistNoteChecklist( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(vertical = 8.dp) .fillMaxWidth() - .clickable { onShowCheckedClick() }, + .clickable { onShowCheckedClick() } + .padding(vertical = 8.dp), ) { Icon( imageVector = Icons.Sharp.ExpandMore, @@ -108,9 +111,28 @@ fun LazyListScope.ChecklistNoteChecklist( ) } } - } else { - item { - Spacer(Modifier.height(4.dp)) + } else item { Spacer(Modifier.height(4.dp)) } + + item { + // "Add item" link: + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = onAddItemClick) + .padding(vertical = 8.dp) + .fillMaxWidth() + ) { + Icon( + imageVector = Icons.Sharp.Add, + contentDescription = null, + modifier = Modifier.padding(horizontal = 12.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + ) + Text( + text = stringResource(R.string.add_item), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + modifier = Modifier.padding(horizontal = 6.dp) + ) } } } diff --git a/app/src/main/java/us/huseli/retain/compose/notescreen/NoteScreen.kt b/app/src/main/java/us/huseli/retain/compose/notescreen/NoteScreen.kt index 25bd1fe..d91fdf9 100644 --- a/app/src/main/java/us/huseli/retain/compose/notescreen/NoteScreen.kt +++ b/app/src/main/java/us/huseli/retain/compose/notescreen/NoteScreen.kt @@ -13,9 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.sharp.Add -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -30,6 +27,7 @@ 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.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange @@ -188,7 +186,9 @@ fun NoteScreen( verticalAlignment = Alignment.CenterVertically, ) { OutlinedTextField( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .onFocusChanged { if (it.isFocused) viewModel.setChecklistItemFocus(null) }, value = note?.title ?: "", onValueChange = { viewModel.setTitle(it) }, textStyle = MaterialTheme.typography.headlineSmall, @@ -199,7 +199,7 @@ fun NoteScreen( keyboardActions = KeyboardActions( onNext = { if (note?.type == NoteType.CHECKLIST && checkedItems.isEmpty() && uncheckedItems.isEmpty()) { - viewModel.insertChecklistItem(text = "", checked = false, index = 0) + viewModel.insertChecklistItem(text = "", checked = false, position = 0) } } ), @@ -258,36 +258,14 @@ fun NoteScreen( backgroundColor = noteColor, focusedItemId = focusedChecklistItemId, onItemFocus = { viewModel.setChecklistItemFocus(it) }, - ) - - item { - // "Add item" link: - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { - viewModel.insertChecklistItem( - text = "", - checked = false, - index = uncheckedItems.size, - ) - } - .padding(vertical = 8.dp) - .fillMaxWidth() - ) { - Icon( - imageVector = Icons.Sharp.Add, - contentDescription = null, - modifier = Modifier.padding(horizontal = 12.dp), - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - ) - Text( - text = stringResource(R.string.add_item), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - modifier = Modifier.padding(horizontal = 6.dp) + onAddItemClick = { + viewModel.insertChecklistItem( + text = "", + checked = false, + position = uncheckedItems.size, ) - } - } + }, + ) } } } diff --git a/app/src/main/java/us/huseli/retain/dao/NoteDao.kt b/app/src/main/java/us/huseli/retain/dao/NoteDao.kt index 9ae1be1..af1b933 100644 --- a/app/src/main/java/us/huseli/retain/dao/NoteDao.kt +++ b/app/src/main/java/us/huseli/retain/dao/NoteDao.kt @@ -18,6 +18,9 @@ interface NoteDao { @Delete suspend fun _delete(notes: Collection) + @Query("SELECT EXISTS(SELECT * FROM note WHERE noteId = :noteId)") + suspend fun _exists(noteId: UUID): Boolean + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun _insert(note: Note) @@ -77,6 +80,7 @@ interface NoteDao { @Transaction suspend fun upsert(note: Note) { _makePlaceFor(note.id, note.position) - _insert(note) + if (_exists(note.id)) update(listOf(note)) + else _insert(note) } } diff --git a/app/src/main/java/us/huseli/retain/dataclasses/entities/ChecklistItem.kt b/app/src/main/java/us/huseli/retain/dataclasses/entities/ChecklistItem.kt index 380a771..ff6bb79 100644 --- a/app/src/main/java/us/huseli/retain/dataclasses/entities/ChecklistItem.kt +++ b/app/src/main/java/us/huseli/retain/dataclasses/entities/ChecklistItem.kt @@ -24,9 +24,6 @@ data class ChecklistItem( @ColumnInfo(name = "checklistItemChecked", defaultValue = "0") val checked: Boolean = false, @ColumnInfo(name = "checklistItemPosition", defaultValue = "0") val position: Int = 0, ) : Comparable { - override fun toString() = - "" - override fun compareTo(other: ChecklistItem): Int = position - other.position override fun equals(other: Any?) = other is ChecklistItem && diff --git a/app/src/main/java/us/huseli/retain/dataclasses/entities/Note.kt b/app/src/main/java/us/huseli/retain/dataclasses/entities/Note.kt index 86aa744..a98df98 100644 --- a/app/src/main/java/us/huseli/retain/dataclasses/entities/Note.kt +++ b/app/src/main/java/us/huseli/retain/dataclasses/entities/Note.kt @@ -22,9 +22,6 @@ data class Note( @ColumnInfo(name = "noteIsDeleted", defaultValue = "0") val isDeleted: Boolean = false, @ColumnInfo(name = "noteIsArchived", defaultValue = "0") val isArchived: Boolean = false, ) : Comparable { - override fun toString() = - "" - override fun compareTo(other: Note) = (updated.epochSecond - other.updated.epochSecond).toInt() override fun equals(other: Any?) = other is Note && diff --git a/app/src/main/java/us/huseli/retain/viewmodels/NoteViewModel.kt b/app/src/main/java/us/huseli/retain/viewmodels/NoteViewModel.kt index 8f9106f..3f8d9c2 100644 --- a/app/src/main/java/us/huseli/retain/viewmodels/NoteViewModel.kt +++ b/app/src/main/java/us/huseli/retain/viewmodels/NoteViewModel.kt @@ -90,13 +90,20 @@ class NoteViewModel @Inject constructor( _selectedImages.value = emptySet() } - fun insertChecklistItem(text: String, checked: Boolean, index: Int): ChecklistItem = - ChecklistItem(text = text, checked = checked, position = index, noteId = _noteId).also { item -> - _checklistItems.value = _checklistItems.value.toMutableList().apply { add(item.position, item) } - setChecklistItemFocus(item) - updateChecklistItemPositions() - save(NotePojo.Component.CHECKLIST_ITEMS) - } + fun insertChecklistItem(text: String, checked: Boolean, position: Int): ChecklistItem { + val item = ChecklistItem(text = text, checked = checked, position = position, noteId = _noteId) + + _checklistItems.value = _checklistItems.value + .map { + if (it.position >= position && it.checked == checked) it.copy(position = it.position + 1) + else it + }.plus(item).sorted() + + setChecklistItemFocus(item) + save(NotePojo.Component.CHECKLIST_ITEMS) + + return item + } fun insertImage(uri: Uri) = viewModelScope.launch { repository.uriToImage(uri, _noteId)?.let { image -> @@ -112,8 +119,8 @@ class NoteViewModel @Inject constructor( _selectedImages.value = _images.value.map { it.filename }.toSet() } - fun setChecklistItemFocus(item: ChecklistItem) { - _focusedChecklistItemId.value = item.id + fun setChecklistItemFocus(item: ChecklistItem?) { + _focusedChecklistItemId.value = item?.id } fun setChecklistItemText(item: ChecklistItem, value: String) { @@ -145,13 +152,11 @@ class NoteViewModel @Inject constructor( * Splits item's text at cursor position, moves the last part to a new * item, moves focus to this item. */ - val index = _checklistItems.value.indexOfFirst { it.id == item.id } val head = textFieldValue.text.substring(0, textFieldValue.selection.start) val tail = textFieldValue.text.substring(textFieldValue.selection.start) - log("onNextItem: item=$item, head=$head, tail=$tail, index=$index") setChecklistItemText(item, head) - insertChecklistItem(text = tail, checked = item.checked, index = index + 1) + insertChecklistItem(text = tail, checked = item.checked, position = item.position + 1) } fun switchItemPositions(from: ItemPosition, to: ItemPosition) { @@ -159,14 +164,17 @@ class NoteViewModel @Inject constructor( * We cannot use ItemPosition.index because the lazy column contains * a whole bunch of other junk than checklist items. */ - val fromIdx = _checklistItems.value.indexOfFirst { it.id == from.key } - val toIdx = _checklistItems.value.indexOfFirst { it.id == to.key } - - if (fromIdx > -1 && toIdx > -1) { - log("switchItemPositions($from, $to) before: ${_checklistItems.value}") - _checklistItems.value = _checklistItems.value.toMutableList().apply { add(toIdx, removeAt(fromIdx)) } - log("switchItemPositions($from, $to) after: ${_checklistItems.value}") - updateChecklistItemPositions() + val fromItem = _checklistItems.value.firstOrNull { it.id == from.key } + val toItem = _checklistItems.value.firstOrNull { it.id == to.key } + + if (fromItem != null && toItem != null) { + _checklistItems.value = _checklistItems.value.map { item -> + when (item.id) { + fromItem.id -> item.copy(position = toItem.position) + toItem.id -> item.copy(position = fromItem.position) + else -> item + } + }.sorted() _isUnsaved.value = true } } @@ -184,8 +192,12 @@ class NoteViewModel @Inject constructor( } fun uncheckAllItems() { - _checklistItems.value = _checklistItems.value.map { it.copy(checked = false) } - updateChecklistItemPositions() + var position = _checklistItems.value.filter { !it.checked }.maxOfOrNull { it.position } ?: -1 + + _checklistItems.value = _checklistItems.value.map { item -> + if (item.checked) item.copy(checked = false, position = ++position) + else item + }.sorted() save(NotePojo.Component.CHECKLIST_ITEMS) } @@ -206,13 +218,12 @@ class NoteViewModel @Inject constructor( } fun updateChecklistItemChecked(item: ChecklistItem, checked: Boolean) { - _checklistItems.value = _checklistItems.value.toMutableList().apply { - val position = filter { it.checked == checked }.takeIf { it.isNotEmpty() }?.maxOf { it.position } ?: -1 + val position = _checklistItems.value.filter { it.checked == checked }.maxOfOrNull { it.position } ?: -1 - removeIf { it.id == item.id } - add(item.copy(checked = checked, position = position + 1)) - } - updateChecklistItemPositions() + _checklistItems.value = _checklistItems.value.map { + if (it.id == item.id) it.copy(checked = checked, position = position + 1) + else it + }.sorted() save(NotePojo.Component.CHECKLIST_ITEMS) } @@ -237,13 +248,6 @@ class NoteViewModel @Inject constructor( _isUnsaved.value = false } - private fun updateChecklistItemPositions() { - _checklistItems.value = _checklistItems.value.mapIndexed { index, item -> - if (item.position != index) item.copy(position = index) else item - } - _isUnsaved.value = true - } - private fun updateImagePositions() { _images.value = _images.value.mapIndexed { index, image -> if (image.position != index) image.copy(position = index) else image diff --git a/build.gradle.kts b/build.gradle.kts index 767ec89..44e259c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("com.android.application") version "8.1.2" apply false id("com.android.library") version "8.1.2" apply false - id("com.google.dagger.hilt.android") version "2.48" apply false + id("com.google.dagger.hilt.android") version "2.48.1" apply false // id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false id("org.jetbrains.kotlin.android") version "1.9.10" apply false