diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 8ce504a..5810c90 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -21,6 +21,12 @@
+
+
+
+
+
+
{
- File(filePath).readLines()
+ val file = File(filePath)
+ println("AndroidRead: Tentative sur ${file.absolutePath}")
+ println("AndroidRead: Existe=${file.exists()} | Lisible=${file.canRead()}")
+
+ if (!file.exists()) {
+ throw IOException("Le fichier n'existe pas : $filePath")
+ }
+ file.bufferedReader(Charsets.UTF_8).useLines { it.toList() }
}
}
} catch (e: IOException) {
@@ -77,32 +84,37 @@ 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/"))
+ override suspend fun saveLocalFile(filePath: String, data: ByteArray) = withContext(Dispatchers.IO) {
+ val file = if (filePath.contains("/") ) {
+ File(filePath)
} else {
- filePath
+ File(getAppPublicFolder(), 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)
+ println("Fichier sauvegardé dans : ${file.absolutePath}")
+
+ if (file.extension.lowercase() == "pdf") {
+ openPdfFile(context = context, filePath = file.absolutePath)
+ }
} catch (e: Exception) {
e.printStackTrace()
throw e
}
}
- // Internal files
- /*override fun getFileName(shortName: String): String {
- return File(context.filesDir, shortName).absolutePath
- }*/
+ private fun getAppPublicFolder(): File {
+ val publicDocsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
+ val appDir = File(publicDocsDir, "Feufaro")
+ if (!appDir.exists()) {
+ appDir.mkdirs()
+ }
+ return appDir
+ }
override fun pickSavePath(defaultName: String): String? {
- val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
- return File(downloadDir, defaultName).absolutePath
+ return File(getAppPublicFolder(), defaultName).absolutePath
}
suspend fun openPdfFile(context: Context, filePath: String) {
val file = File(filePath)
@@ -115,10 +127,9 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
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_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt
index cff8293..4462cbb 100644
--- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt
@@ -1,7 +1,13 @@
package mg.dot.feufaro
import android.Manifest
+import android.app.ComponentCaller
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.os.Environment
+import android.util.Base64
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -13,6 +19,8 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import org.koin.androidx.compose.KoinAndroidContext
+import java.io.File
+import java.util.zip.GZIPInputStream
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -26,6 +34,7 @@ class MainActivity : ComponentActivity() {
setFilePickerActivity(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBar()
+ intent?.let { handleDeepLinkIntent(it) }
setContent {
@@ -34,6 +43,93 @@ class MainActivity : ComponentActivity() {
}
}
}
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ handleDeepLinkIntent(intent)
+ }
+ override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
+ super.onNewIntent(intent, caller)
+ setIntent(intent)
+ handleDeepLinkIntent(intent)
+ }
+
+ private fun decodeAndDecompress(base64Data: String): String {
+ return try {
+ val compressedBytes = Base64.decode(base64Data, Base64.URL_SAFE)
+ java.util.zip.GZIPInputStream(compressedBytes.inputStream()).bufferedReader(Charsets.UTF_8).use {
+ it.readText()
+ }
+ } catch (e: Exception) {
+ println("Decompression Error: ${e.message}")
+ base64Data
+ }
+ }
+ private fun handleDeepLinkIntent(intent: Intent) {
+ println("DeepLink: intent reçu = ${intent.data}")
+
+ val data: Uri = intent.data ?: run {
+ println("DeepLink: data est null")
+ return
+ }
+
+ println("DeepLink: scheme=${data.scheme} host=${data.host}")
+
+ if (data.scheme != "feufaro" || data.host != "song") {
+ println("DeepLink: scheme/host incorrect")
+ return
+ }
+
+ val encodedData = data.getQueryParameter("file") ?: run {
+ println("DeepLink: paramètre 'file' introuvable")
+ return
+ }
+ val rawContent = decodeAndDecompress(encodedData)
+
+ println("DeepLink: rawContent = $rawContent")
+
+ val fileName = rawContent
+ .split("|")
+ .firstOrNull { it.startsWith("t:") }
+ ?.removePrefix("t:")
+ ?.trim()
+
+ println("DeepLink: fileName extrait = $fileName")
+
+ if (fileName.isNullOrBlank()) {
+ println("DeepLink: fileName vide ou null")
+ return
+ }
+
+ val docsDir = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOCUMENTS
+ )
+ println("DeepLink: docsDir = ${docsDir.absolutePath}, exists=${docsDir.exists()}")
+
+ val appDir = File(docsDir, "Feufaro")
+ appDir.mkdirs()
+// docsDir.mkdirs()
+
+ val file = File(appDir, "$fileName.txt")
+
+ try {
+ file.writeText(rawContent)
+ println("DeepLink: fichier créé → ${file.absolutePath}")
+ DeepLinkHandler.hasConsumedDeepLink = true
+ DeepLinkHandler.handleSongContent(rawContent, file.absolutePath)
+ } catch (e: Exception) {
+ println("DeepLink: ERREUR écriture fichier → ${e.message}")
+ }
+ }
+
+ private fun extractFileName(content: String): String? {
+ return content
+ .split("|")
+ .firstOrNull { it.startsWith("H:") }
+ ?.removePrefix("H:")
+ ?.trim()
+ }
private fun hideSystemBar() {
// Pour les versions d'Android plus récentes (API 30+)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
diff --git a/composeApp/src/androidMain/res/xml/file_paths.xml b/composeApp/src/androidMain/res/xml/file_paths.xml
new file mode 100644
index 0000000..013ae58
--- /dev/null
+++ b/composeApp/src/androidMain/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt
index 39b373a..d6acdf2 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt
@@ -43,11 +43,17 @@ fun App() {
LaunchedEffect(currentDisplayConfig) {
if (currentDisplayConfig.playlist.isNotEmpty()) {
- try {
+ if (DeepLinkHandler.hasConsumedDeepLink) {
sharedScreenModel.setPlaylist(currentDisplayConfig.playlist)
- solfaScreenModel.reload()
- } catch (e: Exception) {
- println("Error loading playlist : ${e.message}")
+ } else {
+ if(sharedScreenModel.activeFilePath.value.isEmpty()) {
+ try {
+ sharedScreenModel.setPlaylist(currentDisplayConfig.playlist)
+ solfaScreenModel.reload()
+ } catch (e: Exception) {
+ println("Error loading playlist : ${e.message}")
+ }
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt
index 6a8c2e1..cc60e35 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt
@@ -22,6 +22,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -45,6 +47,7 @@ import mg.dot.feufaro.ui.MainScreenWithDrawer
import kotlin.math.roundToInt
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.ui.MyVerticalScrollbar
@@ -56,7 +59,6 @@ object ScreenSolfa : Screen {
override fun Content() {
val solfaScreenModel = koinScreenModel()
var menuPosition by remember { mutableStateOf(Offset.Zero) }
- var showContextualMenu by remember { mutableStateOf(false) }
var gridWidthPx by rememberSaveable { mutableStateOf(0) }
val sharedScreenModel = koinScreenModel()
val tuoList by sharedScreenModel.tuoList.collectAsState()
@@ -69,7 +71,20 @@ object ScreenSolfa : Screen {
var isScanning by remember { mutableStateOf(false) }
var qrCodeResult by remember { mutableStateOf("Aucun code scanné") }
+ LaunchedEffect(Unit) {
+ DeepLinkHandler.onSongReceived = { content, filePath ->
+ println("2DeepLink: LaunchedEffect reçoit → $filePath")
+ sharedScreenModel.reset()
+ solfaScreenModel.loadFromFile(filePath)
+ }
+// DeepLinkHandler.consumePending("")
+ }
+ DisposableEffect(Unit) {
+ onDispose {
+ DeepLinkHandler.onSongReceived = null
+ }
+ }
MainScreenWithDrawer(
solfaScreenModel,
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DeepLinkHandler.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DeepLinkHandler.kt
new file mode 100644
index 0000000..7294fe1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DeepLinkHandler.kt
@@ -0,0 +1,34 @@
+package mg.dot.feufaro
+
+object DeepLinkHandler {
+ var hasConsumedDeepLink = false
+ private var pendingData: Pair? = null
+
+ var onSongReceived: ((fileContent: String, filePath: String) -> Unit)? = null
+ set(value) {
+ field = value
+ if (value != null) {
+ consumePending()
+ }
+ }
+
+ fun handleSongContent(fileContent: String, filePath: String) {
+ var hasConsumedDeepLink = true
+ val callback = onSongReceived
+ if (callback != null) {
+ println("DeepLink: callback dispo → appel immédiat")
+ callback(fileContent, filePath)
+ } else {
+ println("DeepLink: callback null → mise en attente de $filePath")
+ pendingData = Pair(fileContent, filePath)
+ }
+ }
+
+ private fun consumePending() {
+ pendingData?.let { (content, path) ->
+ println("DeepLink: consommation du contenu en attente pour $path")
+ onSongReceived?.invoke(content, path)
+ pendingData = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt
index 8e772ca..f6bf0fd 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt
@@ -175,7 +175,11 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
}
}
+ fun resetShared() {
+ sharedScreenModel.reset()
+ }
fun parse(sourceFile: String) {
+ println("37: lFL bien reçu $sourceFile")
currentFile = sourceFile
val parseScope = CoroutineScope(Dispatchers.Default)
parseScope.launch {
@@ -224,7 +228,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val midiWriter = MidiWriterKotlin(fileRepository)
midiWriter.process(pitches)
midiWriter.save("whawyd3.mid")
-
+ sharedScreenModel.setStanza(1)
}
}
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt
index d22f025..1c3c52c 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt
@@ -93,8 +93,11 @@ LaunchedEffect(isPlay, isPos) {
focusRequester.requestFocus()
}
}
+ val fileContent = sharedScreenModel.fileContent.value ?: ""
LaunchedEffect(Unit) {
- sharedScreenModel.loadNewSong("$midiFile")
+ if(fileContent == "") {
+ sharedScreenModel.loadNewSong("$midiFile")
+ }
}
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
SimpleDrawerContent(
@@ -134,6 +137,7 @@ LaunchedEffect(isPlay, isPos) {
if (showPrintSettings) {
PrintSettingsDialog(
initialSettings = defaultPrintSettings(),
+// nbStanza = nbStanzas,
onDismiss = { showPrintSettings = false },
onConfirm = { settings ->
showPrintSettings = false
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/QRDisplay.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/QRDisplay.kt
index 83cfa8f..ceca898 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/QRDisplay.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/QRDisplay.kt
@@ -15,15 +15,31 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
+import java.io.ByteArrayOutputStream
+import java.net.URLEncoder
+import java.util.zip.GZIPOutputStream
+import kotlin.io.encoding.Base64
@Composable
fun QRDisplay(sharedScreenModel: SharedScreenModel) {
val content by sharedScreenModel.fileContent
+ fun compressAndEncode(content: String?): String {
+ val bos = ByteArrayOutputStream()
+ GZIPOutputStream(bos).use { it.write(content?.toByteArray(Charsets.UTF_8)) }
+ val compressedBytes = bos.toByteArray()
+ return Base64.UrlSafe.encode(compressedBytes).trim('=')
+ }
+
+ val compressedData = compressAndEncode(content)
+ val uri = "feufaro://song?file=$compressedData"
+// val encodedContent = URLEncoder.encode(content, "UTF-8")
+// val uri = "feufaro://song?file=$encodedContent"
+
val qrCodeImage = remember(content) {
if (content != null) {
- generateQRCode(content!!, size = 800)
+ generateQRCode(uri, size = 800)
} else {
null
}
@@ -45,7 +61,7 @@ fun QRDisplay(sharedScreenModel: SharedScreenModel) {
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(24.dp)
) {
- Text("Scanner ceci: ")
+ Text("Scanner pour ouvrir ce partition")
if (qrCodeImage != null) {
Image(
bitmap = qrCodeImage,
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt
index c61016b..9f92c29 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
+import mg.dot.feufaro.DeepLinkHandler
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.data.GridTUOData
@@ -94,6 +95,14 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun toggleQRCodeVisibility() {
_isQRCodeVisible.value = !_isQRCodeVisible.value
}
+ val qrCodeContent: State
+ get() = mutableStateOf(
+ _activeFilePath.value.takeIf { it.isNotBlank() }?.let { path ->
+ val fileName = path.substringAfterLast("/").substringAfterLast("\\")
+ "feufaro://song?file=${fileName}"
+ }
+ )
+
fun updateSearchTxt(searchValue: String) {
_searchTitle.value = searchValue
}
@@ -341,7 +350,6 @@ 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
diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt
index 2734baf..d5638d1 100644
--- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt
+++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SolfaScreenModel.kt
@@ -1,14 +1,28 @@
package mg.dot.feufaro.viewmodel
import cafe.adriel.voyager.core.model.ScreenModel
+import cafe.adriel.voyager.core.model.screenModelScope
+import mg.dot.feufaro.DeepLinkHandler
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.solfa.Solfa
class SolfaScreenModel(
val fileRepository: FileRepository,
- private val solfa: Solfa
+ val solfa: Solfa
) : ScreenModel {
- init {}
+ init {
+ DeepLinkHandler.onSongReceived = { fileContent, filePath ->
+ println("QR Scan: Chemin reçu, lancement du chargement...")
+ loadExternalFile(filePath)
+ }
+ }
+
+ fun loadExternalFile(path: String) {
+ solfa.resetShared()
+ solfa.parse(path)
+
+ println("QR Scan: Chargement terminé pour $path")
+ }
fun loadNextInPlaylist() {
solfa.loadNextInPlaylist()