Compare commits

..

3 commits

10 changed files with 245 additions and 55 deletions

View file

@ -15,7 +15,8 @@
android:theme="@android:style/Theme.Material.Light.NoActionBar"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity <activity
android:exported="true" android:exported="true"
android:name=".MainActivity"> android:name=".MainActivity"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -85,22 +85,39 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
file.readBytes() file.readBytes()
}*/ }*/
override suspend fun saveLocalFile(filePath: String, data: ByteArray) = withContext(Dispatchers.IO) { override suspend fun saveLocalFile(filePath: String, data: ByteArray) = withContext(Dispatchers.IO) {
try {
if (filePath.startsWith("content://") || filePath.startsWith("file://")) {
val uri = Uri.parse(filePath)
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(data)
outputStream.flush()
} ?: throw Exception("Impossible d'ouvrir l'Uri pour écriture")
} else {
val file = if (filePath.contains("/")) { val file = if (filePath.contains("/")) {
File(filePath) File(filePath)
} else { } else {
File(getPAppPublicFolder(), filePath) File(getPAppPublicFolder(), filePath)
} }
try {
file.parentFile?.mkdirs() file.parentFile?.mkdirs()
file.writeBytes(data) file.writeBytes(data)
println("Fichier sauvegardé dans : ${file.absolutePath}") }
if (file.extension.lowercase() == "pdf") { withContext(Dispatchers.Main) {
openPdfFile(context = context, filePath = file.absolutePath) if (filePath.endsWith(".pdf", ignoreCase = true)) {
openPdfFile(context = context, filePath = filePath)
} else {
Toast.makeText(
context.applicationContext,
"Sauvegarde réussie",
Toast.LENGTH_LONG
).show()
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(context, "Erreur de sauvegarde : ${e.message}", Toast.LENGTH_SHORT).show()
}
throw e throw e
} }
} }
@ -121,8 +138,9 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
return appDir return appDir
} }
override fun pickSavePath(defaultName: String): String? { override fun pickSavePath(defaultName: String, title: String): String? {
return File(getPAppPublicFolder(), defaultName).absolutePath val file = File(context.filesDir, defaultName)
return file?.absolutePath
} }
suspend fun openPdfFile(context: Context, filePath: String) { suspend fun openPdfFile(context: Context, filePath: String) {
val file = File(filePath) val file = File(filePath)

View file

@ -89,12 +89,10 @@ class MainActivity : ComponentActivity() {
println("DeepLink: rawContent = $rawContent") println("DeepLink: rawContent = $rawContent")
val fileName = rawContent val fileName = data.getQueryParameter("name") ?: run {
.split("|") println("DeepLink: paramètre 'name' introuvable")
.firstOrNull { it.startsWith("t:") } return
?.removePrefix("t:") }
?.trim()
println("DeepLink: fileName extrait = $fileName") println("DeepLink: fileName extrait = $fileName")
if (fileName.isNullOrBlank()) { if (fileName.isNullOrBlank()) {
@ -111,7 +109,7 @@ class MainActivity : ComponentActivity() {
appDir.mkdirs() appDir.mkdirs()
// docsDir.mkdirs() // docsDir.mkdirs()
val file = File(appDir, "$fileName.txt") val file = File(appDir, fileName)
try { try {
file.writeText(rawContent) file.writeText(rawContent)

View file

@ -0,0 +1,25 @@
package mg.dot.feufaro.ui
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@Composable
actual fun rememberFileSaveLauncher(
onResult: (String?) -> Unit
): FileSaveLauncher {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/plain")
) { uri ->
onResult(uri?.toString())
}
return remember {
object : FileSaveLauncher {
override fun launch(defaultName: String) {
launcher.launch(defaultName)
}
}
}
}

View file

@ -20,7 +20,7 @@ interface FileRepository {
suspend fun getAppPublicFolder() : File suspend fun getAppPublicFolder() : File
fun getFileName(shortName: String) : String fun getFileName(shortName: String) : String
// suspend fun readFileBytes(filePath: String): ByteArray // suspend fun readFileBytes(filePath: String): ByteArray
fun pickSavePath(defaultName: String): String? fun pickSavePath(defaultName: String, title: String): String?
} }
// This is just a regular class that implements the common 'FileRepository' interface. // This is just a regular class that implements the common 'FileRepository' interface.

View file

@ -145,9 +145,10 @@ LaunchedEffect(isPlay, isPos) {
) )
} }
val favoritePaths by sharedScreenModel.playlistItems.collectAsState() val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = {
TopAppBar( TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = { modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.safeDrawing),
title = {
Column( Column(
modifier = Modifier.fillMaxSize().verticalScroll(scrollState), modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
@ -169,13 +170,17 @@ LaunchedEffect(isPlay, isPos) {
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
} }
}, actions = { }, actions = {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text( Text(
text = songKey, text = songKey,
fontSize = 25.sp, fontSize = 25.sp,
fontWeight = FontWeight.Black, fontWeight = FontWeight.Black,
modifier = Modifier.padding(end = 16.dp)
) )
Spacer(Modifier.width(8.dp)) }
}, colors = TopAppBarColors( }, colors = TopAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary,
@ -228,6 +233,24 @@ LaunchedEffect(isPlay, isPos) {
} }
/*AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
) {
FloatingActionButton(
onClick = {
sharedScreenModel.toggleEditMode()
sharedScreenModel.setExpandedFAB(false)
}, modifier = Modifier.alpha(0.45f)
) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "null",
tint = Color.Blue
)
}
}*/
AnimatedVisibility( AnimatedVisibility(
visible = isExpanded and !showMidiCtrl, visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 }, enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
@ -345,11 +368,14 @@ LaunchedEffect(isPlay, isPos) {
}) { paddingValues -> }) { paddingValues ->
Box( Box(
modifier = Modifier.fillMaxSize().padding(paddingValues).windowInsetsPadding(WindowInsets.ime) modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.ime)
) { ) {
content(PaddingValues(0.dp)) content(PaddingValues(0.dp))
if (sharedScreenModel.isQRCodeVisible.value) { if (sharedScreenModel.isQRCodeVisible.value) {
QRDisplay(sharedScreenModel = sharedScreenModel) QRDisplay(
sharedScreenModel = sharedScreenModel,
fileRepository = solfaScreenModel.fileRepository
)
} else { } else {
if (filteredSongs.isNotEmpty()) { if (filteredSongs.isNotEmpty()) {
Column( Column(

View file

@ -0,0 +1,12 @@
package mg.dot.feufaro.ui
import androidx.compose.runtime.Composable
interface FileSaveLauncher {
fun launch(defaultName: String)
}
@Composable
expect fun rememberFileSaveLauncher(
onResult: (String?) -> Unit
): FileSaveLauncher

View file

@ -6,42 +6,48 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.solfa.ColorPrefixA
import mg.dot.feufaro.solfa.ColorPrefixB
import mg.dot.feufaro.solfa.ColorValue
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.URLEncoder
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
@Composable @Composable
fun QRDisplay(sharedScreenModel: SharedScreenModel) { fun QRDisplay(sharedScreenModel: SharedScreenModel, fileRepository: FileRepository) {
val content by sharedScreenModel.fileContent val content by sharedScreenModel.fileContent
val path by sharedScreenModel.activeFilePath
val fileName = path.substringAfterLast('/').substringAfterLast(':')
fun compressAndEncode(content: String?): String { val qrCodeImage by produceState<ImageBitmap?>(initialValue = null, content, path) {
val expandedSource = expandInclusions(
content = content,
currentFilePath = path,
fileRepository = fileRepository
)
if(expandedSource.isNotEmpty()) {
val bos = ByteArrayOutputStream() val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).use { it.write(content?.toByteArray(Charsets.UTF_8)) } GZIPOutputStream(bos).use { it.write(expandedSource.toByteArray(Charsets.UTF_8)) }
val compressedBytes = bos.toByteArray() val compressedBytes = bos.toByteArray()
return Base64.UrlSafe.encode(compressedBytes).trim('=')
}
val compressedData = compressAndEncode(content) val compressedData = Base64.UrlSafe.encode(compressedBytes).trim('=')
val uri = "feufaro://song?file=$compressedData"
// val encodedContent = URLEncoder.encode(content, "UTF-8")
// val uri = "feufaro://song?file=$encodedContent"
val qrCodeImage = remember(content) { val uri = "feufaro://song?file=${compressedData}&name=$fileName"
if (content != null) { value = generateQRCode(uri, size = 800)
generateQRCode(uri, size = 800)
} else {
null
} }
} }
@ -64,14 +70,79 @@ fun QRDisplay(sharedScreenModel: SharedScreenModel) {
Text("Scanner pour ouvrir ce partition") Text("Scanner pour ouvrir ce partition")
if (qrCodeImage != null) { if (qrCodeImage != null) {
Image( Image(
bitmap = qrCodeImage, bitmap = qrCodeImage!!,
contentDescription = "Code QR du fichier actif", contentDescription = "Code QR du fichier actif",
modifier = Modifier.size(400.dp) modifier = Modifier.size(400.dp)
) )
} else { } else {
Text("Chargement du Code QR ou contenu vide...", modifier = Modifier.padding(16.dp)) Box(
modifier = Modifier
.size(400.dp)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = ColorPrefixA,
strokeWidth = 4.dp,
trackColor = ColorPrefixB.copy(alpha = 0.2f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Génération du QR Code...",
style = MaterialTheme.typography.bodyMedium.copy(
color = ColorValue,
fontWeight = FontWeight.Medium
)
)
} }
} }
} }
} }
} }
}
}
private suspend fun expandInclusions(
content: String?,
currentFilePath: String,
fileRepository: FileRepository
): String {
if(content.isNullOrEmpty()) return ""
val lines = content.split("\n")
val result = StringBuilder()
val lastSlash = currentFilePath.lastIndexOf('/')
val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else ""
lines.forEach { line ->
if (line.startsWith("I0:")) {
try {
val parts = line.substring(3).split(":")
val fileName = parts[0]
val ignorePattern = if (parts.size > 1) parts[1] else ""
val fullPath = directory + fileName
val includedLines = fileRepository.readFileLines(fullPath)
val regexIgnore = if (ignorePattern.isNotEmpty()) Regex(ignorePattern) else null
includedLines.forEach { incLine ->
if (regexIgnore == null || !regexIgnore.containsMatchIn(incLine)) {
result.append(incLine).append("\n")
}
}
} catch (e: Exception) {
result.append("// Erreur inclusion: ${e.message}\n")
}
} else {
result.append(line).append("\n")
}
}
return result.toString().trimEnd()
}

View file

@ -72,8 +72,8 @@ class DesktopFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE com
return "$userHome/$shortName" return "$userHome/$shortName"
} }
override fun pickSavePath(defaultName: String): String? { override fun pickSavePath(defaultName: String, title: String): String? {
val dialog = FileDialog(null as Frame?, "Exporter en PDF", FileDialog.SAVE) val dialog = FileDialog(null as Frame?, title, FileDialog.SAVE)
dialog.directory = System.getProperty("user.home") dialog.directory = System.getProperty("user.home")
dialog.file = defaultName dialog.file = defaultName
dialog.isVisible = true dialog.isVisible = true

View file

@ -0,0 +1,39 @@
package mg.dot.feufaro.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
@Composable
actual fun rememberFileSaveLauncher(
onResult: (String?) -> Unit
): FileSaveLauncher {
return remember {
object : FileSaveLauncher {
override fun launch(defaultName: String) {
CoroutineScope(Dispatchers.IO).launch {
val dialog = FileDialog(null as Frame?, "Sauvegarder la source", FileDialog.SAVE)
dialog.directory = System.getProperty("user.home")
dialog.file = defaultName
dialog.isVisible = true
val path = if (dialog.directory != null && dialog.file != null) {
File(dialog.directory, dialog.file).absolutePath
} else {
null
}
withContext(Dispatchers.Main) {
onResult(path)
}
}
}
}
}
}