Compare commits
2 commits
6e2c83ebb8
...
a4a468e8bd
| Author | SHA1 | Date | |
|---|---|---|---|
| a4a468e8bd | |||
| 0df0675b89 |
12 changed files with 667 additions and 160 deletions
|
|
@ -88,7 +88,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
|||
val file = if (filePath.contains("/") ) {
|
||||
File(filePath)
|
||||
} else {
|
||||
File(getAppPublicFolder(), filePath)
|
||||
File(getPAppPublicFolder(), filePath)
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -104,7 +104,15 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
|||
throw e
|
||||
}
|
||||
}
|
||||
private fun getAppPublicFolder(): File {
|
||||
override suspend fun getAppPublicFolder(): File {
|
||||
val publicDocsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||
val appDir = File(publicDocsDir, "Feufaro")
|
||||
if (!appDir.exists()) {
|
||||
appDir.mkdirs()
|
||||
}
|
||||
return appDir
|
||||
}
|
||||
private fun getPAppPublicFolder(): File {
|
||||
val publicDocsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||
val appDir = File(publicDocsDir, "Feufaro")
|
||||
if (!appDir.exists()) {
|
||||
|
|
@ -114,7 +122,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
|||
}
|
||||
|
||||
override fun pickSavePath(defaultName: String): String? {
|
||||
return File(getAppPublicFolder(), defaultName).absolutePath
|
||||
return File(getPAppPublicFolder(), defaultName).absolutePath
|
||||
}
|
||||
suspend fun openPdfFile(context: Context, filePath: String) {
|
||||
val file = File(filePath)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
actual fun MyVerticalScrollbar (
|
||||
scrollState: ScrollState,
|
||||
modifier: Modifier,
|
||||
scrollState: ScrollState?,
|
||||
lazyListState: LazyListState?,
|
||||
content: @Composable () -> Unit
|
||||
){
|
||||
content()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package mg.dot.feufaro
|
|||
import feufaro.composeapp.generated.resources.Res
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
// Définissez une expect interface. Elle spécifie le contrat de votre repository.
|
||||
// Utilisez 'expect interface' car l'implémentation (actual) variera selon la plateforme.
|
||||
|
|
@ -16,6 +17,7 @@ interface FileRepository {
|
|||
//Lire le dernier dossier d'importation
|
||||
suspend fun saveFile(filePath: String, data: ByteArray)
|
||||
suspend fun saveLocalFile(filePath: String, data: ByteArray)
|
||||
suspend fun getAppPublicFolder() : File
|
||||
fun getFileName(shortName: String) : String
|
||||
// suspend fun readFileBytes(filePath: String): ByteArray
|
||||
fun pickSavePath(defaultName: String): String?
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ object ScreenSolfa : Screen {
|
|||
}
|
||||
}
|
||||
MyVerticalScrollbar(
|
||||
scrollState,
|
||||
scrollState = scrollState,
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import feufaro.composeapp.generated.resources.Res
|
|||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import mg.dot.feufaro.FileRepository
|
||||
import java.io.File
|
||||
|
||||
data class DrawerItem(
|
||||
val id: Int,
|
||||
|
|
@ -14,9 +16,7 @@ data class DrawerItem(
|
|||
|
||||
@Serializable
|
||||
data class FeuIndex(
|
||||
val internalFeu: List<FeuItem>,
|
||||
val externalFeu: List<FeuItem> = emptyList(),
|
||||
val playlist: List<FeuItem> = emptyList()
|
||||
val internalFeu: List<FeuItem>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -28,29 +28,58 @@ data class FeuItem(
|
|||
)
|
||||
|
||||
@OptIn(ExperimentalResourceApi::class)
|
||||
suspend fun getDynamicDrawerItems(): List<DrawerItem> {
|
||||
return try {
|
||||
suspend fun getCombinedList(fileRepository: FileRepository): List<DrawerItem> {
|
||||
val finalList = mutableListOf<DrawerItem>()
|
||||
|
||||
try {
|
||||
val bytes = Res.readBytes("files/feuList.json")
|
||||
val jsonString = bytes.decodeToString()
|
||||
val index = Json.decodeFromString<FeuIndex>(bytes.decodeToString())
|
||||
finalList.addAll(index.internalFeu.map {
|
||||
DrawerItem(it.id, it.fileName.removeSuffix(".txt"), it.title, it.path)
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val format = Json { ignoreUnknownKeys = true }
|
||||
val index = format.decodeFromString<FeuIndex>(jsonString)
|
||||
try {
|
||||
val folderPath = fileRepository.getAppPublicFolder()
|
||||
val directory = File(folderPath.absolutePath)
|
||||
|
||||
index.internalFeu.map { item ->
|
||||
DrawerItem(
|
||||
id = item.id,
|
||||
title = item.fileName.removeSuffix(".txt"),
|
||||
contentTitle = item.title,
|
||||
path = item.path
|
||||
)
|
||||
if (directory.exists() && directory.isDirectory) {
|
||||
directory.listFiles { f -> f.extension == "txt" }?.forEachIndexed { i, file ->
|
||||
finalList.add(DrawerItem(
|
||||
id = 50000 + i,
|
||||
title = file.name.removeSuffix(".txt"),
|
||||
contentTitle = extractTitle(file),
|
||||
path = file.absolutePath
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Erreur JSON : ${e.message}")
|
||||
emptyList()
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return finalList
|
||||
}
|
||||
|
||||
private fun extractTitle(content: String): String? {
|
||||
val regex = Regex("""\|t:([^|]+)""")
|
||||
return regex.find(content)?.groupValues?.get(1)?.trim()
|
||||
}
|
||||
private fun extractTitle(file: File): String {
|
||||
return try {
|
||||
file.useLines { lines ->
|
||||
val titleLine = lines.find { it.contains("|t:") }
|
||||
|
||||
if (titleLine != null) {
|
||||
titleLine.substringAfter("|t:")
|
||||
.substringBefore("|")
|
||||
.trim()
|
||||
} else {
|
||||
file.nameWithoutExtension
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
file.nameWithoutExtension
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import mg.dot.feufaro.getConfigDirectoryPath
|
|||
import mg.dot.feufaro.launchFilePicker
|
||||
import mg.dot.feufaro.midi.MidiPitch
|
||||
import mg.dot.feufaro.midi.MidiWriterKotlin
|
||||
import java.io.File
|
||||
|
||||
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
||||
//@todo: ffpm-172-2 ${O:1} non fonctionnel
|
||||
|
|
@ -163,10 +164,22 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
if (path != null) {
|
||||
screenModelScope.launch(Dispatchers.Default) {
|
||||
sharedScreenModel.reset()
|
||||
parse(path)
|
||||
stateSettings.saveLastUsedDir(path)
|
||||
|
||||
try {
|
||||
val fileName = path.substringAfterLast(File.separatorChar)
|
||||
|
||||
val content = fileRepository.readFileContent(path)
|
||||
|
||||
fileRepository.saveLocalFile(fileName, content.encodeToByteArray())
|
||||
sharedScreenModel.loadItems()
|
||||
parse("${fileRepository.getAppPublicFolder()}/$fileName")
|
||||
|
||||
println("Fichier copié avec succès vers : Documents/Feufaro")
|
||||
} catch (e: Exception) {
|
||||
println("Erreur lors de la copie du fichier : ${e.message}")
|
||||
}
|
||||
}
|
||||
//loadSolfa()
|
||||
} else {
|
||||
println("Pas de dichier")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ LaunchedEffect(isPlay, isPos) {
|
|||
}
|
||||
)
|
||||
}
|
||||
val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
|
||||
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
||||
|
|
@ -352,7 +353,7 @@ LaunchedEffect(isPlay, isPos) {
|
|||
} else {
|
||||
if (filteredSongs.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight().align(Alignment.TopCenter)
|
||||
modifier = Modifier.fillMaxWidth(0.75f).align(Alignment.TopCenter)
|
||||
.background(MaterialTheme.colorScheme.surface).border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
|
|
@ -361,10 +362,23 @@ LaunchedEffect(isPlay, isPos) {
|
|||
) {
|
||||
if (filteredSongs.isNotEmpty()) {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
itemsIndexed(filteredSongs) { index, item ->
|
||||
itemsIndexed(filteredSongs, key = {_, item -> item.path}) { index, item ->
|
||||
val isFavorite = favoritePaths.contains(item)
|
||||
ListItem(
|
||||
headlineContent = { Text(item.title) },
|
||||
supportingContent = { Text(item.contentTitle, maxLines = 1) },
|
||||
trailingContent = {
|
||||
IconButton(
|
||||
onClick = { sharedScreenModel.toggleFavorite(item.path) },
|
||||
modifier = Modifier.size(30.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = if (!isFavorite) Color.LightGray else Color(0xFFFFD700)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
sharedScreenModel.updateSearchTxt("")
|
||||
sharedScreenModel.reset()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
expect fun MyVerticalScrollbar (
|
||||
scrollState: ScrollState,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
scrollState: ScrollState? = null,
|
||||
lazyListState: LazyListState? = null,
|
||||
content: @Composable () -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,39 +1,31 @@
|
|||
package mg.dot.feufaro.ui
|
||||
|
||||
import SharedScreenModel
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Book
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.MusicNote
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.automirrored.filled.EventNote
|
||||
import androidx.compose.material.icons.automirrored.filled.Note
|
||||
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mg.dot.feufaro.data.DrawerItem
|
||||
|
|
@ -41,6 +33,7 @@ import mg.dot.feufaro.getPlatform
|
|||
import mg.dot.feufaro.solfa.Solfa
|
||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SimpleDrawerContent(
|
||||
items: List<DrawerItem>,
|
||||
|
|
@ -57,85 +50,262 @@ fun SimpleDrawerContent(
|
|||
val context = getPlatform()
|
||||
|
||||
val midi = "whawyd3.mid"
|
||||
|
||||
val internalList by sharedScreenModel.internalItems.collectAsState()
|
||||
val externalList by sharedScreenModel.externalItems.collectAsState()
|
||||
val playList by sharedScreenModel.playlistItems.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
var internalExpanded by remember { mutableStateOf(true) }
|
||||
var externalExpanded by remember { mutableStateOf(false) }
|
||||
var playListExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier.width(300.dp)
|
||||
) {
|
||||
Box (
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
{
|
||||
val lazyListState = rememberLazyListState()
|
||||
ScrollableDrawerContent(
|
||||
lazyListState = lazyListState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(modifier = Modifier.weight(1f).padding(0.dp, 20.dp)) {
|
||||
|
||||
MyVerticalScrollbar(
|
||||
lazyListState = listState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight()
|
||||
.padding(end = 2.dp)
|
||||
){
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
stickyHeader {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = Modifier.fillParentMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Liste des solfa disponibles",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
DrawerHeaderSticky(
|
||||
title = "Solfa disponibles",
|
||||
icon = Icons.AutoMirrored.Filled.Note,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
isExpanded = internalExpanded,
|
||||
onToggle = { internalExpanded = !internalExpanded },
|
||||
count = internalList.size
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(items){ index, item ->
|
||||
val isSelected = item.path == activePath
|
||||
val title = item.title
|
||||
var isFfpm = false
|
||||
var isEws = false
|
||||
var isFF = false
|
||||
|
||||
if (title.startsWith("ffpm")) {
|
||||
isFfpm = true
|
||||
} else if(title.startsWith("ews")) {
|
||||
isEws = true
|
||||
} else {
|
||||
isFF = true
|
||||
}
|
||||
if (internalExpanded) {
|
||||
items(internalList) { item ->
|
||||
val isSelected = item.path == activePath
|
||||
NavigationDrawerItem(
|
||||
label = {
|
||||
Text(item.title)
|
||||
},
|
||||
icon = {
|
||||
val isIcon = when {
|
||||
isFfpm -> Icons.AutoMirrored.Filled.MenuBook
|
||||
isEws -> Icons.Filled.MusicNote
|
||||
isFF -> Icons.Filled.Book
|
||||
else -> Icons.Filled.Menu
|
||||
}
|
||||
Icon(
|
||||
isIcon,
|
||||
contentDescription = "",
|
||||
tint = Color.Blue
|
||||
)
|
||||
},
|
||||
badge = {
|
||||
Icon(
|
||||
Icons.Filled.Menu,
|
||||
contentDescription = ""
|
||||
)
|
||||
},
|
||||
selected = if(isSelected){ true } else { false },
|
||||
label = { DrawerItemLabel(item, sharedScreenModel) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
scope.launch { drawerState.close() }
|
||||
sharedScreenModel.reset()
|
||||
solfaScreenModel.loadFromFile(item.path)
|
||||
onSongSelected(midi)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(5.dp),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
colors = NavigationDrawerItemDefaults.colors(
|
||||
selectedContainerColor = Color(0, 157, 255).copy(alpha = 0.1f),
|
||||
selectedTextColor = Color(0, 157, 255)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stickyHeader {
|
||||
DrawerHeaderSticky(
|
||||
title = "Personnel",
|
||||
icon = Icons.AutoMirrored.Filled.EventNote,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
isExpanded = externalExpanded,
|
||||
onToggle = { externalExpanded = !externalExpanded },
|
||||
count = externalList.size
|
||||
)
|
||||
}
|
||||
|
||||
if (externalExpanded) {
|
||||
items(externalList) { item ->
|
||||
val isSelected = item.path == activePath
|
||||
if (item.path != "") {
|
||||
NavigationDrawerItem(
|
||||
label = { DrawerItemLabel(item, sharedScreenModel) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
sharedScreenModel.reset()
|
||||
solfaScreenModel.loadFromFile(item.path)
|
||||
onSongSelected(midi)
|
||||
},
|
||||
shape = RoundedCornerShape(5.dp),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
colors = NavigationDrawerItemDefaults.colors(
|
||||
selectedContainerColor = Color.Blue.copy(alpha = 0.1f),
|
||||
selectedTextColor = Color.Blue
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stickyHeader {
|
||||
DrawerHeaderSticky(
|
||||
title = "Playlist",
|
||||
icon = Icons.AutoMirrored.Filled.StarHalf,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
isExpanded = playListExpanded,
|
||||
onToggle = { playListExpanded = !playListExpanded },
|
||||
count = playList.size
|
||||
)
|
||||
}
|
||||
|
||||
if (playListExpanded) {
|
||||
itemsIndexed(playList) { index, item ->
|
||||
if (item.path.isNotBlank()) {
|
||||
val isSelected = item.path == activePath
|
||||
|
||||
NavigationDrawerItem(
|
||||
label = {
|
||||
DrawerFavorisItemLabel(item, index, isSelected, sharedScreenModel)
|
||||
},
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
solfaScreenModel.loadFromFile(item.path)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
colors = NavigationDrawerItemDefaults.colors(
|
||||
selectedContainerColor = Color(0xFFFFD700).copy(alpha = 0.3f),
|
||||
selectedTextColor = Color(0xFF665500),
|
||||
selectedIconColor = Color(0xFF665500)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Button(onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
}) {
|
||||
Text("Scanner")
|
||||
}
|
||||
Button(onClick = {
|
||||
solfaScreenModel.loadCustomFile()
|
||||
scope.launch { drawerState.close() }
|
||||
}) {
|
||||
Text("Importer")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerAccordionSection(
|
||||
title: String,
|
||||
items: List<DrawerItem>,
|
||||
icon: ImageVector,
|
||||
iconColor: Color,
|
||||
activePath: String,
|
||||
scope: CoroutineScope,
|
||||
solfaScreenModel: SolfaScreenModel,
|
||||
onItemClick: (DrawerItem) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(true) }
|
||||
|
||||
val rotationState by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
letterSpacing = 1.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Surface(
|
||||
color = iconColor.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.extraSmall
|
||||
) {
|
||||
Text(
|
||||
text = "${items.size}",
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = iconColor
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.rotate(rotationState).size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column {
|
||||
items.forEach { item ->
|
||||
val isSelected = item.path == activePath
|
||||
NavigationDrawerItem(
|
||||
label = {
|
||||
Column {
|
||||
Text(
|
||||
text = item.title,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = item.contentTitle,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
},
|
||||
selected = isSelected,
|
||||
onClick = { onItemClick(item) },
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = NavigationDrawerItemDefaults.colors(
|
||||
unselectedContainerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
|
@ -148,7 +318,7 @@ val midi = "whawyd3.mid"
|
|||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
onScannerButtonClick()
|
||||
|
||||
}
|
||||
) {
|
||||
Text("Scanner")
|
||||
|
|
@ -157,7 +327,7 @@ val midi = "whawyd3.mid"
|
|||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
//drawerState.close()
|
||||
}
|
||||
solfaScreenModel.loadCustomFile()
|
||||
}
|
||||
|
|
@ -169,5 +339,134 @@ val midi = "whawyd3.mid"
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerHeaderSticky(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
color: Color,
|
||||
isExpanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
count: Int
|
||||
) {
|
||||
val rotation by animateFloatAsState(if (isExpanded) 180f else 0f)
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth().clickable { onToggle() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = color, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "$title".uppercase(),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold),
|
||||
color = color
|
||||
)
|
||||
Surface(
|
||||
color = Color.Green.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.extraSmall
|
||||
) {
|
||||
Text(
|
||||
text = "${count}",
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Green
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.rotate(rotation)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerItemLabel(item: DrawerItem, sharedScreenModel: SharedScreenModel) {
|
||||
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
|
||||
val isFavorite = favoriteLists.contains(item)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = item.title, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = item.contentTitle, style = MaterialTheme.typography.labelSmall, maxLines = 1)
|
||||
}
|
||||
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = {
|
||||
sharedScreenModel.toggleFavorite(item.path)
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlaylistAddCircle,
|
||||
contentDescription = null,
|
||||
tint = if (!isFavorite) Color.LightGray else Color(0xFFFFD700),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerFavorisItemLabel(item: DrawerItem, index: Int, isSelected: Boolean, sharedScreenModel: SharedScreenModel) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Reorder,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp).size(18.dp),
|
||||
tint = Color.Gray.copy(alpha = 0.5f)
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = item.contentTitle,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
IconButton(
|
||||
onClick = { sharedScreenModel.moveToTop(index) },
|
||||
modifier = Modifier.size(30.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.VerticalAlignTop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = if (isSelected) Color.White else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { sharedScreenModel.toggleFavorite(item.path) },
|
||||
modifier = Modifier.size(30.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = if (isSelected) Color.White else Color(0xFFFFD700)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -18,10 +20,11 @@ import mg.dot.feufaro.DeepLinkHandler
|
|||
import mg.dot.feufaro.FileRepository
|
||||
import mg.dot.feufaro.data.DrawerItem
|
||||
import mg.dot.feufaro.data.GridTUOData
|
||||
import mg.dot.feufaro.data.getDynamicDrawerItems
|
||||
import mg.dot.feufaro.data.getCombinedList
|
||||
import mg.dot.feufaro.solfa.TimeUnitObject
|
||||
import mg.dot.feufaro.midi.FMediaPlayer
|
||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||
import java.io.File
|
||||
|
||||
class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel {
|
||||
private val _nextLabel = MutableStateFlow<String>("Next ...")
|
||||
|
|
@ -68,13 +71,127 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
||||
val drawerItems: StateFlow<List<DrawerItem>> = _drawerItems.asStateFlow()
|
||||
|
||||
val internalItems: StateFlow<List<DrawerItem>> = _drawerItems
|
||||
.map { list -> list.filter { it.path.startsWith("assets://") } }
|
||||
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
val externalItems: StateFlow<List<DrawerItem>> = _drawerItems
|
||||
.map { list ->
|
||||
list.filter { !it.path.startsWith("assets://") }
|
||||
.sortedByDescending { item: DrawerItem ->
|
||||
try {
|
||||
File(item.path).lastModified()
|
||||
} catch (e: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
private val playlistFilename = "playlist.json"
|
||||
private val _playlistPaths = MutableStateFlow<List<String>>(emptyList())
|
||||
val playlistItems: StateFlow<List<DrawerItem>> = combine(_playlistPaths, _drawerItems) { paths, allItems ->
|
||||
paths.mapNotNull { path -> allItems.find { it.path == path } }
|
||||
}.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
fun savePlaylistToDisk(playlist: List<String>) {
|
||||
screenModelScope.launch {
|
||||
try {
|
||||
val itemsJson = playlist.joinToString(
|
||||
separator = "\",\"",
|
||||
prefix = "[\"",
|
||||
postfix = "\"]"
|
||||
).replace("[\"\"]", "[]")
|
||||
|
||||
val finalJson = "{\"playlist\": $itemsJson}"
|
||||
|
||||
val data = finalJson.encodeToByteArray()
|
||||
|
||||
fileRepository.saveLocalFile(playlistFilename, data)
|
||||
|
||||
println("Playlist sauvegardée avec succès !")
|
||||
} catch (e: Exception) {
|
||||
println("Erreur lors de la sauvegarde : ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
fun toggleFavorite(path: String) {
|
||||
val current = _playlistPaths.value.toMutableList()
|
||||
if (current.contains(path)) {
|
||||
current.remove(path)
|
||||
} else {
|
||||
current.add(path)
|
||||
}
|
||||
_playlistPaths.value = current
|
||||
savePlaylistToDisk(current)
|
||||
}
|
||||
|
||||
fun moveUp(index: Int) {
|
||||
if (index > 0) {
|
||||
val current = _playlistPaths.value.toMutableList()
|
||||
val item = current.removeAt(index)
|
||||
current.add(index - 1, item)
|
||||
_playlistPaths.value = current
|
||||
savePlaylistToDisk(current)
|
||||
}
|
||||
}
|
||||
fun moveToTop(index: Int) {
|
||||
val current = _playlistPaths.value.toMutableList()
|
||||
val item = current.removeAt(index)
|
||||
current.add(0, item)
|
||||
_playlistPaths.value = current
|
||||
savePlaylistToDisk(current)
|
||||
}
|
||||
|
||||
fun moveDown(index: Int) {
|
||||
val current = _playlistPaths.value.toMutableList()
|
||||
if (index < current.size - 1) {
|
||||
val item = current.removeAt(index)
|
||||
current.add(index + 1, item)
|
||||
_playlistPaths.value = current
|
||||
savePlaylistToDisk(current)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInPlaylist(path: String): Boolean {
|
||||
return _playlistPaths.value.contains(path)
|
||||
}
|
||||
init {
|
||||
loadItems()
|
||||
}
|
||||
|
||||
private fun loadItems() {
|
||||
fun loadItems() {
|
||||
screenModelScope.launch {
|
||||
_drawerItems.value = getDynamicDrawerItems()
|
||||
_drawerItems.value = getCombinedList(fileRepository)
|
||||
}
|
||||
loadPlaylistFromDisk()
|
||||
}
|
||||
|
||||
private fun loadPlaylistFromDisk() {
|
||||
screenModelScope.launch {
|
||||
try {
|
||||
val publicFolder = fileRepository.getAppPublicFolder()
|
||||
val playlistFilePath = "$publicFolder/$playlistFilename"
|
||||
|
||||
val jsonString = fileRepository.readFileContent(playlistFilePath)
|
||||
|
||||
if (jsonString.isNotBlank()) {
|
||||
val paths = jsonString
|
||||
.replace("{\"playlist\":", "")
|
||||
.replace("}", "")
|
||||
.replace("[", "")
|
||||
.replace("]", "")
|
||||
.replace("\"", "")
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
_playlistPaths.value = paths
|
||||
println("Playlist chargée : ${paths.size} chants")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Erreur lors du chargement de la playlist : ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,16 +46,25 @@ class DesktopFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE com
|
|||
override suspend fun saveFile(filePath: String, data: ByteArray) {
|
||||
val userHome = System.getProperty("user.home")
|
||||
val file = File("$userHome/$filePath")
|
||||
file.writeBytes(data) // Extension Kotlin très efficace
|
||||
file.writeBytes(data)
|
||||
}
|
||||
override suspend fun saveLocalFile(filePath: String, data: ByteArray) {
|
||||
val file = if (filePath.startsWith("/")) {
|
||||
File(filePath) // chemin absolu → utilisé directement
|
||||
File(filePath)
|
||||
} else {
|
||||
File(System.getProperty("user.home"), filePath) // chemin relatif → préfixé
|
||||
File(getAppPublicFolder(), filePath)
|
||||
}
|
||||
file.parentFile?.mkdirs() // crée les dossiers parents si nécessaire
|
||||
file.writeBytes(data) // Extension Kotlin très efficace
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeBytes(data)
|
||||
}
|
||||
|
||||
override suspend fun getAppPublicFolder(): File {
|
||||
val userHome = System.getProperty("user.home")
|
||||
val appDir = File(userHome, "Documents/Feufaro/")
|
||||
if (!appDir.exists()) {
|
||||
appDir.mkdirs()
|
||||
}
|
||||
return appDir
|
||||
}
|
||||
|
||||
override fun getFileName(shortName: String): String {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.rememberScrollbarAdapter
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -14,15 +15,26 @@ import androidx.compose.ui.graphics.Color
|
|||
|
||||
@Composable
|
||||
actual fun MyVerticalScrollbar (
|
||||
scrollState: ScrollState,
|
||||
modifier: Modifier,
|
||||
scrollState: ScrollState?,
|
||||
lazyListState: LazyListState?,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box ( modifier = Modifier.fillMaxSize()){
|
||||
content()
|
||||
|
||||
val adapter = when {
|
||||
lazyListState != null -> rememberScrollbarAdapter(lazyListState)
|
||||
scrollState != null -> rememberScrollbarAdapter(scrollState)
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (adapter != null) {
|
||||
VerticalScrollbar(
|
||||
adapter = rememberScrollbarAdapter(scrollState = scrollState),
|
||||
modifier = modifier.fillMaxHeight(0.5f).background(Color.Green)
|
||||
adapter = adapter,
|
||||
modifier = modifier.fillMaxHeight(0.5f).background(Color.Green.copy(0.75f))
|
||||
)
|
||||
//content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue