diff --git a/composeApp/src/commonMain/composeResources/files/ffpm-241-1.txt b/composeApp/src/commonMain/composeResources/files/ffpm-241-1.txt index f7e95a7..3815d3b 100644 --- a/composeApp/src/commonMain/composeResources/files/ffpm-241-1.txt +++ b/composeApp/src/commonMain/composeResources/files/ffpm-241-1.txt @@ -1,4 +1,4 @@ -M0:|a:M.D. Rakotondranisa, 1890 - 1938|r:7.6.7.6. D|h:Ch. Rasoanaivo, 1876 - 1966|c:D|m:4/4|t:FFPM 242 (1) Ry Jeso ô, Midìna +M0:|a:M.D. Rakotondranisa, 1890 - 1938|r:7.6.7.6. D|h:Ch. Rasoanaivo, 1876 - 1966|c:D|m:4/4|t:FFPM 241 (1) Ry Jeso ô, Midìna I0:ews-120.txt:^(M|E|N2) N2:#l, mdfmmfm m-rddtd s,ddrmfm mffmF-s sfmrDrr m-rtldrd f-mfmmfm ddddt-d Y1:Ry Jeso ô! midìna,/Jereo 'zahay izao;/Atreho sao variana,/Ka tsy mahita\ Anao./Hazoninao ny saina,/Hijery tsara\ Anao,/Mba tsy ho very maina,/'Zao fivoriana\ izao. diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt index ee88f35..b7357f0 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Code +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconToggleButton import androidx.compose.material3.MaterialTheme @@ -83,6 +85,41 @@ object ScreenSolfa : Screen { } } + val activeFilePath = sharedScreenModel.activeFilePath.value + val drawerItems by sharedScreenModel.drawerItems.collectAsState() + val altFiles = remember(activeFilePath, drawerItems) { + if (activeFilePath.isBlank()) { + emptyList() + } else { + val currentFileName = activeFilePath.substringAfterLast('/').substringAfterLast('\\') + val currentNameNoExt = currentFileName.substringBeforeLast('.') + + val regex = Regex("""^([a-zA-Z0-9]+)-(\d+)(?:-(\d+))?$""") + + fun parseFileInfo(fileNameWithoutExt: String): Pair? { + val match = regex.matchEntire(fileNameWithoutExt) ?: return null + val prefix = match.groupValues[1] + val songNum = match.groupValues[2] + val altNum = match.groupValues[3].toIntOrNull() ?: 0 + + val songId = "$prefix-$songNum" + return songId to altNum + } + + val currentInfo = parseFileInfo(currentNameNoExt) ?: return@remember emptyList() + + drawerItems.mapNotNull { item -> + val fileName = item.path.substringAfterLast('/').substringAfterLast('\\').substringBeforeLast('.') + val info = parseFileInfo(fileName) ?: return@mapNotNull null + + if (info.first.equals(currentInfo.first, ignoreCase = true)) { + item to info.second + } else null + } + .sortedBy { it.second } + .map { it.first } + } + } MainScreenWithDrawer( solfaScreenModel, sharedScreenModel, @@ -188,11 +225,31 @@ object ScreenSolfa : Screen { val nextLabel: String by sharedScreenModel.nextLabel.collectAsState() Text(nextLabel) } - MGButton(onClick = { - solfaScreenModel.loadCustomFile() - }) { - val loadFile: String by sharedScreenModel.loadFile.collectAsState() - Text(loadFile) + if (altFiles.size > 1) { + altFiles.forEachIndexed { index, item -> + val isSelected = item.path == activeFilePath + val label = "Feu ${index + 1}" + + FilterChip( + selected = isSelected, + onClick = { + if (!isSelected) { + sharedScreenModel.reset() + solfaScreenModel.loadFromFile(item.path) + } + }, + label = { + Text( + text = label, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal + ) + }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.secondary, + selectedLabelColor = MaterialTheme.colorScheme.onSecondary + ) + ) + } } } FlowRow( 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 465d959..6dc961a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt @@ -11,6 +11,7 @@ data class DrawerItem( val id: Int, val title: String, val contentTitle: String, + val firstNotes: String = "", val path: String ) @@ -34,9 +35,37 @@ suspend fun getCombinedList(fileRepository: FileRepository): List { try { val bytes = Res.readBytes("files/feuList.json") val index = Json.decodeFromString(bytes.decodeToString()) - finalList.addAll(index.internalFeu.map { - DrawerItem(it.id, it.fileName.removeSuffix(".txt"), it.title, it.path) - }) + + val internalFileNames = index.internalFeu.map { it.fileName } + val internalBaseKeysWithAlt = getBaseKeysWithAlt(internalFileNames) + val internalPathMap = index.internalFeu.associate { it.fileName.removeSuffix(".txt") to it.path } + val resolveInternalContent: suspend (String) -> String? = { targetFileName -> + val key = targetFileName.removeSuffix(".txt") + val targetPath = internalPathMap[key] + if (targetPath != null) { + try { fileRepository.readFileContent(targetPath) } catch (e: Exception) { null } + } else null + } + + for (item in index.internalFeu) { + val content = try { fileRepository.readFileContent(item.path) } catch (e: Exception) { "" } + val notes = extractFirstNotes( + fileName = item.fileName, + content = content, + baseKeysWithAlt = internalBaseKeysWithAlt, + resolveContent = resolveInternalContent + ) + + finalList.add( + DrawerItem( + id = item.id, + title = item.fileName.removeSuffix(".txt"), + contentTitle = item.title, + firstNotes = notes, + path = item.path + ) + ) + } } catch (e: Exception) { e.printStackTrace() } @@ -46,13 +75,21 @@ suspend fun getCombinedList(fileRepository: FileRepository): List { val directory = File(folderPath.absolutePath) if (directory.exists() && directory.isDirectory) { - directory.listFiles { f -> f.extension == "txt" }?.forEachIndexed { i, file -> - finalList.add(DrawerItem( - id = 50000 + i, - title = file.name.removeSuffix(".txt"), - contentTitle = extractTitle(file), - path = file.absolutePath - )) + val localFiles = directory.listFiles { f -> f.extension == "txt" } ?: arrayOf() + + val localFileNames = localFiles.map { it.name } + val localBaseKeysWithAlt = getBaseKeysWithAlt(localFileNames) + + localFiles.forEachIndexed { i, file -> + finalList.add( + DrawerItem( + id = 50000 + i, + title = file.nameWithoutExtension, + contentTitle = extractTitle(file), + firstNotes = extractFirstNotes(file, localBaseKeysWithAlt), + path = file.absolutePath + ) + ) } } } catch (e: Exception) { @@ -82,4 +119,115 @@ private fun extractTitle(file: File): String { } catch (e: Exception) { file.nameWithoutExtension } +} + +private val noteFileNameRegex = Regex("""^([a-zA-Z0-9]+)-(\d+)(?:-(\d+))?$""") + +private fun getBaseKeysWithAlt(fileNames: Iterable): Set { + return fileNames.mapNotNull { name -> + val match = noteFileNameRegex.matchEntire(name.removeSuffix(".txt")) ?: return@mapNotNull null + val prefix = match.groupValues[1] + val num = match.groupValues[2] + val alt = match.groupValues[3] + + if (alt.isNotEmpty()) "$prefix-$num" else null + }.toSet() +} + +private fun shouldExtractNotes(fileName: String, baseKeysWithAlt: Set): Boolean { + val nameWithoutExt = fileName.removeSuffix(".txt") + val match = noteFileNameRegex.matchEntire(nameWithoutExt) ?: return false + val prefix = match.groupValues[1] + val num = match.groupValues[2] + val alt = match.groupValues[3] + + val baseKey = "$prefix-$num" + return alt.isNotEmpty() || baseKey in baseKeysWithAlt +} + +private fun extractFirstNotes(file: File, baseKeysWithAlt: Set): String { + if (!shouldExtractNotes(file.name, baseKeysWithAlt)) return "" + + return try { + file.useLines { lines -> + val n1Line = lines.find { it.trim().startsWith("N1:") } + if (n1Line != null) parseN1Notes(n1Line) else "" + } + } catch (e: Exception) { + "" + } +} + +private suspend fun extractFirstNotes( + fileName: String, + content: String, + baseKeysWithAlt: Set, + resolveContent: (suspend (String) -> String?)? = null +): String { + if (!shouldExtractNotes(fileName, baseKeysWithAlt)) return "" + val visited = mutableSetOf(fileName.removeSuffix(".txt")) + return extractNotesFromContent(content, resolveContent, visited) +} + +private val inclusionRegex = Regex("""^I(\d+):([^:]+)(?::\^\(([^)]+)\))?""") + +private data class InclusionInfo( + val order: Int, + val targetFileName: String, + val exclusions: List +) + +private suspend fun extractNotesFromContent( + content: String, + resolveContent: (suspend (String) -> String?)?, + visited: MutableSet +): String { + val lines = content.lines() + val n1Line = lines.find { it.trim().startsWith("N1:") } + if (n1Line != null) { + return parseN1Notes(n1Line) + } + + val inclusions = lines.mapNotNull { line -> + val trimmed = line.trim() + val match = inclusionRegex.find(trimmed) ?: return@mapNotNull null + val order = match.groupValues[1].toIntOrNull() ?: 0 + val fileName = match.groupValues[2].trim() + val exclusionsStr = match.groupValues[3] + val exclusions = if (exclusionsStr.isNotEmpty()) { + exclusionsStr.split("|").map { it.trim() } + } else emptyList() + + InclusionInfo(order, fileName, exclusions) + }.sortedBy { it.order } + + for (inc in inclusions) { + val isN1Excluded = inc.exclusions.any { ex -> "N1" == ex || "N1".startsWith(ex) } + if (isN1Excluded) continue + + val targetKey = inc.targetFileName.removeSuffix(".txt") + if (!visited.add(targetKey)) continue + + val targetContent = resolveContent?.invoke(inc.targetFileName) ?: continue + val notes = extractNotesFromContent(targetContent, resolveContent, visited) + if (notes.isNotEmpty()) { + return notes + } + } + return "" +} + +private val anchorRegex = Regex("""#\s*[drmfsltDRFSTz][ia]?[,'`]*""") +private val userNoteRegex = Regex("""([drmfsltDRFSTz][ia]?|[.,()\s-])""") +private val noteOnlyRegex = Regex("""[drmfsltDRFSTz][ia]?""") + +private fun parseN1Notes(n1Content: String): String { + val rawText = n1Content.substringAfter("N1:") + val cleanedText = rawText.replace(anchorRegex, "") + + return userNoteRegex.findAll(cleanedText) + .map { it.value } + .filter { it.matches(noteOnlyRegex) } + .joinToString("") + .take(12) } \ No newline at end of file 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 68c0163..34019e1 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -1127,7 +1127,31 @@ fun MainScreenWithDrawer( sharedScreenModel.updateSearchTxt("") }, onPrefixClick = { prefix -> - sharedScreenModel.updateSearchTxt(prefix) + val digits = textInput.trim() + val cleanPrefix = prefix.trim().lowercase() + + if (digits.isNotEmpty() && digits.all { it.isDigit() }) { + val candidateSuffixes = listOf("", "-1", "-2") + + val matchingItem = candidateSuffixes.firstNotNullOfOrNull { suffix -> + val targetFileName = "$cleanPrefix-$digits$suffix.txt" + sharedScreenModel.drawerItems.value.find { item -> + item.path.endsWith(targetFileName, ignoreCase = true) + } + } + if (matchingItem != null) { + textInput = "" + sharedScreenModel.updateSearchTxt("") + sharedScreenModel.reset() + solfaScreenModel.loadFromFile(matchingItem.path) + } else { + textInput = prefix + sharedScreenModel.updateSearchTxt(prefix) + } + } else { + textInput = prefix + sharedScreenModel.updateSearchTxt(prefix) + } }, isFullScreen = isFullScreenEnabled ) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt index f0a9524..d4fdbdf 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -23,7 +23,10 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import feufaro.composeapp.generated.resources.Res @@ -154,7 +157,7 @@ fun SimpleDrawerContent( items(internalList) { item -> val isSelected = item.path == activePath val isFavorite = favoriteLists.contains(item) - + NavigationDrawerItem( icon = { Text("\uD834\uDD1E", fontSize = 22.sp) @@ -429,9 +432,20 @@ fun DrawerItemLabel(item: DrawerItem) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { + val hasAlt = item.firstNotes.isNotBlank() Column(modifier = Modifier.weight(1f)) { - Text(text = item.title, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.titleSmall) - Text(text = item.contentTitle, style = MaterialTheme.typography.labelSmall, maxLines = 1) + if(!hasAlt) { + Text(text = item.title, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.titleSmall) + Text(text = item.contentTitle, style = MaterialTheme.typography.labelSmall, maxLines = 1) + } else { + Text(text = item.contentTitle, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.titleSmall, maxLines = 1) + Text(text = item.firstNotes, maxLines = 1, + fontStyle = FontStyle.Italic, style = MaterialTheme.typography.labelSmall.copy( + letterSpacing = 4.sp, + fontWeight = FontWeight.ExtraLight + ) + ) + } } } }