Compare commits

..

No commits in common. "49186a76c8a39f38dbea87219c6d3023a9dbc1f7" and "a196ed4322dcf73601e053e6d64437bcc0128485" have entirely different histories.

10 changed files with 56 additions and 246 deletions

View file

@ -15,8 +15,7 @@
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,39 +85,22 @@ 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 { val file = if (filePath.contains("/") ) {
if (filePath.startsWith("content://") || filePath.startsWith("file://")) { File(filePath)
val uri = Uri.parse(filePath) } else {
context.contentResolver.openOutputStream(uri)?.use { outputStream -> File(getPAppPublicFolder(), filePath)
outputStream.write(data) }
outputStream.flush()
} ?: throw Exception("Impossible d'ouvrir l'Uri pour écriture")
} else {
val file = if (filePath.contains("/")) {
File(filePath)
} else {
File(getPAppPublicFolder(), filePath)
}
file.parentFile?.mkdirs()
file.writeBytes(data)
}
withContext(Dispatchers.Main) { try {
if (filePath.endsWith(".pdf", ignoreCase = true)) { file.parentFile?.mkdirs()
openPdfFile(context = context, filePath = filePath) file.writeBytes(data)
} else { println("Fichier sauvegardé dans : ${file.absolutePath}")
Toast.makeText(
context.applicationContext, if (file.extension.lowercase() == "pdf") {
"Sauvegarde réussie", openPdfFile(context = context, filePath = file.absolutePath)
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
} }
} }
@ -138,9 +121,8 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
return appDir return appDir
} }
override fun pickSavePath(defaultName: String, title: String): String? { override fun pickSavePath(defaultName: String): String? {
val file = File(context.filesDir, defaultName) return File(getPAppPublicFolder(), defaultName).absolutePath
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,10 +89,12 @@ class MainActivity : ComponentActivity() {
println("DeepLink: rawContent = $rawContent") println("DeepLink: rawContent = $rawContent")
val fileName = data.getQueryParameter("name") ?: run { val fileName = rawContent
println("DeepLink: paramètre 'name' introuvable") .split("|")
return .firstOrNull { it.startsWith("t:") }
} ?.removePrefix("t:")
?.trim()
println("DeepLink: fileName extrait = $fileName") println("DeepLink: fileName extrait = $fileName")
if (fileName.isNullOrBlank()) { if (fileName.isNullOrBlank()) {
@ -109,7 +111,7 @@ class MainActivity : ComponentActivity() {
appDir.mkdirs() appDir.mkdirs()
// docsDir.mkdirs() // docsDir.mkdirs()
val file = File(appDir, fileName) val file = File(appDir, "$fileName.txt")
try { try {
file.writeText(rawContent) file.writeText(rawContent)

View file

@ -1,25 +0,0 @@
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, title: String): String? fun pickSavePath(defaultName: 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,10 +145,9 @@ LaunchedEffect(isPlay, isPos) {
) )
} }
val favoritePaths by sharedScreenModel.playlistItems.collectAsState() val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = { Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
TopAppBar( TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.safeDrawing), modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
title = {
Column( Column(
modifier = Modifier.fillMaxSize().verticalScroll(scrollState), modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
@ -170,17 +169,13 @@ LaunchedEffect(isPlay, isPos) {
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
} }
}, actions = { }, actions = {
Box( Text(
modifier = Modifier.fillMaxHeight(), text = songKey,
contentAlignment = Alignment.Center fontSize = 25.sp,
) { fontWeight = FontWeight.Black,
Text( )
text = songKey, Spacer(Modifier.width(8.dp))
fontSize = 25.sp,
fontWeight = FontWeight.Black,
modifier = Modifier.padding(end = 16.dp)
)
}
}, colors = TopAppBarColors( }, colors = TopAppBarColors(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary,
@ -233,24 +228,6 @@ 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 },
@ -368,14 +345,11 @@ LaunchedEffect(isPlay, isPos) {
}) { paddingValues -> }) { paddingValues ->
Box( Box(
modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.ime) modifier = Modifier.fillMaxSize().padding(paddingValues).windowInsetsPadding(WindowInsets.ime)
) { ) {
content(PaddingValues(0.dp)) content(PaddingValues(0.dp))
if (sharedScreenModel.isQRCodeVisible.value) { if (sharedScreenModel.isQRCodeVisible.value) {
QRDisplay( QRDisplay(sharedScreenModel = sharedScreenModel)
sharedScreenModel = sharedScreenModel,
fileRepository = solfaScreenModel.fileRepository
)
} else { } else {
if (filteredSongs.isNotEmpty()) { if (filteredSongs.isNotEmpty()) {
Column( Column(

View file

@ -1,12 +0,0 @@
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,48 +6,42 @@ 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.produceState import androidx.compose.runtime.remember
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, fileRepository: FileRepository) { fun QRDisplay(sharedScreenModel: SharedScreenModel) {
val content by sharedScreenModel.fileContent val content by sharedScreenModel.fileContent
val path by sharedScreenModel.activeFilePath
val fileName = path.substringAfterLast('/').substringAfterLast(':')
val qrCodeImage by produceState<ImageBitmap?>(initialValue = null, content, path) { fun compressAndEncode(content: String?): String {
val expandedSource = expandInclusions( val bos = ByteArrayOutputStream()
content = content, GZIPOutputStream(bos).use { it.write(content?.toByteArray(Charsets.UTF_8)) }
currentFilePath = path, val compressedBytes = bos.toByteArray()
fileRepository = fileRepository return Base64.UrlSafe.encode(compressedBytes).trim('=')
) }
if(expandedSource.isNotEmpty()) {
val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).use { it.write(expandedSource.toByteArray(Charsets.UTF_8)) }
val compressedBytes = bos.toByteArray()
val compressedData = 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 uri = "feufaro://song?file=${compressedData}&name=$fileName" val qrCodeImage = remember(content) {
value = generateQRCode(uri, size = 800) if (content != null) {
generateQRCode(uri, size = 800)
} else {
null
} }
} }
@ -70,79 +64,14 @@ fun QRDisplay(sharedScreenModel: SharedScreenModel, fileRepository: FileReposito
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 {
Box( Text("Chargement du Code QR ou contenu vide...", modifier = Modifier.padding(16.dp))
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, title: String): String? { override fun pickSavePath(defaultName: String): String? {
val dialog = FileDialog(null as Frame?, title, FileDialog.SAVE) val dialog = FileDialog(null as Frame?, "Exporter en PDF", 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

@ -1,39 +0,0 @@
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)
}
}
}
}
}
}