From cf4046e2022b1b6006a1d966cfeda12da6657b36 Mon Sep 17 00:00:00 2001 From: "hasinarak3@gmail.com" Date: Mon, 20 Apr 2026 11:49:13 +0300 Subject: [PATCH] Generate qrCode using DeepLink --- .../src/androidMain/AndroidManifest.xml | 6 ++ .../mg/dot/feufaro/AndroidFileRepository.kt | 43 +++++---- .../kotlin/mg/dot/feufaro/MainActivity.kt | 96 +++++++++++++++++++ .../src/androidMain/res/xml/file_paths.xml | 6 ++ .../commonMain/kotlin/mg/dot/feufaro/App.kt | 14 ++- .../kotlin/mg/dot/feufaro/ScreenSolfa.kt | 17 +++- .../mg/dot/feufaro/data/DeepLinkHandler.kt | 34 +++++++ .../kotlin/mg/dot/feufaro/solfa/Solfa.kt | 6 +- .../kotlin/mg/dot/feufaro/ui/DrawerUI.kt | 6 +- .../kotlin/mg/dot/feufaro/ui/QRDisplay.kt | 20 +++- .../feufaro/viewmodel/SharedScreenModel.kt | 10 +- .../dot/feufaro/viewmodel/SolfaScreenModel.kt | 18 +++- 12 files changed, 248 insertions(+), 28 deletions(-) create mode 100644 composeApp/src/androidMain/res/xml/file_paths.xml create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DeepLinkHandler.kt 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()