Add btn to Export pdf & customize print settings
This commit is contained in:
parent
b3d7429b59
commit
1d35b9227d
10 changed files with 1394 additions and 347 deletions
|
|
@ -69,9 +69,9 @@ private fun measureNoteWidth(font: PDType0Font, fontSize: Float, text: String):
|
|||
|
||||
private fun bestColWidthPt(
|
||||
tuoList: List<TimeUnitObject>,
|
||||
font: org.apache.pdfbox.pdmodel.font.PDType0Font,
|
||||
font: PDType0Font,
|
||||
noteFontSize: Float,
|
||||
lyricFont: org.apache.pdfbox.pdmodel.font.PDType0Font,
|
||||
lyricFont:PDType0Font,
|
||||
lyricFontSize: Float
|
||||
): Float {
|
||||
var maxNoteW = 0f
|
||||
|
|
@ -269,8 +269,12 @@ private fun drawNoteWithSubscript(
|
|||
// ── drawHeader ────────────────────────────────────────────────────────────────
|
||||
private fun drawHeader(
|
||||
cs: PDPageContentStream,
|
||||
noteFont: PDType0Font, lyricFont: PDType0Font?,
|
||||
pageWidth: Float, pageHeight: Float, marginX: Float, usableWidth: Float,
|
||||
noteFont:PDType0Font,
|
||||
lyricFont:PDType0Font?,
|
||||
bolderFont: PDType0Font?,
|
||||
emmentalerfFont: PDType0Font?,
|
||||
pageWidth: Float, pageHeight: Float, marginX: Float, marginY: Float,
|
||||
usableWidth: Float,
|
||||
songTitle: String, songAuthor: String, songRhythm: String,
|
||||
songComposer: String, songKey: String, measure: String,
|
||||
stanza: Int, nbStanza: Int
|
||||
|
|
@ -278,46 +282,100 @@ private fun drawHeader(
|
|||
val titleFont = lyricFont ?: noteFont
|
||||
val normalFont = lyricFont ?: noteFont
|
||||
val smallFont = noteFont
|
||||
val emmentalerFont = emmentalerfFont ?: noteFont
|
||||
val boldFont = bolderFont ?: noteFont
|
||||
|
||||
fun tw(f: PDType0Font, sz: Float, t: String) =
|
||||
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
|
||||
|
||||
// ── Y calculés depuis marginY au lieu de valeurs fixes ────────────────────
|
||||
val titleY = pageHeight - marginY - 5f
|
||||
val line2Y = pageHeight - marginY - 21f
|
||||
val line3Y = pageHeight - marginY - 33f
|
||||
val sepY = line3Y - 5f
|
||||
|
||||
// Ligne 1 — Titre centré
|
||||
val titleSize = 15f
|
||||
val titleW = tw(titleFont, titleSize, songTitle)
|
||||
cs.beginText(); cs.setFont(titleFont, titleSize)
|
||||
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, pageHeight - 48f)
|
||||
cs.showText(songTitle.sanitize()); cs.endText()
|
||||
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, titleY)
|
||||
cs.showText(songTitle.sanitize())
|
||||
cs.endText()
|
||||
|
||||
// Ligne 2 — Auteur | Compositeur
|
||||
val line2Y = pageHeight - 64f; val sz2 = 9f
|
||||
// Ligne 2 — Author | Composer (droite)
|
||||
val sz2 = 9f
|
||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
|
||||
|
||||
val composerW = tw(normalFont, sz2, songComposer)
|
||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||
cs.beginText();
|
||||
cs.setFont(normalFont, sz2)
|
||||
cs.newLineAtOffset(marginX + usableWidth - composerW, line2Y)
|
||||
cs.showText(songComposer.sanitize()); cs.endText()
|
||||
cs.showText(songComposer.sanitize());
|
||||
cs.endText()
|
||||
|
||||
// Ligne 3 — Key | Measure | Rythme
|
||||
val line3Y = pageHeight - 76f; val sz3 = 8f
|
||||
val keyTxt = "Dô dia ${songKey.sanitize()}"
|
||||
cs.beginText(); cs.setFont(smallFont, sz3)
|
||||
cs.newLineAtOffset(marginX, line3Y); cs.showText(keyTxt); cs.endText()
|
||||
// Ligne 3 — Key | Measure | Rhythm
|
||||
val sz3 = 10f
|
||||
val keyTxt = "Dô dia "
|
||||
cs.beginText();
|
||||
cs.setFont(smallFont, sz3)
|
||||
cs.newLineAtOffset(marginX, line3Y);
|
||||
cs.showText(keyTxt);
|
||||
cs.endText()
|
||||
|
||||
val keyW = tw(smallFont, sz3, keyTxt)
|
||||
cs.beginText(); cs.setFont(smallFont, sz3)
|
||||
cs.newLineAtOffset(marginX + keyW + 10f, line3Y)
|
||||
cs.showText(measure.sanitize()); cs.endText()
|
||||
val keyW_2 = tw(smallFont, sz3, keyTxt)
|
||||
val keyTxt2 = "${songKey.sanitize()}"
|
||||
cs.beginText(); cs.setFont(boldFont, sz3+2f)
|
||||
cs.newLineAtOffset(marginX + keyW_2 + 3f, line3Y);
|
||||
cs.showText(keyTxt2);
|
||||
cs.endText()
|
||||
|
||||
val keyW = tw(boldFont, sz3, keyTxt+keyTxt2) + 10f
|
||||
val sz32 = 14f
|
||||
val splitMeaRgx = """(\d+/\d+)\s+(.*)""".toRegex()
|
||||
val matchRes = splitMeaRgx.matchEntire(measure)
|
||||
var timeSignature = ""
|
||||
var titleSignature = ""
|
||||
if(matchRes != null) {
|
||||
timeSignature = matchRes.groupValues[1]
|
||||
titleSignature = matchRes.groupValues[2]
|
||||
}
|
||||
val timesig = timeSignature.toCharArray()
|
||||
|
||||
var ksW = keyW
|
||||
timesig.forEach { c ->
|
||||
cs.beginText();
|
||||
if(c == '/') {
|
||||
cs.setFont(normalFont, sz32)
|
||||
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||
ksW += tw(normalFont, sz32, c.toString())
|
||||
} else {
|
||||
cs.setFont(emmentalerFont, sz32)
|
||||
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||
ksW += tw(emmentalerFont, sz32, c.toString())
|
||||
}
|
||||
cs.showText(c.toString());
|
||||
cs.endText()
|
||||
}
|
||||
cs.beginText();
|
||||
cs.setFont(smallFont, sz3)
|
||||
cs.newLineAtOffset(marginX + ksW + 15f, line3Y)
|
||||
cs.showText("${titleSignature}");
|
||||
cs.endText()
|
||||
|
||||
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
|
||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||
cs.beginText();
|
||||
cs.setFont(normalFont, sz2)
|
||||
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
|
||||
cs.showText(songRhythm.sanitize()); cs.endText()
|
||||
cs.showText(songRhythm.sanitize());
|
||||
cs.endText()
|
||||
|
||||
// Ligne de séparation
|
||||
cs.setStrokingColor(0.6f, 0.6f, 0.6f); cs.setLineWidth(0.4f)
|
||||
cs.moveTo(marginX, line3Y - 5f); cs.lineTo(marginX + usableWidth, line3Y - 5f); cs.stroke()
|
||||
cs.setStrokingColor(0.6f, 0.6f, 0.6f);
|
||||
cs.setLineWidth(0.4f)
|
||||
cs.moveTo(marginX, sepY);
|
||||
cs.lineTo(marginX + usableWidth, sepY);
|
||||
cs.stroke()
|
||||
}
|
||||
|
||||
// ── actual fun ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -333,8 +391,8 @@ actual fun rememberPdfExportAction(
|
|||
songAut: String,
|
||||
songComp: String,
|
||||
songRythm: String
|
||||
): () -> Unit {
|
||||
return {
|
||||
): (PrintSettings) -> Unit {
|
||||
return { settings: PrintSettings ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val (pdfBytes, computedFileName) = generatePdfToBytes(
|
||||
gridData, songTitle, measure, stanza,
|
||||
|
|
@ -360,7 +418,8 @@ private suspend fun generatePdfToBytes(
|
|||
songKey: String,
|
||||
songAut: String,
|
||||
songComp: String,
|
||||
songRythm: String
|
||||
songRythm: String,
|
||||
settings: PrintSettings = defaultPrintSettings()
|
||||
): Pair<ByteArray, String> {
|
||||
|
||||
val tuoList = gridData.tuoList.drop(1)
|
||||
|
|
@ -370,20 +429,21 @@ private suspend fun generatePdfToBytes(
|
|||
fun getVoicesFromPrefix(syl: String): List<Int> =
|
||||
REGEX_PREFIX_DIGITS.findAll(syl).map { it.groupValues[1].toInt() }.toList()
|
||||
|
||||
val markerHeight = 16f
|
||||
val noteFontSize = 12.5f
|
||||
val lyricFontSize = 13.25f
|
||||
val noteLineH = 15f
|
||||
val lyricLineH = 13f
|
||||
val extraSylLineH = 14f
|
||||
val adjustedMargin = 14f
|
||||
val interRowGap = 16f
|
||||
val marginX = 40f
|
||||
val marginY = 50f
|
||||
val headerH = 55f
|
||||
val pageWidth = PDRectangle.A4.width
|
||||
val pageHeight = PDRectangle.A4.height
|
||||
val markerHeight = settings.markerHeight
|
||||
val noteFontSize = settings.noteFontSize
|
||||
val lyricFontSize = settings.lyricFontSize
|
||||
val noteLineH = settings.noteLineH
|
||||
val lyricLineH = settings.lyricLineH
|
||||
val extraSylLineH = settings.extraSylLineH
|
||||
val adjustedMargin = settings.adjustedMargin
|
||||
val interRowGap = settings.interRowGap
|
||||
val marginX = settings.marginX
|
||||
val marginY = settings.marginY
|
||||
val headerH = settings.headerH
|
||||
val pageWidth = settings.pageWidth
|
||||
val pageHeight = settings.pageHeight
|
||||
val usableWidth = pageWidth - 2 * marginX
|
||||
val customNbGrid = settings.nbGridOnMeasure
|
||||
|
||||
// ── Polices ────────────────────────────────────────────────────────────────
|
||||
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
|
||||
|
|
@ -402,8 +462,12 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
// ── Largeur optimale de colonne ────────────────────────────────────────
|
||||
val bestColW = bestColWidthPt(tuoList, customFont, noteFontSize, lyricFont, lyricFontSize)
|
||||
val colCount = ((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
||||
val colWidth = bestColW
|
||||
val colCount = if (customNbGrid == 0) {
|
||||
((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
||||
} else {
|
||||
customNbGrid
|
||||
}
|
||||
val colWidth = usableWidth / colCount
|
||||
|
||||
val maxSylsSize = tuoList.maxOfOrNull { tuo ->
|
||||
(1..nbStanza).maxOfOrNull { sz -> tuo.getSingleSyllable(sz).size } ?: 1
|
||||
|
|
@ -411,7 +475,7 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
// ── Fonctions de dessin locales ────────────────────────────────────────
|
||||
fun newPage(): PDPageContentStream {
|
||||
val page = PDPage(PDRectangle.A4)
|
||||
val page = PDPage(settings.pdRectangle)
|
||||
doc.addPage(page)
|
||||
return PDPageContentStream(doc, page)
|
||||
}
|
||||
|
|
@ -451,27 +515,78 @@ private suspend fun generatePdfToBytes(
|
|||
cs.restoreGraphicsState()
|
||||
}
|
||||
|
||||
// ── Fermata (dessin vectoriel — remplace le rendu glyphe Emmentaler) ──
|
||||
fun drawFermata(cs: PDPageContentStream, x: Float, y: Float, width: Float = 10f, height: Float = 12f) {
|
||||
cs.setStrokingColor(0f, 0f, 0f); cs.setNonStrokingColor(0f, 0f, 0f)
|
||||
val circleRadius = width * 0.22f
|
||||
val circleX = x + width / 2
|
||||
val circleY = y + height * 0.25f
|
||||
val k = 0.5522847498f
|
||||
cs.moveTo(circleX + circleRadius, circleY)
|
||||
cs.curveTo(circleX + circleRadius, circleY + circleRadius * k, circleX + circleRadius * k, circleY + circleRadius, circleX, circleY + circleRadius)
|
||||
cs.curveTo(circleX - circleRadius * k, circleY + circleRadius, circleX - circleRadius, circleY + circleRadius * k, circleX - circleRadius, circleY)
|
||||
cs.curveTo(circleX - circleRadius, circleY - circleRadius * k, circleX - circleRadius * k, circleY - circleRadius, circleX, circleY - circleRadius)
|
||||
cs.curveTo(circleX + circleRadius * k, circleY - circleRadius, circleX + circleRadius, circleY - circleRadius * k, circleX + circleRadius, circleY)
|
||||
fun drawFermata(cs: PDPageContentStream, x: Float, y: Float, scale: Float = 1f) {
|
||||
fun px(svgX: Float) = x + svgX * scale
|
||||
fun py(svgY: Float) = y + svgY * scale
|
||||
|
||||
fun curveTo(
|
||||
cp1x: Float, cp1y: Float,
|
||||
cp2x: Float, cp2y: Float,
|
||||
ex: Float, ey: Float
|
||||
) = cs.curveTo(px(cp1x), py(cp1y), px(cp2x), py(cp2y), px(ex), py(ey))
|
||||
|
||||
fun drawEllipse(cx: Float, cy: Float, rx: Float, ry: Float) {
|
||||
val k = 0.5523f
|
||||
val left = cx - rx; val right = cx + rx
|
||||
val bottom = cy - ry; val top = cy + ry
|
||||
|
||||
cs.moveTo(px(right), py(cy))
|
||||
// quart haut-droit
|
||||
cs.curveTo(
|
||||
px(right), py(cy + ry * k),
|
||||
px(cx + rx * k), py(top),
|
||||
px(cx), py(top)
|
||||
)
|
||||
// quart haut-gauche
|
||||
cs.curveTo(
|
||||
px(cx - rx * k), py(top),
|
||||
px(left), py(cy + ry * k),
|
||||
px(left), py(cy)
|
||||
)
|
||||
// quart bas-gauche
|
||||
cs.curveTo(
|
||||
px(left), py(cy - ry * k),
|
||||
px(cx - rx * k), py(bottom),
|
||||
px(cx), py(bottom)
|
||||
)
|
||||
// quart bas-droit
|
||||
cs.curveTo(
|
||||
px(cx + rx * k), py(bottom),
|
||||
px(right), py(cy - ry * k),
|
||||
px(right), py(cy)
|
||||
)
|
||||
cs.closePath()
|
||||
}
|
||||
|
||||
cs.setNonStrokingColor(0f, 0f, 0f) // RGB noir
|
||||
cs.moveTo(px(0.3034f), py(0.6922f))
|
||||
|
||||
// Dôme extérieur — côté gauche
|
||||
curveTo(2.6605f, 16.6051f, 13.8570f, 30.7498f, 28.0000f, 30.7498f)
|
||||
// Dôme extérieur — côté droit
|
||||
curveTo(42.1430f, 30.7498f, 53.3395f, 16.6051f, 55.6966f, 0.6922f)
|
||||
|
||||
// Épaisseur bord droit
|
||||
curveTo(55.8145f, 0.1028f, 52.8680f, 0.1028f, 52.7502f, 0.6922f)
|
||||
|
||||
// Dôme intérieur — côté droit
|
||||
curveTo(49.8037f, 11.8901f, 42.1430f, 22.4987f, 28.0000f, 22.4987f)
|
||||
// Dôme intérieur — côté gauche
|
||||
curveTo(13.8570f, 22.4987f, 6.1963f, 11.8901f, 3.2498f, 0.6922f)
|
||||
|
||||
// Épaisseur bord gauche
|
||||
curveTo(3.1320f, 0.1028f, 0.1855f, 0.1028f, 0.3034f, 0.6922f)
|
||||
|
||||
cs.closePath()
|
||||
cs.fill()
|
||||
|
||||
drawEllipse(
|
||||
cx = 28.0000f,
|
||||
cy = 5.8492f,
|
||||
rx = 5.5983f,
|
||||
ry = 5.5990f
|
||||
)
|
||||
cs.fill()
|
||||
cs.setLineWidth(1.5f)
|
||||
val arcStartX = x + width * 0.2f; val arcStartY = y + height * 0.55f
|
||||
val arcPeakX = x + width / 2; val arcPeakY = y + height * 0.85f
|
||||
val arcEndX = x + width * 0.8f; val arcEndY = y + height * 0.55f
|
||||
cs.moveTo(arcStartX, arcStartY)
|
||||
cs.curveTo(arcStartX + width * 0.15f, arcPeakY, arcPeakX - width * 0.15f, arcPeakY, arcPeakX, arcPeakY)
|
||||
cs.curveTo(arcPeakX + width * 0.15f, arcPeakY, arcEndX - width * 0.15f, arcStartY, arcEndX, arcStartY)
|
||||
cs.stroke()
|
||||
}
|
||||
|
||||
// ── drawMarkers ────────────────────────────────────────────────────────
|
||||
|
|
@ -493,7 +608,7 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
// Point d'orgue → drawFermata (pas de GeneralPath sur Android)
|
||||
if (markerText.contains("𝄐") || tuo.pTemplate.template.contains("𝄐")) {
|
||||
drawFermata(cs, colX + colWidth / 2 - 5f, markerY - 2f, 10f, 12f)
|
||||
drawFermata(cs, colX + colWidth / 2 - 5f, markerY - 2f, 0.3f)
|
||||
}
|
||||
|
||||
// Marqueur texte
|
||||
|
|
@ -540,8 +655,8 @@ private suspend fun generatePdfToBytes(
|
|||
var y = pageHeight - marginY - headerH
|
||||
|
||||
drawHeader(
|
||||
cs, customFont, lyricFont,
|
||||
pageWidth, pageHeight, marginX, usableWidth,
|
||||
cs, customFont, lyricFont, markerBoldFont, markerEmmetFont,
|
||||
pageWidth, pageHeight, marginX, marginY, usableWidth,
|
||||
songTitle = songTitle,
|
||||
songAuthor = songAut,
|
||||
songRhythm = songRythm,
|
||||
|
|
@ -812,7 +927,7 @@ private suspend fun generatePdfToBytes(
|
|||
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||
|
||||
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
||||
// val isAllZeroSpacing = customSpacings.all { it == 0 }
|
||||
|
||||
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) {
|
||||
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package mg.dot.feufaro.pdf
|
||||
|
||||
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
|
||||
|
||||
actual data class PrintSettings actual constructor(
|
||||
actual val pageSize : PageSize,
|
||||
actual val orientation : PageOrientation,
|
||||
actual val markerHeight : Float,
|
||||
actual val noteFontSize : Float,
|
||||
actual val lyricFontSize : Float,
|
||||
actual val noteLineH : Float,
|
||||
actual val lyricLineH : Float,
|
||||
actual val extraSylLineH : Float,
|
||||
actual val adjustedMargin : Float,
|
||||
actual val interRowGap : Float,
|
||||
actual val marginX : Float,
|
||||
actual val marginY : Float,
|
||||
actual val headerH : Float,
|
||||
actual val nbGridOnMeasure: Int
|
||||
) {
|
||||
val pdRectangle: PDRectangle
|
||||
get() {
|
||||
val base = when (pageSize) {
|
||||
PageSize.A4 -> PDRectangle.A4
|
||||
PageSize.A3 -> PDRectangle.A3
|
||||
PageSize.LETTER -> PDRectangle.LETTER
|
||||
}
|
||||
return if (orientation == PageOrientation.PORTRAIT) base
|
||||
else PDRectangle(base.height, base.width)
|
||||
}
|
||||
|
||||
actual val pageWidth : Float get() = pdRectangle.width
|
||||
actual val pageHeight : Float get() = pdRectangle.height
|
||||
}
|
||||
|
||||
actual fun defaultPrintSettings(): PrintSettings = PrintSettings(
|
||||
pageSize = PageSize.A4,
|
||||
orientation = PageOrientation.PORTRAIT,
|
||||
markerHeight = 16f,
|
||||
noteFontSize = 12f,
|
||||
lyricFontSize = 12.5f,
|
||||
noteLineH = 17f,
|
||||
lyricLineH = 15f,
|
||||
extraSylLineH = 15f,
|
||||
adjustedMargin = 14f,
|
||||
interRowGap = 22f,
|
||||
marginX = 42.5f,
|
||||
marginY = 42.5f,
|
||||
headerH = 55f,
|
||||
nbGridOnMeasure = 0
|
||||
)
|
||||
|
|
@ -16,4 +16,4 @@ expect fun rememberPdfExportAction(
|
|||
songAut: String,
|
||||
songComp: String,
|
||||
songRythm: String
|
||||
): () -> Unit
|
||||
): (PrintSettings) -> Unit
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package mg.dot.feufaro.pdf
|
||||
|
||||
enum class PageSize(val label: String) {
|
||||
A4("A4"),
|
||||
A3("A3"),
|
||||
LETTER("Letter")
|
||||
}
|
||||
|
||||
enum class PageOrientation(val label: String) {
|
||||
PORTRAIT("Portrait"),
|
||||
LANDSCAPE("Paysage")
|
||||
}
|
||||
|
||||
expect class PrintSettings(
|
||||
pageSize : PageSize,
|
||||
orientation : PageOrientation,
|
||||
markerHeight : Float,
|
||||
noteFontSize : Float,
|
||||
lyricFontSize : Float,
|
||||
noteLineH : Float,
|
||||
lyricLineH : Float,
|
||||
extraSylLineH : Float,
|
||||
adjustedMargin : Float,
|
||||
interRowGap : Float,
|
||||
marginX : Float,
|
||||
marginY : Float,
|
||||
headerH : Float,
|
||||
nbGridOnMeasure : Int
|
||||
) {
|
||||
val pageSize : PageSize
|
||||
val orientation : PageOrientation
|
||||
val markerHeight : Float
|
||||
val noteFontSize : Float
|
||||
val lyricFontSize : Float
|
||||
val noteLineH : Float
|
||||
val lyricLineH : Float
|
||||
val extraSylLineH : Float
|
||||
val adjustedMargin : Float
|
||||
val interRowGap : Float
|
||||
val marginX : Float
|
||||
val marginY : Float
|
||||
val headerH : Float
|
||||
val nbGridOnMeasure : Int
|
||||
val pageWidth : Float
|
||||
val pageHeight : Float
|
||||
}
|
||||
|
||||
expect fun defaultPrintSettings(): PrintSettings
|
||||
|
|
@ -560,7 +560,9 @@ fun LazyVerticalGridTUO(
|
|||
val itemMinBaseWidth = bestTUOWidth(tuoList)
|
||||
val gridWidthDp = with(density) { gridWidthPx.toDp() }
|
||||
|
||||
val gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) {
|
||||
val gridCount by sharedScreenModel.gridCount.collectAsState()
|
||||
|
||||
var gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) {
|
||||
if (gridWidthDp == 0.dp) return@remember columnGroup
|
||||
var calculatedCols =
|
||||
(gridWidthDp / (itemMinBaseWidth + horizontalArrangementSpacing)).toInt()
|
||||
|
|
@ -572,6 +574,7 @@ fun LazyVerticalGridTUO(
|
|||
val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup)
|
||||
actualColumnCount
|
||||
}
|
||||
gridColumnCount += gridCount
|
||||
val flowRowSize: Float = 0.98f / gridColumnCount
|
||||
|
||||
val currentStanza = viewModel.stanza
|
||||
|
|
|
|||
|
|
@ -18,14 +18,17 @@ import androidx.compose.ui.draw.alpha
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import mg.dot.feufaro.data.getDrawerItems
|
||||
import mg.dot.feufaro.pdf.PrintSettings
|
||||
import mg.dot.feufaro.pdf.defaultPrintSettings
|
||||
import mg.dot.feufaro.ui.PrintSettingsDialog
|
||||
import mg.dot.feufaro.pdf.rememberPdfExportAction
|
||||
import mg.dot.feufaro.solfa.Solfa
|
||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
|
|
@ -111,7 +114,10 @@ LaunchedEffect(isPlay, isPos) {
|
|||
)
|
||||
}, content = {
|
||||
val scope = rememberCoroutineScope()
|
||||
val pdfExportAction: () -> Unit = rememberPdfExportAction(
|
||||
var showPrintSettings by remember { mutableStateOf(false) }
|
||||
var pendingPrintSettings by remember { mutableStateOf<PrintSettings?>(null) }
|
||||
|
||||
val pdfExportAction: (PrintSettings) -> Unit = rememberPdfExportAction(
|
||||
scope = scope,
|
||||
fileRepository = solfaScreenModel.fileRepository,
|
||||
gridData = sharedScreenModel.currentGridData,
|
||||
|
|
@ -124,6 +130,17 @@ LaunchedEffect(isPlay, isPos) {
|
|||
songComp = songComposer,
|
||||
songRythm = songRhythm
|
||||
)
|
||||
|
||||
if (showPrintSettings) {
|
||||
PrintSettingsDialog(
|
||||
initialSettings = defaultPrintSettings(),
|
||||
onDismiss = { showPrintSettings = false },
|
||||
onConfirm = { settings ->
|
||||
showPrintSettings = false
|
||||
pdfExportAction(settings)
|
||||
}
|
||||
)
|
||||
}
|
||||
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
||||
|
|
@ -171,6 +188,42 @@ LaunchedEffect(isPlay, isPos) {
|
|||
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp)
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded and !showMidiCtrl,
|
||||
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
|
||||
) {
|
||||
Row {
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
sharedScreenModel.descGridCount(1)
|
||||
}, modifier = Modifier.size(30.dp).alpha(0.45f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Remove,
|
||||
contentDescription = null,
|
||||
tint = Color.Blue
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
sharedScreenModel.addGridCount(1)
|
||||
}, modifier = Modifier.size(30.dp).alpha(0.45f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
tint = Color.Blue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded and !showMidiCtrl,
|
||||
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||
|
|
@ -198,7 +251,7 @@ LaunchedEffect(isPlay, isPos) {
|
|||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
pdfExportAction()
|
||||
showPrintSettings = !showPrintSettings
|
||||
}, modifier = Modifier.alpha(0.45f)
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,653 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.PathParser
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import mg.dot.feufaro.pdf.PageOrientation
|
||||
import mg.dot.feufaro.pdf.PageSize
|
||||
import mg.dot.feufaro.pdf.PrintSettings
|
||||
import mg.dot.feufaro.pdf.defaultPrintSettings
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@Composable
|
||||
fun PrintSettingsDialog(
|
||||
initialSettings: PrintSettings = defaultPrintSettings(),
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (PrintSettings) -> Unit,
|
||||
) {
|
||||
// ── État local (copie mutable des paramètres) ─────────────────────────────
|
||||
var pageSize by remember { mutableStateOf(initialSettings.pageSize) }
|
||||
var orientation by remember { mutableStateOf(initialSettings.orientation) }
|
||||
var noteFontSize by remember { mutableStateOf(initialSettings.noteFontSize) }
|
||||
var lyricFontSize by remember { mutableStateOf(initialSettings.lyricFontSize) }
|
||||
var markerHeight by remember { mutableStateOf(initialSettings.markerHeight) }
|
||||
var noteLineH by remember { mutableStateOf(initialSettings.noteLineH) }
|
||||
var lyricLineH by remember { mutableStateOf(initialSettings.lyricLineH) }
|
||||
var extraSylLineH by remember { mutableStateOf(initialSettings.extraSylLineH) }
|
||||
var adjustedMargin by remember { mutableStateOf(initialSettings.adjustedMargin) }
|
||||
var interRowGap by remember { mutableStateOf(initialSettings.interRowGap) }
|
||||
var marginX by remember { mutableStateOf(initialSettings.marginX) }
|
||||
var marginY by remember { mutableStateOf(initialSettings.marginY) }
|
||||
var headerH by remember { mutableStateOf(initialSettings.headerH) }
|
||||
var nbGridOnMeasure by remember { mutableStateOf(initialSettings.nbGridOnMeasure) }
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.92f)
|
||||
.fillMaxHeight(0.92f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
tonalElevation = 8.dp,
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// ── Barre de titre ────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.padding(horizontal = 20.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Paramètres d'impression",
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
contentDescription = "Fermer",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Corps scrollable en deux colonnes ─────────────────────────
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// ── Colonne gauche : Format papier + Preview ──────────────
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
SectionTitle("Format de page")
|
||||
|
||||
Row{
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(0.7f)
|
||||
) {
|
||||
LabelRow("Nombre de colonne d'une portée")
|
||||
}
|
||||
Column {
|
||||
TextField(
|
||||
value = if (nbGridOnMeasure == 0) "" else nbGridOnMeasure.toString(),
|
||||
onValueChange = { newValue ->
|
||||
val number = newValue.toIntOrNull()?.coerceAtMost(25)
|
||||
if (number != null) {
|
||||
nbGridOnMeasure = number
|
||||
} else if (newValue.isEmpty()) {
|
||||
nbGridOnMeasure = 0
|
||||
}
|
||||
},
|
||||
modifier = Modifier.height(50.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
// Taille papier
|
||||
LabelRow("Taille du papier")
|
||||
SegmentedRow(
|
||||
options = PageSize.entries.map { it.label },
|
||||
selected = pageSize.ordinal,
|
||||
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] }
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
// ── Preview miniature de la page ──────────────────────
|
||||
SectionTitle("Aperçu")
|
||||
val currentSettings = PrintSettings(
|
||||
pageSize = pageSize,
|
||||
orientation = orientation,
|
||||
noteFontSize = noteFontSize,
|
||||
lyricFontSize = lyricFontSize,
|
||||
markerHeight = markerHeight,
|
||||
noteLineH = noteLineH,
|
||||
lyricLineH = lyricLineH,
|
||||
extraSylLineH = extraSylLineH,
|
||||
adjustedMargin = adjustedMargin,
|
||||
interRowGap = interRowGap,
|
||||
marginX = marginX,
|
||||
marginY = marginY,
|
||||
headerH = headerH,
|
||||
nbGridOnMeasure = nbGridOnMeasure
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
PagePreview(
|
||||
settings = currentSettings,
|
||||
modifier = Modifier
|
||||
.height(250.dp) // Hauteur fixe demandée
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Colonne droite : Paramètres typographiques / marges ───
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
SectionTitle("Typographie")
|
||||
FloatSlider(
|
||||
label = "Taille fonte notes",
|
||||
value = noteFontSize,
|
||||
range = 8f..18f,
|
||||
unit = "pt"
|
||||
) { noteFontSize = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Taille fonte paroles",
|
||||
value = lyricFontSize,
|
||||
range = 8f..18f,
|
||||
unit = "pt"
|
||||
) { lyricFontSize = it }
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
SectionTitle("Espacements")
|
||||
|
||||
FloatSlider(
|
||||
label = "Hauteur zone marqueurs",
|
||||
value = markerHeight,
|
||||
range = 8f..32f,
|
||||
unit = "mm"
|
||||
) { markerHeight = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Espace entre voix",
|
||||
value = noteLineH,
|
||||
range = 10f..34f,
|
||||
unit = "mm"
|
||||
) { noteLineH = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Espace lignes paroles restantes",
|
||||
value = lyricLineH,
|
||||
range = 10f..30f,
|
||||
unit = "mm"
|
||||
) { lyricLineH = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Espace syl0 / syl1+",
|
||||
value = extraSylLineH,
|
||||
range = 8f..30f,
|
||||
unit = "mm"
|
||||
) { extraSylLineH = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Marge ajustée syl0",
|
||||
value = adjustedMargin,
|
||||
range = 4f..30f,
|
||||
unit = "mm"
|
||||
) { adjustedMargin = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Écart entre portées",
|
||||
value = interRowGap,
|
||||
range = 10f..60f,
|
||||
unit = "mm"
|
||||
) { interRowGap = it }
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
SectionTitle("Marges")
|
||||
|
||||
FloatSlider(
|
||||
label = "Marge gauche/droite",
|
||||
value = marginX,
|
||||
range = 15f..80f,
|
||||
unit = "mm"
|
||||
) { marginX = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Marge haut/bas",
|
||||
value = marginY,
|
||||
range = 15f..80f,
|
||||
unit = "mm"
|
||||
) { marginY = it }
|
||||
|
||||
FloatSlider(
|
||||
label = "Hauteur en-tête",
|
||||
value = headerH,
|
||||
range = 49f..90f,
|
||||
unit = "mm"
|
||||
) { headerH = it }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Boutons bas ───────────────────────────────────────────────
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Réinitialiser
|
||||
TextButton(onClick = {
|
||||
val def = defaultPrintSettings()
|
||||
pageSize = def.pageSize
|
||||
orientation = def.orientation
|
||||
noteFontSize = def.noteFontSize
|
||||
lyricFontSize = def.lyricFontSize
|
||||
markerHeight = def.markerHeight
|
||||
noteLineH = def.noteLineH
|
||||
lyricLineH = def.lyricLineH
|
||||
extraSylLineH = def.extraSylLineH
|
||||
adjustedMargin = def.adjustedMargin
|
||||
interRowGap = def.interRowGap
|
||||
marginX = def.marginX
|
||||
marginY = def.marginY
|
||||
headerH = def.headerH
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Spacer(Modifier.weight(1f))
|
||||
|
||||
OutlinedButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
onConfirm(
|
||||
PrintSettings(
|
||||
pageSize = pageSize,
|
||||
orientation = orientation,
|
||||
noteFontSize = noteFontSize,
|
||||
lyricFontSize = lyricFontSize,
|
||||
markerHeight = markerHeight,
|
||||
noteLineH = noteLineH,
|
||||
lyricLineH = lyricLineH,
|
||||
extraSylLineH = extraSylLineH,
|
||||
adjustedMargin = adjustedMargin,
|
||||
interRowGap = interRowGap,
|
||||
marginX = marginX,
|
||||
marginY = marginY,
|
||||
headerH = headerH,
|
||||
nbGridOnMeasure = nbGridOnMeasure
|
||||
)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PictureAsPdf,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Composants internes ───────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun SectionTitle(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelRow(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SegmentedRow(
|
||||
options: List<String>,
|
||||
selected: Int,
|
||||
onSelect: (Int) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
) {
|
||||
options.forEachIndexed { index, label ->
|
||||
val isSelected = index == selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.background(
|
||||
if (isSelected) MaterialTheme.colorScheme.primary
|
||||
else Color.Transparent
|
||||
)
|
||||
.clickable { onSelect(index) }
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider flottant avec label + valeur affichée.
|
||||
*/
|
||||
fun Float.toMm(): Float {
|
||||
return this * 0.3528f
|
||||
}
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FloatSlider(
|
||||
label: String,
|
||||
value: Float,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
unit: String,
|
||||
onValueChange: (Float) -> Unit
|
||||
) {
|
||||
val displayValue = if (unit == "mm") value.toMm() else value
|
||||
val roundedDisplayValue = (displayValue * 2).roundToInt() / 2f
|
||||
val primaryTeal = Color(0xFF009688)
|
||||
val lightTeal = Color(0xFFB2DFDB)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
// 1. Label de description fixe à gauche
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
// 2. Zone du Slider avec la valeur flottante au-dessus
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||
val widthPx = constraints.maxWidth.toFloat()
|
||||
|
||||
// Calcul de la position X du texte pour suivre le thumb
|
||||
// On normalise la valeur entre 0.0 et 1.0
|
||||
val fraction = (value - range.start) / (range.endInclusive - range.start)
|
||||
val textOffset = (widthPx * fraction)
|
||||
|
||||
// Affichage de la valeur qui "flotte" au-dessus du thumb
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp) // Espace pour le texte
|
||||
) {
|
||||
Text(
|
||||
text = "$roundedDisplayValue $unit",
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = primaryTeal,
|
||||
modifier = Modifier.graphicsLayer() {
|
||||
translationX = textOffset - (30.dp.toPx()) // Ajustement pour centrer le texte
|
||||
}
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = { onValueChange((it * 2).roundToInt() / 2f) },
|
||||
valueRange = range,
|
||||
steps = (((range.endInclusive - range.start) / 0.5f).toInt() - 1).coerceAtLeast(0),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 15.dp), // Décale le slider sous le texte
|
||||
thumb = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(15.dp)
|
||||
.background(primaryTeal, CircleShape)
|
||||
)
|
||||
},
|
||||
track = { sliderState ->
|
||||
SliderDefaults.Track(
|
||||
sliderState = sliderState,
|
||||
modifier = Modifier.height(8.dp),
|
||||
colors = SliderDefaults.colors(
|
||||
activeTrackColor = primaryTeal,
|
||||
inactiveTrackColor = lightTeal
|
||||
),
|
||||
drawStopIndicator = null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Mini preview de la page
|
||||
*/
|
||||
@Composable
|
||||
private fun PagePreview(
|
||||
settings: PrintSettings,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isLandscape = settings.orientation == PageOrientation.LANDSCAPE
|
||||
val ratio = if (isLandscape) 1.414f else 0.707f
|
||||
val baseScale = 200f / 595f // Échelle pour l'affichage
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(ratio)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(Color.White)
|
||||
.border(1.dp, Color.Gray.copy(alpha = 0.3f), RoundedCornerShape(4.dp)),
|
||||
contentAlignment = Alignment.TopStart
|
||||
) {
|
||||
// Conversion des unités en DP
|
||||
val mx = (settings.marginX * baseScale).dp
|
||||
val my = (settings.marginY * baseScale).dp
|
||||
val hH = (settings.headerH * baseScale).dp
|
||||
val nH = (settings.noteLineH * baseScale).dp // Espace entre 2 voix
|
||||
val iG = (settings.interRowGap * baseScale).dp // Entre portées
|
||||
val mH = (settings.markerHeight * baseScale).dp // Zone orange
|
||||
val aM = (settings.adjustedMargin * baseScale).dp // Marge syl0
|
||||
val eS = (settings.extraSylLineH * baseScale).dp // Espace syl0/syl1
|
||||
val pR = (settings.lyricLineH * baseScale).dp // Espace syl0/syl1
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = mx, vertical = my)
|
||||
) {
|
||||
// 1. HEADER
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(hH)
|
||||
.border(0.5.dp, Color.Red.copy(alpha = 0.5f))
|
||||
.background(Color.Red.copy(alpha = 0.05f))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(iG / 2))
|
||||
|
||||
// 2. PORTÉES
|
||||
val repeatedPort = if(settings.orientation == PageOrientation.LANDSCAPE) 2 else 3
|
||||
repeat(repeatedPort) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(0.5.dp, Color(0xFF2ECC71).copy(alpha = 0.4f)) // Cadre Vert
|
||||
.padding(2.dp)
|
||||
) {
|
||||
// Ligne des Marqueurs
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(mH / 2)
|
||||
.border(0.5.dp, Color(0xFFE67E22)) // Orange
|
||||
.background(Color(0xFFE67E22).copy(alpha = 0.1f))
|
||||
)
|
||||
|
||||
// Lignes des Notes par voix
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AccoladeShape(modifier = Modifier.width(6.dp).height(nH * 2.5f))
|
||||
|
||||
Column(modifier = Modifier.weight(1f).padding(start = 2.dp)) {
|
||||
repeat(4) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(nH / 3)
|
||||
.border(0.3.dp, Color(0xFF00BCD4)) // Bleu Ciel
|
||||
.background(Color(0xFF00BCD4).copy(alpha = 0.1f))
|
||||
)
|
||||
Spacer(modifier = Modifier.height(nH / 6))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paroles Syl0 et Syl1
|
||||
Spacer(modifier = Modifier.height(aM / 2))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.85f)
|
||||
.border(0.5.dp, Color(0xFF9B59B6)) // Violet
|
||||
.padding(1.dp)
|
||||
) {
|
||||
// Syl0
|
||||
Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(Color(0xFF9B59B6).copy(alpha = 0.3f)))
|
||||
Spacer(modifier = Modifier.height(eS / 4))
|
||||
// Syl1
|
||||
Box(modifier = Modifier.fillMaxWidth(0.9f).height(2.dp).background(Color(0xFF9B59B6).copy(alpha = 0.3f)))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(iG))
|
||||
}
|
||||
|
||||
// 3. PAROLES RESTANTES
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(0.5.dp, Color.Red.copy(alpha = 0.5f))
|
||||
.background(Color.Red.copy(alpha = 0.05f))
|
||||
.padding(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
repeat(4) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(if (index % 2 == 0) 0.95f else 0.85f)
|
||||
.height(2.dp)
|
||||
.background(Color.Red.copy(alpha = 0.2f))
|
||||
)
|
||||
Spacer(modifier = Modifier.height(pR / 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccoladeShape(modifier: Modifier) {
|
||||
Canvas(modifier = modifier) {
|
||||
// Le path que tu as fourni (normalisé sur une grille de 24x24 environ)
|
||||
val pathData = "M14 21C12.8954 21 12 20.1046 12 19V15.3255C12 14.8363 12 14.5917 11.9447 14.3615C11.8957 14.1575 11.8149 13.9624 11.7053 13.7834C11.5816 13.5816 11.4086 13.4086 11.0627 13.0627L10 12L11.0627 10.9373C11.4086 10.5914 11.5816 10.4184 11.7053 10.2166C11.8149 10.0376 11.8957 9.84254 11.9447 9.63846C12 9.40829 12 9.1637 12 8.67452V5C12 3.89543 12.8954 3 14 3"
|
||||
|
||||
val path = PathParser().parsePathString(pathData).toPath()
|
||||
val pathBounds = path.getBounds()
|
||||
val scaleX = size.width / pathBounds.width
|
||||
val scaleY = size.height / pathBounds.height
|
||||
|
||||
drawContext.canvas.save()
|
||||
drawContext.canvas.scale(scaleX, scaleY)
|
||||
drawContext.canvas.translate(-pathBounds.left, -pathBounds.top)
|
||||
|
||||
drawPath(
|
||||
path = path,
|
||||
color = Color.Black,
|
||||
style = androidx.compose.ui.graphics.drawscope.Stroke(
|
||||
width = 0.552f,
|
||||
cap = androidx.compose.ui.graphics.StrokeCap.Round,
|
||||
join = androidx.compose.ui.graphics.StrokeJoin.Round
|
||||
)
|
||||
)
|
||||
drawContext.canvas.restore()
|
||||
}
|
||||
}
|
||||
|
|
@ -183,6 +183,20 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_activeIndex.value = clamped
|
||||
}
|
||||
}
|
||||
private val _gridCount = MutableStateFlow(0)
|
||||
val gridCount: StateFlow<Int> = _gridCount.asStateFlow()
|
||||
|
||||
fun addGridCount(nbGrid: Int) {
|
||||
_gridCount.value += nbGrid
|
||||
}
|
||||
fun descGridCount(nbGrid: Int) {
|
||||
if(_gridCount.value > 0) {
|
||||
_gridCount.value -= nbGrid
|
||||
}
|
||||
}
|
||||
fun resetGridCount() {
|
||||
_gridCount.value = 0
|
||||
}
|
||||
fun seekToGrid(gridIndex: Int) {
|
||||
_mediaPlayer?.seekToGrid(gridIndex)
|
||||
}
|
||||
|
|
@ -327,6 +341,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
fun loadNewSong(newMidiFile: String) {
|
||||
_mediaPlayer?.stop()
|
||||
_mediaPlayer?.release()
|
||||
_stanza.value = 1
|
||||
_mediaPlayer = null
|
||||
_isPos.value = true
|
||||
_isPlay.value = false
|
||||
|
|
@ -436,6 +451,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_tuoTimestamps.value = emptyList()
|
||||
updateSearchTxt("")
|
||||
tempTimeUnitObjectList.clear()
|
||||
resetGridCount()
|
||||
}
|
||||
|
||||
fun lastTUO(): TimeUnitObject? {
|
||||
|
|
@ -464,7 +480,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
} catch (e: NumberFormatException) {
|
||||
_stanza.value = 0
|
||||
}
|
||||
loadNewSong("whawyd3.mid")
|
||||
}
|
||||
|
||||
fun setSongKey(theSongKey: String) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import org.apache.pdfbox.io.RandomAccessReadBuffer
|
|||
import org.apache.pdfbox.pdmodel.PDDocument
|
||||
import org.apache.pdfbox.pdmodel.PDPage
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font
|
||||
import org.apache.pdfbox.util.Matrix
|
||||
import java.awt.Color
|
||||
|
|
@ -388,7 +387,6 @@ private fun drawTUOUnderlines(
|
|||
}
|
||||
|
||||
// ── Y ── identique à Compose : voiceNumber * totalH / nbNotes → bas de la ligne de voix
|
||||
// En PDFBox Y croît vers le haut donc on descend de 2f sous la baseline
|
||||
// val underlineY = y - voiceLineIndex * noteLineH - 2f
|
||||
|
||||
if (xEnd > xStart) {
|
||||
|
|
@ -450,12 +448,17 @@ private fun drawNoteWithSubscript(
|
|||
val ch = text[i]
|
||||
val str = ch.toString()
|
||||
cs.beginText()
|
||||
when(ch) {
|
||||
'₁', '₂', '₃', '¹', '²', '³' -> {
|
||||
cs.setFont(font, fontSize * 0.6f)
|
||||
} else -> {
|
||||
cs.setFont(font, fontSize)
|
||||
}
|
||||
}
|
||||
cs.newLineAtOffset(curX, y)
|
||||
cs.showText(str.sanitize())
|
||||
cs.endText()
|
||||
curX += font.getStringWidth(str.sanitize()) / 1000f * fontSize
|
||||
// }
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
|
@ -465,7 +468,10 @@ private fun drawHeader(
|
|||
cs: PDPageContentStream,
|
||||
noteFont: PDType0Font,
|
||||
lyricFont: PDType0Font?,
|
||||
pageWidth: Float, pageHeight: Float, marginX: Float, usableWidth: Float,
|
||||
bolderFont: PDType0Font?,
|
||||
emmentalerfFont: PDType0Font?,
|
||||
pageWidth: Float, pageHeight: Float, marginX: Float, marginY: Float,
|
||||
usableWidth: Float,
|
||||
songTitle: String, songAuthor: String, songRhythm: String,
|
||||
songComposer: String, songKey: String, measure: String,
|
||||
stanza: Int, nbStanza: Int
|
||||
|
|
@ -473,20 +479,28 @@ private fun drawHeader(
|
|||
val titleFont = lyricFont ?: noteFont
|
||||
val normalFont = lyricFont ?: noteFont
|
||||
val smallFont = noteFont
|
||||
val emmentalerFont = emmentalerfFont ?: noteFont
|
||||
val boldFont = bolderFont ?: noteFont
|
||||
|
||||
fun tw(f: PDType0Font, sz: Float, t: String) =
|
||||
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
|
||||
|
||||
// ── Y calculés depuis marginY au lieu de valeurs fixes ────────────────────
|
||||
val titleY = pageHeight - marginY - 5f
|
||||
val line2Y = pageHeight - marginY - 21f
|
||||
val line3Y = pageHeight - marginY - 33f
|
||||
val sepY = line3Y - 5f
|
||||
|
||||
// Ligne 1 — Titre centré
|
||||
val titleSize = 15f
|
||||
val titleW = tw(titleFont, titleSize, songTitle)
|
||||
cs.beginText(); cs.setFont(titleFont, titleSize)
|
||||
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, pageHeight - 48f)
|
||||
cs.showText(songTitle.sanitize());
|
||||
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, titleY)
|
||||
cs.showText(songTitle.sanitize())
|
||||
cs.endText()
|
||||
|
||||
// Ligne 2 — Author | Composer (droite)
|
||||
val line2Y = pageHeight - 64f; val sz2 = 9f
|
||||
val sz2 = 9f
|
||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
|
||||
|
||||
|
|
@ -497,35 +511,71 @@ private fun drawHeader(
|
|||
cs.showText(songComposer.sanitize());
|
||||
cs.endText()
|
||||
|
||||
// Ligne 3 — Key | Measure | Rythm Stanza (droite)
|
||||
val line3Y = pageHeight - 76f; val sz3 = 8f
|
||||
val keyTxt = "Dô dia ${songKey.sanitize()}"
|
||||
// Ligne 3 — Key | Measure | Rhythm
|
||||
val sz3 = 10f
|
||||
val keyTxt = "Dô dia "
|
||||
cs.beginText();
|
||||
cs.setFont(smallFont, sz3)
|
||||
cs.newLineAtOffset(marginX, line3Y); cs.showText(keyTxt);
|
||||
cs.newLineAtOffset(marginX, line3Y);
|
||||
cs.showText(keyTxt);
|
||||
cs.endText()
|
||||
|
||||
val keyW = tw(smallFont, sz3, keyTxt)
|
||||
val measureTxt = "${measure.sanitize()}"
|
||||
val keyW_2 = tw(smallFont, sz3, keyTxt)
|
||||
val keyTxt2 = "${songKey.sanitize()}"
|
||||
cs.beginText(); cs.setFont(boldFont, sz3+2f)
|
||||
cs.newLineAtOffset(marginX + keyW_2 + 3f, line3Y);
|
||||
cs.showText(keyTxt2);
|
||||
cs.endText()
|
||||
|
||||
val keyW = tw(boldFont, sz3, keyTxt+keyTxt2) + 10f
|
||||
val sz32 = 14f
|
||||
val splitMeaRgx = """(\d+/\d+)\s+(.*)""".toRegex()
|
||||
val matchRes = splitMeaRgx.matchEntire(measure)
|
||||
var timeSignature = ""
|
||||
var titleSignature = ""
|
||||
if(matchRes != null) {
|
||||
timeSignature = matchRes.groupValues[1]
|
||||
titleSignature = matchRes.groupValues[2]
|
||||
}
|
||||
val timesig = timeSignature.toCharArray()
|
||||
|
||||
var ksW = keyW
|
||||
timesig.forEach { c ->
|
||||
cs.beginText();
|
||||
if(c == '/') {
|
||||
cs.setFont(normalFont, sz32)
|
||||
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||
ksW += tw(normalFont, sz32, c.toString())
|
||||
} else {
|
||||
cs.setFont(emmentalerFont, sz32)
|
||||
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||
ksW += tw(emmentalerFont, sz32, c.toString())
|
||||
}
|
||||
cs.showText(c.toString());
|
||||
cs.endText()
|
||||
}
|
||||
cs.beginText();
|
||||
cs.setFont(smallFont, sz3)
|
||||
cs.newLineAtOffset(marginX + keyW + 10f, line3Y)
|
||||
cs.showText("${measure.sanitize()}");
|
||||
cs.newLineAtOffset(marginX + ksW + 15f, line3Y)
|
||||
cs.showText("${titleSignature}");
|
||||
cs.endText()
|
||||
|
||||
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
|
||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||
cs.beginText();
|
||||
cs.setFont(normalFont, sz2)
|
||||
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
|
||||
cs.showText(songRhythm.sanitize()); cs.endText()
|
||||
|
||||
cs.showText(songRhythm.sanitize());
|
||||
cs.endText()
|
||||
|
||||
// Ligne de séparation
|
||||
cs.setStrokingColor(0.6f, 0.6f, 0.6f); cs.setLineWidth(0.4f)
|
||||
cs.moveTo(marginX, line3Y - 5f); cs.lineTo(marginX + usableWidth, line3Y - 5f); cs.stroke()
|
||||
cs.setStrokingColor(0.6f, 0.6f, 0.6f);
|
||||
cs.setLineWidth(0.4f)
|
||||
cs.moveTo(marginX, sepY);
|
||||
cs.lineTo(marginX + usableWidth, sepY);
|
||||
cs.stroke()
|
||||
}
|
||||
|
||||
|
||||
|
||||
actual fun rememberPdfExportAction(
|
||||
scope: CoroutineScope,
|
||||
fileRepository: FileRepository,
|
||||
|
|
@ -538,13 +588,14 @@ actual fun rememberPdfExportAction(
|
|||
songAut: String,
|
||||
songComp: String,
|
||||
songRythm: String
|
||||
): () -> Unit {
|
||||
return {
|
||||
): (PrintSettings) -> Unit {
|
||||
return { settings: PrintSettings ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
// 1. Générer le PDF en mémoire (ByteArray)
|
||||
val (pdfBytes, computedFileName) = generatePdfToBytes(
|
||||
gridData, songTitle, measure, stanza,
|
||||
nbStanza, songKey, songAut, songComp, songRythm
|
||||
nbStanza, songKey, songAut, songComp, songRythm,
|
||||
settings
|
||||
)
|
||||
val chosenPath = withContext(Dispatchers.Main) {
|
||||
fileRepository.pickSavePath(computedFileName)
|
||||
|
|
@ -565,7 +616,8 @@ private suspend fun generatePdfToBytes(
|
|||
songKey: String,
|
||||
songAut: String,
|
||||
songComp: String,
|
||||
songRythm: String
|
||||
songRythm: String,
|
||||
settings: PrintSettings = defaultPrintSettings()
|
||||
): Pair<ByteArray, String> {
|
||||
val tuoList = gridData.tuoList.drop(1)
|
||||
|
||||
|
|
@ -578,20 +630,21 @@ private suspend fun generatePdfToBytes(
|
|||
.toList()
|
||||
}
|
||||
|
||||
val markerHeight = 16f
|
||||
val noteFontSize = 12f
|
||||
val lyricFontSize = 12.5f
|
||||
val noteLineH = 17f // espace entre 2 voix
|
||||
val lyricLineH = 15f // espace entre lignes de lyrics
|
||||
val extraSylLineH = 15f // espace entre syl0 et syl1+ (cas zero spacing)
|
||||
val adjustedMargin = 14f // espace entre syl0 du bloc de notes
|
||||
val interRowGap = 22f // espace entre deux portées
|
||||
val marginX = 42.5f
|
||||
val marginY = 42.5f
|
||||
val headerH = 55f // hauteur réservée au header
|
||||
val pageWidth = PDRectangle.A4.width
|
||||
val pageHeight = PDRectangle.A4.height
|
||||
val markerHeight = settings. markerHeight
|
||||
val noteFontSize = settings.noteFontSize
|
||||
val lyricFontSize = settings.lyricFontSize
|
||||
val noteLineH = settings.noteLineH
|
||||
val lyricLineH = settings.lyricLineH
|
||||
val extraSylLineH = settings.extraSylLineH
|
||||
val adjustedMargin = settings.adjustedMargin
|
||||
val interRowGap = settings.interRowGap
|
||||
val marginX = settings.marginX
|
||||
val marginY = settings.marginY
|
||||
val headerH = settings.headerH
|
||||
val pageWidth = settings.pageWidth
|
||||
val pageHeight = settings.pageHeight
|
||||
val usableWidth = pageWidth - 2 * marginX
|
||||
val customNbGrid = settings.nbGridOnMeasure
|
||||
|
||||
// ── Polices ────────────────────────────────────────────────────
|
||||
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
|
||||
|
|
@ -614,17 +667,20 @@ private suspend fun generatePdfToBytes(
|
|||
)
|
||||
// ── Calcul de la largeur optimale de colonne (= bestTUOWidth) ──
|
||||
val bestColW = bestColWidthPt(
|
||||
/*tuoList, customFont,
|
||||
if (lyricFont is PDType0Font) lyricFont else customFont,
|
||||
noteFontSize, lyricFontSize, stanza*/
|
||||
tuoList,
|
||||
customFont,
|
||||
noteFontSize,
|
||||
lyricFont,
|
||||
lyricFontSize
|
||||
)
|
||||
val colCount = ((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
||||
val colWidth = bestColW
|
||||
val colCount = if (customNbGrid == 0) {
|
||||
((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
||||
} else {
|
||||
customNbGrid
|
||||
}
|
||||
val finalColWidth = usableWidth / colCount
|
||||
//println("ici Colc est $colCount")
|
||||
val colWidth = finalColWidth
|
||||
|
||||
|
||||
val maxSylsSize = tuoList.maxOfOrNull { tuo -> // lyrics Alt
|
||||
|
|
@ -635,7 +691,7 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
// ── Fonctions de dessin ────────────────────────────────────
|
||||
fun newPage(): PDPageContentStream {
|
||||
val page = PDPage(PDRectangle.A4)
|
||||
val page = PDPage(settings.pdRectangle)
|
||||
doc.addPage(page)
|
||||
val cs = PDPageContentStream(doc, page)
|
||||
|
||||
|
|
@ -832,8 +888,8 @@ private suspend fun generatePdfToBytes(
|
|||
var y = pageHeight - marginY - headerH
|
||||
|
||||
drawHeader(
|
||||
cs, customFont, lyricFont as? PDType0Font,
|
||||
pageWidth, pageHeight, marginX, usableWidth,
|
||||
cs, customFont, lyricFont as? PDType0Font, markerBoldFont, markerEmmetFont,
|
||||
pageWidth, pageHeight, marginX, marginY, usableWidth,
|
||||
songTitle = songTitle,
|
||||
songAuthor = songAut,
|
||||
songRhythm = songRythm,
|
||||
|
|
@ -844,9 +900,18 @@ private suspend fun generatePdfToBytes(
|
|||
nbStanza = nbStanza
|
||||
)
|
||||
|
||||
val rows = tuoList.chunked(colCount)
|
||||
val allRows = tuoList.chunked(colCount)
|
||||
// si dernier portée n'a qu'une séparateur pas de note on dessine sur l'avant dernier
|
||||
val hasTrailingBarlineRow = allRows.size > 1 &&
|
||||
allRows.last().size == 1 &&
|
||||
allRows.last().first().noteAsMultiString().trim().isEmpty() &&
|
||||
allRows.last().first().sep0 in listOf("|", "/")
|
||||
|
||||
val rows = if (hasTrailingBarlineRow) allRows.dropLast(1) else allRows
|
||||
|
||||
rows.forEachIndexed { rowIdx, rowTuos ->
|
||||
// println("Portée n°: $rowIdx: && rs ${rows.size}")
|
||||
val isLastVisualRow = rowIdx == rows.size - 1
|
||||
val maxNoteLines = (rowTuos.maxOfOrNull { it.noteAsMultiString().split("\n").size } ?: 1) + 1
|
||||
val maxLyricLines = rowTuos.maxOfOrNull { it.getSingleSyllable(stanza).size } ?: 0
|
||||
|
||||
|
|
@ -871,13 +936,11 @@ private suspend fun generatePdfToBytes(
|
|||
// Point de départ des notes (en tenant compte des marqueurs)
|
||||
val notesStartY = if (rowHasMarkers) y - markerHeight else y
|
||||
|
||||
// On regarde sur toute la ligne quelles voix (0,1,2,3) ont des notes
|
||||
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
|
||||
fun singingVoiceIndices(tuo: TimeUnitObject): List<Int> {
|
||||
return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line ->
|
||||
val clean = line.trim()
|
||||
// On considère que ça "chante" si ce n'est pas vide,
|
||||
// pas un simple tiret de prolongation, et que ça contient une note (do, re, mi...)
|
||||
|
||||
val sings = clean.isNotBlank()
|
||||
&& !clean.all { it == '-' || it.isWhitespace() }
|
||||
&& clean != "—" && clean != "–"
|
||||
|
|
@ -901,7 +964,6 @@ private suspend fun generatePdfToBytes(
|
|||
val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||
|
||||
// NOUVEAU : Est-ce qu'une autre ligne (syl1, syl2...) contient un préfixe "1." ?
|
||||
val uneAutreVoixPrendLeSoprano = (1..4).any { vIdx ->
|
||||
val s = rawSyls.getOrNull(vIdx) ?: ""
|
||||
s.contains("1.") && REGEX_HAS_LYRIC.containsMatchIn(s.replace(REGEX_CLEAN, ""))
|
||||
|
|
@ -924,10 +986,10 @@ private suspend fun generatePdfToBytes(
|
|||
val prefixInts = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
||||
|
||||
if (prefixInts.isNotEmpty()) {
|
||||
val hasTrioATB = prefixInts.size == 3 && prefixInts.containsAll(listOf(2, 3, 4))
|
||||
val hasDuoSA = prefixInts.size == 2 && prefixInts.containsAll(listOf(1, 2))
|
||||
val hasDuoAT = prefixInts.size == 2 && prefixInts.containsAll(listOf(2, 3))
|
||||
val hasDuoTB = prefixInts.size == 2 && prefixInts.containsAll(listOf(3, 4))
|
||||
val hasTrioATB = prefixInts.size == 3 && prefixInts.containsAll(listOf(2, 3, 4))
|
||||
val isUnisson = prefixInts.containsAll(listOf(1, 2, 3, 4))
|
||||
|
||||
// Logique simplifiée pour le calcul des spacings (Duo consécutif)
|
||||
|
|
@ -945,19 +1007,14 @@ private suspend fun generatePdfToBytes(
|
|||
else if (hasDuoSA) {
|
||||
spacings[1] = 1
|
||||
}
|
||||
/*else if (hasDuoAT) {
|
||||
spacings[1] = 0
|
||||
spacings[2] = 1
|
||||
}*/
|
||||
else if (hasDuoTB) {
|
||||
if (syl0HasRealText) { spacings[1] = 1 }
|
||||
spacings[3] = 1
|
||||
}
|
||||
else {
|
||||
// Cas Standard (Individuel)
|
||||
prefixInts.forEach { voiceNum ->
|
||||
when (voiceNum) {
|
||||
1 -> if (syl0HasRealText) spacings[0] = 1
|
||||
1 -> if (syl0HasRealText && prefixInts.contains(1)) spacings[0] = 1
|
||||
2 -> if (!prefixInts.contains(1)) spacings[1] = 1
|
||||
3 -> if (!prefixInts.contains(2)) spacings[2] = 1
|
||||
4 -> if (!prefixInts.contains(3)) spacings[3] = 1
|
||||
|
|
@ -972,7 +1029,7 @@ private suspend fun generatePdfToBytes(
|
|||
val customSpacings = spacings.toList()
|
||||
|
||||
|
||||
/*if (rowIdx == 7 || (rowIdx == 8)) {
|
||||
/*if (rowIdx == 2 || (rowIdx == 2)) {
|
||||
println("╠══════════════════════════════════════════════════════════════╣")
|
||||
println("║ customSpacings calculés : $customSpacings ")
|
||||
println("╚══════════════════════════════════════════════════════════════╝")
|
||||
|
|
@ -1014,7 +1071,6 @@ private suspend fun generatePdfToBytes(
|
|||
}
|
||||
}
|
||||
}
|
||||
println("\n" + "═".repeat(64))
|
||||
}*/
|
||||
|
||||
// println("\n[FIN SCAN ROW $rowIdx]")
|
||||
|
|
@ -1034,10 +1090,10 @@ private suspend fun generatePdfToBytes(
|
|||
drawHairPinForTUO(
|
||||
cs = cs,
|
||||
tuo = tuo,
|
||||
gridColumnCount = colCount, // tu as déjà colCount
|
||||
gridColumnCount = colCount,
|
||||
colX = x,
|
||||
colWidth = colWidth,
|
||||
markerY = y, // ton y actuel
|
||||
markerY = y,
|
||||
noteLineH = noteLineH
|
||||
)
|
||||
}
|
||||
|
|
@ -1093,6 +1149,27 @@ private suspend fun generatePdfToBytes(
|
|||
rowTuos.forEachIndexed { colIdx, tuo ->
|
||||
val x = marginX + colIdx * colWidth
|
||||
|
||||
// on dessine à droite
|
||||
if (hasTrailingBarlineRow && isLastVisualRow && colIdx == rowTuos.size - 1) {
|
||||
val finalX = x + colWidth
|
||||
val sepToDraw = allRows.last().first().sep0
|
||||
|
||||
cs.setStrokingColor(0f, 0f, 0f)
|
||||
cs.setLineWidth(0.8f)
|
||||
|
||||
val sepW = textWidth(customFont, noteFontSize, "|")
|
||||
cs.moveTo(finalX - sepW, lineTop)
|
||||
cs.lineTo(finalX - sepW, lineBottom)
|
||||
cs.stroke()
|
||||
|
||||
if (sepToDraw == "/") {
|
||||
cs.moveTo(finalX - (sepW * 0.5f), lineTop)
|
||||
cs.lineTo(finalX - (sepW * 0.5f), lineBottom)
|
||||
cs.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val sepW = textWidth(customFont, noteFontSize, "|")
|
||||
val noteX = when (tuo.sep0) {
|
||||
"/", "|" -> {
|
||||
|
|
@ -1110,8 +1187,7 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
|
||||
val noteLines = tuo.noteAsMultiString().split("\n")
|
||||
|
||||
// println("\n=== ROW0 col[$colIdx] sep0='${tuo.sep0}' noteX=$noteX ===")
|
||||
// println("\n=== ROW0 col[$colIdx] sep0='${tuo.sep0}' note=${noteLines[0]} ===")
|
||||
// println(" noteAsMultiString raw = ${tuo.noteAsMultiString().replace("\n", "\\n")}")
|
||||
// println(" noteLines.size = ${noteLines.size}")
|
||||
noteLines.forEachIndexed { li, line ->
|
||||
|
|
@ -1201,6 +1277,15 @@ private suspend fun generatePdfToBytes(
|
|||
// println("║ lyricLineH = $lyricLineH")
|
||||
|
||||
// Numéro de stanza
|
||||
var hasLyrics = false
|
||||
allTemps.forEachIndexed { sIdx, syllables ->
|
||||
syllables.forEachIndexed { lineIdx, syl ->
|
||||
if(syl.isNotEmpty()) hasLyrics=true
|
||||
}
|
||||
}
|
||||
|
||||
// println("hasL? = $hasLyrics")
|
||||
if(hasLyrics) {
|
||||
val stanzaNumTxt = "$currentStanza."
|
||||
val stanzaNumSize = lyricFontSize * 0.85f
|
||||
val stanzaNumFont = if (lyricFont is PDType0Font) lyricFont else customFont
|
||||
|
|
@ -1215,25 +1300,6 @@ private suspend fun generatePdfToBytes(
|
|||
cs.showText(stanzaNumTxt)
|
||||
cs.endText()
|
||||
|
||||
// Syllabes
|
||||
/*allTemps.forEachIndexed { sIdx, syllables ->
|
||||
val x = marginX + sIdx * colWidth
|
||||
val noteX = if (rowTuos.getOrNull(sIdx)?.sep0 in listOf("/", "|")) x + 3f else x
|
||||
|
||||
syllables.forEachIndexed { lineIdx, syl ->
|
||||
val (spacedSyl, alignLeft) = spacedSyllable(
|
||||
syl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize
|
||||
)
|
||||
val lyricX = if (alignLeft) noteX
|
||||
else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
|
||||
|
||||
cs.beginText()
|
||||
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
||||
cs.newLineAtOffset(lyricX, yLyric - lineIdx * lyricLineH)
|
||||
cs.showText(spacedSyl.sanitize())
|
||||
cs.endText()
|
||||
}
|
||||
}*/
|
||||
fun drawInOverrideSlot(
|
||||
cs: PDPageContentStream,
|
||||
targetIdx: Int, // 0=S, 1=A, 2=T, 3=B, ou -1 pour le remplacement direct de syl0
|
||||
|
|
@ -1320,21 +1386,25 @@ private suspend fun generatePdfToBytes(
|
|||
val cleanSyl0 = cleanSyllable(rawSyl0)
|
||||
val cleanSyl = cleanSyllable(rawSyl)
|
||||
|
||||
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||
val syl0LyricsIsFree = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||
|
||||
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
||||
|
||||
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) {
|
||||
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0)
|
||||
if (prefixes.contains(1) && isAllZeroSpacing && syl0LyricsIsFree) {
|
||||
println("DEBUG: Force SYL1 sur ligne principale (Rouge) -> $cleanSyl")
|
||||
|
||||
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
||||
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
||||
|
||||
cs.saveGraphicsState()
|
||||
cs.beginText()
|
||||
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
||||
cs.setNonStrokingColor(Color.RED)
|
||||
cs.newLineAtOffset(lyricX, currentYLyric)
|
||||
cs.showText(spacedSyl.sanitize())
|
||||
cs.endText()
|
||||
cs.restoreGraphicsState()
|
||||
|
||||
} else if (isAllZeroSpacing) {
|
||||
// ── CAS SIMPLE : pas d'espace entre notes → syl1+ descend sous syl0 ──
|
||||
|
|
@ -1359,8 +1429,6 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
val rawSyl0 = syllables.getOrNull(0) ?: ""
|
||||
val cleanSyl0 = cleanSyllable(rawSyl0)
|
||||
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||
|
||||
|
||||
val hasActualText = REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) &&
|
||||
|
|
@ -1374,17 +1442,7 @@ private suspend fun generatePdfToBytes(
|
|||
|
||||
// --- CAS SPÉCIAL : LE PRÉFIXE "1." (SOPRANO) ---
|
||||
if (voiceNum == 1) {
|
||||
// A. PRIORITÉ : Si la ligne principale est libre, on s'installe dessus
|
||||
if (lignePrincipaleEstLibre && !filledSlots.contains(-1)) {
|
||||
drawInOverrideSlot(
|
||||
cs, -1, cleanSyl, sIdx, lineIdx, allTemps, noteX, colWidth,
|
||||
customFont, lyricFontSize, currentYLyric, noteLineH, customSpacings,
|
||||
isDirectY = true // Pose sur yLyric
|
||||
)
|
||||
filledSlots.add(-1)
|
||||
}
|
||||
// B. REPLI : Si la ligne 0 est occupée, on regarde si un espace (1) est ouvert dans cS
|
||||
else if (customSpacings.getOrElse(0) { 0 } == 1 && !filledSlots.contains(0)) {
|
||||
if (customSpacings.getOrElse(0) { 0 } == 1 && !filledSlots.contains(0)) {
|
||||
drawInOverrideSlot(
|
||||
cs,
|
||||
0,
|
||||
|
|
@ -1443,6 +1501,7 @@ private suspend fun generatePdfToBytes(
|
|||
if (bottomOfCurrentStanza < lowestYOfThisRow) {
|
||||
lowestYOfThisRow = bottomOfCurrentStanza
|
||||
}
|
||||
}
|
||||
// val bottomOfStanza = yLyric - (maxLyricLines * lyricLineH)
|
||||
// if (bottomOfStanza < lowestYOfThisRow) lowestYOfThisRow = bottomOfStanza
|
||||
}
|
||||
|
|
@ -1494,7 +1553,7 @@ private suspend fun generatePdfToBytes(
|
|||
// println(" DC[$tuoIdx]: '$dcPart'")
|
||||
}
|
||||
if (tuo.pTemplate.markerToString().contains("DC")) {
|
||||
// println(" 🎯 DC MARQUEUR[$tuoIdx]")
|
||||
// println(" DC MARQUEUR[$tuoIdx]")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1509,15 +1568,14 @@ private suspend fun generatePdfToBytes(
|
|||
val dcPart = syls.drop(1).joinToString(" ") { it.trim() }
|
||||
|
||||
|
||||
// ✅ 2e : NORMAL (si existe)
|
||||
// NORMAL (si existe)
|
||||
if (normal.isNotEmpty()) {
|
||||
result.append(normal).append(" ")
|
||||
// println(" → AJOUT NORMAL[$tuoIdx]: '$normal'")
|
||||
}
|
||||
// ✅ 1er : CROCHET DC (TOUJOURS, même si normal vide)
|
||||
// CROCHET DC (TOUJOURS, même si normal vide)
|
||||
if (dcPart.isNotEmpty()) {
|
||||
result.append("[$dcPart] ")
|
||||
// println(" 🟦 CROCHET DC[$tuoIdx]: '[$dcPart]'")
|
||||
// dcPart
|
||||
result.append("[${cleanSyllable(dcPart)}] ")
|
||||
}
|
||||
|
||||
// 3. DC MARQUEUR (parenthèses)
|
||||
|
|
@ -1739,7 +1797,7 @@ private suspend fun generatePdfToBytes(
|
|||
os.contains("win") -> Runtime.getRuntime().exec(arrayOf("explorer", outputDir))
|
||||
}
|
||||
emmentalerTTF.close()
|
||||
println("✅ PDF : $outputPath")*/
|
||||
println(" PDF : $outputPath")*/
|
||||
val computedFileName = "${cleanTitle}_R_${rStr}_U_${uStr}.pdf"
|
||||
|
||||
// Sérialiser en mémoire
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package mg.dot.feufaro.pdf
|
||||
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle
|
||||
|
||||
actual data class PrintSettings actual constructor(
|
||||
actual val pageSize : PageSize,
|
||||
actual val orientation : PageOrientation,
|
||||
actual val markerHeight : Float,
|
||||
actual val noteFontSize : Float,
|
||||
actual val lyricFontSize : Float,
|
||||
actual val noteLineH : Float,
|
||||
actual val lyricLineH : Float,
|
||||
actual val extraSylLineH : Float,
|
||||
actual val adjustedMargin : Float,
|
||||
actual val interRowGap : Float,
|
||||
actual val marginX : Float,
|
||||
actual val marginY : Float,
|
||||
actual val headerH : Float,
|
||||
actual val nbGridOnMeasure: Int
|
||||
) {
|
||||
val pdRectangle: PDRectangle
|
||||
get() {
|
||||
val base = when (pageSize) {
|
||||
PageSize.A4 -> PDRectangle.A4
|
||||
PageSize.A3 -> PDRectangle.A3
|
||||
PageSize.LETTER -> PDRectangle.LETTER
|
||||
}
|
||||
return if (orientation == PageOrientation.PORTRAIT) base
|
||||
else PDRectangle(base.height, base.width)
|
||||
}
|
||||
|
||||
actual val pageWidth : Float get() = pdRectangle.width
|
||||
actual val pageHeight : Float get() = pdRectangle.height
|
||||
}
|
||||
|
||||
actual fun defaultPrintSettings(): PrintSettings = PrintSettings(
|
||||
pageSize = PageSize.A4,
|
||||
orientation = PageOrientation.PORTRAIT,
|
||||
markerHeight = 16f,
|
||||
noteFontSize = 12f,
|
||||
lyricFontSize = 12.5f,
|
||||
noteLineH = 17f,
|
||||
lyricLineH = 15f,
|
||||
extraSylLineH = 15f,
|
||||
adjustedMargin = 14f,
|
||||
interRowGap = 22f,
|
||||
marginX = 42.5f,
|
||||
marginY = 42.5f,
|
||||
headerH = 55f,
|
||||
nbGridOnMeasure = 0
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue