Skip to content

Commit 1416c1c

Browse files
committed
Add manual sort for notes via drag and drop
1 parent dde6d3d commit 1416c1c

25 files changed

Lines changed: 1261 additions & 381 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 10,
5+
"identityHash": "6cdd7edd0a8e7bc0b6ba1a706986c6a2",
6+
"entities": [
7+
{
8+
"tableName": "BaseNote",
9+
"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)",
10+
"fields": [
11+
{
12+
"fieldPath": "id",
13+
"columnName": "id",
14+
"affinity": "INTEGER",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "type",
19+
"columnName": "type",
20+
"affinity": "TEXT",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "folder",
25+
"columnName": "folder",
26+
"affinity": "TEXT",
27+
"notNull": true
28+
},
29+
{
30+
"fieldPath": "color",
31+
"columnName": "color",
32+
"affinity": "TEXT",
33+
"notNull": true
34+
},
35+
{
36+
"fieldPath": "title",
37+
"columnName": "title",
38+
"affinity": "TEXT",
39+
"notNull": true
40+
},
41+
{
42+
"fieldPath": "pinned",
43+
"columnName": "pinned",
44+
"affinity": "INTEGER",
45+
"notNull": true
46+
},
47+
{
48+
"fieldPath": "timestamp",
49+
"columnName": "timestamp",
50+
"affinity": "INTEGER",
51+
"notNull": true
52+
},
53+
{
54+
"fieldPath": "modifiedTimestamp",
55+
"columnName": "modifiedTimestamp",
56+
"affinity": "INTEGER",
57+
"notNull": true
58+
},
59+
{
60+
"fieldPath": "labels",
61+
"columnName": "labels",
62+
"affinity": "TEXT",
63+
"notNull": true
64+
},
65+
{
66+
"fieldPath": "body",
67+
"columnName": "body",
68+
"affinity": "TEXT",
69+
"notNull": true
70+
},
71+
{
72+
"fieldPath": "spans",
73+
"columnName": "spans",
74+
"affinity": "TEXT",
75+
"notNull": true
76+
},
77+
{
78+
"fieldPath": "items",
79+
"columnName": "items",
80+
"affinity": "TEXT",
81+
"notNull": true
82+
},
83+
{
84+
"fieldPath": "images",
85+
"columnName": "images",
86+
"affinity": "TEXT",
87+
"notNull": true
88+
},
89+
{
90+
"fieldPath": "files",
91+
"columnName": "files",
92+
"affinity": "TEXT",
93+
"notNull": true
94+
},
95+
{
96+
"fieldPath": "audios",
97+
"columnName": "audios",
98+
"affinity": "TEXT",
99+
"notNull": true
100+
},
101+
{
102+
"fieldPath": "reminders",
103+
"columnName": "reminders",
104+
"affinity": "TEXT",
105+
"notNull": true
106+
},
107+
{
108+
"fieldPath": "viewMode",
109+
"columnName": "viewMode",
110+
"affinity": "TEXT",
111+
"notNull": true
112+
},
113+
{
114+
"fieldPath": "sortIdx",
115+
"columnName": "sortIdx",
116+
"affinity": "INTEGER",
117+
"notNull": false
118+
}
119+
],
120+
"primaryKey": {
121+
"autoGenerate": true,
122+
"columnNames": [
123+
"id"
124+
]
125+
},
126+
"indices": [
127+
{
128+
"name": "index_BaseNote_id_folder_pinned_timestamp_labels",
129+
"unique": false,
130+
"columnNames": [
131+
"id",
132+
"folder",
133+
"pinned",
134+
"timestamp",
135+
"labels"
136+
],
137+
"orders": [],
138+
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
139+
},
140+
{
141+
"name": "index_BaseNote_sortIdx",
142+
"unique": false,
143+
"columnNames": [
144+
"sortIdx"
145+
],
146+
"orders": [],
147+
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_sortIdx` ON `${TABLE_NAME}` (`sortIdx`)"
148+
}
149+
],
150+
"foreignKeys": []
151+
},
152+
{
153+
"tableName": "Label",
154+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))",
155+
"fields": [
156+
{
157+
"fieldPath": "value",
158+
"columnName": "value",
159+
"affinity": "TEXT",
160+
"notNull": true
161+
}
162+
],
163+
"primaryKey": {
164+
"autoGenerate": false,
165+
"columnNames": [
166+
"value"
167+
]
168+
},
169+
"indices": [],
170+
"foreignKeys": []
171+
}
172+
],
173+
"views": [],
174+
"setupQueries": [
175+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
176+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6cdd7edd0a8e7bc0b6ba1a706986c6a2')"
177+
]
178+
}
179+
}

app/src/main/java/com/philkes/notallyx/data/NotallyDatabase.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import java.io.File
3333
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
3434

3535
@TypeConverters(Converters::class)
36-
@Database(entities = [BaseNote::class, Label::class], version = 9)
36+
@Database(entities = [BaseNote::class, Label::class], version = 10)
3737
abstract class NotallyDatabase : RoomDatabase() {
3838

3939
abstract fun getLabelDao(): LabelDao
@@ -161,6 +161,7 @@ abstract class NotallyDatabase : RoomDatabase() {
161161
Migration7,
162162
Migration8,
163163
Migration9,
164+
Migration10,
164165
)
165166
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
166167
System.loadLibrary("sqlcipher")
@@ -300,5 +301,20 @@ abstract class NotallyDatabase : RoomDatabase() {
300301
)
301302
}
302303
}
304+
305+
object Migration10 : Migration(9, 10) {
306+
override fun migrate(db: SupportSQLiteDatabase) {
307+
db.execSQL("ALTER TABLE `BaseNote` ADD COLUMN `sortIdx` INTEGER")
308+
db.execSQL(
309+
"CREATE INDEX IF NOT EXISTS `index_BaseNote_sortIdx` ON `BaseNote` (`sortIdx`)"
310+
)
311+
db.execSQL(
312+
"DROP INDEX IF EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels_sortIdx` "
313+
)
314+
db.execSQL(
315+
"CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `BaseNote` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
316+
)
317+
}
318+
}
303319
}
304320
}

