From e9bb6585628349400e7f6ee8d21c66ec91a5aa8c Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:21:32 +0200 Subject: [PATCH 01/22] initial filter in sql --- .../1.json | 50 +++++-- .../2.json | 112 ++++++++++++++++ .../wiiznokes/gitnote/data/AppPreferences.kt | 4 +- .../github/wiiznokes/gitnote/data/room/Dao.kt | 67 ++++++++++ .../gitnote/data/room/RepoDatabase.kt | 6 +- .../wiiznokes/gitnote/data/room/Schema.kt | 10 +- .../github/wiiznokes/gitnote/ui/model/Grid.kt | 26 ++-- .../ui/screen/settings/SettingsScreen.kt | 11 -- .../gitnote/ui/viewmodel/GridViewModel.kt | 122 ++++++++++-------- app/src/main/res/values/strings.xml | 9 +- 10 files changed, 314 insertions(+), 103 deletions(-) create mode 100644 app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/2.json diff --git a/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json b/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json index a99e76bc..94e36728 100644 --- a/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json +++ b/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "fe8e7f5104ac0aee857ec1d34cd0615a", + "identityHash": "629876da98854b1836f711fc3d20c0a0", "entities": [ { "tableName": "NoteFolders", @@ -26,9 +26,7 @@ "columnNames": [ "relativePath" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "Notes", @@ -64,15 +62,51 @@ "columnNames": [ "relativePath" ] + } + }, + { + "tableName": "NotesFts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`relativePath` TEXT NOT NULL, `content` TEXT NOT NULL, content=`Notes`)", + "fields": [ + { + "fieldPath": "relativePath", + "columnName": "relativePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "Notes", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" }, - "indices": [], - "foreignKeys": [] + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_BEFORE_UPDATE BEFORE UPDATE ON `Notes` BEGIN DELETE FROM `NotesFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_BEFORE_DELETE BEFORE DELETE ON `Notes` BEGIN DELETE FROM `NotesFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_AFTER_UPDATE AFTER UPDATE ON `Notes` BEGIN INSERT INTO `NotesFts`(`docid`, `relativePath`, `content`) VALUES (NEW.`rowid`, NEW.`relativePath`, NEW.`content`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_AFTER_INSERT AFTER INSERT ON `Notes` BEGIN INSERT INTO `NotesFts`(`docid`, `relativePath`, `content`) VALUES (NEW.`rowid`, NEW.`relativePath`, NEW.`content`); END" + ] } ], - "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, 'fe8e7f5104ac0aee857ec1d34cd0615a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '629876da98854b1836f711fc3d20c0a0')" ] } } \ No newline at end of file diff --git a/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/2.json b/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/2.json new file mode 100644 index 00000000..70e7df58 --- /dev/null +++ b/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/2.json @@ -0,0 +1,112 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "629876da98854b1836f711fc3d20c0a0", + "entities": [ + { + "tableName": "NoteFolders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`relativePath` TEXT NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`relativePath`))", + "fields": [ + { + "fieldPath": "relativePath", + "columnName": "relativePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "relativePath" + ] + } + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`relativePath` TEXT NOT NULL, `content` TEXT NOT NULL, `lastModifiedTimeMillis` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`relativePath`))", + "fields": [ + { + "fieldPath": "relativePath", + "columnName": "relativePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModifiedTimeMillis", + "columnName": "lastModifiedTimeMillis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "relativePath" + ] + } + }, + { + "tableName": "NotesFts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`relativePath` TEXT NOT NULL, `content` TEXT NOT NULL, content=`Notes`)", + "fields": [ + { + "fieldPath": "relativePath", + "columnName": "relativePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "Notes", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_BEFORE_UPDATE BEFORE UPDATE ON `Notes` BEGIN DELETE FROM `NotesFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_BEFORE_DELETE BEFORE DELETE ON `Notes` BEGIN DELETE FROM `NotesFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_AFTER_UPDATE AFTER UPDATE ON `Notes` BEGIN INSERT INTO `NotesFts`(`docid`, `relativePath`, `content`) VALUES (NEW.`rowid`, NEW.`relativePath`, NEW.`content`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_AFTER_INSERT AFTER INSERT ON `Notes` BEGIN INSERT INTO `NotesFts`(`docid`, `relativePath`, `content`) VALUES (NEW.`rowid`, NEW.`relativePath`, NEW.`content`); END" + ] + } + ], + "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, '629876da98854b1836f711fc3d20c0a0')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt index ac32abd8..19f48dc5 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt @@ -8,7 +8,6 @@ import io.github.wiiznokes.gitnote.ui.model.Cred import io.github.wiiznokes.gitnote.ui.model.CredType import io.github.wiiznokes.gitnote.ui.model.NoteMinWidth import io.github.wiiznokes.gitnote.ui.model.SortOrder -import io.github.wiiznokes.gitnote.ui.model.SortType import io.github.wiiznokes.gitnote.ui.model.StorageConfiguration import io.github.wiiznokes.gitnote.ui.theme.Theme import kotlinx.coroutines.runBlocking @@ -106,9 +105,8 @@ class AppPreferences( val provider = enumPreference("provider", ProviderType.GitHub) - val sortType = enumPreference("sortType", SortType.Modification) - val sortOrder = enumPreference("sortOrder", SortOrder.Ascending) + val sortOrder = enumPreference("sortOrder", SortOrder.MostRecent) val noteMinWidth = enumPreference("noteMinWidth", NoteMinWidth.Default) val showFullNoteHeight = booleanPreference("showFullNoteHeight", false) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index b73c5c19..f79a8e79 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -5,8 +5,12 @@ import android.webkit.MimeTypeMap import androidx.room.Dao import androidx.room.Delete import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Upsert +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import io.github.wiiznokes.gitnote.data.platform.NodeFs +import io.github.wiiznokes.gitnote.ui.model.SortOrder import kotlinx.coroutines.flow.Flow private const val TAG = "Dao" @@ -81,6 +85,69 @@ interface RepoDatabaseDao { fun allNotes(): Flow> + @RawQuery(observedEntities = [Note::class]) + fun gridNotesRaw(query: SupportSQLiteQuery) : Flow> + + fun gridNotes( + currentNoteFolderRelativePath: String, + sortOrder: SortOrder, + ) : Flow> { + + val (sortColumn, order) = when (sortOrder) { + SortOrder.AZ -> "relativePath" to "ASC" + SortOrder.ZA -> "relativePath" to "DESC" + SortOrder.MostRecent -> "lastModifiedTimeMillis" to "DESC" + SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" + } + + val sql = """ + SELECT * + FROM Notes + WHERE relativePath LIKE :currentNoteFolderRelativePath || '%' + ORDER BY $sortColumn $order + """.trimIndent() + + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath)) + return this.gridNotesRaw(query) + } + + + fun gridNotesWithQuery( + currentNoteFolderRelativePath: String, + sortOrder: SortOrder, + query: String, + ) : Flow> { + + val (sortColumn, order) = when (sortOrder) { + SortOrder.AZ -> "relativePath" to "ASC" + SortOrder.ZA -> "relativePath" to "DESC" + SortOrder.MostRecent -> "lastModifiedTimeMillis" to "DESC" + SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" + } + + val sql = """ + SELECT Notes.*, + CASE + WHEN NotesFts.relativePath MATCH :query THEN 1 + WHEN NotesFts.content MATCH :query THEN 0 + ELSE -1 + END AS matchPriority + FROM Notes + JOIN NotesFts ON NotesFts.rowid = Notes.rowid + WHERE + Notes.relativePath LIKE :currentNoteFolderRelativePath || '%' + AND + NotesFts MATCH :query + ORDER BY matchPriority DESC, $sortColumn $order + """.trimIndent() + + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath, query)) + return this.gridNotesRaw(query) + } + + //fun drawerFolders(): Flow> + + @Upsert suspend fun insertNoteFolder(noteFolder: NoteFolder) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt index 9fe13efe..2b2cef4e 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt @@ -14,8 +14,8 @@ import kotlin.random.Random private const val TAG = "RepoDatabase" @Database( - entities = [NoteFolder::class, Note::class], - version = 1 + entities = [NoteFolder::class, Note::class, NoteFts::class], + version = 2 ) abstract class RepoDatabase : RoomDatabase() { @@ -40,7 +40,7 @@ abstract class RepoDatabase : RoomDatabase() { klass = RepoDatabase::class.java, name = TAG ) - .fallbackToDestructiveMigration(false) + .fallbackToDestructiveMigration(true) .addCallback(onMigration) .build() } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Schema.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Schema.kt index 4348a445..2a8bd221 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Schema.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Schema.kt @@ -2,6 +2,7 @@ package io.github.wiiznokes.gitnote.data.room import android.os.Parcelable import androidx.room.Entity +import androidx.room.Fts4 import io.github.wiiznokes.gitnote.BuildConfig import io.github.wiiznokes.gitnote.data.platform.NodeFs import io.github.wiiznokes.gitnote.data.removeFirstAndLastSlash @@ -135,4 +136,11 @@ data class Note( } override fun hashCode(): Int = id -} \ No newline at end of file +} + +@Fts4(contentEntity = Note::class) +@Entity(tableName = "NotesFts") +data class NoteFts( + val relativePath: String, + val content: String +) \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt index 5da2f146..f8c6f363 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt @@ -4,28 +4,18 @@ import io.github.wiiznokes.gitnote.MyApp import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.room.Note - -enum class SortType { - Modification, - AlphaNumeric; - - override fun toString(): String { - val res = when (this) { - Modification -> R.string.modification_sort_type - AlphaNumeric -> R.string.alpha_numeric_sort_type - } - return MyApp.appModule.uiHelper.getString(res) - } -} - enum class SortOrder { - Ascending, - Descending; + AZ, + ZA, + MostRecent, + Oldest; override fun toString(): String { val res = when (this) { - Ascending -> R.string.ascending_sort_order - Descending -> R.string.descending_sort_order + AZ -> R.string.az_sort_order + ZA -> R.string.za_sort_order + MostRecent -> R.string.most_recent_sort_order + Oldest -> R.string.oldest_sort_order } return MyApp.appModule.uiHelper.getString(res) } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt index f04bcc9c..8dab90c8 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt @@ -37,7 +37,6 @@ import io.github.wiiznokes.gitnote.ui.destination.SettingsDestination import io.github.wiiznokes.gitnote.ui.model.FileExtension import io.github.wiiznokes.gitnote.ui.model.NoteMinWidth import io.github.wiiznokes.gitnote.ui.model.SortOrder -import io.github.wiiznokes.gitnote.ui.model.SortType import io.github.wiiznokes.gitnote.ui.theme.Theme import io.github.wiiznokes.gitnote.ui.viewmodel.SettingsViewModel import kotlinx.coroutines.launch @@ -86,16 +85,6 @@ fun SettingsScreen( title = stringResource(R.string.grid) ) { - val sortType by vm.prefs.sortType.getAsState() - MultipleChoiceSettings( - title = stringResource(R.string.sort_type), - subtitle = sortType.toString(), - options = SortType.entries, - onOptionClick = { - vm.update { vm.prefs.sortType.update(it) } - } - ) - val sortOrder by vm.prefs.sortOrder.getAsState() MultipleChoiceSettings( title = stringResource(R.string.sort_order), diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 9ffe8939..fc470c6c 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -13,20 +13,17 @@ import io.github.wiiznokes.gitnote.helper.NameValidation import io.github.wiiznokes.gitnote.manager.StorageManager import io.github.wiiznokes.gitnote.ui.model.FileExtension import io.github.wiiznokes.gitnote.ui.model.GridNote -import io.github.wiiznokes.gitnote.ui.model.SortOrder.Ascending -import io.github.wiiznokes.gitnote.ui.model.SortOrder.Descending -import io.github.wiiznokes.gitnote.ui.model.SortType.AlphaNumeric -import io.github.wiiznokes.gitnote.ui.model.SortType.Modification import io.github.wiiznokes.gitnote.ui.screen.app.DrawerFolderModel -import io.github.wiiznokes.gitnote.ui.utils.fuzzySort import io.github.wiiznokes.gitnote.utils.mapAndCombine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlin.math.max @@ -204,43 +201,59 @@ class GridViewModel : ViewModel() { } - private val notes = allNotes.combine(currentNoteFolderRelativePath) { allNotes, path -> - if (path.isEmpty()) { - allNotes +// private val notes = allNotes.combine(currentNoteFolderRelativePath) { allNotes, path -> +// if (path.isEmpty()) { +// allNotes +// } else { +// allNotes.filter { +// it.relativePath.startsWith("$path/") +// } +// } +// }.let { filteredNotesFlow -> +// combine( +// filteredNotesFlow, prefs.sortType.getFlow(), prefs.sortOrder.getFlow() +// ) { filteredNotes, sortType, sortOrder -> +// +// when (sortType) { +// Modification -> when (sortOrder) { +// Ascending -> filteredNotes.sortedByDescending { it.lastModifiedTimeMillis } +// Descending -> filteredNotes.sortedBy { it.lastModifiedTimeMillis } +// } +// +// AlphaNumeric -> when (sortOrder) { +// Ascending -> filteredNotes.sortedBy { it.fullName() } +// Descending -> filteredNotes.sortedByDescending { it.fullName() } +// } +// } +// } +// }.combine(query) { allNotesInCurrentPath, query -> +// if (query.isNotEmpty()) { +// fuzzySort(query, allNotesInCurrentPath) +// } else { +// allNotesInCurrentPath +// } +// }.stateIn( +// CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() +// ) + + @OptIn(ExperimentalCoroutinesApi::class) + val notes2 = combine( + currentNoteFolderRelativePath, + prefs.sortOrder.getFlow(), + query, + ) { currentNoteFolderRelativePath, sortOrder, query -> + Triple(currentNoteFolderRelativePath, sortOrder, query) + }.flatMapLatest { triple -> + val (currentNoteFolderRelativePath, sortOrder, query) = triple + + if (query.isEmpty()) { + dao.gridNotes(currentNoteFolderRelativePath, sortOrder) } else { - allNotes.filter { - it.relativePath.startsWith("$path/") - } - } - }.let { filteredNotesFlow -> - combine( - filteredNotesFlow, prefs.sortType.getFlow(), prefs.sortOrder.getFlow() - ) { filteredNotes, sortType, sortOrder -> - - when (sortType) { - Modification -> when (sortOrder) { - Ascending -> filteredNotes.sortedByDescending { it.lastModifiedTimeMillis } - Descending -> filteredNotes.sortedBy { it.lastModifiedTimeMillis } - } - - AlphaNumeric -> when (sortOrder) { - Ascending -> filteredNotes.sortedBy { it.fullName() } - Descending -> filteredNotes.sortedByDescending { it.fullName() } - } - } - } - }.combine(query) { allNotesInCurrentPath, query -> - if (query.isNotEmpty()) { - fuzzySort(query, allNotesInCurrentPath) - } else { - allNotesInCurrentPath + dao.gridNotesWithQuery(currentNoteFolderRelativePath, sortOrder, query) } - }.stateIn( - CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() - ) - + } - val gridNotes = notes.mapAndCombine { notes -> + val gridNotes = notes2.mapAndCombine { notes -> notes.groupBy { it.nameWithoutExtension() } @@ -268,7 +281,7 @@ class GridViewModel : ViewModel() { notesFolders.filter { it.parentPath() == path } - }.combine(notes) { folders, notes -> + }.combine(notes2) { folders, notes -> folders.map { folder -> val (noteCount, lastModifiedTimeMillis) = notes @@ -285,20 +298,21 @@ class GridViewModel : ViewModel() { } }.let { folders -> combine( - folders, prefs.sortType.getFlow(), prefs.sortOrder.getFlow() - ) { folders, sortType, sortOrder -> - - when (sortType) { - Modification -> when (sortOrder) { - Ascending -> folders.sortedByDescending { it.lastModifiedTimeMillis } - Descending -> folders.sortedBy { it.lastModifiedTimeMillis } - } - - AlphaNumeric -> when (sortOrder) { - Ascending -> folders.sortedBy { it.noteFolder.fullName() } - Descending -> folders.sortedByDescending { it.noteFolder.fullName() } - } - } + folders, prefs.sortOrder.getFlow() + ) { folders, sortOrder -> + +// when (sortType) { +// Modification -> when (sortOrder) { +// Ascending -> folders.sortedByDescending { it.lastModifiedTimeMillis } +// Descending -> folders.sortedBy { it.lastModifiedTimeMillis } +// } +// +// AlphaNumeric -> when (sortOrder) { +// Ascending -> folders.sortedBy { it.noteFolder.fullName() } +// Descending -> folders.sortedByDescending { it.noteFolder.fullName() } +// } +// } + folders } }.stateIn( CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bc43949..30f67237 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,10 +3,10 @@ System Light Dark - Modification - Alpha numeric - Ascending - Descending + A-Z + Z-A + Most recent + Oldest New folder name Create new folder Yes @@ -17,7 +17,6 @@ Search in notes User interface Dynamic colors - Sort type Sort Order Minimal width of a note Show long notes entirely From aa445dee63e36774a169449bdb7c12d5c711fae4 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:58:22 +0200 Subject: [PATCH 02/22] fix escaping --- app/build.gradle.kts | 1 + .../github/wiiznokes/gitnote/data/room/Dao.kt | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abcf3df8..61e8eb30 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,6 +108,7 @@ android { debug { applicationIdSuffix = ".debug" + signingConfig = signingConfigs.getByName("nightly") } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index f79a8e79..a7b5b55f 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -125,23 +125,34 @@ interface RepoDatabaseDao { SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" } + + fun ftsEscape(query: String): String { + + // todo: change this when FTS5 is supported by room https://issuetracker.google.com/issues/146824830 + val specialChars: List = listOf("\"", "*", "-", "(", ")", "<", ">", ":", "^", "~", "'", "AND", "OR", "NOT") + + if (specialChars.any { query.contains(it) }) { + val escapedQuery = query.replace("\"", "\"\"") + return "\"$escapedQuery\" * " + } else { + return "$query*" + } + } + val sql = """ - SELECT Notes.*, - CASE - WHEN NotesFts.relativePath MATCH :query THEN 1 - WHEN NotesFts.content MATCH :query THEN 0 - ELSE -1 - END AS matchPriority + SELECT Notes.* FROM Notes JOIN NotesFts ON NotesFts.rowid = Notes.rowid WHERE Notes.relativePath LIKE :currentNoteFolderRelativePath || '%' AND NotesFts MATCH :query - ORDER BY matchPriority DESC, $sortColumn $order + ORDER BY $sortColumn $order """.trimIndent() - val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath, query)) + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath, ftsEscape(query))) + + return this.gridNotesRaw(query) } From a856357c3589464a4b526434321edef77023df4e Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:38:46 +0200 Subject: [PATCH 03/22] add rank function --- app/build.gradle.kts | 1 + .../github/wiiznokes/gitnote/data/room/Dao.kt | 45 ++++++++++++++++++- .../gitnote/data/room/RepoDatabase.kt | 15 +++++++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 1 + 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61e8eb30..dbadab7a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -167,6 +167,7 @@ dependencies { implementation(libs.room.ktx) annotationProcessor(libs.room.compiler) ksp(libs.room.compiler) + implementation(libs.sqlite) // Compose Navigation implementation(libs.reimagined.navigation) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index a7b5b55f..c44bcc48 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -11,7 +11,11 @@ import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.wiiznokes.gitnote.data.platform.NodeFs import io.github.wiiznokes.gitnote.ui.model.SortOrder +import io.requery.android.database.sqlite.SQLiteDatabase import kotlinx.coroutines.flow.Flow +import java.nio.ByteBuffer +import java.nio.ByteOrder + private const val TAG = "Dao" @@ -140,14 +144,14 @@ interface RepoDatabaseDao { } val sql = """ - SELECT Notes.* + SELECT Notes.*, rank(matchinfo(NotesFts, 'pcx')) AS score FROM Notes JOIN NotesFts ON NotesFts.rowid = Notes.rowid WHERE Notes.relativePath LIKE :currentNoteFolderRelativePath || '%' AND NotesFts MATCH :query - ORDER BY $sortColumn $order + ORDER BY score DESC, $sortColumn $order """.trimIndent() val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath, ftsEscape(query))) @@ -199,4 +203,41 @@ interface RepoDatabaseDao { removeAllNoteFolder() removeAllNote() } +} + +object Rank: SQLiteDatabase.Function { + override fun callback( + args: SQLiteDatabase.Function.Args?, + result: SQLiteDatabase.Function.Result? + ) { + if (args == null || result == null) return + + val blob = args.getBlob(0) ?: return + + val buffer = ByteBuffer.wrap(blob).order(ByteOrder.nativeOrder()) + + val phraseCount = buffer.int + val columnCount = buffer.int + + var score = 0.0 + + for (phrase in 0 until phraseCount) { + for (column in 0 until columnCount) { + + val hitsThisRow = buffer.int + val hitsAllRows = buffer.int + val docsWithHits = buffer.int + + val weight = when (column) { + 0 -> 2.0 // relativePath column + else -> 1.0 // content or others + } + + score += weight * hitsThisRow + } + } + + result.set(score) + } + } \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt index 2b2cef4e..5a563c0f 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt @@ -7,6 +7,10 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.sqlite.db.SupportSQLiteDatabase import io.github.wiiznokes.gitnote.MyApp +import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory +import io.requery.android.database.sqlite.SQLiteDatabase +import io.requery.android.database.sqlite.SQLiteDatabaseConfiguration +import io.requery.android.database.sqlite.SQLiteFunction import kotlinx.coroutines.runBlocking import kotlin.random.Random @@ -42,6 +46,17 @@ abstract class RepoDatabase : RoomDatabase() { ) .fallbackToDestructiveMigration(true) .addCallback(onMigration) + .openHelperFactory { configuration -> + val config = SQLiteDatabaseConfiguration( + context.filesDir.toPath().resolve(TAG).toString(), + SQLiteDatabase.OPEN_CREATE or SQLiteDatabase.OPEN_READWRITE + ) + + config.functions.add(SQLiteFunction("rank", 1, Rank)) + + val options = RequerySQLiteOpenHelperFactory.ConfigurationOptions { config } + RequerySQLiteOpenHelperFactory(listOf(options)).create(configuration) + } .build() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index afc7a811..50f9eb05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ activity-compose = "1.11.0" datastore-preferences = "1.1.7" compose-bom = "2025.09.00" room = "2.8.0" +sqlite = "3.49.0" compose-richtext = "1.0.0-alpha03" # todo: remove this dep and use the standard one reimagined-navigation = "1.5.0" @@ -43,6 +44,7 @@ compose-material-icons-extended = { group = "androidx.compose.material", name = room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +sqlite = { group = "com.github.requery", name = "sqlite-android", version.ref = "sqlite" } # Markdown richtext-commonmark = { group = "com.halilibo.compose-richtext", name = "richtext-commonmark", version.ref = "compose-richtext" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7327862e..2c1de85f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://jitpack.io") } } } From 7ffe38cdfc7b975aa3c1e266776e19ff18e3dbec Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:40:32 +0200 Subject: [PATCH 04/22] basic drawer folder support --- .../wiiznokes/gitnote/RepoDatabaseTest.kt | 57 ++++++++++++ .../github/wiiznokes/gitnote/data/room/Dao.kt | 87 ++++++++++++++++++- .../gitnote/data/room/RepoDatabase.kt | 19 +++- .../gitnote/ui/screen/app/DrawerScreen.kt | 4 +- .../gitnote/ui/viewmodel/GridViewModel.kt | 23 ++++- 5 files changed, 180 insertions(+), 10 deletions(-) create mode 100644 app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt diff --git a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt new file mode 100644 index 00000000..05ea18c8 --- /dev/null +++ b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt @@ -0,0 +1,57 @@ +package io.github.wiiznokes.gitnote + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.github.wiiznokes.gitnote.data.room.Note +import io.github.wiiznokes.gitnote.data.room.NoteFolder +import io.github.wiiznokes.gitnote.data.room.RepoDatabase +import io.github.wiiznokes.gitnote.data.room.RepoDatabase.Companion.buildFactory +import io.github.wiiznokes.gitnote.data.room.RepoDatabaseDao +import io.requery.android.database.sqlite.SQLiteDatabaseConfiguration.MEMORY_DB_PATH +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RepoDatabaseTest { + + private lateinit var db: RepoDatabase + private lateinit var dao: RepoDatabaseDao + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + + // https://issuetracker.google.com/issues/454083281 + db = Room.inMemoryDatabaseBuilder( + context = context, + klass = RepoDatabase::class.java + ) + .allowMainThreadQueries() + .openHelperFactory(buildFactory(MEMORY_DB_PATH)) + .build() + + + dao = db.repoDatabaseDao + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testDrawerFoldersQuery() = runTest { + + dao.insertNoteFolder(NoteFolder.new("notes")) + dao.insertNoteFolder(NoteFolder.new("notes/work")) + dao.insertNote(Note.new("notes/work/a.txt")) + + dao.testing() + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index c44bcc48..4b9ff56d 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -11,6 +11,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.wiiznokes.gitnote.data.platform.NodeFs import io.github.wiiznokes.gitnote.ui.model.SortOrder +import io.github.wiiznokes.gitnote.ui.screen.app.DrawerFolderModel import io.requery.android.database.sqlite.SQLiteDatabase import kotlinx.coroutines.flow.Flow import java.nio.ByteBuffer @@ -92,6 +93,7 @@ interface RepoDatabaseDao { @RawQuery(observedEntities = [Note::class]) fun gridNotesRaw(query: SupportSQLiteQuery) : Flow> + // todo: duplicate ? fun gridNotes( currentNoteFolderRelativePath: String, sortOrder: SortOrder, @@ -115,7 +117,7 @@ interface RepoDatabaseDao { return this.gridNotesRaw(query) } - + // todo: duplicate ? fun gridNotesWithQuery( currentNoteFolderRelativePath: String, sortOrder: SortOrder, @@ -160,7 +162,60 @@ interface RepoDatabaseDao { return this.gridNotesRaw(query) } - //fun drawerFolders(): Flow> + + @RawQuery(observedEntities = [Note::class, NoteFolder::class]) + fun gridDrawerFoldersRaw(query: SupportSQLiteQuery) : Flow> + + fun drawerFoldersSortByName( + currentNoteFolderRelativePath: String, + asc: Boolean, + ): Flow> { + + val order = if (asc) { + "ASC" + } else { + "DESC" + } + + val sql = """ + SELECT f.relativePath, f.id, COUNT(n.relativePath) + FROM NoteFolders AS f + LEFT JOIN Notes AS n ON n.relativePath LIKE f.relativePath || '%' + WHERE parentPath(f.relativePath) = :currentNoteFolderRelativePath + GROUP BY f.relativePath + ORDER BY fullName(f.relativePath) $order + """.trimIndent() + + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath)) + return this.gridDrawerFoldersRaw(query) + } + + data class Testing( + val relativePath: String, + val id: Int, + val relativePathNote: String, + ) + + @RawQuery + fun debugQuery(query: SupportSQLiteQuery): List + + fun testing() { + + val sql = """ + SELECT f.relativePath, f.id, n.relativePath AS notePath + FROM NoteFolders AS f + LEFT JOIN Notes AS n ON n.relativePath LIKE f.relativePath || '%' + WHERE parentPath(f.relativePath) = ? + GROUP BY f.relativePath + ORDER BY f.relativePath ASC + """.trimIndent() + + val query = SimpleSQLiteQuery(sql, arrayOf("")) + val results = this.debugQuery(query) + + Log.d("SQL_DEBUG", results.joinToString("\n")) + } + @Upsert @@ -240,4 +295,32 @@ object Rank: SQLiteDatabase.Function { result.set(score) } +} + +object ParentPath: SQLiteDatabase.Function { + override fun callback( + args: SQLiteDatabase.Function.Args?, + result: SQLiteDatabase.Function.Result? + ) { + if (args == null || result == null) return + + val path = args.getString(0) ?: return + + if (path == "") return + + result.set(path.substringBeforeLast("/", missingDelimiterValue = "")) + } +} + +object FullName: SQLiteDatabase.Function { + override fun callback( + args: SQLiteDatabase.Function.Args?, + result: SQLiteDatabase.Function.Result? + ) { + if (args == null || result == null) return + + val path = args.getString(0) ?: return + + result.set(path.substringAfterLast("/")) + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt index 5a563c0f..280397eb 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/RepoDatabase.kt @@ -6,6 +6,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper import io.github.wiiznokes.gitnote.MyApp import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import io.requery.android.database.sqlite.SQLiteDatabase @@ -46,19 +47,29 @@ abstract class RepoDatabase : RoomDatabase() { ) .fallbackToDestructiveMigration(true) .addCallback(onMigration) - .openHelperFactory { configuration -> + .openHelperFactory(buildFactory(context.filesDir.toPath().resolve(TAG).toString())) + .build() + } + + fun buildFactory(path: String): SupportSQLiteOpenHelper.Factory { + return object : SupportSQLiteOpenHelper.Factory { + override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper { val config = SQLiteDatabaseConfiguration( - context.filesDir.toPath().resolve(TAG).toString(), + path, SQLiteDatabase.OPEN_CREATE or SQLiteDatabase.OPEN_READWRITE ) config.functions.add(SQLiteFunction("rank", 1, Rank)) + config.functions.add(SQLiteFunction("parentPath", 1, ParentPath)) + config.functions.add(SQLiteFunction("fullName", 1, FullName)) val options = RequerySQLiteOpenHelperFactory.ConfigurationOptions { config } - RequerySQLiteOpenHelperFactory(listOf(options)).create(configuration) + return RequerySQLiteOpenHelperFactory(listOf(options)).create(configuration) } - .build() + + } } + } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt index 3cf3114a..63753d33 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/DrawerScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.room.Embedded import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.room.NoteFolder import io.github.wiiznokes.gitnote.ui.component.CustomDropDown @@ -66,9 +67,8 @@ import kotlinx.coroutines.launch private const val TAG = "DrawerScreen" data class DrawerFolderModel( + @Embedded val noteFolder: NoteFolder, val noteCount: Int, - val noteFolder: NoteFolder, - val lastModifiedTimeMillis: Long, ) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index fc470c6c..65289a67 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -13,6 +13,7 @@ import io.github.wiiznokes.gitnote.helper.NameValidation import io.github.wiiznokes.gitnote.manager.StorageManager import io.github.wiiznokes.gitnote.ui.model.FileExtension import io.github.wiiznokes.gitnote.ui.model.GridNote +import io.github.wiiznokes.gitnote.ui.model.SortOrder import io.github.wiiznokes.gitnote.ui.screen.app.DrawerFolderModel import io.github.wiiznokes.gitnote.utils.mapAndCombine import kotlinx.coroutines.CoroutineScope @@ -274,8 +275,26 @@ class GridViewModel : ViewModel() { CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() ) - + @OptIn(ExperimentalCoroutinesApi::class) val drawerFolders = combine( + currentNoteFolderRelativePath, + prefs.sortOrder.getFlow(), + ) { currentNoteFolderRelativePath, sortOrder -> + Pair(currentNoteFolderRelativePath, sortOrder) + }.flatMapLatest { pair -> + val (currentNoteFolderRelativePath, sortOrder) = pair + + when (sortOrder) { + SortOrder.AZ -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, true) + SortOrder.ZA -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, false) + SortOrder.MostRecent -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, true) + SortOrder.Oldest -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, true) + } + }.stateIn( + CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() + ) + + val drawerFolders2 = combine( dao.allNoteFolders(), currentNoteFolderRelativePath ) { notesFolders, path -> notesFolders.filter { @@ -292,7 +311,7 @@ class GridViewModel : ViewModel() { DrawerFolderModel( noteCount = noteCount, - lastModifiedTimeMillis = lastModifiedTimeMillis, +// lastModifiedTimeMillis = lastModifiedTimeMillis, noteFolder = folder ) } From 80f6e9d7c60ca0c8dfc56f80512501523bbe93c3 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:53:27 +0200 Subject: [PATCH 05/22] fix test --- .../wiiznokes/gitnote/RepoDatabaseTest.kt | 17 ++++++++++++----- .../github/wiiznokes/gitnote/data/room/Dao.kt | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt index 05ea18c8..6695ab48 100644 --- a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt +++ b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt @@ -16,6 +16,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +private const val TAG = "RepoDatabaseTest" + @RunWith(AndroidJUnit4::class) class RepoDatabaseTest { @@ -27,9 +29,10 @@ class RepoDatabaseTest { val context = ApplicationProvider.getApplicationContext() // https://issuetracker.google.com/issues/454083281 - db = Room.inMemoryDatabaseBuilder( + db = Room.databaseBuilder( context = context, - klass = RepoDatabase::class.java + klass = RepoDatabase::class.java, + name = TAG ) .allowMainThreadQueries() .openHelperFactory(buildFactory(MEMORY_DB_PATH)) @@ -47,9 +50,13 @@ class RepoDatabaseTest { @Test fun testDrawerFoldersQuery() = runTest { - dao.insertNoteFolder(NoteFolder.new("notes")) - dao.insertNoteFolder(NoteFolder.new("notes/work")) - dao.insertNote(Note.new("notes/work/a.txt")) + dao.insertNoteFolder(NoteFolder.new("")) + dao.insertNoteFolder(NoteFolder.new("test1")) + dao.insertNoteFolder(NoteFolder.new("test1/test1.2")) + + + dao.insertNote(Note.new("test1/1-1.md")) + dao.insertNote(Note.new("test1/test1.2/1.2-1.md")) dao.testing() diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index 4b9ff56d..d9ae19e1 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -193,7 +193,7 @@ interface RepoDatabaseDao { data class Testing( val relativePath: String, val id: Int, - val relativePathNote: String, + val relativePathNote: String?, ) @RawQuery From 76bdf8ddf96b2b26c3898f27d473ed7b97823396 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:25:12 +0100 Subject: [PATCH 06/22] fix note count for folders --- .../wiiznokes/gitnote/RepoDatabaseTest.kt | 4 + .../github/wiiznokes/gitnote/data/room/Dao.kt | 25 ++--- .../gitnote/ui/viewmodel/GridViewModel.kt | 92 +------------------ 3 files changed, 20 insertions(+), 101 deletions(-) diff --git a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt index 6695ab48..036cd48d 100644 --- a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt +++ b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt @@ -52,11 +52,15 @@ class RepoDatabaseTest { dao.insertNoteFolder(NoteFolder.new("")) dao.insertNoteFolder(NoteFolder.new("test1")) + dao.insertNoteFolder(NoteFolder.new("test2")) dao.insertNoteFolder(NoteFolder.new("test1/test1.2")) dao.insertNote(Note.new("test1/1-1.md")) + dao.insertNote(Note.new("test1/1-2.md")) dao.insertNote(Note.new("test1/test1.2/1.2-1.md")) + dao.insertNote(Note.new("test2/2-1.md")) + dao.insertNote(Note.new("test1/1-3.md")) dao.testing() diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index d9ae19e1..20e643c6 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -166,24 +166,25 @@ interface RepoDatabaseDao { @RawQuery(observedEntities = [Note::class, NoteFolder::class]) fun gridDrawerFoldersRaw(query: SupportSQLiteQuery) : Flow> - fun drawerFoldersSortByName( + fun drawerFolders( currentNoteFolderRelativePath: String, - asc: Boolean, + sortOrder: SortOrder, ): Flow> { - val order = if (asc) { - "ASC" - } else { - "DESC" + val (sortColumn, order) = when (sortOrder) { + SortOrder.AZ -> "f.relativePath" to "ASC" + SortOrder.ZA -> "f.relativePath" to "DESC" + SortOrder.MostRecent -> "MAX(n.lastModifiedTimeMillis)" to "DESC" + SortOrder.Oldest -> "MAX(n.lastModifiedTimeMillis)" to "ASC" } val sql = """ - SELECT f.relativePath, f.id, COUNT(n.relativePath) + SELECT f.relativePath, f.id, COUNT(n.relativePath) as noteCount FROM NoteFolders AS f LEFT JOIN Notes AS n ON n.relativePath LIKE f.relativePath || '%' - WHERE parentPath(f.relativePath) = :currentNoteFolderRelativePath + WHERE parentPath(f.relativePath) = ? GROUP BY f.relativePath - ORDER BY fullName(f.relativePath) $order + ORDER BY $sortColumn $order """.trimIndent() val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath)) @@ -193,7 +194,7 @@ interface RepoDatabaseDao { data class Testing( val relativePath: String, val id: Int, - val relativePathNote: String?, + val noteCount: Int, ) @RawQuery @@ -202,12 +203,12 @@ interface RepoDatabaseDao { fun testing() { val sql = """ - SELECT f.relativePath, f.id, n.relativePath AS notePath + SELECT f.relativePath, f.id, COUNT(n.relativePath) as noteCount FROM NoteFolders AS f LEFT JOIN Notes AS n ON n.relativePath LIKE f.relativePath || '%' WHERE parentPath(f.relativePath) = ? GROUP BY f.relativePath - ORDER BY f.relativePath ASC + ORDER BY MAX(n.lastModifiedTimeMillis) DESC """.trimIndent() val query = SimpleSQLiteQuery(sql, arrayOf("")) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 65289a67..ff46af58 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -13,8 +13,6 @@ import io.github.wiiznokes.gitnote.helper.NameValidation import io.github.wiiznokes.gitnote.manager.StorageManager import io.github.wiiznokes.gitnote.ui.model.FileExtension import io.github.wiiznokes.gitnote.ui.model.GridNote -import io.github.wiiznokes.gitnote.ui.model.SortOrder -import io.github.wiiznokes.gitnote.ui.screen.app.DrawerFolderModel import io.github.wiiznokes.gitnote.utils.mapAndCombine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -27,7 +25,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlin.math.max class GridViewModel : ViewModel() { @@ -202,43 +199,8 @@ class GridViewModel : ViewModel() { } -// private val notes = allNotes.combine(currentNoteFolderRelativePath) { allNotes, path -> -// if (path.isEmpty()) { -// allNotes -// } else { -// allNotes.filter { -// it.relativePath.startsWith("$path/") -// } -// } -// }.let { filteredNotesFlow -> -// combine( -// filteredNotesFlow, prefs.sortType.getFlow(), prefs.sortOrder.getFlow() -// ) { filteredNotes, sortType, sortOrder -> -// -// when (sortType) { -// Modification -> when (sortOrder) { -// Ascending -> filteredNotes.sortedByDescending { it.lastModifiedTimeMillis } -// Descending -> filteredNotes.sortedBy { it.lastModifiedTimeMillis } -// } -// -// AlphaNumeric -> when (sortOrder) { -// Ascending -> filteredNotes.sortedBy { it.fullName() } -// Descending -> filteredNotes.sortedByDescending { it.fullName() } -// } -// } -// } -// }.combine(query) { allNotesInCurrentPath, query -> -// if (query.isNotEmpty()) { -// fuzzySort(query, allNotesInCurrentPath) -// } else { -// allNotesInCurrentPath -// } -// }.stateIn( -// CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() -// ) - @OptIn(ExperimentalCoroutinesApi::class) - val notes2 = combine( + val note = combine( currentNoteFolderRelativePath, prefs.sortOrder.getFlow(), query, @@ -254,7 +216,7 @@ class GridViewModel : ViewModel() { } } - val gridNotes = notes2.mapAndCombine { notes -> + val gridNotes = note.mapAndCombine { notes -> notes.groupBy { it.nameWithoutExtension() } @@ -284,55 +246,7 @@ class GridViewModel : ViewModel() { }.flatMapLatest { pair -> val (currentNoteFolderRelativePath, sortOrder) = pair - when (sortOrder) { - SortOrder.AZ -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, true) - SortOrder.ZA -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, false) - SortOrder.MostRecent -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, true) - SortOrder.Oldest -> dao.drawerFoldersSortByName(currentNoteFolderRelativePath, true) - } - }.stateIn( - CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() - ) - - val drawerFolders2 = combine( - dao.allNoteFolders(), currentNoteFolderRelativePath - ) { notesFolders, path -> - notesFolders.filter { - it.parentPath() == path - } - }.combine(notes2) { folders, notes -> - folders.map { folder -> - - val (noteCount, lastModifiedTimeMillis) = notes - .filter { it.parentPath().startsWith(folder.relativePath) } - .fold(Pair(0, Long.MIN_VALUE)) { (count, max), note -> - (count + 1) to max(max, note.lastModifiedTimeMillis) - } - - DrawerFolderModel( - noteCount = noteCount, -// lastModifiedTimeMillis = lastModifiedTimeMillis, - noteFolder = folder - ) - } - }.let { folders -> - combine( - folders, prefs.sortOrder.getFlow() - ) { folders, sortOrder -> - -// when (sortType) { -// Modification -> when (sortOrder) { -// Ascending -> folders.sortedByDescending { it.lastModifiedTimeMillis } -// Descending -> folders.sortedBy { it.lastModifiedTimeMillis } -// } -// -// AlphaNumeric -> when (sortOrder) { -// Ascending -> folders.sortedBy { it.noteFolder.fullName() } -// Descending -> folders.sortedByDescending { it.noteFolder.fullName() } -// } -// } - folders - } + dao.drawerFolders(currentNoteFolderRelativePath, sortOrder) }.stateIn( CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() ) From c8457111109dd62a77b920ec514282e915bc457b Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:27:57 +0100 Subject: [PATCH 07/22] add sortOrderFolder --- .../io/github/wiiznokes/gitnote/data/AppPreferences.kt | 1 + .../gitnote/ui/screen/settings/SettingsScreen.kt | 10 ++++++++++ .../wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt | 6 ++---- app/src/main/res/values/strings.xml | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt index 19f48dc5..68d6f6fc 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/AppPreferences.kt @@ -107,6 +107,7 @@ class AppPreferences( val sortOrder = enumPreference("sortOrder", SortOrder.MostRecent) + val sortOrderFolder = enumPreference("sortOrderFolder", SortOrder.AZ) val noteMinWidth = enumPreference("noteMinWidth", NoteMinWidth.Default) val showFullNoteHeight = booleanPreference("showFullNoteHeight", false) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt index 8dab90c8..7ae75b14 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt @@ -95,6 +95,16 @@ fun SettingsScreen( } ) + val sortOrderFolder by vm.prefs.sortOrderFolder.getAsState() + MultipleChoiceSettings( + title = stringResource(R.string.sort_order_folder), + subtitle = sortOrderFolder.toString(), + options = SortOrder.entries, + onOptionClick = { + vm.update { vm.prefs.sortOrderFolder.update(it) } + } + ) + val noteMinWidth by vm.prefs.noteMinWidth.getAsState() MultipleChoiceSettings( title = stringResource(R.string.minimal_note_width), diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index ff46af58..19ca101c 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -54,8 +54,7 @@ class GridViewModel : ViewModel() { private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - - + private val _currentNoteFolderRelativePath = MutableStateFlow( if (prefs.rememberLastOpenedFolder.getBlocking()) { @@ -240,12 +239,11 @@ class GridViewModel : ViewModel() { @OptIn(ExperimentalCoroutinesApi::class) val drawerFolders = combine( currentNoteFolderRelativePath, - prefs.sortOrder.getFlow(), + prefs.sortOrderFolder.getFlow(), ) { currentNoteFolderRelativePath, sortOrder -> Pair(currentNoteFolderRelativePath, sortOrder) }.flatMapLatest { pair -> val (currentNoteFolderRelativePath, sortOrder) = pair - dao.drawerFolders(currentNoteFolderRelativePath, sortOrder) }.stateIn( CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30f67237..d3aaa45f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ User interface Dynamic colors Sort Order + Sort Order folder Minimal width of a note Show long notes entirely Remember last opened folder From 4b8fcb7a89d17531dc7882fb3c9272582f20758f Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:44:41 +0100 Subject: [PATCH 08/22] fix for selected notes --- .../github/wiiznokes/gitnote/RepoDatabaseTest.kt | 4 ++++ .../io/github/wiiznokes/gitnote/data/room/Dao.kt | 14 +++++++------- .../gitnote/ui/viewmodel/GridViewModel.kt | 9 +++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt index 036cd48d..9ecf9bfc 100644 --- a/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt +++ b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt @@ -64,5 +64,9 @@ class RepoDatabaseTest { dao.testing() + + println(dao.isNoteExist("test1/1-1.md")) + println(dao.isNoteExist("test1/1-1.md2")) + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index 20e643c6..eefa2e45 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -79,16 +79,16 @@ interface RepoDatabaseDao { } + @Query("SELECT * FROM NoteFolders WHERE relativePath = ''") suspend fun rootNoteFolder(): NoteFolder - @Query("SELECT * FROM NoteFolders") - fun allNoteFolders(): Flow> - - - @Query("SELECT * FROM Notes") - fun allNotes(): Flow> - + @Query(""" + SELECT EXISTS( + SELECT 1 FROM Notes WHERE relativePath = :relativePath + ) + """) + suspend fun isNoteExist(relativePath: String): Boolean @RawQuery(observedEntities = [Note::class]) fun gridNotesRaw(query: SupportSQLiteQuery) : Flow> diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 19ca101c..d48519c7 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -54,7 +54,7 @@ class GridViewModel : ViewModel() { private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - + private val _currentNoteFolderRelativePath = MutableStateFlow( if (prefs.rememberLastOpenedFolder.getBlocking()) { @@ -73,16 +73,13 @@ class GridViewModel : ViewModel() { get() = _selectedNotes.asStateFlow() - private val allNotes = dao.allNotes() - - init { Log.d(TAG, "init") CoroutineScope(Dispatchers.IO).launch { - allNotes.collect { allNotes -> + gridNotes.collect { selectedNotes.value.filter { selectedNote -> - allNotes.contains(selectedNote) + dao.isNoteExist(selectedNote.relativePath) }.let { newSelectedNotes -> _selectedNotes.emit(newSelectedNotes) } From 6ea352a7cf0826137114081edc4756832255f421 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:02:27 +0100 Subject: [PATCH 09/22] limit file size --- .../github/wiiznokes/gitnote/data/platform/FileSystem.kt | 5 +++++ .../java/io/github/wiiznokes/gitnote/data/room/Dao.kt | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/platform/FileSystem.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/platform/FileSystem.kt index a2ad11c7..3a3e2585 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/platform/FileSystem.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/platform/FileSystem.kt @@ -20,6 +20,7 @@ import kotlin.io.path.deleteExisting import kotlin.io.path.deleteRecursively import kotlin.io.path.exists import kotlin.io.path.extension +import kotlin.io.path.fileSize import kotlin.io.path.forEachDirectoryEntry import kotlin.io.path.getLastModifiedTime import kotlin.io.path.isDirectory @@ -44,6 +45,10 @@ sealed class NodeFs( protected val pathFs: Path = Paths.get(path) ) { + fun fileSize() : Long { + return pathFs.fileSize() + } + fun lastModifiedTime(): FileTime { return pathFs.getLastModifiedTime() } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index eefa2e45..73370213 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -20,6 +20,8 @@ import java.nio.ByteOrder private const val TAG = "Dao" +private const val LIMIT_FILE_SIZE_DB = 2 * 1024 * 1024 + @Dao interface RepoDatabaseDao { @@ -50,6 +52,12 @@ interface RepoDatabaseDao { return@forEachNodeFs } + val fileSize = nodeFs.fileSize() + if (fileSize > LIMIT_FILE_SIZE_DB) { + Log.d(TAG, "skipped ${nodeFs.path} with mime type $mimeType because size was above $LIMIT_FILE_SIZE_DB ($fileSize)") + return@forEachNodeFs + } + val relativePath = nodeFs.path.substring(startIndex = rootLength) val note = Note.new( relativePath = relativePath, From 4244ce5329d9aa2a90dd795094d08ed67ba89019 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:30:48 +0100 Subject: [PATCH 10/22] filter timestamp --- app/src/main/rust/Cargo.lock | 23 ++++++ app/src/main/rust/Cargo.toml | 1 + app/src/main/rust/src/libgit2/mod.rs | 108 ++++++++++++++------------- 3 files changed, 80 insertions(+), 52 deletions(-) diff --git a/app/src/main/rust/Cargo.lock b/app/src/main/rust/Cargo.lock index ab666c7a..fb09fdd2 100644 --- a/app/src/main/rust/Cargo.lock +++ b/app/src/main/rust/Cargo.lock @@ -274,6 +274,7 @@ dependencies = [ "jni", "libgit2-sys", "log", + "mime_guess", "rand_core", "ssh-key", "zeroize", @@ -491,6 +492,22 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "openssl-probe" version = "0.1.6" @@ -791,6 +808,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/app/src/main/rust/Cargo.toml b/app/src/main/rust/Cargo.toml index 1c5cc7d3..b6147a49 100644 --- a/app/src/main/rust/Cargo.toml +++ b/app/src/main/rust/Cargo.toml @@ -19,6 +19,7 @@ ssh-key = { version = "0.6", default-features = false, features = ["alloc", "ed2 anyhow = "1" zeroize = "1" libgit2-sys = "0.18" +mime_guess = "2" [profile.release] diff --git a/app/src/main/rust/src/libgit2/mod.rs b/app/src/main/rust/src/libgit2/mod.rs index b72a5972..b8163717 100644 --- a/app/src/main/rust/src/libgit2/mod.rs +++ b/app/src/main/rust/src/libgit2/mod.rs @@ -10,6 +10,7 @@ use git2::{ CertificateCheckStatus, FetchOptions, IndexAddOption, Progress, PushOptions, RemoteCallbacks, Repository, Signature, StatusOptions, TreeWalkMode, TreeWalkResult, }; +use mime_guess::mime; use crate::{Cred, Error, ProgressCB}; @@ -286,6 +287,54 @@ pub fn is_change() -> Result { Ok(count > 0) } +fn is_extension_supported(str: &str) -> bool { + let mimes = mime_guess::from_ext(str); + mimes.iter().any(|mime| mime.type_() == mime::TEXT) +} + +fn find_timestamp(repo: &Repository, file_path: String) -> anyhow::Result> { + // Use revwalk to find the last commit that touched this path + let mut revwalk = repo.revwalk()?; + revwalk.push_head()?; + revwalk.set_sorting(git2::Sort::TIME)?; + + for oid_result in revwalk { + let oid = oid_result?; + let commit = repo.find_commit(oid)?; + + // Check if this commit touches the file + if commit + .tree()? + .get_path(std::path::Path::new(&file_path)) + .is_ok() + { + // We want to check if this commit modified the file_path compared to its parent(s) + let parents = commit.parents().collect::>(); + let is_modified = if parents.is_empty() { + // Initial commit, consider as modified + true + } else { + // Compare trees between commit and its first parent + let parent_tree = parents[0].tree()?; + let current_tree = commit.tree()?; + + let diff = repo.diff_tree_to_tree( + Some(&parent_tree), + Some(¤t_tree), + Some(git2::DiffOptions::new().pathspec(&file_path)), + )?; + + diff.deltas().len() > 0 + }; + + if is_modified { + return Ok(Some((file_path, commit.time().seconds() * 1000))); + } + } + } + Ok(None) +} + pub fn get_timestamps() -> Result, Error> { let repo = REPO.lock().expect("repo lock"); let repo = repo.as_ref().expect("repo"); @@ -293,68 +342,23 @@ pub fn get_timestamps() -> Result, Error> { // Get HEAD commit let head = repo.head()?.peel_to_commit()?; - // We'll build a map from file path -> last commit time (u64) let mut file_timestamps = HashMap::new(); // Get the list of files in the repo at HEAD let tree = head.tree()?; - // Collect all file paths - let mut file_paths = Vec::new(); tree.walk(TreeWalkMode::PreOrder, |root, entry| { - if let Some(name) = entry.name() { - let full_path = format!("{root}{name}"); - if entry.kind() == Some(git2::ObjectType::Blob) { - file_paths.push(full_path); + if entry.kind() == Some(git2::ObjectType::Blob) + && let Some(name) = entry.name() + && is_extension_supported(name) + { + let path = format!("{root}/{name}"); + if let Ok(Some((path, time))) = find_timestamp(repo, path) { + file_timestamps.insert(path, time); } } TreeWalkResult::Ok })?; - // For each file, find last commit that modified it - for file_path in file_paths { - // Use revwalk to find the last commit that touched this path - let mut revwalk = repo.revwalk()?; - revwalk.push_head()?; - revwalk.set_sorting(git2::Sort::TIME)?; - - for oid_result in revwalk { - let oid = oid_result?; - let commit = repo.find_commit(oid)?; - - // Check if this commit touches the file - if commit - .tree()? - .get_path(std::path::Path::new(&file_path)) - .is_ok() - { - // We want to check if this commit modified the file_path compared to its parent(s) - let parents = commit.parents().collect::>(); - let is_modified = if parents.is_empty() { - // Initial commit, consider as modified - true - } else { - // Compare trees between commit and its first parent - let parent_tree = parents[0].tree()?; - let current_tree = commit.tree()?; - - let diff = repo.diff_tree_to_tree( - Some(&parent_tree), - Some(¤t_tree), - Some(git2::DiffOptions::new().pathspec(&file_path)), - )?; - - diff.deltas().len() > 0 - }; - - if is_modified { - // Store commit time - file_timestamps.insert(file_path.clone(), commit.time().seconds() * 1000); - break; - } - } - } - } - Ok(file_timestamps) } From 7ac8405b4d1e11f6cdc9a842a9e5a1af98f607ac Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 01:38:08 +0100 Subject: [PATCH 11/22] Update 1.json --- .../1.json | 50 +++---------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json b/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json index 94e36728..a99e76bc 100644 --- a/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json +++ b/app/schemas/io.github.wiiznokes.gitnote.data.room.RepoDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "629876da98854b1836f711fc3d20c0a0", + "identityHash": "fe8e7f5104ac0aee857ec1d34cd0615a", "entities": [ { "tableName": "NoteFolders", @@ -26,7 +26,9 @@ "columnNames": [ "relativePath" ] - } + }, + "indices": [], + "foreignKeys": [] }, { "tableName": "Notes", @@ -62,51 +64,15 @@ "columnNames": [ "relativePath" ] - } - }, - { - "tableName": "NotesFts", - "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`relativePath` TEXT NOT NULL, `content` TEXT NOT NULL, content=`Notes`)", - "fields": [ - { - "fieldPath": "relativePath", - "columnName": "relativePath", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [] - }, - "ftsVersion": "FTS4", - "ftsOptions": { - "tokenizer": "simple", - "tokenizerArgs": [], - "contentTable": "Notes", - "languageIdColumnName": "", - "matchInfo": "FTS4", - "notIndexedColumns": [], - "prefixSizes": [], - "preferredOrder": "ASC" }, - "contentSyncTriggers": [ - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_BEFORE_UPDATE BEFORE UPDATE ON `Notes` BEGIN DELETE FROM `NotesFts` WHERE `docid`=OLD.`rowid`; END", - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_BEFORE_DELETE BEFORE DELETE ON `Notes` BEGIN DELETE FROM `NotesFts` WHERE `docid`=OLD.`rowid`; END", - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_AFTER_UPDATE AFTER UPDATE ON `Notes` BEGIN INSERT INTO `NotesFts`(`docid`, `relativePath`, `content`) VALUES (NEW.`rowid`, NEW.`relativePath`, NEW.`content`); END", - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_NotesFts_AFTER_INSERT AFTER INSERT ON `Notes` BEGIN INSERT INTO `NotesFts`(`docid`, `relativePath`, `content`) VALUES (NEW.`rowid`, NEW.`relativePath`, NEW.`content`); END" - ] + "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, '629876da98854b1836f711fc3d20c0a0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe8e7f5104ac0aee857ec1d34cd0615a')" ] } } \ No newline at end of file From b8fb47f8aee101e0109512f8f2ba1c4fe9fcbe02 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:27:48 +0100 Subject: [PATCH 12/22] add paging --- app/build.gradle.kts | 3 + .../github/wiiznokes/gitnote/data/room/Dao.kt | 8 ++- .../gitnote/ui/screen/app/grid/GridScreen.kt | 14 ++++- .../gitnote/ui/viewmodel/GridViewModel.kt | 60 ++++++++++--------- gradle/libs.versions.toml | 5 ++ 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dbadab7a..4fcbb5b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -168,6 +168,9 @@ dependencies { annotationProcessor(libs.room.compiler) ksp(libs.room.compiler) implementation(libs.sqlite) + implementation(libs.paging) + implementation(libs.paging.compose) + implementation(libs.room.paging) // Compose Navigation implementation(libs.reimagined.navigation) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index 73370213..cf86141e 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -2,6 +2,8 @@ package io.github.wiiznokes.gitnote.data.room import android.util.Log import android.webkit.MimeTypeMap +import androidx.paging.PagingSource +import androidx.paging.PagingState import androidx.room.Dao import androidx.room.Delete import androidx.room.Query @@ -99,13 +101,13 @@ interface RepoDatabaseDao { suspend fun isNoteExist(relativePath: String): Boolean @RawQuery(observedEntities = [Note::class]) - fun gridNotesRaw(query: SupportSQLiteQuery) : Flow> + fun gridNotesRaw(query: SupportSQLiteQuery) : PagingSource // todo: duplicate ? fun gridNotes( currentNoteFolderRelativePath: String, sortOrder: SortOrder, - ) : Flow> { + ) : PagingSource { val (sortColumn, order) = when (sortOrder) { SortOrder.AZ -> "relativePath" to "ASC" @@ -130,7 +132,7 @@ interface RepoDatabaseDao { currentNoteFolderRelativePath: String, sortOrder: SortOrder, query: String, - ) : Flow> { + ) : PagingSource { val (sortColumn, order) = when (sortOrder) { SortOrder.AZ -> "relativePath" to "ASC" diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt index a4472f21..b729c5e1 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan -import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator @@ -64,6 +63,7 @@ import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.room.Note import io.github.wiiznokes.gitnote.ui.component.CustomDropDown @@ -173,7 +173,7 @@ private fun GridView( selectedNotes: List, padding: PaddingValues, ) { - val gridNotes by vm.gridNotes.collectAsState() + val gridNotes = vm.gridNotes.collectAsLazyPagingItems() val gridState = rememberLazyStaggeredGridState() @@ -222,7 +222,14 @@ private fun GridView( Spacer(modifier = Modifier.height(topBarHeight + 40.dp + 15.dp)) } - items(items = gridNotes, key = { it.note.id }) { gridNote -> + + items(count = gridNotes.itemCount, + key = { index -> + val note = gridNotes[index]!! + note.note.id + } + ) { index -> + val gridNote = gridNotes[index]!! val dropDownExpanded = remember { mutableStateOf(false) @@ -338,6 +345,7 @@ private fun GridView( } } + item( span = StaggeredGridItemSpan.FullLine ) { diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index d48519c7..f2a1fccf 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -3,6 +3,11 @@ package io.github.wiiznokes.gitnote.ui.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map import io.github.wiiznokes.gitnote.MyApp import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.AppPreferences @@ -13,7 +18,6 @@ import io.github.wiiznokes.gitnote.helper.NameValidation import io.github.wiiznokes.gitnote.manager.StorageManager import io.github.wiiznokes.gitnote.ui.model.FileExtension import io.github.wiiznokes.gitnote.ui.model.GridNote -import io.github.wiiznokes.gitnote.utils.mapAndCombine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,15 +80,15 @@ class GridViewModel : ViewModel() { init { Log.d(TAG, "init") - CoroutineScope(Dispatchers.IO).launch { - gridNotes.collect { - selectedNotes.value.filter { selectedNote -> - dao.isNoteExist(selectedNote.relativePath) - }.let { newSelectedNotes -> - _selectedNotes.emit(newSelectedNotes) - } - } - } +// CoroutineScope(Dispatchers.IO).launch { +// gridNotes.collect { +// selectedNotes.value.filter { selectedNote -> +// dao.isNoteExist(selectedNote.relativePath) +// }.let { newSelectedNotes -> +// _selectedNotes.emit(newSelectedNotes) +// } +// } +// } } fun refresh() { @@ -196,7 +200,7 @@ class GridViewModel : ViewModel() { @OptIn(ExperimentalCoroutinesApi::class) - val note = combine( + val notes = combine( currentNoteFolderRelativePath, prefs.sortOrder.getFlow(), query, @@ -205,32 +209,30 @@ class GridViewModel : ViewModel() { }.flatMapLatest { triple -> val (currentNoteFolderRelativePath, sortOrder, query) = triple - if (query.isEmpty()) { - dao.gridNotes(currentNoteFolderRelativePath, sortOrder) - } else { - dao.gridNotesWithQuery(currentNoteFolderRelativePath, sortOrder, query) - } - } + Pager( + config = PagingConfig(pageSize = 50), + pagingSourceFactory = { + if (query.isEmpty()) { + dao.gridNotes(currentNoteFolderRelativePath, sortOrder) + } else { + dao.gridNotesWithQuery(currentNoteFolderRelativePath, sortOrder, query) + } + } + ).flow.cachedIn(viewModelScope) + }.stateIn( + CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), PagingData.empty() + ) - val gridNotes = note.mapAndCombine { notes -> - notes.groupBy { - it.nameWithoutExtension() - } - }.combine(selectedNotes) { (notes, notesGroupByName), selectedNotes -> + val gridNotes = combine(notes, selectedNotes) { notes, selectedNotes -> notes.map { note -> val name = note.nameWithoutExtension() GridNote( - // if there is more than one note with the same name, draw the full path - title = if (notesGroupByName[name]!!.size > 1) { - note.relativePath - } else { - name - }, selected = selectedNotes.contains(note), note = note + title = name, selected = selectedNotes.contains(note), note = note ) } }.stateIn( - CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() + CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), PagingData.empty() ) @OptIn(ExperimentalCoroutinesApi::class) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50f9eb05..9ff70d90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ datastore-preferences = "1.1.7" compose-bom = "2025.09.00" room = "2.8.0" sqlite = "3.49.0" +paging = "3.3.6" compose-richtext = "1.0.0-alpha03" # todo: remove this dep and use the standard one reimagined-navigation = "1.5.0" @@ -45,6 +46,10 @@ room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = " room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } sqlite = { group = "com.github.requery", name = "sqlite-android", version.ref = "sqlite" } +paging = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } +paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } +room-paging = { group = "androidx.room", name = "room-paging", version.ref = "room" } + # Markdown richtext-commonmark = { group = "com.halilibo.compose-richtext", name = "richtext-commonmark", version.ref = "compose-richtext" } From 93183582038fdf79b1aa9c17bd38685b6d542aaf Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:33:41 +0100 Subject: [PATCH 13/22] fix selected notes --- .../gitnote/ui/viewmodel/GridViewModel.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index f2a1fccf..8f7a7f61 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -79,22 +79,21 @@ class GridViewModel : ViewModel() { init { Log.d(TAG, "init") + } -// CoroutineScope(Dispatchers.IO).launch { -// gridNotes.collect { -// selectedNotes.value.filter { selectedNote -> -// dao.isNoteExist(selectedNote.relativePath) -// }.let { newSelectedNotes -> -// _selectedNotes.emit(newSelectedNotes) -// } -// } -// } + suspend fun refreshSelectedNotes() { + selectedNotes.value.filter { selectedNote -> + dao.isNoteExist(selectedNote.relativePath) + }.let { newSelectedNotes -> + _selectedNotes.emit(newSelectedNotes) + } } fun refresh() { CoroutineScope(Dispatchers.IO).launch { _isRefreshing.emit(true) storageManager.updateDatabaseAndRepo() + refreshSelectedNotes() _isRefreshing.emit(false) } } From e9afbc1ab42df93efb72f4206c61c18053568b02 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:14:21 +0100 Subject: [PATCH 14/22] implement show full path when duplicate --- .../github/wiiznokes/gitnote/data/room/Dao.kt | 46 ++++++++++++------- .../github/wiiznokes/gitnote/ui/model/Grid.kt | 6 ++- .../gitnote/ui/screen/app/grid/GridScreen.kt | 3 +- .../gitnote/ui/viewmodel/GridViewModel.kt | 16 ++----- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index cf86141e..9202baca 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -12,6 +12,7 @@ import androidx.room.Upsert import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import io.github.wiiznokes.gitnote.data.platform.NodeFs +import io.github.wiiznokes.gitnote.ui.model.GridNote import io.github.wiiznokes.gitnote.ui.model.SortOrder import io.github.wiiznokes.gitnote.ui.screen.app.DrawerFolderModel import io.requery.android.database.sqlite.SQLiteDatabase @@ -101,13 +102,12 @@ interface RepoDatabaseDao { suspend fun isNoteExist(relativePath: String): Boolean @RawQuery(observedEntities = [Note::class]) - fun gridNotesRaw(query: SupportSQLiteQuery) : PagingSource + fun gridNotesRaw(query: SupportSQLiteQuery) : PagingSource - // todo: duplicate ? fun gridNotes( currentNoteFolderRelativePath: String, sortOrder: SortOrder, - ) : PagingSource { + ) : PagingSource { val (sortColumn, order) = when (sortOrder) { SortOrder.AZ -> "relativePath" to "ASC" @@ -117,9 +117,17 @@ interface RepoDatabaseDao { } val sql = """ - SELECT * - FROM Notes - WHERE relativePath LIKE :currentNoteFolderRelativePath || '%' + WITH notes_with_filename AS ( + SELECT *, fullName(relativePath) AS fileName + FROM Notes + WHERE relativePath LIKE :currentNoteFolderRelativePath || '%' + ) + SELECT *, + CASE + WHEN COUNT(*) OVER (PARTITION BY fileName) = 1 THEN 1 + ELSE 0 + END AS isUnique + FROM notes_with_filename ORDER BY $sortColumn $order """.trimIndent() @@ -127,12 +135,11 @@ interface RepoDatabaseDao { return this.gridNotesRaw(query) } - // todo: duplicate ? fun gridNotesWithQuery( currentNoteFolderRelativePath: String, sortOrder: SortOrder, query: String, - ) : PagingSource { + ) : PagingSource { val (sortColumn, order) = when (sortOrder) { SortOrder.AZ -> "relativePath" to "ASC" @@ -141,7 +148,6 @@ interface RepoDatabaseDao { SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" } - fun ftsEscape(query: String): String { // todo: change this when FTS5 is supported by room https://issuetracker.google.com/issues/146824830 @@ -156,13 +162,21 @@ interface RepoDatabaseDao { } val sql = """ - SELECT Notes.*, rank(matchinfo(NotesFts, 'pcx')) AS score - FROM Notes - JOIN NotesFts ON NotesFts.rowid = Notes.rowid - WHERE - Notes.relativePath LIKE :currentNoteFolderRelativePath || '%' - AND - NotesFts MATCH :query + WITH notes_with_filename AS ( + SELECT Notes.*, rank(matchinfo(NotesFts, 'pcx')) AS score, fullName(relativePath) as fileName + FROM Notes + JOIN NotesFts ON NotesFts.rowid = Notes.rowid + WHERE + Notes.relativePath LIKE :currentNoteFolderRelativePath || '%' + AND + NotesFts MATCH :query + ) + SELECT *, + CASE + WHEN COUNT(*) OVER (PARTITION BY fileName) = 1 THEN 1 + ELSE 0 + END AS isUnique + FROM notes_with_filename ORDER BY score DESC, $sortColumn $order """.trimIndent() diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt index f8c6f363..328bbc82 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Grid.kt @@ -1,5 +1,6 @@ package io.github.wiiznokes.gitnote.ui.model +import androidx.room.Embedded import io.github.wiiznokes.gitnote.MyApp import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.room.Note @@ -34,7 +35,8 @@ enum class NoteMinWidth(val size: Int) { } data class GridNote( + @Embedded val note: Note, - val title: String, - val selected: Boolean, + val isUnique: Boolean, + val selected: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt index b729c5e1..dbab1069 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt @@ -309,7 +309,8 @@ private fun GridView( horizontalAlignment = Alignment.Start, ) { Text( - text = if (showFullPathOfNotes.value) gridNote.note.relativePath else gridNote.title, + text = if (showFullPathOfNotes.value || !gridNote.isUnique) gridNote.note.relativePath else + gridNote.note.nameWithoutExtension(), modifier = Modifier.padding(bottom = 6.dp), overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleMedium.copy( diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 8f7a7f61..efe070d3 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -199,7 +199,7 @@ class GridViewModel : ViewModel() { @OptIn(ExperimentalCoroutinesApi::class) - val notes = combine( + val gridNotes = combine( currentNoteFolderRelativePath, prefs.sortOrder.getFlow(), query, @@ -218,16 +218,10 @@ class GridViewModel : ViewModel() { } } ).flow.cachedIn(viewModelScope) - }.stateIn( - CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), PagingData.empty() - ) - - val gridNotes = combine(notes, selectedNotes) { notes, selectedNotes -> - notes.map { note -> - val name = note.nameWithoutExtension() - - GridNote( - title = name, selected = selectedNotes.contains(note), note = note + }.combine(selectedNotes) { gridNotes, selectedNotes -> + gridNotes.map { gridNote -> + gridNote.copy( + selected = selectedNotes.contains(gridNote.note) ) } }.stateIn( From b4497cff8adc01c65d4a04ef3acafd868d50501d Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:28:30 +0100 Subject: [PATCH 15/22] Update GridViewModel.kt --- .../io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index efe070d3..3fd03a0d 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -17,7 +17,6 @@ import io.github.wiiznokes.gitnote.data.room.RepoDatabase import io.github.wiiznokes.gitnote.helper.NameValidation import io.github.wiiznokes.gitnote.manager.StorageManager import io.github.wiiznokes.gitnote.ui.model.FileExtension -import io.github.wiiznokes.gitnote.ui.model.GridNote import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi From 52d06e3acc87b727b0d4d4e1738e9e4bf4a08778 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:48:49 +0100 Subject: [PATCH 16/22] sort by filename --- .../io/github/wiiznokes/gitnote/data/room/Dao.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index 9202baca..96e24e64 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -110,8 +110,8 @@ interface RepoDatabaseDao { ) : PagingSource { val (sortColumn, order) = when (sortOrder) { - SortOrder.AZ -> "relativePath" to "ASC" - SortOrder.ZA -> "relativePath" to "DESC" + SortOrder.AZ -> "fileName" to "ASC" + SortOrder.ZA -> "fileName" to "DESC" SortOrder.MostRecent -> "lastModifiedTimeMillis" to "DESC" SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" } @@ -142,8 +142,8 @@ interface RepoDatabaseDao { ) : PagingSource { val (sortColumn, order) = when (sortOrder) { - SortOrder.AZ -> "relativePath" to "ASC" - SortOrder.ZA -> "relativePath" to "DESC" + SortOrder.AZ -> "fileName" to "ASC" + SortOrder.ZA -> "fileName" to "DESC" SortOrder.MostRecent -> "lastModifiedTimeMillis" to "DESC" SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" } @@ -196,18 +196,18 @@ interface RepoDatabaseDao { ): Flow> { val (sortColumn, order) = when (sortOrder) { - SortOrder.AZ -> "f.relativePath" to "ASC" - SortOrder.ZA -> "f.relativePath" to "DESC" + SortOrder.AZ -> "folderName" to "ASC" + SortOrder.ZA -> "folderName" to "DESC" SortOrder.MostRecent -> "MAX(n.lastModifiedTimeMillis)" to "DESC" SortOrder.Oldest -> "MAX(n.lastModifiedTimeMillis)" to "ASC" } val sql = """ - SELECT f.relativePath, f.id, COUNT(n.relativePath) as noteCount + SELECT f.relativePath, f.id, COUNT(n.relativePath) as noteCount, fullName(f.relativePath) as folderName FROM NoteFolders AS f LEFT JOIN Notes AS n ON n.relativePath LIKE f.relativePath || '%' WHERE parentPath(f.relativePath) = ? - GROUP BY f.relativePath + GROUP BY f.relativePath, f.id, folderName ORDER BY $sortColumn $order """.trimIndent() From 4d90039b99c2a8a885df0fd3454797b6cb96becd Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:55:31 +0100 Subject: [PATCH 17/22] fix --- app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt index 96e24e64..0cdc6c85 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/data/room/Dao.kt @@ -163,7 +163,7 @@ interface RepoDatabaseDao { val sql = """ WITH notes_with_filename AS ( - SELECT Notes.*, rank(matchinfo(NotesFts, 'pcx')) AS score, fullName(relativePath) as fileName + SELECT Notes.*, rank(matchinfo(NotesFts, 'pcx')) AS score, fullName(Notes.relativePath) as fileName FROM Notes JOIN NotesFts ON NotesFts.rowid = Notes.rowid WHERE From ebb33501c84a0268d969877a22a93e6149d09e41 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:29:32 +0100 Subject: [PATCH 18/22] add reload database buttons --- .../wiiznokes/gitnote/manager/StorageManager.kt | 8 ++++---- .../gitnote/ui/component/CustomDropDown.kt | 3 ++- .../gitnote/ui/screen/app/grid/GridScreen.kt | 5 ++++- .../gitnote/ui/screen/app/grid/TopGrid.kt | 15 +++++++++++++-- .../gitnote/ui/screen/settings/SettingsScreen.kt | 9 +++++++++ .../gitnote/ui/viewmodel/GridViewModel.kt | 8 ++++++++ .../gitnote/ui/viewmodel/SettingsViewModel.kt | 13 +++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 55 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt b/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt index e77f148c..ce847a3e 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/manager/StorageManager.kt @@ -95,13 +95,13 @@ class StorageManager { * The caller must ensure that all files has been committed * to keep the database in sync with the remote repo */ - private suspend fun updateDatabaseWithoutLocker(): Result { + private suspend fun updateDatabaseWithoutLocker(force: Boolean = false): Result { val fsCommit = gitManager.lastCommit() val databaseCommit = prefs.databaseCommit.get() Log.d(TAG, "fsCommit: $fsCommit, databaseCommit: $databaseCommit") - if (fsCommit == databaseCommit) { + if (!force && fsCommit == databaseCommit) { Log.d(TAG, "last commit is already loaded in data base") return success(Unit) } @@ -120,8 +120,8 @@ class StorageManager { /** * See the documentation of [updateDatabaseWithoutLocker] */ - suspend fun updateDatabase(): Result = locker.withLock { - updateDatabaseWithoutLocker() + suspend fun updateDatabase(force: Boolean = false): Result = locker.withLock { + updateDatabaseWithoutLocker(force) } /** diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/CustomDropDown.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/CustomDropDown.kt index b52fde0e..847ffa1a 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/CustomDropDown.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/CustomDropDown.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.core.util.TypedValueCompat.pxToDp +import androidx.compose.ui.platform.LocalResources private val TAG = "CustomDropDown" @@ -41,7 +42,7 @@ fun CustomDropDown( mutableStateOf(Offset.Zero) } ) { - val m = LocalContext.current.resources.displayMetrics + val m = LocalResources.current.displayMetrics val x = pxToDp(clickPosition.value.x, m).dp val y = pxToDp(clickPosition.value.y, m).dp val offset = DpOffset(x, y) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt index dbab1069..4558927c 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/GridScreen.kt @@ -152,7 +152,10 @@ fun GridScreen( drawerState = drawerState, onSettingsClick = onSettingsClick, searchFocusRequester = searchFocusRequester, - padding = padding + padding = padding, + onReloadDatabase = { + vm.reloadDatabase() + } ) } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt index 3601adf5..5b7476dc 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/grid/TopGrid.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope +import io.github.wiiznokes.gitnote.BuildConfig import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.manager.SyncState import io.github.wiiznokes.gitnote.ui.component.CustomDropDown @@ -82,6 +83,7 @@ fun TopBar( drawerState: DrawerState, onSettingsClick: () -> Unit, searchFocusRequester: FocusRequester, + onReloadDatabase: () -> Unit, ) { AnimatedContent( @@ -95,7 +97,8 @@ fun TopBar( drawerState = drawerState, vm = vm, onSettingsClick = onSettingsClick, - searchFocusRequester = searchFocusRequester + searchFocusRequester = searchFocusRequester, + onReloadDatabase = onReloadDatabase, ) } else { SelectableTopBar( @@ -115,7 +118,8 @@ private fun SearchBar( drawerState: DrawerState, vm: GridViewModel, onSettingsClick: () -> Unit, - searchFocusRequester: FocusRequester + searchFocusRequester: FocusRequester, + onReloadDatabase: () -> Unit, ) { @@ -213,6 +217,7 @@ private fun SearchBar( val readOnlyMode = vm.prefs.isReadOnlyModeActive.getAsState().value + @Suppress("KotlinConstantConditions") CustomDropDown( expanded = expanded, options = listOf( @@ -230,6 +235,12 @@ private fun SearchBar( } } ), + if (BuildConfig.BUILD_TYPE != "release") { + CustomDropDownModel( + text = stringResource(R.string.reload_database), + onClick = onReloadDatabase + ) + } else null ) ) } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt index 7ae75b14..e6618d59 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/settings/SettingsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Button import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -262,6 +263,14 @@ fun SettingsScreen( } ) + DefaultSettingsRow( + title = stringResource(R.string.reload_database), + startIcon = Icons.Default.Refresh, + onClick = { + vm.reloadDatabase() + } + ) + DefaultSettingsRow( title = stringResource(R.string.show_logs), startIcon = Icons.AutoMirrored.Filled.Article, diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt index 3fd03a0d..2ead3f8c 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/GridViewModel.kt @@ -240,5 +240,13 @@ class GridViewModel : ViewModel() { CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() ) + fun reloadDatabase() { + CoroutineScope(Dispatchers.IO).launch { + val res = storageManager.updateDatabase(force = true) + res.onFailure { + uiHelper.makeToast("$it") + } + } + } } diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SettingsViewModel.kt index 6dfa7638..a7295314 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SettingsViewModel.kt @@ -4,6 +4,7 @@ package io.github.wiiznokes.gitnote.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.wiiznokes.gitnote.MyApp +import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.data.AppPreferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,4 +27,16 @@ class SettingsViewModel : ViewModel() { storageManager.closeRepo() } } + + fun reloadDatabase() { + CoroutineScope(Dispatchers.IO).launch { + val res = storageManager.updateDatabase(force = true) + res.onFailure { + uiHelper.makeToast("$it") + } + res.onSuccess { + uiHelper.makeToast(uiHelper.getString(R.string.success_reload)) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3aaa45f..ce8e62b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,6 +37,8 @@ Do you really want to close the repository? Version Show logs + Reload the database + Successfully reloaded the database Report an issue Source code Libraries From 5a69cb9f3fc67ebf912dcc5071702a533b79d9a6 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:48:23 +0100 Subject: [PATCH 19/22] fix timestamps function --- app/src/main/rust/src/libgit2/mod.rs | 36 +++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/app/src/main/rust/src/libgit2/mod.rs b/app/src/main/rust/src/libgit2/mod.rs index b8163717..90a46bb8 100644 --- a/app/src/main/rust/src/libgit2/mod.rs +++ b/app/src/main/rust/src/libgit2/mod.rs @@ -288,7 +288,7 @@ pub fn is_change() -> Result { } fn is_extension_supported(str: &str) -> bool { - let mimes = mime_guess::from_ext(str); + let mimes = mime_guess::from_path(str); mimes.iter().any(|mime| mime.type_() == mime::TEXT) } @@ -309,22 +309,24 @@ fn find_timestamp(repo: &Repository, file_path: String) -> anyhow::Result