Compare commits
No commits in common. "49186a76c8a39f38dbea87219c6d3023a9dbc1f7" and "a196ed4322dcf73601e053e6d64437bcc0128485" have entirely different histories.
49186a76c8
...
a196ed4322
10 changed files with 56 additions and 246 deletions
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue