diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abcf3df8..4fcbb5b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,6 +108,7 @@ android { debug { applicationIdSuffix = ".debug" + signingConfig = signingConfigs.getByName("nightly") } } @@ -166,6 +167,10 @@ dependencies { implementation(libs.room.ktx) 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/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/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..9ecf9bfc --- /dev/null +++ b/app/src/androidTest/java/io/github/wiiznokes/gitnote/RepoDatabaseTest.kt @@ -0,0 +1,72 @@ +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 + +private const val TAG = "RepoDatabaseTest" + +@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.databaseBuilder( + context = context, + klass = RepoDatabase::class.java, + name = TAG + ) + .allowMainThreadQueries() + .openHelperFactory(buildFactory(MEMORY_DB_PATH)) + .build() + + + dao = db.repoDatabaseDao + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testDrawerFoldersQuery() = runTest { + + 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() + + + 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/MainActivity.kt b/app/src/main/java/io/github/wiiznokes/gitnote/MainActivity.kt index cd9155c8..69b698b1 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/MainActivity.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/MainActivity.kt @@ -68,18 +68,19 @@ class MainActivity : ComponentActivity() { } else { Log.d(TAG, "launch as EDIT_IS_UNSAVED") Destination.App( - AppDestination.Edit(EditParams.Saved( - note = saveInfo.previousNote, - editType = saveInfo.editType, - name = saveInfo.name, - content = saveInfo.content - )) + AppDestination.Edit( + EditParams.Saved( + note = saveInfo.previousNote, + editType = saveInfo.editType, + name = saveInfo.name, + content = saveInfo.content + ) + ) ) } } else Destination.App(AppDestination.Grid) - } - else Destination.Setup(SetupDestination.Main) + } else Destination.Setup(SetupDestination.Main) } 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..f7a19d31 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 @@ -79,6 +78,7 @@ class AppPreferences( password = userPassPassword.get() ) } + CredType.Ssh -> Cred.Ssh( username = this.sshUsername.get(), publicKey = this.publicKey.get(), @@ -95,20 +95,22 @@ class AppPreferences( publicKey.update(cred.publicKey) privateKey.update(cred.privateKey) } + is Cred.UserPassPlainText -> { credType.update(CredType.UserPassPlainText) userPassUsername.update(cred.username) userPassPassword.update(cred.password) } + null -> credType.update(CredType.None) } } 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 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/data/platform/FileSystem.kt b/app/src/main/java/io/github/wiiznokes/gitnote/data/platform/FileSystem.kt index a2ad11c7..705045ed 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 b73c5c19..3631d539 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,15 +2,28 @@ package io.github.wiiznokes.gitnote.data.room import android.util.Log import android.webkit.MimeTypeMap +import androidx.paging.PagingSource 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.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 import kotlinx.coroutines.flow.Flow +import java.nio.ByteBuffer +import java.nio.ByteOrder + private const val TAG = "Dao" +private const val LIMIT_FILE_SIZE_DB = 2 * 1024 * 1024 + @Dao interface RepoDatabaseDao { @@ -41,10 +54,20 @@ 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, - lastModifiedTimeMillis = timestamps.get(relativePath) ?: nodeFs.lastModifiedTime().toMillis(), + lastModifiedTimeMillis = timestamps.get(relativePath) + ?: nodeFs.lastModifiedTime().toMillis(), content = nodeFs.readText(), ) insertNote(note) @@ -73,12 +96,155 @@ interface RepoDatabaseDao { @Query("SELECT * FROM NoteFolders WHERE relativePath = ''") suspend fun rootNoteFolder(): NoteFolder - @Query("SELECT * FROM NoteFolders") - fun allNoteFolders(): 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): PagingSource + + fun gridNotes( + currentNoteFolderRelativePath: String, + sortOrder: SortOrder, + ): PagingSource { + + val (sortColumn, order) = when (sortOrder) { + SortOrder.AZ -> "fileName" to "ASC" + SortOrder.ZA -> "fileName" to "DESC" + SortOrder.MostRecent -> "lastModifiedTimeMillis" to "DESC" + SortOrder.Oldest -> "lastModifiedTimeMillis" to "ASC" + } + + val sql = """ + 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() + + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath)) + return this.gridNotesRaw(query) + } + + fun gridNotesWithQuery( + currentNoteFolderRelativePath: String, + sortOrder: SortOrder, + query: String, + ): PagingSource { + + val (sortColumn, order) = when (sortOrder) { + SortOrder.AZ -> "fileName" to "ASC" + SortOrder.ZA -> "fileName" to "DESC" + SortOrder.MostRecent -> "lastModifiedTimeMillis" to "DESC" + 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 = """ + WITH notes_with_filename AS ( + SELECT Notes.*, rank(matchinfo(NotesFts, 'pcx')) AS score, fullName(Notes.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() + + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath, ftsEscape(query))) + + + return this.gridNotesRaw(query) + } + + + @RawQuery(observedEntities = [Note::class, NoteFolder::class]) + fun gridDrawerFoldersRaw(query: SupportSQLiteQuery): Flow> + + fun drawerFolders( + currentNoteFolderRelativePath: String, + sortOrder: SortOrder, + ): Flow> { + + val (sortColumn, order) = when (sortOrder) { + 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, 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, f.id, folderName + ORDER BY $sortColumn $order + """.trimIndent() + + val query = SimpleSQLiteQuery(sql, arrayOf(currentNoteFolderRelativePath)) + return this.gridDrawerFoldersRaw(query) + } - @Query("SELECT * FROM Notes") - fun allNotes(): Flow> + data class Testing( + val relativePath: String, + val id: Int, + val noteCount: Int, + ) + + @RawQuery + fun debugQuery(query: SupportSQLiteQuery): List + + fun testing() { + + val sql = """ + 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 MAX(n.lastModifiedTimeMillis) DESC + """.trimIndent() + + val query = SimpleSQLiteQuery(sql, arrayOf("")) + val results = this.debugQuery(query) + + Log.d("SQL_DEBUG", results.joinToString("\n")) + } @Upsert @@ -121,4 +287,74 @@ 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 + buffer.int + buffer.int + + if (hitsThisRow != 0) { + // relativePath column + if (column == 0) { + result.set(2.0) + return + } + // content column + else { + score = 1.0 + } + } + } + } + + 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 9fe13efe..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,7 +6,12 @@ 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 +import io.requery.android.database.sqlite.SQLiteDatabaseConfiguration +import io.requery.android.database.sqlite.SQLiteFunction import kotlinx.coroutines.runBlocking import kotlin.random.Random @@ -14,8 +19,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,10 +45,31 @@ abstract class RepoDatabase : RoomDatabase() { klass = RepoDatabase::class.java, name = TAG ) - .fallbackToDestructiveMigration(false) + .fallbackToDestructiveMigration(true) .addCallback(onMigration) + .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( + 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 } + return RequerySQLiteOpenHelperFactory(listOf(options)).create(configuration) + } + + } + } + } } 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/helper/StoragePermissionHelper.kt b/app/src/main/java/io/github/wiiznokes/gitnote/helper/StoragePermissionHelper.kt index 462a8f79..65a3388f 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/helper/StoragePermissionHelper.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/helper/StoragePermissionHelper.kt @@ -3,13 +3,12 @@ package io.github.wiiznokes.gitnote.helper import android.Manifest import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Environment import android.provider.Settings import androidx.activity.result.contract.ActivityResultContract +import androidx.core.net.toUri import io.github.wiiznokes.gitnote.BuildConfig import io.github.wiiznokes.gitnote.helper.StoragePermissionHelper.Companion.isPermissionGranted -import androidx.core.net.toUri class StoragePermissionHelper { diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt b/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt index 6a85ec6a..b346628d 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/manager/GitManager.kt @@ -5,10 +5,6 @@ import androidx.annotation.Keep import io.github.wiiznokes.gitnote.MyApp import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.ui.model.Cred -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.Result.Companion.failure @@ -67,7 +63,7 @@ class GitManager { } isLibInitialized = true } - success(f()) + success(f()) } catch (e: Exception) { e.printStackTrace() failure(e) 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..7ac898c5 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 @@ -16,7 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.core.util.TypedValueCompat.pxToDp @@ -41,7 +41,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/component/SetupPage.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt index dd3a6ad6..dfdca4fc 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/component/SetupPage.kt @@ -3,7 +3,6 @@ package io.github.wiiznokes.gitnote.ui.component import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -63,8 +62,7 @@ private fun SetupTitle( title: String, ) { Text( - modifier = Modifier - , + modifier = Modifier, text = title, fontSize = 17.sp, fontWeight = FontWeight.SemiBold diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/AppDestination.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/AppDestination.kt index 80cbf2e3..2d99f84f 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/AppDestination.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/destination/AppDestination.kt @@ -34,7 +34,7 @@ sealed class EditParams : Parcelable { ) : EditParams() fun fileExtension(): FileExtension { - return when(this) { + return when (this) { is Idle -> this.note.fileExtension() is Saved -> this.note.fileExtension() } 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..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,31 +1,22 @@ 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 - -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) } @@ -44,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/model/Init.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Init.kt index 236091d1..9721211b 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Init.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/model/Init.kt @@ -5,9 +5,8 @@ import io.github.wiiznokes.gitnote.data.AppPreferences import kotlinx.parcelize.Parcelize - @Parcelize -sealed class Cred: Parcelable { +sealed class Cred : Parcelable { data class UserPassPlainText( val username: String, val password: String 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/screen/app/edit/EditScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/edit/EditScreen.kt index 07d0a882..71fa728d 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/edit/EditScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/app/edit/EditScreen.kt @@ -52,7 +52,6 @@ import io.github.wiiznokes.gitnote.ui.viewmodel.edit.MarkDownVM import io.github.wiiznokes.gitnote.ui.viewmodel.edit.TextVM import io.github.wiiznokes.gitnote.ui.viewmodel.edit.newEditViewModel import io.github.wiiznokes.gitnote.ui.viewmodel.edit.newMarkDownVM -import kotlin.text.startsWith private const val TAG = "EditScreen" 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..a29fa8ca 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 @@ -152,7 +152,10 @@ fun GridScreen( drawerState = drawerState, onSettingsClick = onSettingsClick, searchFocusRequester = searchFocusRequester, - padding = padding + padding = padding, + onReloadDatabase = { + vm.reloadDatabase() + } ) } @@ -173,7 +176,7 @@ private fun GridView( selectedNotes: List, padding: PaddingValues, ) { - val gridNotes by vm.gridNotes.collectAsState() + val gridNotes = vm.gridNotes.collectAsLazyPagingItems() val gridState = rememberLazyStaggeredGridState() @@ -222,7 +225,15 @@ 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) @@ -302,7 +313,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( @@ -338,6 +350,7 @@ private fun GridView( } } + item( span = StaggeredGridItemSpan.FullLine ) { 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..fe95cf1c 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 ) ) } @@ -335,8 +346,6 @@ private fun SelectableTopBar( } - - @Composable private fun SyncStateIcon( state: SyncState, 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..b7d4ae28 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 @@ -5,11 +5,11 @@ import android.content.ClipDescription import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material.icons.automirrored.filled.MenuBook 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 @@ -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,23 +85,23 @@ fun SettingsScreen( title = stringResource(R.string.grid) ) { - val sortType by vm.prefs.sortType.getAsState() + val sortOrder by vm.prefs.sortOrder.getAsState() MultipleChoiceSettings( - title = stringResource(R.string.sort_type), - subtitle = sortType.toString(), - options = SortType.entries, + title = stringResource(R.string.sort_order), + subtitle = sortOrder.toString(), + options = SortOrder.entries, onOptionClick = { - vm.update { vm.prefs.sortType.update(it) } + vm.update { vm.prefs.sortOrder.update(it) } } ) - val sortOrder by vm.prefs.sortOrder.getAsState() + val sortOrderFolder by vm.prefs.sortOrderFolder.getAsState() MultipleChoiceSettings( - title = stringResource(R.string.sort_order), - subtitle = sortOrder.toString(), + title = stringResource(R.string.sort_order_folder), + subtitle = sortOrderFolder.toString(), options = SortOrder.entries, onOptionClick = { - vm.update { vm.prefs.sortOrder.update(it) } + vm.update { vm.prefs.sortOrderFolder.update(it) } } ) @@ -263,6 +262,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/screen/setup/remote/AuthorizeGitNoteScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt index 7699824e..24e5fb4a 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/AuthorizeGitNoteScreen.kt @@ -51,7 +51,7 @@ fun AuthorizeGitNoteScreen( }, enabled = authState.isClickable() && authState != AuthState.Success ) { - if (!authState.isLoading()) { + if (!authState.isLoading()) { Text(text = stringResource(R.string.authorize_gitnote)) } else { Text(text = authState.message()) diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectGenerateNewKeysScreen.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectGenerateNewKeysScreen.kt index 5a0015ab..447c7d45 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectGenerateNewKeysScreen.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectGenerateNewKeysScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.ui.component.AppPage -import io.github.wiiznokes.gitnote.ui.component.SetupLine import io.github.wiiznokes.gitnote.ui.component.SetupPage diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectProvider.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectProvider.kt index eff13db1..ebdc2d36 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectProvider.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/screen/setup/remote/SelectProvider.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.tooling.preview.Preview import io.github.wiiznokes.gitnote.R import io.github.wiiznokes.gitnote.provider.ProviderType import io.github.wiiznokes.gitnote.ui.component.AppPage -import io.github.wiiznokes.gitnote.ui.component.SetupLine import io.github.wiiznokes.gitnote.ui.component.SetupPage diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/theme/Theme.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/theme/Theme.kt index adf4ad44..0e4a2fd2 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/theme/Theme.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/theme/Theme.kt @@ -1,7 +1,6 @@ package io.github.wiiznokes.gitnote.ui.theme import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme 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..b596daab 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 @@ -12,24 +17,17 @@ 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 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 class GridViewModel : ViewModel() { @@ -60,8 +58,6 @@ class GridViewModel : ViewModel() { val isRefreshing: StateFlow = _isRefreshing.asStateFlow() - - private val _currentNoteFolderRelativePath = MutableStateFlow( if (prefs.rememberLastOpenedFolder.getBlocking()) { prefs.lastOpenedFolder.getBlocking() @@ -79,20 +75,15 @@ class GridViewModel : ViewModel() { get() = _selectedNotes.asStateFlow() - private val allNotes = dao.allNotes() - - init { Log.d(TAG, "init") + } - CoroutineScope(Dispatchers.IO).launch { - allNotes.collect { allNotes -> - selectedNotes.value.filter { selectedNote -> - allNotes.contains(selectedNote) - }.let { newSelectedNotes -> - _selectedNotes.emit(newSelectedNotes) - } - } + suspend fun refreshSelectedNotes() { + selectedNotes.value.filter { selectedNote -> + dao.isNoteExist(selectedNote.relativePath) + }.let { newSelectedNotes -> + _selectedNotes.emit(newSelectedNotes) } } @@ -100,6 +91,7 @@ class GridViewModel : ViewModel() { CoroutineScope(Dispatchers.IO).launch { _isRefreshing.emit(true) storageManager.updateDatabaseAndRepo() + refreshSelectedNotes() _isRefreshing.emit(false) } } @@ -204,105 +196,56 @@ 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() } + @OptIn(ExperimentalCoroutinesApi::class) + val gridNotes = combine( + currentNoteFolderRelativePath, + prefs.sortOrder.getFlow(), + query, + ) { currentNoteFolderRelativePath, sortOrder, query -> + Triple(currentNoteFolderRelativePath, sortOrder, query) + }.flatMapLatest { triple -> + val (currentNoteFolderRelativePath, sortOrder, query) = triple + + Pager( + config = PagingConfig(pageSize = 50), + pagingSourceFactory = { + if (query.isEmpty()) { + dao.gridNotes(currentNoteFolderRelativePath, sortOrder) + } else { + dao.gridNotesWithQuery(currentNoteFolderRelativePath, sortOrder, query) } } - } - }.combine(query) { allNotesInCurrentPath, query -> - if (query.isNotEmpty()) { - fuzzySort(query, allNotesInCurrentPath) - } else { - allNotesInCurrentPath - } - }.stateIn( - CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() - ) - - - val gridNotes = notes.mapAndCombine { notes -> - notes.groupBy { - it.nameWithoutExtension() - } - }.combine(selectedNotes) { (notes, notesGroupByName), 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 + ).flow.cachedIn(viewModelScope) + }.combine(selectedNotes) { gridNotes, selectedNotes -> + gridNotes.map { gridNote -> + gridNote.copy( + selected = selectedNotes.contains(gridNote.note) ) } }.stateIn( - CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), emptyList() + CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), PagingData.empty() ) - + @OptIn(ExperimentalCoroutinesApi::class) val drawerFolders = combine( - dao.allNoteFolders(), currentNoteFolderRelativePath - ) { notesFolders, path -> - notesFolders.filter { - it.parentPath() == path - } - }.combine(notes) { 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.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() } - } - } - } + currentNoteFolderRelativePath, + 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() ) + 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/MainViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/MainViewModel.kt index 81211ee7..432ce7a8 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/MainViewModel.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class MainViewModel: ViewModel() { +class MainViewModel : ViewModel() { val prefs: AppPreferences = MyApp.appModule.appPreferences private val gitManager = MyApp.appModule.gitManager 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/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt index a9c19949..c294cec7 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/SetupViewModel.kt @@ -41,22 +41,25 @@ interface SetupViewModelI { remoteUrl: String, cred: Cred? = null, onSuccess: () -> Unit - ) {} + ) { + } fun createRepoAutomatic( repoName: String, storageConfig: StorageConfiguration, onSuccess: () -> Unit - ) {} + ) { + } fun cloneRepoAutomatic( repoName: String, storageConfig: StorageConfiguration, onSuccess: () -> Unit - ) {} + ) { + } } -class SetupViewModelMock: SetupViewModelI +class SetupViewModelMock : SetupViewModelI class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewModelI { @@ -148,7 +151,6 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM } - fun checkPathForClone(repoPath: String): Result { val result = NodeFs.Folder.fromPath(repoPath).isEmptyDirectory() result.onFailure { @@ -339,13 +341,13 @@ class SetupViewModel(val authFlow: SharedFlow) : ViewModel(), SetupViewM sealed class InitState { - data object Idle: InitState() + data object Idle : InitState() - open fun isClickable() : Boolean = true + open fun isClickable(): Boolean = true open fun isLoading(): Boolean = false open fun message(): String = "" - sealed class AuthState: InitState() { + sealed class AuthState : InitState() { data object Idle : AuthState() data object GetAccessToken : AuthState() data object FetchRepos : AuthState() @@ -354,7 +356,8 @@ sealed class InitState { data object Error : AuthState() override fun isClickable(): Boolean = this is Idle || this is Error || this is Success - override fun isLoading(): Boolean = this is GetAccessToken || this is FetchRepos || this is GetUserInfo + override fun isLoading(): Boolean = + this is GetAccessToken || this is FetchRepos || this is GetUserInfo override fun message(): String { return when (this) { @@ -368,7 +371,7 @@ sealed class InitState { } } - sealed class AuthStep2: InitState() { + sealed class AuthStep2 : InitState() { data object Idle : AuthStep2() data object CreateRepo : AuthStep2() data object AddDeployKey : AuthStep2() @@ -388,7 +391,7 @@ sealed class InitState { } - sealed class CloneState: InitState() { + sealed class CloneState : InitState() { data object Idle : CloneState() data class Cloning(val percent: Int) : CloneState() data object Error : CloneState() diff --git a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/MarkDownVM.kt b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/MarkDownVM.kt index b5e5a5ba..6cd3348a 100644 --- a/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/MarkDownVM.kt +++ b/app/src/main/java/io/github/wiiznokes/gitnote/ui/viewmodel/edit/MarkDownVM.kt @@ -1,6 +1,5 @@ package io.github.wiiznokes.gitnote.ui.viewmodel.edit -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel diff --git a/app/src/main/res/drawable/cloud_alert_24px.xml b/app/src/main/res/drawable/cloud_alert_24px.xml index 53bc270a..2a74e778 100644 --- a/app/src/main/res/drawable/cloud_alert_24px.xml +++ b/app/src/main/res/drawable/cloud_alert_24px.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bc43949..ce8e62b4 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,8 +17,8 @@ Search in notes User interface Dynamic colors - Sort type Sort Order + Sort Order folder Minimal width of a note Show long notes entirely Remember last opened folder @@ -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 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/lib.rs b/app/src/main/rust/src/lib.rs index 106a9281..02631c55 100644 --- a/app/src/main/rust/src/lib.rs +++ b/app/src/main/rust/src/lib.rs @@ -15,6 +15,7 @@ extern crate log; mod utils; mod key_gen; mod libgit2; +mod mime_types; #[cfg(test)] mod test; diff --git a/app/src/main/rust/src/libgit2/mod.rs b/app/src/main/rust/src/libgit2/mod.rs index b72a5972..5c58aa41 100644 --- a/app/src/main/rust/src/libgit2/mod.rs +++ b/app/src/main/rust/src/libgit2/mod.rs @@ -11,7 +11,7 @@ use git2::{ Repository, Signature, StatusOptions, TreeWalkMode, TreeWalkResult, }; -use crate::{Cred, Error, ProgressCB}; +use crate::{Cred, Error, ProgressCB, mime_types::is_extension_supported}; mod merge; @@ -286,6 +286,51 @@ pub fn is_change() -> Result { Ok(count > 0) } +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 parent = commit.parents().next(); + + let is_modified = match parent { + Some(parent) => { + // Compare trees between commit and its first parent + let parent_tree = parent.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 + } + // Initial commit, consider as modified + None => true, + }; + + 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 +338,25 @@ 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() + && let Some(extension) = Path::new(name).extension() + && let Some(extension) = extension.to_str() + && is_extension_supported(extension) + { + 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) } diff --git a/app/src/main/rust/src/libgit2/test.rs b/app/src/main/rust/src/libgit2/test.rs index 0ba6fb57..c280560d 100644 --- a/app/src/main/rust/src/libgit2/test.rs +++ b/app/src/main/rust/src/libgit2/test.rs @@ -9,3 +9,17 @@ fn timestamp() { dbg!(&res); } + +#[test] +#[ignore = "local repo"] +fn timestamp2() { + open_repo("../../../../../note-pv").unwrap(); + + let res = get_timestamps(); + + let mut res = res.unwrap().into_iter().collect::>(); + + res.sort_by(|a, b| a.1.cmp(&b.1)); + + dbg!(&res); +} diff --git a/app/src/main/rust/src/mime_types.rs b/app/src/main/rust/src/mime_types.rs new file mode 100644 index 00000000..6b7beca2 --- /dev/null +++ b/app/src/main/rust/src/mime_types.rs @@ -0,0 +1,69 @@ +use mime_guess::mime; + +const ADDITIONAL_SUPPORTED_EXTENSIONS: &[&str] = &["sh", "fish", "ps1", "bat"]; + +pub fn is_extension_supported(extension: &str) -> bool { + if ADDITIONAL_SUPPORTED_EXTENSIONS.contains(&extension) { + return true; + } + + let mimes = mime_guess::from_ext(extension); + mimes.iter().any(|mime| mime.type_() == mime::TEXT) +} + +#[cfg(test)] +mod test { + use std::{collections::HashMap, fs, path::Path}; + + use super::*; + + #[test] + #[ignore = "local repo"] + fn check_extension() { + let path = Path::new("../../../../../note-pv"); + let mut counts: HashMap = HashMap::new(); + + // Recursively walk through the directory + fn visit_dir(dir: &Path, counts: &mut HashMap) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + visit_dir(&path, counts); + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + *counts.entry(ext.to_lowercase()).or_insert(0) += 1; + } + } + } + } + + visit_dir(path, &mut counts); + + // Sort by number of files (descending) + let mut extensions = counts + .into_iter() + .map(|(extension, count)| { + let supported = { + let mimes = mime_guess::from_ext(&extension); + mimes.iter().any(|mime| mime.type_() == mime::TEXT) + }; + + let supported_manually = + ADDITIONAL_SUPPORTED_EXTENSIONS.contains(&extension.as_str()); + + (extension, count, supported, supported_manually) + }) + .collect::>(); + + extensions.sort_by(|a, b| b.1.cmp(&a.1)); + extensions.sort_by(|a, b| b.2.cmp(&a.2)); + + println!("extension supported supported_manually count"); + // Print results + for (ext, count, supported, supported_manually) in extensions { + println!( + "{ext:<10} {supported:<5} {supported_manually:<5} {count:<6}" + ); + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index afc7a811..9ff70d90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ activity-compose = "1.11.0" 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" @@ -43,6 +45,11 @@ 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" } +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" } 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") } } }