Complete contentDescription for all icons - verse selection on print settings

This commit is contained in:
Hasinjato 2026-09-02 14:00:10 +03:00
parent f5eb92a374
commit 9b783200df
19 changed files with 312 additions and 104 deletions

View file

@ -3,8 +3,9 @@ package mg.dot.feufaro.pdf
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.pdmodel.PDPage
import com.tom_roush.pdfbox.pdmodel.PDPageContentStream
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
import com.tom_roush.pdfbox.pdmodel.font.PDType0Font
import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject
import com.tom_roush.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState
import com.tom_roush.pdfbox.util.Matrix
import feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.CoroutineScope
@ -249,26 +250,49 @@ private fun drawNoteWithModulation(
// ── drawNoteWithSubscript ─────────────────────────────────────────────────────
private fun drawNoteWithSubscript(
cs: PDPageContentStream, font: PDType0Font, fontSize: Float,
cs: PDPageContentStream, font: PDType0Font,
boldFont: PDType0Font?,
fontSize: Float,
text: String, x: Float, y: Float
) {
var curX = x
val superscriptOffset = fontSize * 0.35f
val subscriptOffset = fontSize * 0.2f
for (char in text) {
val str = char.toString().sanitize()
val isSuperscript = char in setOf('¹', '²', '³')
val isSubscript = char in setOf('₁', '₂', '₃')
val isSuperscript = char in setOf('¹', '²', '³', '⁴', '⁵')
val isSubscript = char in setOf('₁', '₂', '₃', '₄', '₅')
val isSpecialSymbol = char == '•' || char == ','
val isPunctuation = char in setOf('•', ',')
val activeFont = if (isSpecialSymbol && boldFont != null) boldFont else font
val currentSize = if (isSuperscript || isSubscript) fontSize * 0.6f else fontSize
val currentY = if (isSuperscript) y + superscriptOffset else y
val currentY = when {
isSuperscript -> y + superscriptOffset
isSubscript -> y - subscriptOffset
else -> y
}
cs.beginText()
cs.setFont(font, currentSize)
cs.newLineAtOffset(curX, currentY)
cs.setFont(activeFont, currentSize)
if (isSuperscript || isSubscript) {
cs.newLineAtOffset(curX - (currentSize * 0.2f), currentY)
} else {
cs.newLineAtOffset(curX, currentY)
}
cs.showText(str)
cs.endText()
curX += font.getStringWidth(str) / 1000f * currentSize + 0.5f
val charWidth = font.getStringWidth(str) / 1000f * currentSize
curX += when {
isSuperscript || isSubscript -> charWidth * 0.3f
isPunctuation -> charWidth
else -> charWidth + 0.5f
}
}
}
@ -384,6 +408,33 @@ private fun drawHeader(
cs.stroke()
}
private fun drawFooterLogo(
doc: PDDocument,
cs: PDPageContentStream,
logoBytes: ByteArray,
pageWidth: Float
) {
try {
val pdImage = PDImageXObject.createFromByteArray(doc, logoBytes, "logo_footer")
val imgWidth = 100f
val imgHeight = 100f * (pdImage.height.toFloat() / pdImage.width.toFloat())
val x = (pageWidth - imgWidth) / 2f
val y = 40f
cs.saveGraphicsState()
val graphicsState = PDExtendedGraphicsState()
graphicsState.nonStrokingAlphaConstant = 0.15f
cs.setGraphicsStateParameters(graphicsState)
cs.drawImage(pdImage, x, y, imgWidth, imgHeight)
cs.restoreGraphicsState()
} catch (e: Exception) {
println("⚠️ Erreur lors du dessin du logo en bas de page : ${e.message}")
}
}
// ── actual fun ────────────────────────────────────────────────────────────────
actual fun rememberPdfExportAction(
scope: CoroutineScope,
@ -452,11 +503,12 @@ private suspend fun generatePdfToBytes(
val customNbGrid = settings.nbGridOnMeasure
// ── Polices ────────────────────────────────────────────────────────────────
val noteFontBytes = Res.readBytes("font/PTSerif-Regular.ttf")
val noteFontBytes = Res.readBytes("font/Lucida-Sans-Unicode.ttf")
val lyricFontBytes = Res.readBytes("font/LinLibertine_R.ttf")
val markerItalicBoldFontBytes = Res.readBytes("font/PT_Serif_Bold_Italic.ttf")
val markerBoldFontBytes = Res.readBytes("font/PTSerif_Bold.ttf")
val emmentalerFontBytes = Res.readBytes("font/Emmentaler.ttf")
val logoBytes = Res.readBytes("drawable/icon.png")
PDDocument().use { doc ->
@ -479,11 +531,34 @@ private suspend fun generatePdfToBytes(
(1..nbStanza).maxOfOrNull { sz -> tuo.getSingleSyllable(sz).size } ?: 1
} ?: 1
val hasOverrideLyrics = maxSylsSize >= 2
val allStanzas = (1..nbStanza).toList()
val selectedInSettings = settings.selectedStanzasInScore.filter { it in allStanzas }
val (stanzasRemaining, stanzasToShow) = when {
selectedInSettings.isNotEmpty() -> {
val bottom = if (!hasOverrideLyrics) {
selectedInSettings
} else {
listOf(selectedInSettings.first())
}
val score = allStanzas.filter { it !in bottom }
score to bottom
}
else -> {
emptyList<Int>() to emptyList<Int>()
}
}
// ── Fonctions de dessin locales ────────────────────────────────────────
fun newPage(): PDPageContentStream {
val page = PDPage(settings.pdRectangle)
doc.addPage(page)
return PDPageContentStream(doc, page)
val cs = PDPageContentStream(doc, page)
drawFooterLogo(doc, cs, logoBytes, pageWidth)
return cs
}
fun drawCurlyBrace(cs: PDPageContentStream, x: Float, top: Float, bottom: Float) {
@ -837,7 +912,7 @@ private suspend fun generatePdfToBytes(
val currentY = notesStartY - (vLi * noteLineH)
if (line.isNotBlank()) {
if (line.contains(">")) drawNoteWithModulation(cs, customFont, noteFontSize, line, noteX, currentY)
else drawNoteWithSubscript(cs, customFont, noteFontSize, line, noteX, currentY)
else drawNoteWithSubscript(cs, customFont, markerBoldFont, noteFontSize, line, noteX, currentY)
}
}
@ -850,7 +925,7 @@ private suspend fun generatePdfToBytes(
val totalAreaSlots = lastVirtualIndex + customSpacings[lastVoiceIndex]
val finalBraceBottom = notesStartY - (totalAreaSlots * noteLineH)
val stanzasToShow = if (maxSylsSize >= 2) listOf(1) else if (nbStanza >= 2) listOf(1, 2) else listOf(1)
//val stanzasToShow = if (maxSylsSize >= 2) listOf(1) else if (nbStanza >= 2) listOf(1, 2) else listOf(1)
var lowestYOfThisRow = finalBraceBottom
stanzasToShow.forEachIndexed { stanzaIdx, currentStanza ->
@ -1022,10 +1097,6 @@ private suspend fun generatePdfToBytes(
y = lowestYOfThisRow - interRowGap
}
// ── Stanzas restantes en texte ─────────────────────────────────────────
val stanzasDisplayed = if (maxSylsSize >= 2) listOf(1) else if (nbStanza >= 2) listOf(1, 2) else listOf(1)
val stanzasRemaining = (1..nbStanza).filter { it !in stanzasDisplayed }
if (stanzasRemaining.isNotEmpty()) {
val textFont = lyricFont
val textSize = lyricFontSize
@ -1059,7 +1130,7 @@ private suspend fun generatePdfToBytes(
val normal = syls.getOrNull(0)?.trim() ?: ""
val dcPart = syls.drop(1).joinToString(" ") { it.trim() }
if (normal.isNotEmpty()) result.append(normal).append(" ")
if (dcPart.isNotEmpty()) result.append("[$dcPart] ")
/*if (dcPart.isNotEmpty()) result.append("[$dcPart] ")*/
val marker = tuo.pTemplate.markerToString()
if (marker.contains("DC") && !inDcBlock && allDcLyrics.isNotEmpty()) {
result.append("(")

View file

@ -16,7 +16,8 @@ actual data class PrintSettings actual constructor(
actual val marginX : Float,
actual val marginY : Float,
actual val headerH : Float,
actual val nbGridOnMeasure: Int
actual val nbGridOnMeasure: Int,
actual val selectedStanzasInScore: List<Int>
) {
val pdRectangle: PDRectangle
get() {

View file

@ -25,7 +25,8 @@ expect class PrintSettings(
marginX : Float,
marginY : Float,
headerH : Float,
nbGridOnMeasure : Int
nbGridOnMeasure : Int,
selectedStanzasInScore: List<Int> = listOf(1, 2)
) {
val pageSize : PageSize
val orientation : PageOrientation
@ -43,6 +44,7 @@ expect class PrintSettings(
val nbGridOnMeasure : Int
val pageWidth : Float
val pageHeight : Float
val selectedStanzasInScore: List<Int>
}
expect fun defaultPrintSettings(): PrintSettings

View file

@ -30,7 +30,7 @@ fun GridActionMenu(
Icon(
modifier = Modifier.width(50.dp),
imageVector = Icons.Default.Add,
contentDescription = null
contentDescription = "Ajoute une grille"
)
}
}
@ -47,7 +47,7 @@ fun GridActionMenu(
Icon(
modifier = Modifier.width(50.dp),
imageVector = Icons.Default.Delete,
contentDescription = null
contentDescription = "Enlèver une grille"
)
}
}

View file

@ -221,7 +221,7 @@ private fun ExpandableGroup(
Icons.Default.RemoveCircleOutline
else
Icons.Default.AddCircleOutline,
contentDescription = null,
contentDescription = "Btn marker ${group.title}",
tint = MaterialTheme.colorScheme.primary
)
}

View file

@ -263,7 +263,7 @@ fun TUODetailDialog(
) {
Icon(
imageVector = Icons.Default.KeyboardDoubleArrowLeft,
contentDescription = "Diminuer",
contentDescription = "Diminuer z time",
tint = if (silentDurationOrder.indexOf(editState.silentDuration) > 0) Color.LightGray else Color.DarkGray
)
}
@ -297,7 +297,7 @@ fun TUODetailDialog(
) {
Icon(
imageVector = Icons.Default.KeyboardDoubleArrowRight,
contentDescription = "Augmenter",
contentDescription = "Augmenter z time",
tint = if (silentDurationOrder.indexOf(editState.silentDuration) < silentDurationOrder.lastIndex) Color.LightGray else Color.DarkGray
)
}
@ -313,7 +313,7 @@ fun TUODetailDialog(
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Précédent",
contentDescription = "Précédent grid",
tint = Color.White
)
}
@ -335,7 +335,7 @@ fun TUODetailDialog(
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Suivant",
contentDescription = "Suivant grid",
tint = Color.White
)
}
@ -418,7 +418,7 @@ fun TUODetailDialog(
Icon(
imageVector = Icons.Default.Add,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = null
contentDescription = "btn add barre mesure"
)
}
}
@ -479,7 +479,7 @@ fun TUODetailDialog(
Icon(
imageVector = Icons.Sharp.Delete,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = "Enlever"
contentDescription = "Enlever marker"
)
}
}
@ -512,7 +512,7 @@ fun TUODetailDialog(
Icon(
imageVector = Icons.Default.Check,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = null
contentDescription = "Complete marker"
)
}
}
@ -541,7 +541,7 @@ fun TUODetailDialog(
Icon(
imageVector = Icons.Default.Edit,
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f),
contentDescription = null
contentDescription = "Btn add marker"
)
}
}
@ -748,7 +748,7 @@ fun TUODetailDialog(
tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(
alpha = 1.5f
),
contentDescription = null
contentDescription = "Ajouter enlever ligne lyrics"
)
}
}
@ -766,7 +766,7 @@ fun TUODetailDialog(
IconButton(onClick = onDismiss, modifier = Modifier.size(20.dp)) {
Icon(
Icons.Default.Clear,
contentDescription = null,
contentDescription = "Btn cancel edit",
tint = Color.LightGray
)
}
@ -795,7 +795,7 @@ fun TUODetailDialog(
) {
Icon(
Icons.Default.Build,
contentDescription = null,
contentDescription = "Btn save edit",
tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f)
)
}

View file

@ -1501,7 +1501,7 @@ fun EditSourceCompose(
Modifier.fillMaxWidth().background(Color(0xFFF5F5F5)).padding(2.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Description, contentDescription = null, modifier = Modifier.size(16.dp))
Icon(Icons.Default.Description, contentDescription = "new partition", modifier = Modifier.size(16.dp))
Text(
text = sourceTitle.ifEmpty { "nouveau_feufaro.txt" },
modifier = Modifier.padding(start = 8.dp).widthIn(max = 160.dp),

View file

@ -233,14 +233,14 @@ fun AboutDialog(onDismiss: () -> Unit) {
leadingIcon = {
Icon(
imageVector = Icons.Default.Code,
contentDescription = null,
contentDescription = "Developpers",
modifier = Modifier.size(16.dp)
)
},
trailingIcon = {
Icon(
imageVector = Icons.AutoMirrored.Default.OpenInNew,
contentDescription = null,
contentDescription = "Open in tab",
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary
)

View file

@ -263,7 +263,7 @@ fun MainScreenWithDrawer(
if (showPrintSettings) {
PrintSettingsDialog(
initialSettings = defaultPrintSettings(),
// nbStanza = nbStanzas,
totalStanzas = nbStanzas,
onDismiss = { showPrintSettings = false },
onConfirm = { settings ->
showPrintSettings = false
@ -389,7 +389,7 @@ fun MainScreenWithDrawer(
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
contentDescription = "Icon search on leading",
tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(20.dp)
)
@ -408,7 +408,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = if (isNumericKeyboard) Icons.Default.Abc else Icons.Filled._123,
contentDescription = null,
contentDescription = "Btn to switch abc 123",
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(22.dp)
)
@ -424,7 +424,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null,
contentDescription = "Annuler recherche",
tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(18.dp)
)
@ -483,7 +483,7 @@ fun MainScreenWithDrawer(
IconButton(onClick = {
scope.launch { drawerState.open() }
}) {
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu Latéral")
}
}
}, actions = {
@ -566,7 +566,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
contentDescription = null,
contentDescription = "Transposer de moins",
tint = Color.White
)
}
@ -609,7 +609,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = centralIcon,
contentDescription = null,
contentDescription = "Valider transpose",
tint = centralTint
)
}
@ -633,7 +633,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
contentDescription = "Transposer de plus",
tint = Color.White
)
}
@ -1024,7 +1024,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = if (isSearchActive) Icons.Default.Close else Icons.Default.Search,
contentDescription = null,
contentDescription = "Btn search",
tint = MaterialTheme.colorScheme.onTertiary
)
}
@ -1098,7 +1098,7 @@ fun MainScreenWithDrawer(
leadingContent = {
Icon(
imageVector = Icons.Default.SearchOff,
contentDescription = null,
contentDescription = "Not result search",
tint = MaterialTheme.colorScheme.error
)
}
@ -1142,7 +1142,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = if (isFavorite) Icons.Default.FavoriteBorder else Icons.Default.Favorite,
contentDescription = null,
contentDescription = "Favorite btn",
modifier = Modifier.scale(scale),
tint = MaterialTheme.colorScheme.onSecondary
)
@ -1153,7 +1153,7 @@ fun MainScreenWithDrawer(
leadingContent = {
Icon(
imageVector = if (item.path.startsWith("assets://")) Icons.Default.ArrowDropDownCircle else Icons.Default.Folder,
contentDescription = null,
contentDescription = "Part interne ou personnel",
modifier = Modifier.size(30.dp)
)
},
@ -1166,7 +1166,7 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = Icons.AutoMirrored.Default.PlaylistAdd,
contentDescription = null,
contentDescription = "Btn add playlist",
tint = if (!onPlaylist) Color.LightGray else Color(
0xFFFFD700
)
@ -1295,7 +1295,7 @@ private fun getIntervalToC(key: String): Int {
@Composable
private fun MyFAB(
onClick: () -> Unit,
size: Dp ?= 55.dp,
size: Dp = 55.dp,
icon: ImageVector
) {
FloatingActionButton(
@ -1306,7 +1306,7 @@ private fun MyFAB(
) {
Icon(
imageVector = icon,
contentDescription = null,
contentDescription = "FAB ${icon.name}",
tint = MaterialTheme.colorScheme.primary
)
}

View file

@ -398,7 +398,7 @@ fun MidiControlPanel(
Icon(
imageVector = Icons.Default.Loop,
contentDescription = null,
contentDescription = "AB Looping",
tint = if (isLooping) Color.White else Color.Black,
modifier = Modifier.size(16.dp)
)

View file

@ -44,7 +44,7 @@ fun ModernVolumeSlider(
volume < 0.5f -> Icons.AutoMirrored.Filled.VolumeDown
else -> Icons.AutoMirrored.Filled.VolumeUp
},
contentDescription = null,
contentDescription = "Volume slider",
tint = if (volume > 0) MaterialTheme.colorScheme.onSecondary else Color.Gray,
modifier = Modifier.size(20.dp)
)

View file

@ -110,7 +110,7 @@ fun NewPartition(
) {
Icon(
imageVector = Icons.Filled.MusicNote,
contentDescription = null,
contentDescription = "music note title",
tint = AccentBlue
)
}
@ -373,7 +373,7 @@ fun NewPartition(
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = null,
contentDescription = "Btn create part",
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(6.dp))

View file

@ -33,6 +33,7 @@ import kotlin.math.roundToInt
@Composable
fun PrintSettingsDialog(
initialSettings: PrintSettings = defaultPrintSettings(),
totalStanzas: Int = 4,
onDismiss: () -> Unit,
onConfirm: (PrintSettings) -> Unit
) {
@ -51,6 +52,7 @@ fun PrintSettingsDialog(
var marginY by remember { mutableStateOf(initialSettings.marginY) }
var headerH by remember { mutableStateOf(initialSettings.headerH) }
var nbGridOnMeasure by remember { mutableStateOf(initialSettings.nbGridOnMeasure) }
var selectedStanzasInScore by remember { mutableStateOf(initialSettings.selectedStanzasInScore) }
Dialog(
properties = DialogProperties(
@ -134,8 +136,6 @@ fun PrintSettingsDialog(
}
}
Spacer(Modifier.height(4.dp))
// Taille papier
LabelRow("Taille du papier")
SegmentedRow(
@ -144,17 +144,76 @@ fun PrintSettingsDialog(
onSelect = { pageSize = PageSize.entries[it] }
)
Spacer(Modifier.height(4.dp))
// Orientation
LabelRow("Orientation")
SegmentedRow(
options = PageOrientation.entries.map { it.label },
selected = orientation.ordinal,
onSelect = { orientation = PageOrientation.entries[it] }
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
LabelRow("Orientation")
Spacer(Modifier.height(4.dp))
SegmentedRow(
options = PageOrientation.entries.map { it.label },
selected = orientation.ordinal,
onSelect = { orientation = PageOrientation.entries[it] }
)
}
Spacer(Modifier.height(8.dp))
Column(
modifier = Modifier.weight(1f)
) {
LabelRow("Versets sous les notes")
Spacer(Modifier.height(12.dp))
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
(1..totalStanzas).forEach { stanza ->
val isSelected = stanza in selectedStanzasInScore
val backgroundColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
Color.DarkGray
}
val contentColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.6f)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(22.dp)
.clip(RoundedCornerShape(4.dp))
.background(backgroundColor)
.clickable {
selectedStanzasInScore = if (isSelected) {
selectedStanzasInScore - stanza
} else {
if (selectedStanzasInScore.size >= 2) {
(selectedStanzasInScore.drop(1) + stanza).sorted()
} else {
(selectedStanzasInScore + stanza).sorted()
}
}
}
) {
Text(
text = "$stanza",
color = contentColor,
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
// ── Preview miniature de la page ──────────────────────
SectionTitle("Aperçu")
@ -306,10 +365,11 @@ fun PrintSettingsDialog(
marginX = def.marginX
marginY = def.marginY
headerH = def.headerH
selectedStanzasInScore = def.selectedStanzasInScore
}) {
Icon(
imageVector = Icons.Default.RestartAlt,
contentDescription = null,
contentDescription = "Réinitialiser paramètre impression",
modifier = Modifier.size(18.dp)
)
}
@ -319,7 +379,7 @@ fun PrintSettingsDialog(
OutlinedButton(onClick = onDismiss) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
contentDescription = "Fermer paramètre impression",
modifier = Modifier.size(18.dp)
)
}
@ -340,13 +400,14 @@ fun PrintSettingsDialog(
marginX = marginX,
marginY = marginY,
headerH = headerH,
nbGridOnMeasure = nbGridOnMeasure
nbGridOnMeasure = nbGridOnMeasure,
selectedStanzasInScore = selectedStanzasInScore
)
)
}) {
Icon(
imageVector = Icons.Default.PictureAsPdf,
contentDescription = null,
contentDescription = "Enregistrer en pdf",
modifier = Modifier.size(18.dp)
)
}

View file

@ -188,7 +188,7 @@ fun Settings(
thumbContent = {
Icon(
imageVector = if (tempDarkMode) Icons.Default.DarkMode else Icons.Default.LightMode,
contentDescription = null,
contentDescription = "Btn dark mode",
tint = MaterialTheme.colorScheme.onSecondary
)
},
@ -411,7 +411,7 @@ fun Settings(
) {
Icon(
imageVector = Icons.AutoMirrored.Default.Undo,
contentDescription = null,
contentDescription = "Btn reset dynamic volumes",
tint = MaterialTheme.colorScheme.error
)
}
@ -661,7 +661,7 @@ fun Settings(
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
contentDescription = "Selected instru",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
@ -809,7 +809,7 @@ fun SettingsAccordion(
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
contentDescription = "Setting $title",
tint = if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.rotate(arrowRotationDegree)
)

View file

@ -114,7 +114,7 @@ fun SimpleDrawerContent(
)
) {
Icon(
imageVector = Icons.AutoMirrored.Default.NoteAdd, contentDescription = null, tint = MaterialTheme.colorScheme.error
imageVector = Icons.AutoMirrored.Default.NoteAdd, contentDescription = "Btn create partition", tint = MaterialTheme.colorScheme.error
)
}
@ -172,7 +172,7 @@ fun SimpleDrawerContent(
) {
Icon(
imageVector = Icons.Default.PlaylistAddCircle,
contentDescription = null,
contentDescription = "Btn add playlist drawer intern",
tint = if (!onPlaylist) Color.LightGray else Color(0xFFFFD700),
modifier = Modifier.size(18.dp)
)
@ -225,7 +225,7 @@ fun SimpleDrawerContent(
) {
Icon(
imageVector = Icons.Default.PlaylistAddCircle,
contentDescription = null,
contentDescription = "Btn add playlist drawer perso",
tint = if (!onPlaylist) Color.LightGray else Color(0xFFFFD700),
modifier = Modifier.size(18.dp)
)
@ -279,7 +279,7 @@ fun SimpleDrawerContent(
) {
Icon(
imageVector = Icons.Default.VerticalAlignTop,
contentDescription = null,
contentDescription = "Btn align playlist drawer",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
@ -291,7 +291,7 @@ fun SimpleDrawerContent(
) {
Icon(
imageVector = Icons.Default.PlaylistRemove,
contentDescription = null,
contentDescription = "Btn remove playlist drawer",
tint = if (isSelected) Color.LightGray else Color(0xFFFFD700)
)
}
@ -343,7 +343,7 @@ fun SimpleDrawerContent(
) {
Icon(
imageVector = Icons.Default.StarRate,
contentDescription = null,
contentDescription = "Btn add favorite",
tint = if (isSelected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.tertiary
)
}
@ -383,7 +383,7 @@ fun SimpleDrawerContent(
) {
Image(
painter = painterResource(Res.drawable.icon),
contentDescription = null,
contentDescription = "Icon feufarooo",
modifier = Modifier.size(45.dp)
)
}
@ -397,7 +397,7 @@ fun SimpleDrawerContent(
solfaScreenModel.loadCustomFile()
scope.launch { drawerState.close() }
}) {
Icon(Icons.Default.FolderOpen, contentDescription = null, tint = MaterialTheme.colorScheme.onSecondary)
Icon(Icons.Default.FolderOpen, contentDescription = "Btn to load custom file", tint = MaterialTheme.colorScheme.onSecondary)
}
}
@ -410,7 +410,7 @@ fun SimpleDrawerContent(
onSettingsCheck()
scope.launch { drawerState.close() }
}) {
Icon(Icons.Default.Settings, contentDescription = null, tint = MaterialTheme.colorScheme.onSecondary)
Icon(Icons.Default.Settings, contentDescription = "Btn to settings", tint = MaterialTheme.colorScheme.onSecondary)
}
}
@ -423,7 +423,7 @@ fun SimpleDrawerContent(
onAboutCheck()
scope.launch { drawerState.close() }
}) {
Icon(Icons.Default.Info, contentDescription = null, tint = MaterialTheme.colorScheme.onSecondary)
Icon(Icons.Default.Info, contentDescription = "Btn to about", tint = MaterialTheme.colorScheme.onSecondary)
}
}
}
@ -449,7 +449,7 @@ fun DrawerHeaderSticky(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
Icon(icon, contentDescription = "Title $title", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
Spacer(Modifier.width(12.dp))
Text(
text = title.uppercase(),
@ -470,7 +470,7 @@ fun DrawerHeaderSticky(
}
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
contentDescription = "Title $title arrow",
modifier = Modifier.rotate(rotation)
)
}
@ -526,7 +526,7 @@ fun CustomSwitchItem(
thumbContent = {
Icon(
imageVector = if (checked) thumbIcon else Icons.Default.Visibility,
contentDescription = null,
contentDescription = "Toggle $label",
modifier = Modifier.size(SwitchDefaults.IconSize)
)
}

View file

@ -73,7 +73,7 @@ fun SolfaKeyboard(
) {
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardTab,
contentDescription = null,
contentDescription = "Solfakey tab",
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(2.dp))
@ -181,7 +181,7 @@ fun SolfaKeyboard(
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
contentDescription = "Solfakey close",
tint = MaterialTheme.colorScheme.onErrorContainer
)
}
@ -197,7 +197,7 @@ fun SolfaKeyboard(
) {
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardBackspace,
contentDescription = null,
contentDescription = "Solfakey backspace",
tint = Color.White
)
}
@ -213,7 +213,7 @@ fun SolfaKeyboard(
) {
Icon(
imageVector = Icons.Default.Save,
contentDescription = null,
contentDescription = "Solfakey save",
tint = Color.White
)
}

View file

@ -15,6 +15,8 @@ import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.pdmodel.PDPage
import org.apache.pdfbox.pdmodel.PDPageContentStream
import org.apache.pdfbox.pdmodel.font.PDType0Font
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState
import org.apache.pdfbox.util.Matrix
import java.awt.Color
import java.awt.geom.GeneralPath
@ -441,6 +443,7 @@ private fun drawNoteWithModulation(
private fun drawNoteWithSubscript(
cs: PDPageContentStream,
font: PDType0Font,
boldFont: PDType0Font?,
fontSize: Float,
text: String,
x: Float,
@ -448,21 +451,41 @@ private fun drawNoteWithSubscript(
) {
var curX = x
val superscriptOffset = fontSize * 0.35f
val subscriptOffset = fontSize * 0.2f
for (char in text) {
val str = char.toString().sanitize()
val isSuperscript = char in setOf('¹', '²', '³')
val isSubscript = char in setOf('₁', '₂', '₃')
val isSuperscript = char in setOf('¹', '²', '³', '⁴', '⁵')
val isSubscript = char in setOf('₁', '₂', '₃', '₄', '₅')
val isSpecialSymbol = char == '•' || char == ','
val isPunctuation = char in setOf('•', ',')
val activeFont = if (isSpecialSymbol && boldFont != null) boldFont else font
val currentSize = if (isSuperscript || isSubscript) fontSize * 0.6f else fontSize
val currentY = if (isSuperscript) y + superscriptOffset else y
val currentY = when {
isSuperscript -> y + superscriptOffset
isSubscript -> y - subscriptOffset
else -> y
}
cs.beginText()
cs.setFont(font, currentSize)
cs.newLineAtOffset(curX, currentY)
cs.setFont(activeFont, currentSize)
if (isSuperscript || isSubscript) {
cs.newLineAtOffset(curX - (currentSize * 0.2f), currentY)
} else {
cs.newLineAtOffset(curX, currentY)
}
cs.showText(str)
cs.endText()
curX += font.getStringWidth(str) / 1000f * currentSize + 0.5f
val charWidth = font.getStringWidth(str) / 1000f * currentSize
curX += when {
isSuperscript || isSubscript -> charWidth * 0.3f
isPunctuation -> charWidth
else -> charWidth + 0.5f
}
}
}
@ -578,6 +601,34 @@ private fun drawHeader(
cs.stroke()
}
fun drawWatermark(
doc: PDDocument,
cs: PDPageContentStream,
logoBytes: ByteArray,
pageWidth: Float,
pageHeight: Float
) {
try {
val pdImage = PDImageXObject.createFromByteArray(doc, logoBytes, "logo_footer")
val imgWidth = 100f
val imgHeight = 100f * (pdImage.height.toFloat() / pdImage.width.toFloat())
val x = (pageWidth - imgWidth) / 2f
val y = 40f
cs.saveGraphicsState()
val graphicsState = PDExtendedGraphicsState()
graphicsState.nonStrokingAlphaConstant = 0.15f
cs.setGraphicsStateParameters(graphicsState)
cs.drawImage(pdImage, x, y, imgWidth, imgHeight)
cs.restoreGraphicsState()
} catch (e: Exception) {
println("⚠️ Erreur lors du chargement du logo du bas de page : ${e.message}")
}
}
actual fun rememberPdfExportAction(
scope: CoroutineScope,
@ -650,11 +701,13 @@ private suspend fun generatePdfToBytes(
val customNbGrid = settings.nbGridOnMeasure
// ── Polices ────────────────────────────────────────────────────
val noteFontBytes = Res.readBytes("font/PTSerif-Regular.ttf")
val noteFontBytes = Res.readBytes("font/Lucida-Sans-Unicode.ttf")
val lyricFontBytes = Res.readBytes("font/LinLibertine_R.ttf")
val markerItalicBoldFontBytes = Res.readBytes("font/PT_Serif_Bold_Italic.ttf")
val markerBoldFontBytes = Res.readBytes("font/PTSerif_Bold.ttf")
val emmentalerFontBytes = Res.readBytes("font/Emmentaler.ttf")
val logoBytes = Res.readBytes("drawable/icon.png")
PDDocument().use { doc ->
@ -692,12 +745,33 @@ private suspend fun generatePdfToBytes(
} ?: 1
} ?: 1
val hasOverrideLyrics = maxSylsSize >= 2
val allStanzas = (1..nbStanza).toList()
val selectedInSettings = settings.selectedStanzasInScore.filter { it in allStanzas }
val (stanzasRemaining, stanzasToShow) = when {
selectedInSettings.isNotEmpty() -> {
val bottom = if (!hasOverrideLyrics) {
selectedInSettings
} else {
listOf(selectedInSettings.first())
}
val score = allStanzas.filter { it !in bottom }
score to bottom
}
else -> {
emptyList<Int>() to emptyList<Int>()
}
}
// ── Fonctions de dessin ────────────────────────────────────
fun newPage(): PDPageContentStream {
val page = PDPage(settings.pdRectangle)
doc.addPage(page)
val cs = PDPageContentStream(doc, page)
drawWatermark(doc, cs, logoBytes, pageWidth, pageHeight)
return cs
}
@ -1225,7 +1299,7 @@ private suspend fun generatePdfToBytes(
if (line.contains(">")) {
drawNoteWithModulation(cs, customFont, noteFontSize, line, noteX, currentY)
} else {
drawNoteWithSubscript(cs, customFont, noteFontSize, line, noteX, currentY)
drawNoteWithSubscript(cs, customFont, markerBoldFont, noteFontSize, line, noteX, currentY)
}
}
// Pour écrire dans l'espace vide JUSTE en dessous de cette voix :
@ -1262,7 +1336,7 @@ private suspend fun generatePdfToBytes(
// La position Y réelle du bas de l'accolade
val finalBraceBottom = notesStartY - (totalAreaSlots * noteLineH)
val stanzasToShow = if (maxSylsSize >= 2) listOf(1) else if (nbStanza >= 2) listOf(1, 2) else listOf(1)
//val stanzasToShow = if (maxSylsSize >= 2) listOf(1) else if (nbStanza >= 2) listOf(1, 2) else listOf(1)
val verticalMargin = 25f // Espace de sécurité entre l'accolade et les paroles
// val finalBraceBottom = braceBottom // On l'a calculé plus haut
@ -1527,8 +1601,6 @@ private suspend fun generatePdfToBytes(
} else {
if (nbStanza >= 2) listOf(1, 2) else listOf(1)
}
val stanzasRemaining = (1..nbStanza).filter { it !in stanzasDisplayed }
if (stanzasRemaining.isNotEmpty()) {
val textFont = if (lyricFont is PDType0Font) lyricFont else customFont
val textSize = lyricFontSize
@ -1588,7 +1660,7 @@ private suspend fun generatePdfToBytes(
// CROCHET DC (TOUJOURS, même si normal vide)
if (dcPart.isNotEmpty()) {
// dcPart
result.append("[${cleanSyllable(dcPart)}] ")
//result.append("[${cleanSyllable(dcPart)}] ")
}
// 3. DC MARQUEUR (parenthèses)

View file

@ -16,7 +16,8 @@ actual data class PrintSettings actual constructor(
actual val marginX : Float,
actual val marginY : Float,
actual val headerH : Float,
actual val nbGridOnMeasure: Int
actual val nbGridOnMeasure: Int,
actual val selectedStanzasInScore: List<Int>
) {
val pdRectangle: PDRectangle
get() {