Generate PDF partition - desktop&android
This commit is contained in:
parent
fa28916eca
commit
e0b98c5758
22 changed files with 2981 additions and 10 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
1041
composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt
Normal file
1041
composeApp/src/androidMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
1752
composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt
Normal file
1752
composeApp/src/desktopMain/kotlin/mg/dot/feufaro/pdf/PdfExport.kt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -13,6 +13,7 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue