diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt index 5f71e3f..ff27666 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/AndroidFileRepository.kt @@ -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) diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt index 1b48dd7..cf52595 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt index 358081a..34a4350 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt @@ -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? diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt index cc60e35..aa0c053 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ScreenSolfa.kt @@ -67,6 +67,7 @@ object ScreenSolfa : Screen { val gridTUOData = GridTUOData(measure, tuoList, stanza) val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() + var showContextualMenu = false var viewportHeight by remember { mutableStateOf(0) } var isScanning by remember { mutableStateOf(false) } @@ -154,7 +155,7 @@ object ScreenSolfa : Screen { ) Text(text = measureString) Text(text = "Stanza: $stanza") - ScreenTranspose.Content() + //ScreenTranspose.Content() } LazyVerticalGridTUO( gridTUOData, @@ -219,7 +220,7 @@ object ScreenSolfa : Screen { } } MyVerticalScrollbar( - scrollState, + scrollState = scrollState, modifier = Modifier.align(Alignment.CenterEnd) ) {} } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt index f68f0a2..465d959 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/data/DrawerItem.kt @@ -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, - val externalFeu: List = emptyList(), - val playlist: List = emptyList() + val internalFeu: List ) @Serializable @@ -28,29 +28,58 @@ data class FeuItem( ) @OptIn(ExperimentalResourceApi::class) -suspend fun getDynamicDrawerItems(): List { - return try { +suspend fun getCombinedList(fileRepository: FileRepository): List { + val finalList = mutableListOf() + + try { val bytes = Res.readBytes("files/feuList.json") - val jsonString = bytes.decodeToString() + val index = Json.decodeFromString(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(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 + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt index 8a29980..62226fe 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -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") } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt index 5d08819..f36100b 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/DrawerUI.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt index 1405086..e04b51e 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt @@ -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 ) diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt index 4445207..e8af5cf 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ui/SimpleDrawerContent.kt @@ -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, @@ -56,118 +49,424 @@ fun SimpleDrawerContent( val context = getPlatform() -val midi = "whawyd3.mid" + 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() - ) { - LazyColumn( - state = lazyListState, + 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) ){ - stickyHeader { - Surface( - color = MaterialTheme.colorScheme.surfaceContainerHigh, - modifier = Modifier.fillParentMaxWidth() - ) { - Text( - "Liste des solfa disponibles", - style = MaterialTheme.typography.titleLarge + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize() + ) { + stickyHeader { + 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 - } - 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 + if (internalExpanded) { + items(internalList) { item -> + val isSelected = item.path == activePath + 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(0, 157, 255).copy(alpha = 0.1f), + selectedTextColor = Color(0, 157, 255) ) - }, - badge = { - Icon( - Icons.Filled.Menu, - contentDescription = "" ) - }, - selected = if(isSelected){ true } else { false }, - onClick = { - scope.launch { - drawerState.close() - } - sharedScreenModel.reset() - solfaScreenModel.loadFromFile(item.path) - onSongSelected(midi) } - ) - } - } - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row ( + } - ) { - Spacer(modifier = Modifier.width(10.dp)) - Button( - onClick = { - onScannerButtonClick() + 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 + ) + ) } - ) { - Text("Scanner") } - Spacer(modifier = Modifier.width(10.dp)) - Button( - onClick = { - scope.launch { - drawerState.close() + } + + 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) + ) + ) } - solfaScreenModel.loadCustomFile() - } - ) { - Text("Importer") } } } } } + 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, + 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.CenterHorizontally) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row ( + + ) { + Spacer(modifier = Modifier.width(10.dp)) + Button( + onClick = { + + } + ) { + Text("Scanner") + } + Spacer(modifier = Modifier.width(10.dp)) + Button( + onClick = { + scope.launch { + //drawerState.close() + } + solfaScreenModel.loadCustomFile() + } + ) { + Text("Importer") + } + } + } + } + } +} + +@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) + ) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index c3cc2ce..7cb3bc2 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -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("Next ...") @@ -68,13 +71,127 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode private val _drawerItems = MutableStateFlow>(emptyList()) val drawerItems: StateFlow> = _drawerItems.asStateFlow() + val internalItems: StateFlow> = _drawerItems + .map { list -> list.filter { it.path.startsWith("assets://") } } + .stateIn(screenModelScope, SharingStarted.Lazily, emptyList()) + + val externalItems: StateFlow> = _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>(emptyList()) + val playlistItems: StateFlow> = combine(_playlistPaths, _drawerItems) { paths, allItems -> + paths.mapNotNull { path -> allItems.find { it.path == path } } + }.stateIn(screenModelScope, SharingStarted.Lazily, emptyList()) + + fun savePlaylistToDisk(playlist: List) { + 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}") + } } } diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt index 95569e2..e7c38b5 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopFileRepository.kt @@ -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 { diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt index 887bc39..45b25be 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/ui/MyVerticalScrollbar.kt @@ -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()){ - VerticalScrollbar( - adapter = rememberScrollbarAdapter(scrollState = scrollState), - modifier = modifier.fillMaxHeight(0.5f).background(Color.Green) - ) - //content() + content() + + val adapter = when { + lazyListState != null -> rememberScrollbarAdapter(lazyListState) + scrollState != null -> rememberScrollbarAdapter(scrollState) + else -> null + } + + if (adapter != null) { + VerticalScrollbar( + adapter = adapter, + modifier = modifier.fillMaxHeight(0.5f).background(Color.Green.copy(0.75f)) + ) + //content() + } } }