Generate PDF partition - desktop&android

This commit is contained in:
hasinarak3@gmail.com 2026-04-03 11:30:19 +03:00
parent fa28916eca
commit e0b98c5758
22 changed files with 2981 additions and 10 deletions

View file

@ -53,6 +53,7 @@ kotlin {
implementation(libs.koin.androidx.compose) implementation(libs.koin.androidx.compose)
implementation("com.google.zxing:core:3.5.4") implementation("com.google.zxing:core:3.5.4")
implementation("com.github.billthefarmer:mididriver:1.25") implementation("com.github.billthefarmer:mididriver:1.25")
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
} }
commonMain.dependencies { commonMain.dependencies {
// implementation(compose.components.resources) // implementation(compose.components.resources)
@ -86,6 +87,7 @@ kotlin {
implementation(libs.kotlinx.coroutinesSwing) implementation(libs.kotlinx.coroutinesSwing)
implementation("com.google.zxing:core:3.5.4") implementation("com.google.zxing:core:3.5.4")
implementation("com.google.zxing:javase:3.5.4") implementation("com.google.zxing:javase:3.5.4")
implementation("org.apache.pdfbox:pdfbox:3.0.7")
} }
} }
targets.all { targets.all {

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application <application
android:name=".AndroidApp" android:name=".AndroidApp"
android:allowBackup="true" android:allowBackup="true"
@ -18,6 +21,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="mg.dot.feufaro.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View file

@ -1,6 +1,7 @@
package mg.dot.feufaro package mg.dot.feufaro
import android.app.Application import android.app.Application
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import org.koin.core.context.GlobalContext.startKoin import org.koin.core.context.GlobalContext.startKoin
import mg.dot.feufaro.di.commonModule // Importez votre module commun import mg.dot.feufaro.di.commonModule // Importez votre module commun
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@ -12,6 +13,7 @@ import mg.dot.feufaro.di.platformModule
class AndroidApp: Application() { class AndroidApp: Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
PDFBoxResourceLoader.init(this)
startKoin { startKoin {
// Log Koin messages (INFO pour le développement, ERROR pour la production) // Log Koin messages (INFO pour le développement, ERROR pour la production)
androidLogger(Level.INFO) androidLogger(Level.INFO)

View file

@ -1,7 +1,11 @@
package mg.dot.feufaro package mg.dot.feufaro
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Environment
import android.widget.Toast
import androidx.core.content.FileProvider
import feufaro.composeapp.generated.resources.Res import feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -62,10 +66,10 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
} }
} }
override fun getFileName(shortName: String): String { /*override fun getFileName(shortName: String): String {
val folder = context.getExternalFilesDir(null) val folder = context.getExternalFilesDir(null)
return "$folder/$shortName" return "$folder/$shortName"
} }*/
// Dans androidMain / AndroidFileRepository.kt // Dans androidMain / AndroidFileRepository.kt
/*override suspend fun readFileBytes(filePath: String): ByteArray = withContext(Dispatchers.IO) { /*override suspend fun readFileBytes(filePath: String): ByteArray = withContext(Dispatchers.IO) {
val folder = context.getExternalFilesDir(null) val folder = context.getExternalFilesDir(null)
@ -73,5 +77,59 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
if (!file.exists()) throw IOException("File not found: ${file.absolutePath}") if (!file.exists()) throw IOException("File not found: ${file.absolutePath}")
file.readBytes() file.readBytes()
}*/ }*/
override suspend fun saveLocalFile(filePath: String, data: ByteArray) {
val cleanPath = if (filePath.contains("/storage/emulated/0/")) {
filePath.substring(filePath.indexOf("/storage/emulated/0/"))
} else {
filePath
}
val file = File(cleanPath)
try {
file.parentFile?.mkdirs()
file.writeBytes(data)
println("Fichier sauvegardé avec succès dans : ${file.absolutePath}")
openPdfFile(context = context, filePath = file.absolutePath)
} catch (e: Exception) {
e.printStackTrace()
throw e
}
}
override fun getFileName(shortName: String): String {
return File(context.filesDir, shortName).absolutePath
}
override fun pickSavePath(defaultName: String): String? {
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
return File(downloadDir, defaultName).absolutePath
}
suspend fun openPdfFile(context: Context, filePath: String) {
val file = File(filePath)
if (!file.exists()) return
// 1. Transformer le File en Uri sécurisée
val uri: Uri = FileProvider.getUriForFile(
context,
"mg.dot.feufaro.fileprovider",
file
)
// uvrir un PDF
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/pdf")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // Important !
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
withContext(Dispatchers.Main) {
Toast.makeText(context.applicationContext, "Fichier sauvegardé avec succès", Toast.LENGTH_LONG).show()
}
context.startActivity(intent)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(context.applicationContext, "Aucun lecteur PDF trouvé", Toast.LENGTH_SHORT).show()
}
}
}
} }

