Compare commits
3 commits
a196ed4322
...
49186a76c8
| Author | SHA1 | Date | |
|---|---|---|---|
| 49186a76c8 | |||
| 184cb48c92 | |||
| 76f99cadce |
10 changed files with 245 additions and 55 deletions
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
val file = if (filePath.contains("/") ) {
|
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)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue