Auto-open search(Android), note preview on drawer whn part alt, alt switcher btn
This commit is contained in:
parent
78876d7ec0
commit
f85dfd9e9d
5 changed files with 263 additions and 20 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue