From 1416c1cbc5246a24bff35528baec5f1bf20b13aa Mon Sep 17 00:00:00 2001 From: Crustack Date: Thu, 5 Mar 2026 12:08:06 +0100 Subject: [PATCH 1/2] Add manual sort for notes via drag and drop --- .../10.json | 179 ++++++ .../philkes/notallyx/data/NotallyDatabase.kt | 18 +- .../philkes/notallyx/data/dao/BaseNoteDao.kt | 35 +- .../philkes/notallyx/data/model/BaseNote.kt | 11 +- .../notallyx/data/model/ModelExtensions.kt | 10 + .../notallyx/presentation/UiExtensions.kt | 4 +- .../activity/main/ActionModeBinding.kt | 323 +++++++++++ .../activity/main/MainActivity.kt | 534 ++++++++---------- .../activity/main/fragment/NotallyFragment.kt | 152 ++++- .../settings/PreferenceBindingExtensions.kt | 13 +- .../fragment/settings/SettingsFragment.kt | 9 - .../activity/note/RecordAudioActivity.kt | 33 +- .../presentation/view/main/BaseNoteAdapter.kt | 245 +++++++- .../view/main/sorting/BaseNoteManualSort.kt | 19 + .../presentation/viewmodel/BaseNoteModel.kt | 14 + .../presentation/viewmodel/NotallyModel.kt | 1 + .../viewmodel/preference/NotesSorting.kt | 9 +- .../com/philkes/notallyx/utils/ActionMode.kt | 6 + .../notallyx/utils/backup/ImportExtensions.kt | 7 + app/src/main/res/drawable/sort.xml | 11 + app/src/main/res/layout/activity_main.xml | 1 + app/src/main/res/layout/dialog_notes_sort.xml | 1 + app/src/main/res/layout/fragment_settings.xml | 4 - app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 1 - 25 files changed, 1261 insertions(+), 381 deletions(-) create mode 100644 app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json create mode 100644 app/src/main/java/com/philkes/notallyx/presentation/activity/main/ActionModeBinding.kt create mode 100644 app/src/main/java/com/philkes/notallyx/presentation/view/main/sorting/BaseNoteManualSort.kt create mode 100644 app/src/main/res/drawable/sort.xml diff --git a/app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json b/app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json new file mode 100644 index 00000000..5dfd5500 --- /dev/null +++ b/app/schemas/com.philkes.notallyx.data.NotallyDatabase/10.json @@ -0,0 +1,179 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "6cdd7edd0a8e7bc0b6ba1a706986c6a2", + "entities": [ + { + "tableName": "BaseNote", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL, `reminders` TEXT NOT NULL, `viewMode` TEXT NOT NULL, `sortIdx` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "modifiedTimestamp", + "columnName": "modifiedTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "labels", + "columnName": "labels", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "spans", + "columnName": "spans", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "items", + "columnName": "items", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "files", + "columnName": "files", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "audios", + "columnName": "audios", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reminders", + "columnName": "reminders", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "viewMode", + "columnName": "viewMode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortIdx", + "columnName": "sortIdx", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_BaseNote_id_folder_pinned_timestamp_labels", + "unique": false, + "columnNames": [ + "id", + "folder", + "pinned", + "timestamp", + "labels" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)" + }, + { + "name": "index_BaseNote_sortIdx", + "unique": false, + "columnNames": [ + "sortIdx" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_sortIdx` ON `${TABLE_NAME}` (`sortIdx`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Label", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))", + "fields": [ + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "value" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6cdd7edd0a8e7bc0b6ba1a706986c6a2')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt b/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt index 5e40774e..2e1c3a35 100644 --- a/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt +++ b/app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt @@ -33,7 +33,7 @@ import java.io.File import net.zetetic.database.sqlcipher.SupportOpenHelperFactory @TypeConverters(Converters::class) -@Database(entities = [BaseNote::class, Label::class], version = 9) +@Database(entities = [BaseNote::class, Label::class], version = 10) abstract class NotallyDatabase : RoomDatabase() { abstract fun getLabelDao(): LabelDao @@ -161,6 +161,7 @@ abstract class NotallyDatabase : RoomDatabase() { Migration7, Migration8, Migration9, + Migration10, ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { System.loadLibrary("sqlcipher") @@ -300,5 +301,20 @@ abstract class NotallyDatabase : RoomDatabase() { ) } } + + object Migration10 : Migration(9, 10) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `BaseNote` ADD COLUMN `sortIdx` INTEGER") + db.execSQL( + "CREATE INDEX IF NOT EXISTS `index_BaseNote_sortIdx` ON `BaseNote` (`sortIdx`)" + ) + db.execSQL( + "DROP INDEX IF EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels_sortIdx` " + ) + db.execSQL( + "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `BaseNote` (`id`, `folder`, `pinned`, `timestamp`, `labels`)" + ) + } + } } } diff --git a/app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt b/app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt index cae40fff..050735ca 100644 --- a/app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt +++ b/app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt @@ -7,6 +7,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RawQuery +import androidx.room.Transaction import androidx.room.Update import androidx.sqlite.db.SupportSQLiteQuery import com.philkes.notallyx.R @@ -57,6 +58,10 @@ interface BaseNoteDao { } suspend fun insertSafe(context: ContextWrapper, baseNote: BaseNote): Long { + if (baseNote.sortIdx == null) { + val maxSortIdx = getMaxSortIdx() ?: 0 + baseNote.sortIdx = maxSortIdx + 1 + } val (truncated, note) = baseNote.truncated() if (truncated) { context.log( @@ -75,17 +80,19 @@ interface BaseNoteDao { } suspend fun insertSafe(context: ContextWrapper, baseNotes: List): List { + val maxSortIdx = getMaxSortIdx() ?: 0 val truncatedNotes = mutableListOf() var truncatedCharacterSize = 0 - val notes = - baseNotes.map { baseNote -> - val (truncated, note) = baseNote.truncated() - if (truncated) { - truncatedCharacterSize += baseNote.body.length - truncatedNotes.add(note) - } - note + baseNotes.forEachIndexed { index, baseNote -> + if (baseNote.sortIdx == null) { + baseNote.sortIdx = (maxSortIdx + 1 + index) + } + val (truncated, note) = baseNote.truncated() + if (truncated) { + truncatedCharacterSize += baseNote.body.length + truncatedNotes.add(note) } + } if (truncatedNotes.isNotEmpty()) { context.log( TAG, @@ -99,7 +106,7 @@ interface BaseNoteDao { ) ) } - return insert(notes) + return insert(baseNotes) } @Insert suspend fun insert(baseNotes: List): List @@ -244,6 +251,16 @@ interface BaseNoteDao { * * In this case, an exception will be thrown. It is the caller's responsibility to handle it. */ + @Query("UPDATE BaseNote SET sortIdx = :sortIdx WHERE id = :id") + fun updateSortIdx(id: Long, sortIdx: Int) + + @Transaction + fun updateSortIndices(idToIdx: Map) { + idToIdx.forEach { (id, idx) -> updateSortIdx(id, idx) } + } + + @Query("SELECT MAX(sortIdx) FROM BaseNote") fun getMaxSortIdx(): Int? + suspend fun updateChecked(id: Long, position: Int, checked: Boolean) { val items = requireNotNull(get(id), { "updateChecked: Note with id '$id' does not exist" }).items diff --git a/app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt b/app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt index 7563796b..f737557e 100644 --- a/app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt +++ b/app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt @@ -7,7 +7,13 @@ import androidx.room.PrimaryKey /** Format: `#RRGGBB` or `#AARRGGBB` or [BaseNote.COLOR_DEFAULT] */ typealias ColorString = String -@Entity(indices = [Index(value = ["id", "folder", "pinned", "timestamp", "labels"])]) +@Entity( + indices = + [ + Index(value = ["id", "folder", "pinned", "timestamp", "labels"]), + Index(value = ["sortIdx"]), + ] +) data class BaseNote( @PrimaryKey(autoGenerate = true) val id: Long, val type: Type, @@ -26,6 +32,7 @@ data class BaseNote( val audios: List