diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt index 6dc961a..ae7d5e1 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt @@ -7,13 +7,49 @@ import kotlinx.serialization.json.Json import mg.dot.feufaro.FileRepository import java.io.File +fun String.normalizeForSearch(): String { + return this.lowercase().filter { it.isLetterOrDigit() } +} data class DrawerItem( val id: Int, val title: String, val contentTitle: String, val firstNotes: String = "", val path: String -) +) { + fun matchesQuery(query: String): Boolean { + if (query.isBlank()) return true + val cleanQuery = query.normalizeForSearch() + val cleanTitle = title.normalizeForSearch() + val cleanContent = contentTitle.normalizeForSearch() + return (cleanTitle + cleanContent).contains(cleanQuery) + } + + fun getSearchScore(query: String): Int { + if (query.isBlank()) return 0 + + val cleanQuery = query.normalizeForSearch() + val cleanTitle = title.normalizeForSearch() + val cleanContent = contentTitle.normalizeForSearch() + + if (cleanTitle == cleanQuery) return 1000 + if (cleanTitle.startsWith(cleanQuery)) { + val nextCharIndex = cleanQuery.length + + val isNextCharDigit = nextCharIndex < cleanTitle.length && cleanTitle[nextCharIndex].isDigit() + + return if (isNextCharDigit) { + 100 + } else { + 800 + } + } + if (cleanTitle.contains(cleanQuery)) return 50 + if (cleanContent.contains(cleanQuery)) return 10 + + return 0 + } +} @Serializable data class FeuIndex( diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index 69f36a9..b0344aa 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -248,15 +248,12 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode } @OptIn(ExperimentalCoroutinesApi::class) - val filteredSongs: StateFlow> = searchTitle - .mapLatest { currentTitle -> - val searchTxt = if (currentTitle == "") "" else currentTitle.trim() - return@mapLatest if(searchTxt.isEmpty()) { - emptyList() + val filteredSongs: StateFlow> = combine(drawerItems, searchTitle) { items, query -> + if (query.isBlank()) { + items } else { - drawerItems.value.filter { item -> - item.contentTitle.contains(searchTxt, ignoreCase = true) - } + items.filter { item -> item.matchesQuery(query) } + .sortedByDescending { item -> item.getSearchScore(query) } } }.stateIn ( scope = CoroutineScope(Dispatchers.Default),