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

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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
android:name=".AndroidApp"
android:allowBackup="true"
@ -18,6 +21,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</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>
</manifest>

View file

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

View file

@ -1,7 +1,11 @@
package mg.dot.feufaro
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.widget.Toast
import androidx.core.content.FileProvider
import feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.Dispatchers
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)
return "$folder/$shortName"
}
}*/
// Dans androidMain / AndroidFileRepository.kt
/*override suspend fun readFileBytes(filePath: String): ByteArray = withContext(Dispatchers.IO) {
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}")
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
import android.Manifest
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat
import android.view.WindowManager
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import org.koin.androidx.compose.KoinAndroidContext
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
1
)
enableEdgeToEdge()
super.onCreate(savedInstanceState)
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
//Lire le dernier dossier d'importation
suspend fun saveFile(filePath: String, data: ByteArray)
suspend fun saveLocalFile(filePath: String, data: ByteArray)
fun getFileName(shortName: String) : String
// suspend fun readFileBytes(filePath: String): ByteArray
fun pickSavePath(defaultName: String): String?
}
// 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 { Solfa(get(), get()) }
single { SolfaScreenModel(get()) }
single { SolfaScreenModel(get(), 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 }
override?.forEachIndexed { index, 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) {
"E" -> smartELyrics(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)) {
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
}
}

View file

@ -755,6 +755,12 @@ fun LazyVerticalGridTUO(
horizontalArrangement = Arrangement.Start
) {
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 ->
Column(
modifier = Modifier
@ -763,7 +769,9 @@ fun LazyVerticalGridTUO(
val noteWidth = columnWidthDp
val density = LocalDensity.current
syllables.joinToString { " " }
syllables.mapIndexed { index, syl ->
val cleanSyllables = cleanAllTemps[syls_i]
cleanSyllables.forEachIndexed { index, syl ->
var textWidth by remember { mutableStateOf(0.dp) }
@ -777,7 +785,7 @@ fun LazyVerticalGridTUO(
val isTooLong = textWidthDp > containerWidthDp
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 = dynamicSpaceSyl,
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.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.rememberPdfExportAction
import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.viewmodel.SolfaScreenModel
@ -47,6 +49,11 @@ fun MainScreenWithDrawer(
val filteredSongs by sharedScreenModel.filteredSongs.collectAsState()
val songKey = sharedScreenModel.songKey.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 currentActivePath = Solfa.currentFile
@ -103,6 +110,20 @@ LaunchedEffect(isPlay, isPos) {
}
)
}, 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 = {
TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
@ -176,7 +197,9 @@ LaunchedEffect(isPlay, isPos) {
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {}, modifier = Modifier.alpha(0.45f)
onClick = {
pdfExportAction()
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.Print,
@ -243,12 +266,12 @@ LaunchedEffect(isPlay, isPos) {
delay(100)
sharedScreenModel.setDragging(false)
}
println("DrawerUI:335: mihetsika $newPos")
//println("DrawerUI:335: mihetsika $newPos")
},
mediaPlayer = player,
onVolumeChange = { newVolume ->
sharedScreenModel.setVolume(newVolume)
println("Changement volume $newVolume -l $volumelevel")
// println("Changement volume $newVolume -l $volumelevel")
},
onVoiceVolumeChange = { index, volume ->
player?.updateVoiceVolume(index, volume)

View file

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

View file

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

View file

@ -4,6 +4,8 @@ import feufaro.composeapp.generated.resources.Res
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mg.dot.feufaro.FileRepository
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -46,9 +48,29 @@ class DesktopFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE com
val file = File("$userHome/$filePath")
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 {
val userHome = System.getProperty("user.home")
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 {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}