Auto-open search(Android), note preview on drawer whn part alt, alt switcher btn

This commit is contained in:
Hasinjato 2026-08-18 12:11:51 +03:00
parent 78876d7ec0
commit f85dfd9e9d
5 changed files with 263 additions and 20 deletions

View file

@ -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.

View file

@ -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<String, Int>? {
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(

View file

@ -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<DrawerItem> {
try {
val bytes = Res.readBytes("files/feuList.json")
val index = Json.decodeFromString<FeuIndex>(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<DrawerItem> {
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) {
@ -83,3 +120,114 @@ private fun extractTitle(file: File): String {
file.nameWithoutExtension
}
}
private val noteFileNameRegex = Regex("""^([a-zA-Z0-9]+)-(\d+)(?:-(\d+))?$""")
private fun getBaseKeysWithAlt(fileNames: Iterable<String>): Set<String> {
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<String>): 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>): 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<String>,
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<String>
)
private suspend fun extractNotesFromContent(
content: String,
resolveContent: (suspend (String) -> String?)?,
visited: MutableSet<String>
): 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)
}

View file

@ -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
)

View file

@ -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
@ -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
)
)
}
}
}
}