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) I0:ews-120.txt:^(M|E|N2)
N2:#l, mdfmmfm m-rddtd s,ddrmfm mffmF-s sfmrDrr m-rtldrd f-mfmmfm ddddt-d 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. 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.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Code 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.Icon
import androidx.compose.material3.IconToggleButton import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme 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( MainScreenWithDrawer(
solfaScreenModel, solfaScreenModel,
sharedScreenModel, sharedScreenModel,
@ -188,11 +225,31 @@ object ScreenSolfa : Screen {
val nextLabel: String by sharedScreenModel.nextLabel.collectAsState() val nextLabel: String by sharedScreenModel.nextLabel.collectAsState()
Text(nextLabel) Text(nextLabel)
} }
MGButton(onClick = { if (altFiles.size > 1) {
solfaScreenModel.loadCustomFile() altFiles.forEachIndexed { index, item ->
}) { val isSelected = item.path == activeFilePath
val loadFile: String by sharedScreenModel.loadFile.collectAsState() val label = "Feu ${index + 1}"
Text(loadFile)
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( FlowRow(

View file

@ -11,6 +11,7 @@ data class DrawerItem(
val id: Int, val id: Int,
val title: String, val title: String,
val contentTitle: String, val contentTitle: String,
val firstNotes: String = "",
val path: String val path: String
) )
@ -34,9 +35,37 @@ suspend fun getCombinedList(fileRepository: FileRepository): List<DrawerItem> {
try { try {
val bytes = Res.readBytes("files/feuList.json") val bytes = Res.readBytes("files/feuList.json")
val index = Json.decodeFromString<FeuIndex>(bytes.decodeToString()) 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) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -46,13 +75,21 @@ suspend fun getCombinedList(fileRepository: FileRepository): List<DrawerItem> {
val directory = File(folderPath.absolutePath) val directory = File(folderPath.absolutePath)
if (directory.exists() && directory.isDirectory) { if (directory.exists() && directory.isDirectory) {
directory.listFiles { f -> f.extension == "txt" }?.forEachIndexed { i, file -> val localFiles = directory.listFiles { f -> f.extension == "txt" } ?: arrayOf()
finalList.add(DrawerItem(
id = 50000 + i, val localFileNames = localFiles.map { it.name }
title = file.name.removeSuffix(".txt"), val localBaseKeysWithAlt = getBaseKeysWithAlt(localFileNames)
contentTitle = extractTitle(file),
path = file.absolutePath 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) { } catch (e: Exception) {
@ -83,3 +120,114 @@ private fun extractTitle(file: File): String {
file.nameWithoutExtension 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("") sharedScreenModel.updateSearchTxt("")
}, },
onPrefixClick = { prefix -> 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 isFullScreen = isFullScreenEnabled
) )

View file

@ -23,7 +23,10 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector 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.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import feufaro.composeapp.generated.resources.Res import feufaro.composeapp.generated.resources.Res
@ -429,9 +432,20 @@ fun DrawerItemLabel(item: DrawerItem) {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
val hasAlt = item.firstNotes.isNotBlank()
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(text = item.title, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.titleSmall) if(!hasAlt) {
Text(text = item.contentTitle, style = MaterialTheme.typography.labelSmall, maxLines = 1) 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
)
)
}
} }
} }
} }