app/src/main/java/com/philkes/notallyx/data/dao/BaseNoteDao.kt

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.room.Insert
77
import androidx.room.OnConflictStrategy
88
import androidx.room.Query
99
import androidx.room.RawQuery
10+
import androidx.room.Transaction
1011
import androidx.room.Update
1112
import androidx.sqlite.db.SupportSQLiteQuery
1213
import com.philkes.notallyx.R
@@ -57,6 +58,10 @@ interface BaseNoteDao {
5758
}
5859

5960
suspend fun insertSafe(context: ContextWrapper, baseNote: BaseNote): Long {
61+
if (baseNote.sortIdx == null) {
62+
val maxSortIdx = getMaxSortIdx() ?: 0
63+
baseNote.sortIdx = maxSortIdx + 1
64+
}
6065
val (truncated, note) = baseNote.truncated()
6166
if (truncated) {
6267
context.log(
@@ -75,17 +80,19 @@ interface BaseNoteDao {
7580
}
7681

7782
suspend fun insertSafe(context: ContextWrapper, baseNotes: List<BaseNote>): List<Long> {
83+
val maxSortIdx = getMaxSortIdx() ?: 0
7884
val truncatedNotes = mutableListOf<BaseNote>()
7985
var truncatedCharacterSize = 0
80-
val notes =
81-
baseNotes.map { baseNote ->
82-
val (truncated, note) = baseNote.truncated()
83-
if (truncated) {
84-
truncatedCharacterSize += baseNote.body.length
85-
truncatedNotes.add(note)
86-
}
87-
note
86+
baseNotes.forEachIndexed { index, baseNote ->
87+
if (baseNote.sortIdx == null) {
88+
baseNote.sortIdx = (maxSortIdx + 1 + index)
89+
}
90+
val (truncated, note) = baseNote.truncated()
91+
if (truncated) {
92+
truncatedCharacterSize += baseNote.body.length
93+
truncatedNotes.add(note)
8894
}
95+
}
8996
if (truncatedNotes.isNotEmpty()) {
9097
context.log(
9198
TAG,
@@ -99,7 +106,7 @@ interface BaseNoteDao {
99106
)
100107
)
101108
}
102-
return insert(notes)
109+
return insert(baseNotes)
103110
}
104111

105112
@Insert suspend fun insert(baseNotes: List<BaseNote>): List<Long>
@@ -244,6 +251,16 @@ interface BaseNoteDao {
244251
*
245252
* In this case, an exception will be thrown. It is the caller's responsibility to handle it.
246253
*/
254+
@Query("UPDATE BaseNote SET sortIdx = :sortIdx WHERE id = :id")
255+
fun updateSortIdx(id: Long, sortIdx: Int)
256+
257+
@Transaction
258+
fun updateSortIndices(idToIdx: Map<Long, Int>) {
259+
idToIdx.forEach { (id, idx) -> updateSortIdx(id, idx) }
260+
}
261+
262+
@Query("SELECT MAX(sortIdx) FROM BaseNote") fun getMaxSortIdx(): Int?
263+
247264
suspend fun updateChecked(id: Long, position: Int, checked: Boolean) {
248265
val items =
249266
requireNotNull(get(id), { "updateChecked: Note with id '$id' does not exist" }).items

app/src/main/java/com/philkes/notallyx/data/model/BaseNote.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import androidx.room.PrimaryKey
77
/** Format: `#RRGGBB` or `#AARRGGBB` or [BaseNote.COLOR_DEFAULT] */
88
typealias ColorString = String
99

10-
@Entity(indices = [Index(value = ["id", "folder", "pinned", "timestamp", "labels"])])
10+
@Entity(
11+
indices =
12+
[
13+
Index(value = ["id", "folder", "pinned", "timestamp", "labels"]),
14+
Index(value = ["sortIdx"]),
15+
]
16+
)
1117
data class BaseNote(
1218
@PrimaryKey(autoGenerate = true) val id: Long,
1319
val type: Type,
@@ -26,6 +32,7 @@ data class BaseNote(
2632
val audios: List<Audio>,
2733
val reminders: List<Reminder>,
2834
val viewMode: NoteViewMode,
35+
var sortIdx: Int? = null,
2936
) : Item {
3037

3138
companion object {
@@ -55,6 +62,7 @@ data class BaseNote(
5562
if (audios != other.audios) return false
5663
if (reminders != other.reminders) return false
5764
if (viewMode != other.viewMode) return false
65+
if (sortIdx != other.sortIdx) return false
5866

5967
return true
6068
}
@@ -76,6 +84,7 @@ data class BaseNote(
7684
result = 31 * result + audios.hashCode()
7785
result = 31 * result + reminders.hashCode()
7886
result = 31 * result + viewMode.hashCode()
87+
result = 31 * result + (sortIdx ?: 0)
7988
return result
8089
}
8190
}

app/src/main/java/com/philkes/notallyx/data/model/ModelExtensions.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ fun String.toBaseNote(): BaseNote {
134134
val audios = Converters.jsonToAudios(jsonObject.getArrayOrEmpty("audios"))
135135
val reminders = Converters.jsonToReminders(jsonObject.getArrayOrEmpty("reminders"))
136136
val viewMode = NoteViewMode.valueOfOrDefault(jsonObject.getStringOrDefault("viewMode", ""))
137+
val sortIdx = jsonObject.getIntOrNull("sortIdx")
137138
return BaseNote(
138139
id,
139140
type,
@@ -152,6 +153,7 @@ fun String.toBaseNote(): BaseNote {
152153
audios,
153154
reminders,
154155
viewMode,
156+
sortIdx,
155157
)
156158
}
157159

@@ -163,6 +165,14 @@ private fun JSONObject.getStringOrDefault(key: String, defaultValue: String): St
163165
}
164166
}
165167

168+
private fun JSONObject.getIntOrNull(key: String): Int? {
169+
return try {
170+
getInt(key)
171+
} catch (exception: JSONException) {
172+
null
173+
}
174+
}
175+
166176
private fun JSONObject.getArrayOrEmpty(key: String): JSONArray {
167177
return try {
168178
getJSONArray(key)

app/src/main/java/com/philkes/notallyx/presentation/UiExtensions.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ fun Menu.add(
235235
showAsAction: Int = MenuItem.SHOW_AS_ACTION_IF_ROOM,
236236
groupId: Int = Menu.NONE,
237237
itemId: Int = Menu.NONE,
238-
order: Int = Menu.NONE,
238+
order: Int = Menu.FIRST,
239239
onClick: (item: MenuItem) -> Unit,
240240
): MenuItem {
241241
val menuItem =
@@ -399,7 +399,7 @@ fun Folder.movedToResId(): Int {
399399
}
400400

401401
fun RadioGroup.checkedTag(): Any {
402-
return this.findViewById<RadioButton?>(this.checkedRadioButtonId).tag
402+
return this.findViewById<RadioButton>(this.checkedRadioButtonId).tag
403403
}
404404

405405
fun Context.showKeyboard(view: View) {

0 commit comments

Comments
 (0)