diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md index 50fdb9c6..c0cec2ab 100644 --- a/TRANSLATIONS.md +++ b/TRANSLATIONS.md @@ -19,34 +19,34 @@ See [Android Translations Converter](https://github.com/Crustack/android-transla | Language | Coverage | |----------|----------| -| 🇺🇸 English | 100% (337/337) | -| 🇪🇸 Catalan | 19% (65/337) | -| 🇨🇿 Czech | 92% (313/337) | -| 🇩🇰 Danish | 20% (69/337) | -| 🇩🇪 German | 99% (335/337) | -| 🇬🇷 Greek | 21% (72/337) | -| 🇪🇸 Spanish | 93% (314/337) | -| 🇫🇷 French | 97% (327/337) | -| 🇭🇺 Hungarian | 19% (65/337) | -| 🇮🇩 Indonesian | 22% (75/337) | -| 🇮🇹 Italian | 86% (291/337) | -| 🇯🇵 Japanese | 21% (73/337) | -| 🇲🇲 Burmese | 26% (90/337) | -| 🇳🇴 Norwegian Bokmål | 31% (106/337) | -| 🇳🇱 Dutch | 62% (212/337) | -| 🇳🇴 Norwegian Nynorsk | 31% (106/337) | -| 🇵🇱 Polish | 89% (300/337) | -| 🇧🇷 Portuguese (Brazil) | 92% (312/337) | -| 🇵🇹 Portuguese (Portugal) | 21% (71/337) | -| 🇷🇴 Romanian | 89% (301/337) | -| 🇷🇺 Russian | 90% (305/337) | -| 🇸🇰 Slovak | 19% (65/337) | -| 🇸🇮 Slovenian | 32% (109/337) | -| 🇸🇪 Swedish | 18% (63/337) | -| 🇵🇭 Tagalog | 19% (65/337) | -| 🇹🇷 Turkish | 21% (73/337) | -| 🇺🇦 Ukrainian | 98% (331/337) | -| 🇻🇳 Vietnamese | 31% (107/337) | -| 🇨🇳 Chinese (Simplified) | 97% (328/337) | -| 🇹🇼 Chinese (Traditional) | 87% (294/337) | +| 🇺🇸 English | 100% (339/339) | +| 🇪🇸 Catalan | 19% (65/339) | +| 🇨🇿 Czech | 92% (313/339) | +| 🇩🇰 Danish | 20% (69/339) | +| 🇩🇪 German | 98% (335/339) | +| 🇬🇷 Greek | 21% (72/339) | +| 🇪🇸 Spanish | 92% (314/339) | +| 🇫🇷 French | 96% (327/339) | +| 🇭🇺 Hungarian | 19% (65/339) | +| 🇮🇩 Indonesian | 22% (75/339) | +| 🇮🇹 Italian | 85% (291/339) | +| 🇯🇵 Japanese | 21% (73/339) | +| 🇲🇲 Burmese | 26% (90/339) | +| 🇳🇴 Norwegian Bokmål | 31% (106/339) | +| 🇳🇱 Dutch | 62% (212/339) | +| 🇳🇴 Norwegian Nynorsk | 31% (106/339) | +| 🇵🇱 Polish | 88% (300/339) | +| 🇧🇷 Portuguese (Brazil) | 92% (312/339) | +| 🇵🇹 Portuguese (Portugal) | 20% (71/339) | +| 🇷🇴 Romanian | 88% (301/339) | +| 🇷🇺 Russian | 89% (305/339) | +| 🇸🇰 Slovak | 19% (65/339) | +| 🇸🇮 Slovenian | 32% (109/339) | +| 🇸🇪 Swedish | 18% (63/339) | +| 🇵🇭 Tagalog | 19% (65/339) | +| 🇹🇷 Turkish | 21% (73/339) | +| 🇺🇦 Ukrainian | 97% (331/339) | +| 🇻🇳 Vietnamese | 31% (107/339) | +| 🇨🇳 Chinese (Simplified) | 96% (328/339) | +| 🇹🇼 Chinese (Traditional) | 86% (294/339) | \ No newline at end of file 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