View file

@ -1,19 +1,26 @@
package mg.dot.feufaro package mg.dot.feufaro
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import android.view.WindowManager
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import org.koin.androidx.compose.KoinAndroidContext import org.koin.androidx.compose.KoinAndroidContext
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
1
)
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setFilePickerActivity(this) setFilePickerActivity(this)

File diff suppressed because it is too large Load diff

View file

@ -15,8 +15,10 @@ interface FileRepository {
suspend fun getOutputStream(filePath: String): OutputStream suspend fun getOutputStream(filePath: String): OutputStream
//Lire le dernier dossier d'importation //Lire le dernier dossier d'importation
suspend fun saveFile(filePath: String, data: ByteArray) suspend fun saveFile(filePath: String, data: ByteArray)
suspend fun saveLocalFile(filePath: String, data: ByteArray)
fun getFileName(shortName: String) : String fun getFileName(shortName: String) : String
// suspend fun readFileBytes(filePath: String): ByteArray // suspend fun readFileBytes(filePath: String): ByteArray
fun pickSavePath(defaultName: String): String?
} }
// This is just a regular class that implements the common 'FileRepository' interface. // This is just a regular class that implements the common 'FileRepository' interface.

View file

@ -15,7 +15,7 @@ val commonModule = module {
single { SharedScreenModel(fileRepository = get()) } single { SharedScreenModel(fileRepository = get()) }
single { Solfa(get(), get()) } single { Solfa(get(), get()) }
single { SolfaScreenModel(get()) } single { SolfaScreenModel(get(), get()) }
single { MusicXML(get()) } single { MusicXML(get()) }
} }

View file

@ -0,0 +1,19 @@
package mg.dot.feufaro.pdf
import kotlinx.coroutines.CoroutineScope
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.data.GridTUOData
expect fun rememberPdfExportAction(
scope: CoroutineScope,
fileRepository: FileRepository,
gridData: GridTUOData,
songTitle: String,
measure: String,
stanza: Int,
nbStanza: Int,
songKey: String,
songAut: String,
songComp: String,
songRythm: String
): () -> Unit

View file

