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 ae7d5e1..d52f7f3 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt @@ -17,12 +17,50 @@ data class DrawerItem( val firstNotes: String = "", val path: String ) { + private data class TitleComponents( + val prefix: String, + val number: String, + val version: String? + ) + + private fun parseComponents(str: String): TitleComponents? { + // préfixe (lettres), numéro (chiffres), version optionnelle + val regex = Regex("""^([a-zA-Z]+)[-_\s]*(\d+)(?:[-_\s]*(\d+))?""") + val match = regex.find(str.trim()) ?: return null + val prefix = match.groupValues[1].lowercase() + val number = match.groupValues[2] + val version = match.groupValues[3].ifEmpty { null } + return TitleComponents(prefix, number, version) + } + 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) + + if ((cleanTitle + cleanContent).contains(cleanQuery) || (cleanContent + cleanTitle).contains(cleanQuery)) { + return true + } + + val queryComponents = parseComponents(query) + val titleComponents = parseComponents(title) + if (queryComponents != null && titleComponents != null) { + if (queryComponents.prefix == titleComponents.prefix) { + return true + } + } + + val queryLetters = query.filter { it.isLetter() }.lowercase() + val queryDigits = query.filter { it.isDigit() } + + if (queryLetters.isNotEmpty() && queryDigits.isNotEmpty()) { + val lettersMatch = cleanContent.contains(queryLetters) || cleanTitle.contains(queryLetters) + val digitsMatch = title.contains(queryDigits) || cleanTitle.contains(queryDigits) || cleanContent.contains(queryDigits) + return lettersMatch && digitsMatch + } + + return false } fun getSearchScore(query: String): Int { @@ -32,20 +70,66 @@ data class DrawerItem( val cleanTitle = title.normalizeForSearch() val cleanContent = contentTitle.normalizeForSearch() - if (cleanTitle == cleanQuery) return 1000 - if (cleanTitle.startsWith(cleanQuery)) { - val nextCharIndex = cleanQuery.length + val queryComponents = parseComponents(query) + val titleComponents = parseComponents(title) - val isNextCharDigit = nextCharIndex < cleanTitle.length && cleanTitle[nextCharIndex].isDigit() + if (queryComponents != null && titleComponents != null) { + val samePrefix = queryComponents.prefix == titleComponents.prefix + val sameNumber = queryComponents.number == titleComponents.number + val sameVersion = queryComponents.version == titleComponents.version - return if (isNextCharDigit) { - 100 - } else { - 800 + if (samePrefix && sameNumber && sameVersion) { + return 3000 + } + + if (samePrefix && sameNumber) { + return 2500 + } + + if (samePrefix && !sameNumber) { + if (cleanTitle == cleanQuery) { + return 1200 + } } } - if (cleanTitle.contains(cleanQuery)) return 50 - if (cleanContent.contains(cleanQuery)) return 10 + + if (title.equals(query, ignoreCase = true)) return 2000 + + if (cleanTitle == cleanQuery) return 1000 + + val titleDigitBlocks = Regex("\\d+").findAll(title).map { it.value }.toList() + val queryDigits = query.filter { it.isDigit() } + val queryLetters = query.filter { it.isLetter() }.lowercase() + + if (cleanTitle.startsWith(cleanQuery)) { + if (queryDigits.isNotEmpty() && titleDigitBlocks.contains(queryDigits)) { + return 950 + } + return 800 + } + + if (queryLetters.isEmpty() && queryDigits.isNotEmpty()) { + if (titleDigitBlocks.contains(queryDigits)) return 1100 + if (cleanTitle.contains(queryDigits)) return 400 + if (cleanContent.contains(queryDigits)) return 200 + } + + if (queryLetters.isNotEmpty() && queryDigits.isNotEmpty()) { + val lettersInContent = cleanContent.contains(queryLetters) + val lettersInTitle = cleanTitle.contains(queryLetters) + val digitsInTitleBlock = titleDigitBlocks.contains(queryDigits) + val digitsInTitle = cleanTitle.contains(queryDigits) + + if ((lettersInContent || lettersInTitle) && digitsInTitleBlock) { + return 850 + } + if ((lettersInContent || lettersInTitle) && digitsInTitle) { + return 500 + } + } + + if (cleanTitle.contains(cleanQuery)) return 600 + if (cleanContent.contains(cleanQuery)) return 100 return 0 } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index 29a0f6d..da68338 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -1093,37 +1094,52 @@ fun MainScreenWithDrawer( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), modifier = Modifier.fillMaxHeight() ) { - val sortedSongs = remember(filteredSongs) { - filteredSongs.sortedWith( - compareBy { item -> - !item.path.startsWith("assets://") - }.thenComparing { a, b -> - val regex = Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)") - val chunksA = a.title.lowercase().split(regex) - val chunksB = b.title.lowercase().split(regex) - var result = 0 - val size = minOf(chunksA.size, chunksB.size) - for (i in 0 until size) { - val chunkA = chunksA[i] - val chunkB = chunksB[i] - val numA = chunkA.toLongOrNull() - val numB = chunkB.toLongOrNull() - result = if (numA != null && numB != null) { - numA.compareTo(numB) - } else { - chunkA.compareTo(chunkB) - } - if (result != 0) break - } - if (result == 0) { - chunksA.size.compareTo(chunksB.size) + val sortedSongs = remember(filteredSongs, textInput) { + val naturalComparator = Comparator { a, b -> + val regex = Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)") + val chunksA = a.title.lowercase().split(regex) + val chunksB = b.title.lowercase().split(regex) + var result = 0 + val size = minOf(chunksA.size, chunksB.size) + for (i in 0 until size) { + val chunkA = chunksA[i] + val chunkB = chunksB[i] + val numA = chunkA.toLongOrNull() + val numB = chunkB.toLongOrNull() + result = if (numA != null && numB != null) { + numA.compareTo(numB) } else { - result + chunkA.compareTo(chunkB) } + if (result != 0) break } - ) + if (result == 0) chunksA.size.compareTo(chunksB.size) else result + } + + if (textInput.isBlank()) { + filteredSongs.sortedWith( + compareBy { item -> !item.path.startsWith("assets://") } + .thenComparing(naturalComparator) + ) + } else { + filteredSongs.sortedWith( + compareByDescending { item -> item.getSearchScore(textInput) } + .thenBy { item -> !item.path.startsWith("assets://") } + .thenComparing(naturalComparator) + ) + } } - LazyColumn(Modifier.fillMaxSize()) { + val searchListState = rememberLazyListState() + + LaunchedEffect(textInput) { + if (sortedSongs.isNotEmpty()) { + searchListState.scrollToItem(0) + } + } + LazyColumn( + state = searchListState, + modifier = Modifier.fillMaxSize() + ) { if (sortedSongs.isEmpty() && textInput.isNotEmpty()) { item { ListItem(