Generate qrCode using DeepLink
This commit is contained in:
parent
1d35b9227d
commit
cf4046e202
12 changed files with 248 additions and 28 deletions
|
|
@ -21,6 +21,12 @@
|
|||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="feufaro" android:host="song" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,14 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
|||
}
|
||||
|
||||
else -> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
6
composeApp/src/androidMain/res/xml/file_paths.xml
Normal file
6
composeApp/src/androidMain/res/xml/file_paths.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-files-path name="external_files" path="." />
|
||||
<files-path name="internal_files" path="." />
|
||||
<external-path name="external_files" path="Documents/Feufaro" />
|
||||
</paths>
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SolfaScreenModel>()
|
||||
var menuPosition by remember { mutableStateOf(Offset.Zero) }
|
||||
var showContextualMenu by remember { mutableStateOf(false) }
|
||||
var gridWidthPx by rememberSaveable { mutableStateOf(0) }
|
||||
val sharedScreenModel = koinScreenModel<SharedScreenModel>()
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package mg.dot.feufaro
|
||||
|
||||
object DeepLinkHandler {
|
||||
var hasConsumedDeepLink = false
|
||||
private var pendingData: Pair<String, String>? = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<String?>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue