Compare commits

..

No commits in common. "a4a468e8bd81ea3012b471eb3b472bc156d00e89" and "6e2c83ebb8d6983fc36e2d72e1bf92da0091e1ea" have entirely different histories.

12 changed files with 160 additions and 667 deletions

View file

@ -88,7 +88,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
val file = if (filePath.contains("/") ) {
File(filePath)
} else {
File(getPAppPublicFolder(), filePath)
File(getAppPublicFolder(), filePath)
}
try {
@ -104,15 +104,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
throw e
}
}
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 {
private fun getAppPublicFolder(): File {
val publicDocsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
val appDir = File(publicDocsDir, "Feufaro")
if (!appDir.exists()) {
@ -122,7 +114,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
}
override fun pickSavePath(defaultName: String): String? {
return File(getPAppPublicFolder(), defaultName).absolutePath
return File(getAppPublicFolder(), defaultName).absolutePath
}
suspend fun openPdfFile(context: Context, filePath: String) {
val file = File(filePath)

View file

@ -1,15 +1,13 @@
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()

View file

@ -3,7 +3,6 @@ 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.
@ -17,7 +16,6 @@ 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?

View file

@ -220,7 +220,7 @@ object ScreenSolfa : Screen {
}
}
MyVerticalScrollbar(
scrollState = scrollState,
scrollState,
modifier = Modifier.align(Alignment.CenterEnd)
) {}
}

View file

@ -4,8 +4,6 @@ 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,
@ -16,7 +14,9 @@ data class DrawerItem(
@Serializable
data class FeuIndex(
val internalFeu: List<FeuItem>
val internalFeu: List<FeuItem>,
val externalFeu: List<FeuItem> = emptyList(),
val playlist: List<FeuItem> = emptyList()
)
@Serializable
@ -28,58 +28,29 @@ data class FeuItem(
)
@OptIn(ExperimentalResourceApi::class)
suspend fun getCombinedList(fileRepository: FileRepository): List<DrawerItem> {
val finalList = mutableListOf<DrawerItem>()
try {
suspend fun getDynamicDrawerItems(): List<DrawerItem> {
return try {
val bytes = Res.readBytes("files/feuList.json")
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 jsonString = bytes.decodeToString()
try {
val folderPath = fileRepository.getAppPublicFolder()
val directory = File(folderPath.absolutePath)
val format = Json { ignoreUnknownKeys = true }
val index = format.decodeFromString<FeuIndex>(jsonString)
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
))
}
index.internalFeu.map { item ->
DrawerItem(
id = item.id,
title = item.fileName.removeSuffix(".txt"),
contentTitle = item.title,
path = item.path
)
}
} catch (e: Exception) {
e.printStackTrace()
println("Erreur JSON : ${e.message}")
emptyList()
}
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
}
}

View file

@ -13,7 +13,6 @@ 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
@ -164,22 +163,10 @@ 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")
}

View file

@ -144,7 +144,6 @@ 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 = {
@ -353,7 +352,7 @@ LaunchedEffect(isPlay, isPos) {
} else {
if (filteredSongs.isNotEmpty()) {
Column(
modifier = Modifier.fillMaxWidth(0.75f).align(Alignment.TopCenter)
modifier = Modifier.fillMaxWidth().fillMaxHeight().align(Alignment.TopCenter)
.background(MaterialTheme.colorScheme.surface).border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
@ -362,23 +361,10 @@ LaunchedEffect(isPlay, isPos) {
) {
if (filteredSongs.isNotEmpty()) {
LazyColumn(Modifier.fillMaxSize()) {
itemsIndexed(filteredSongs, key = {_, item -> item.path}) { index, item ->
val isFavorite = favoritePaths.contains(item)
itemsIndexed(filteredSongs) { index, 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()

View file

@ -1,14 +1,12 @@
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,
scrollState: ScrollState? = null,
lazyListState: LazyListState? = null,
content: @Composable () -> Unit
content: @Composable () -> Unit,
)

View file

@ -1,31 +1,39 @@
package mg.dot.feufaro.ui
import SharedScreenModel
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.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.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.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.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.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
@ -33,7 +41,6 @@ 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>,
@ -49,424 +56,118 @@ 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) }
val midi = "whawyd3.mid"
ModalDrawerSheet(
modifier = Modifier.width(300.dp)
) {
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)
Box (
modifier = Modifier.fillMaxSize()
)
{
val lazyListState = rememberLazyListState()
ScrollableDrawerContent(
lazyListState = lazyListState,
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
state = lazyListState,
){
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
stickyHeader {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
modifier = Modifier.fillParentMaxWidth()
) {
Text(
"Liste des solfa disponibles",
style = MaterialTheme.typography.titleLarge
)
}
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)
)
)
}
}
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")
}
}
}
}
}
itemsIndexed(items){ index, item ->
val isSelected = item.path == activePath
val title = item.title
var isFfpm = false
var isEws = false
var isFF = false
@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
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
)
},
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)
}
},
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 (
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Spacer(modifier = Modifier.width(10.dp))
Button(
onClick = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row (
}
) {
Text("Scanner")
}
Spacer(modifier = Modifier.width(10.dp))
Button(
onClick = {
scope.launch {
//drawerState.close()
) {
Spacer(modifier = Modifier.width(10.dp))
Button(
onClick = {
onScannerButtonClick()
}
) {
Text("Scanner")
}
Spacer(modifier = Modifier.width(10.dp))
Button(
onClick = {
scope.launch {
drawerState.close()
}
solfaScreenModel.loadCustomFile()
}
) {
Text("Importer")
}
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)
)
}
}
}

View file

@ -11,8 +11,6 @@ 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
@ -20,11 +18,10 @@ 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.getCombinedList
import mg.dot.feufaro.data.getDynamicDrawerItems
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 ...")
@ -71,127 +68,13 @@ 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()
}
fun loadItems() {
private fun loadItems() {
screenModelScope.launch {
_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}")
}
_drawerItems.value = getDynamicDrawerItems()
}
}

View file

@ -46,25 +46,16 @@ 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)
file.writeBytes(data) // Extension Kotlin très efficace
}
override suspend fun saveLocalFile(filePath: String, data: ByteArray) {
val file = if (filePath.startsWith("/")) {
File(filePath)
File(filePath) // chemin absolu → utilisé directement
} else {
File(getAppPublicFolder(), filePath)
File(System.getProperty("user.home"), filePath) // chemin relatif → préfixé
}
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
file.parentFile?.mkdirs() // crée les dossiers parents si nécessaire
file.writeBytes(data) // Extension Kotlin très efficace
}
override fun getFileName(shortName: String): String {

View file

@ -7,7 +7,6 @@ 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
@ -15,26 +14,15 @@ 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 = adapter,
modifier = modifier.fillMaxHeight(0.5f).background(Color.Green.copy(0.75f))
)
//content()
}
VerticalScrollbar(
adapter = rememberScrollbarAdapter(scrollState = scrollState),
modifier = modifier.fillMaxHeight(0.5f).background(Color.Green)
)
//content()
}
}