diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt index 15306fb..6967cee 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt @@ -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() to emptyList() + } + } + // ── 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("(") diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt index ada3914..d8e539a 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt @@ -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 ) { val pdRectangle: PDRectangle get() { diff --git a/composeApp/src/commonMain/composeResources/font/Lucida-Sans-Unicode.ttf b/composeApp/src/commonMain/composeResources/font/Lucida-Sans-Unicode.ttf new file mode 100644 index 0000000..8840730 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/Lucida-Sans-Unicode.ttf differ diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt index e6d9d1e..71bdb2a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt @@ -25,7 +25,8 @@ expect class PrintSettings( marginX : Float, marginY : Float, headerH : Float, - nbGridOnMeasure : Int + nbGridOnMeasure : Int, + selectedStanzasInScore: List = 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 } expect fun defaultPrintSettings(): PrintSettings \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt index 16cceb5..9e263d6 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt @@ -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" ) } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt index 2652598..a2fa3d4 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt @@ -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 ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt index 29e3889..4a41382 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -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) ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 7a92140..86db629 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -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), diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/AboutScreen.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/AboutScreen.kt index acdef49..2bbb6ed 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/AboutScreen.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/AboutScreen.kt @@ -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 ) 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 612e9e9..468c6fc 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -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 ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt index edfb0bb..b66d9a1 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MidiControlPanel.kt @@ -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) ) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/ModernVolumeSlider.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/ModernVolumeSlider.kt index cbe7d24..518fb84 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/ModernVolumeSlider.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/ModernVolumeSlider.kt @@ -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) ) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt index 5337663..6f50969 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/NewPartition.kt @@ -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)) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/PrintSettingsDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/PrintSettingsDialog.kt index f611c72..bdf2ff9 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/PrintSettingsDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/PrintSettingsDialog.kt @@ -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) ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt index e16bb22..f9f6b64 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/Settings.kt @@ -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) ) 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 3d190cd..182e235 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -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) ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt index 50b6f56..e814d97 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SolfaKeyboard.kt @@ -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 ) } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt index d2b1bfd..666ba65 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt @@ -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() to emptyList() + } + } + // ── 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) diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt index 81a82e6..4f0e0dd 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PrintSettings.kt @@ -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 ) { val pdRectangle: PDRectangle get() {