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">
<activity
android:exported="true"
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize">
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -85,39 +85,22 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
file.readBytes()
}*/
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("/")) {
File(filePath)
} else {
File(getPAppPublicFolder(), filePath)
}
file.parentFile?.mkdirs()
file.writeBytes(data)
}
val file = if (filePath.contains("/") ) {
File(filePath)
} else {
File(getPAppPublicFolder(), filePath)
}
withContext(Dispatchers.Main) {
if (filePath.endsWith(".pdf", ignoreCase = true)) {
openPdfFile(context = context, filePath = filePath)
} else {
Toast.makeText(
context.applicationContext,
"Sauvegarde réussie",
Toast.LENGTH_LONG
).show()
}
try {
file.parentFile?.mkdirs()
file.writeBytes(data)
println("Fichier sauvegardé dans : ${file.absolutePath}")
if (file.extension.lowercase() == "pdf") {
openPdfFile(context = context, filePath = file.absolutePath)
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(context, "Erreur de sauvegarde : ${e.message}", Toast.LENGTH_SHORT).show()
}
throw e
}
}
@ -138,9 +121,8 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
return appDir
}
override fun pickSavePath(defaultName: String, title: String): String? {
val file = File(context.filesDir, defaultName)
return file?.absolutePath
override fun pickSavePath(defaultName: String): String? {
return File(getPAppPublicFolder(), defaultName).absolutePath
}
suspend fun openPdfFile(context: Context, filePath: String) {
val file = File(filePath)

View file

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

View file

@ -145,10 +145,9 @@ LaunchedEffect(isPlay, isPos) {
)
}
val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = {
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
TopAppBar(
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.safeDrawing),
title = {
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
verticalArrangement = Arrangement.Center
@ -170,17 +169,13 @@ LaunchedEffect(isPlay, isPos) {
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
}
}, actions = {
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(
text = songKey,
fontSize = 25.sp,
fontWeight = FontWeight.Black,
modifier = Modifier.padding(end = 16.dp)
)
}
Text(
text = songKey,
fontSize = 25.sp,
fontWeight = FontWeight.Black,
)
Spacer(Modifier.width(8.dp))
}, colors = TopAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
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(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
@ -368,14 +345,11 @@ LaunchedEffect(isPlay, isPos) {
}) { paddingValues ->
Box(
modifier = Modifier.fillMaxSize().padding(paddingValues).consumeWindowInsets(paddingValues).windowInsetsPadding(WindowInsets.ime)
modifier = Modifier.fillMaxSize().padding(paddingValues).windowInsetsPadding(WindowInsets.ime)
) {
content(PaddingValues(0.dp))
if (sharedScreenModel.isQRCodeVisible.value) {
QRDisplay(
sharedScreenModel = sharedScreenModel,
fileRepository = solfaScreenModel.fileRepository
)
QRDisplay(sharedScreenModel = sharedScreenModel)
} else {
if (filteredSongs.isNotEmpty()) {
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.layout.*
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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 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.net.URLEncoder
import java.util.zip.GZIPOutputStream
import kotlin.io.encoding.Base64
@Composable
fun QRDisplay(sharedScreenModel: SharedScreenModel, fileRepository: FileRepository) {
fun QRDisplay(sharedScreenModel: SharedScreenModel) {
val content by sharedScreenModel.fileContent
val path by sharedScreenModel.activeFilePath
val fileName = path.substringAfterLast('/').substringAfterLast(':')
val qrCodeImage by produceState<ImageBitmap?>(initialValue = null, content, path) {
val expandedSource = expandInclusions(
content = content,
currentFilePath = path,
fileRepository = fileRepository
)
if(expandedSource.isNotEmpty()) {
val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).use { it.write(expandedSource.toByteArray(Charsets.UTF_8)) }
val compressedBytes = bos.toByteArray()
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 = 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"
value = generateQRCode(uri, size = 800)
val qrCodeImage = remember(content) {
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")
if (qrCodeImage != null) {
Image(
bitmap = qrCodeImage!!,
bitmap = qrCodeImage,
contentDescription = "Code QR du fichier actif",
modifier = Modifier.size(400.dp)
)
} else {
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
)
)
}
}
Text("Chargement du Code QR ou contenu vide...", modifier = Modifier.padding(16.dp))
}
}
}
}
}
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"
}
override fun pickSavePath(defaultName: String, title: String): String? {
val dialog = FileDialog(null as Frame?, title, FileDialog.SAVE)
override fun pickSavePath(defaultName: String): String? {
val dialog = FileDialog(null as Frame?, "Exporter en PDF", FileDialog.SAVE)
dialog.directory = System.getProperty("user.home")
dialog.file = defaultName
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)
}
}
}
}
}
}