@ -746,6 +746,15 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val override = O.getOrElse(overrideNumber) { null } val override = O.getOrElse(overrideNumber) { null }
override?.forEachIndexed { index, value -> override?.forEachIndexed { index, value ->
val matchResult = Regex("v(\\d+):(.*)").find(value) val matchResult = Regex("v(\\d+):(.*)").find(value)
// 1. Extraire le numéro de voix (ex: "2")
val voiceNum = matchResult?.groupValues?.get(1) ?: ""
val prefix = if (voiceNum.isNotEmpty()) {
// .map transforme chaque caractère '3', '4' en "3.", "4."
// .joinToString("") les colle ensemble -> "3.4."
voiceNum.map { "$it." }.joinToString("")
} else {
""
}
val overrideString = when (smartLyricsType) { val overrideString = when (smartLyricsType) {
"E" -> smartELyrics(matchResult?.groupValues[2] ?: value) "E" -> smartELyrics(matchResult?.groupValues[2] ?: value)
"Y" -> smartYLyrics(matchResult?.groupValues[2] ?: value) "Y" -> smartYLyrics(matchResult?.groupValues[2] ?: value)
@ -753,7 +762,11 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
} }
for (iN in (1..9)) { for (iN in (1..9)) {
if (matchResult?.groupValues[1]?.contains(iN.digitToChar()) ?: false) { if (matchResult?.groupValues[1]?.contains(iN.digitToChar()) ?: false) {
val overrideSyllabus = overrideString.split(Regex("[_/]")).iterator() val overrideSyllabus = overrideString.split(Regex("[_/]"))
.map { syllable ->
if (syllable.isNotBlank()) "$prefix$syllable" else syllable
}
.iterator()
overrideIterator[iN] = overrideSyllabus overrideIterator[iN] = overrideSyllabus
} }
} }

View file

@ -755,6 +755,12 @@ fun LazyVerticalGridTUO(
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
val columnWidthDp = gridWidthDp / gridColumnCount val columnWidthDp = gridWidthDp / gridColumnCount
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
val cleanAllTemps = allTemps.map { column ->
column.map { syllable ->
syllable.replace(REGEX_CLEAN_PREFIX, "").trim()
}
}
allTemps.forEachIndexed { syls_i, syllables -> allTemps.forEachIndexed { syls_i, syllables ->
Column( Column(
modifier = Modifier modifier = Modifier
@ -763,7 +769,9 @@ fun LazyVerticalGridTUO(
val noteWidth = columnWidthDp val noteWidth = columnWidthDp
val density = LocalDensity.current val density = LocalDensity.current
syllables.joinToString { " " } syllables.joinToString { " " }
syllables.mapIndexed { index, syl ->
val cleanSyllables = cleanAllTemps[syls_i]
cleanSyllables.forEachIndexed { index, syl ->
var textWidth by remember { mutableStateOf(0.dp) } var textWidth by remember { mutableStateOf(0.dp) }
@ -777,7 +785,7 @@ fun LazyVerticalGridTUO(
val isTooLong = textWidthDp > containerWidthDp val isTooLong = textWidthDp > containerWidthDp
val spacer = makeSpaceBetweenSyllables(textMeasurer, columnWidthDp) val spacer = makeSpaceBetweenSyllables(textMeasurer, columnWidthDp)
val (dynamicSpaceSyl, alignmentText) = spacer(syl, allTemps, syls_i, index) val (dynamicSpaceSyl, alignmentText) = spacer(syl, cleanAllTemps, syls_i, index)
Text( Text(
text = dynamicSpaceSyl, text = dynamicSpaceSyl,
modifier = Modifier modifier = Modifier

View file

@ -22,9 +22,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.data.getDrawerItems
import mg.dot.feufaro.pdf.rememberPdfExportAction
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
@ -47,6 +49,11 @@ fun MainScreenWithDrawer(
val filteredSongs by sharedScreenModel.filteredSongs.collectAsState() val filteredSongs by sharedScreenModel.filteredSongs.collectAsState()
val songKey = sharedScreenModel.songKey.collectAsState().value val songKey = sharedScreenModel.songKey.collectAsState().value
val measure = sharedScreenModel.measure.collectAsState().value val measure = sharedScreenModel.measure.collectAsState().value
val stanza = sharedScreenModel.stanza.collectAsState().value
val nbStanzas = sharedScreenModel.nbStanzas.collectAsState().value
val songAuthor = sharedScreenModel.songAuthor.collectAsState().value
val songComposer = sharedScreenModel.songComposer.collectAsState().value
val songRhythm = sharedScreenModel.songRhythm.collectAsState().value
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val currentActivePath = Solfa.currentFile val currentActivePath = Solfa.currentFile
@ -103,6 +110,20 @@ LaunchedEffect(isPlay, isPos) {
} }
) )
}, content = { }, content = {
val scope = rememberCoroutineScope()
val pdfExportAction: () -> Unit = rememberPdfExportAction(
scope = scope,
fileRepository = solfaScreenModel.fileRepository,
gridData = sharedScreenModel.currentGridData,
songTitle = songTitle,
measure = measure,
stanza = stanza,
nbStanza = nbStanzas,
songKey = songKey,
songAut = songAuthor,
songComp = songComposer,
songRythm = songRhythm
)
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
TopAppBar( TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = { modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
@ -176,7 +197,9 @@ LaunchedEffect(isPlay, isPos) {
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 } exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = {}, modifier = Modifier.alpha(0.45f) onClick = {
pdfExportAction()
}, modifier = Modifier.alpha(0.45f)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Print, imageVector = Icons.Filled.Print,
@ -243,12 +266,12 @@ LaunchedEffect(isPlay, isPos) {
delay(100) delay(100)
sharedScreenModel.setDragging(false) sharedScreenModel.setDragging(false)
} }
println("DrawerUI:335: mihetsika $newPos") //println("DrawerUI:335: mihetsika $newPos")
}, },
mediaPlayer = player, mediaPlayer = player,
onVolumeChange = { newVolume -> onVolumeChange = { newVolume ->
sharedScreenModel.setVolume(newVolume) sharedScreenModel.setVolume(newVolume)
println("Changement volume $newVolume -l $volumelevel") // println("Changement volume $newVolume -l $volumelevel")
}, },
onVoiceVolumeChange = { index, volume -> onVoiceVolumeChange = { index, volume ->
player?.updateVoiceVolume(index, volume) player?.updateVoiceVolume(index, volume)

View file

@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import mg.dot.feufaro.FileRepository import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.data.DrawerItem import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.data.getDrawerItems import mg.dot.feufaro.data.getDrawerItems
import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.solfa.TimeUnitObject
import mg.dot.feufaro.midi.FMediaPlayer import mg.dot.feufaro.midi.FMediaPlayer
@ -163,6 +164,12 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
private val _activeIndex = MutableStateFlow(-1) private val _activeIndex = MutableStateFlow(-1)
val activeIndex: StateFlow<Int> = _activeIndex.asStateFlow() val activeIndex: StateFlow<Int> = _activeIndex.asStateFlow()
val currentGridData: GridTUOData
get() = GridTUOData(
tuoList = _tuoList.value,
measure = _measure.value,
stanza = _stanza.value
)
fun updateActiveIndex(currentPosMs: Long) { fun updateActiveIndex(currentPosMs: Long) {
val currentPosMicros = currentPosMs * 1000 val currentPosMicros = currentPosMs * 1000
val index = _tuoTimestamps.value.indexOfLast { it <= currentPosMicros } val index = _tuoTimestamps.value.indexOfLast { it <= currentPosMicros }

View file

@ -1,9 +1,11 @@
package mg.dot.feufaro.viewmodel package mg.dot.feufaro.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
class SolfaScreenModel( class SolfaScreenModel(
val fileRepository: FileRepository,
private val solfa: Solfa private val solfa: Solfa
) : ScreenModel { ) : ScreenModel {
init {} init {}

View file

@ -4,6 +4,8 @@ import feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mg.dot.feufaro.FileRepository import mg.dot.feufaro.FileRepository
import java.awt.FileDialog
import java.awt.Frame
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@ -46,9 +48,29 @@ class DesktopFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE com
val file = File("$userHome/$filePath") val file = File("$userHome/$filePath")
file.writeBytes(data) // Extension Kotlin très efficace file.writeBytes(data) // Extension Kotlin très efficace
} }
override suspend fun saveLocalFile(filePath: String, data: ByteArray) {
val file = if (filePath.startsWith("/")) {
File(filePath) // chemin absolu → utilisé directement
} else {
File(System.getProperty("user.home"), filePath) // chemin relatif → préfixé
}
file.parentFile?.mkdirs() // crée les dossiers parents si nécessaire
file.writeBytes(data) // Extension Kotlin très efficace
}
override fun getFileName(shortName: String): String { override fun getFileName(shortName: String): String {
val userHome = System.getProperty("user.home") val userHome = System.getProperty("user.home")
return "$userHome/$shortName" return "$userHome/$shortName"
} }
override fun pickSavePath(defaultName: String): String? {
val dialog = FileDialog(null as Frame?, "Exporter en PDF", FileDialog.SAVE)
dialog.directory = System.getProperty("user.home")
dialog.file = defaultName
dialog.isVisible = true
return if (dialog.directory != null && dialog.file != null) {
File(dialog.directory, dialog.file).absolutePath
} else null
}
} }

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://jitpack.io") }
} }
} }