Implements persistent playlist & separate in(ex)ternal sources on drawer content
This commit is contained in:
parent
41d79b3a20
commit
0df0675b89
12 changed files with 669 additions and 161 deletions
|
|
@ -88,7 +88,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
||||||
val file = if (filePath.contains("/") ) {
|
val file = if (filePath.contains("/") ) {
|
||||||
File(filePath)
|
File(filePath)
|
||||||
} else {
|
} else {
|
||||||
File(getAppPublicFolder(), filePath)
|
File(getPAppPublicFolder(), filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -104,7 +104,15 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
||||||
throw e
|
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 publicDocsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||||
val appDir = File(publicDocsDir, "Feufaro")
|
val appDir = File(publicDocsDir, "Feufaro")
|
||||||
if (!appDir.exists()) {
|
if (!appDir.exists()) {
|
||||||
|
|
@ -114,7 +122,7 @@ class AndroidFileRepository(private val context: Context) : FileRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pickSavePath(defaultName: String): String? {
|
override fun pickSavePath(defaultName: String): String? {
|
||||||
return File(getAppPublicFolder(), defaultName).absolutePath
|
return File(getPAppPublicFolder(), defaultName).absolutePath
|
||||||
}
|
}
|
||||||
suspend fun openPdfFile(context: Context, filePath: String) {
|
suspend fun openPdfFile(context: Context, filePath: String) {
|
||||||
val file = File(filePath)
|
val file = File(filePath)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
package mg.dot.feufaro.ui
|
package mg.dot.feufaro.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun MyVerticalScrollbar (
|
actual fun MyVerticalScrollbar (
|
||||||
scrollState: ScrollState,
|
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
scrollState: ScrollState?,
|
||||||
|
lazyListState: LazyListState?,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
){
|
){
|
||||||
content()
|
content()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package mg.dot.feufaro
|
||||||
import feufaro.composeapp.generated.resources.Res
|
import feufaro.composeapp.generated.resources.Res
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
// Définissez une expect interface. Elle spécifie le contrat de votre repository.
|
// 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.
|
// Utilisez 'expect interface' car l'implémentation (actual) variera selon la plateforme.
|
||||||
|
|
@ -16,6 +17,7 @@ interface FileRepository {
|
||||||
//Lire le dernier dossier d'importation
|
//Lire le dernier dossier d'importation
|
||||||
suspend fun saveFile(filePath: String, data: ByteArray)
|
suspend fun saveFile(filePath: String, data: ByteArray)
|
||||||
suspend fun saveLocalFile(filePath: String, data: ByteArray)
|
suspend fun saveLocalFile(filePath: String, data: ByteArray)
|
||||||
|
suspend fun getAppPublicFolder() : File
|
||||||
fun getFileName(shortName: String) : String
|
fun getFileName(shortName: String) : String
|
||||||
// suspend fun readFileBytes(filePath: String): ByteArray
|
// suspend fun readFileBytes(filePath: String): ByteArray
|
||||||
fun pickSavePath(defaultName: String): String?
|
fun pickSavePath(defaultName: String): String?
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ object ScreenSolfa : Screen {
|
||||||
val gridTUOData = GridTUOData(measure, tuoList, stanza)
|
val gridTUOData = GridTUOData(measure, tuoList, stanza)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
var showContextualMenu = false
|
||||||
var viewportHeight by remember { mutableStateOf(0) }
|
var viewportHeight by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
var isScanning by remember { mutableStateOf(false) }
|
var isScanning by remember { mutableStateOf(false) }
|
||||||
|
|
@ -154,7 +155,7 @@ object ScreenSolfa : Screen {
|
||||||
)
|
)
|
||||||
Text(text = measureString)
|
Text(text = measureString)
|
||||||
Text(text = "Stanza: $stanza")
|
Text(text = "Stanza: $stanza")
|
||||||
ScreenTranspose.Content()
|
//ScreenTranspose.Content()
|
||||||
}
|
}
|
||||||
LazyVerticalGridTUO(
|
LazyVerticalGridTUO(
|
||||||
gridTUOData,
|
gridTUOData,
|
||||||
|
|
@ -219,7 +220,7 @@ object ScreenSolfa : Screen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MyVerticalScrollbar(
|
MyVerticalScrollbar(
|
||||||
scrollState,
|
scrollState = scrollState,
|
||||||
modifier = Modifier.align(Alignment.CenterEnd)
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import feufaro.composeapp.generated.resources.Res
|
||||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import mg.dot.feufaro.FileRepository
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
data class DrawerItem(
|
data class DrawerItem(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
|
@ -14,9 +16,7 @@ data class DrawerItem(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class FeuIndex(
|
data class FeuIndex(
|
||||||
val internalFeu: List<FeuItem>,
|
val internalFeu: List<FeuItem>
|
||||||
val externalFeu: List<FeuItem> = emptyList(),
|
|
||||||
val playlist: List<FeuItem> = emptyList()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -28,29 +28,58 @@ data class FeuItem(
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
@OptIn(ExperimentalResourceApi::class)
|
||||||
suspend fun getDynamicDrawerItems(): List<DrawerItem> {
|
suspend fun getCombinedList(fileRepository: FileRepository): List<DrawerItem> {
|
||||||
return try {
|
val finalList = mutableListOf<DrawerItem>()
|
||||||
|
|
||||||
|
try {
|
||||||
val bytes = Res.readBytes("files/feuList.json")
|
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 }
|
try {
|
||||||
val index = format.decodeFromString<FeuIndex>(jsonString)
|
val folderPath = fileRepository.getAppPublicFolder()
|
||||||
|
val directory = File(folderPath.absolutePath)
|
||||||
|
|
||||||
index.internalFeu.map { item ->
|
if (directory.exists() && directory.isDirectory) {
|
||||||
DrawerItem(
|
directory.listFiles { f -> f.extension == "txt" }?.forEachIndexed { i, file ->
|
||||||
id = item.id,
|
finalList.add(DrawerItem(
|
||||||
title = item.fileName.removeSuffix(".txt"),
|
id = 50000 + i,
|
||||||
contentTitle = item.title,
|
title = file.name.removeSuffix(".txt"),
|
||||||
path = item.path
|
contentTitle = extractTitle(file),
|
||||||
)
|
path = file.absolutePath
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Erreur JSON : ${e.message}")
|
e.printStackTrace()
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return finalList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractTitle(content: String): String? {
|
private fun extractTitle(content: String): String? {
|
||||||
val regex = Regex("""\|t:([^|]+)""")
|
val regex = Regex("""\|t:([^|]+)""")
|
||||||
return regex.find(content)?.groupValues?.get(1)?.trim()
|
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.launchFilePicker
|
||||||
import mg.dot.feufaro.midi.MidiPitch
|
import mg.dot.feufaro.midi.MidiPitch
|
||||||
import mg.dot.feufaro.midi.MidiWriterKotlin
|
import mg.dot.feufaro.midi.MidiWriterKotlin
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
//@todo: split voices (ffpm19/ews22) ${S:mfs} in N4:, idem ffpm-212
|
||||||
//@todo: ffpm-172-2 ${O:1} non fonctionnel
|
//@todo: ffpm-172-2 ${O:1} non fonctionnel
|
||||||
|
|
@ -163,10 +164,22 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
screenModelScope.launch(Dispatchers.Default) {
|
screenModelScope.launch(Dispatchers.Default) {
|
||||||
sharedScreenModel.reset()
|
sharedScreenModel.reset()
|
||||||
parse(path)
|
|
||||||
stateSettings.saveLastUsedDir(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 {
|
} else {
|
||||||
println("Pas de dichier")
|
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 = {
|
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
||||||
|
|
@ -352,7 +353,7 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
} else {
|
} else {
|
||||||
if (filteredSongs.isNotEmpty()) {
|
if (filteredSongs.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth().fillMaxHeight().align(Alignment.TopCenter)
|
modifier = Modifier.fillMaxWidth(0.75f).align(Alignment.TopCenter)
|
||||||
.background(MaterialTheme.colorScheme.surface).border(
|
.background(MaterialTheme.colorScheme.surface).border(
|
||||||
1.dp,
|
1.dp,
|
||||||
MaterialTheme.colorScheme.outlineVariant,
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
|
@ -361,10 +362,23 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
) {
|
) {
|
||||||
if (filteredSongs.isNotEmpty()) {
|
if (filteredSongs.isNotEmpty()) {
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
itemsIndexed(filteredSongs) { index, item ->
|
itemsIndexed(filteredSongs, key = {_, item -> item.path}) { index, item ->
|
||||||
|
val isFavorite = favoritePaths.contains(item)
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(item.title) },
|
headlineContent = { Text(item.title) },
|
||||||
supportingContent = { Text(item.contentTitle, maxLines = 1) },
|
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 {
|
modifier = Modifier.clickable {
|
||||||
sharedScreenModel.updateSearchTxt("")
|
sharedScreenModel.updateSearchTxt("")
|
||||||
sharedScreenModel.reset()
|
sharedScreenModel.reset()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
package mg.dot.feufaro.ui
|
package mg.dot.feufaro.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun MyVerticalScrollbar (
|
expect fun MyVerticalScrollbar (
|
||||||
scrollState: ScrollState,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable () -> Unit,
|
scrollState: ScrollState? = null,
|
||||||
|
lazyListState: LazyListState? = null,
|
||||||
|
content: @Composable () -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,31 @@
|
||||||
package mg.dot.feufaro.ui
|
package mg.dot.feufaro.ui
|
||||||
|
|
||||||
import SharedScreenModel
|
import SharedScreenModel
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.*
|
||||||
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.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Book
|
import androidx.compose.material.icons.automirrored.filled.EventNote
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.automirrored.filled.Note
|
||||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
import androidx.compose.material.icons.automirrored.filled.StarHalf
|
||||||
import androidx.compose.material.icons.filled.MusicNote
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.runtime.*
|
||||||
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.Color
|
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.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mg.dot.feufaro.data.DrawerItem
|
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.solfa.Solfa
|
||||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleDrawerContent(
|
fun SimpleDrawerContent(
|
||||||
items: List<DrawerItem>,
|
items: List<DrawerItem>,
|
||||||
|
|
@ -56,118 +49,424 @@ fun SimpleDrawerContent(
|
||||||
|
|
||||||
val context = getPlatform()
|
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(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.width(300.dp)
|
modifier = Modifier.width(300.dp)
|
||||||
) {
|
) {
|
||||||
Box (
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.fillMaxSize()
|
Box(modifier = Modifier.weight(1f).padding(0.dp, 20.dp)) {
|
||||||
)
|
|
||||||
{
|
MyVerticalScrollbar(
|
||||||
val lazyListState = rememberLazyListState()
|
lazyListState = listState,
|
||||||
ScrollableDrawerContent(
|
modifier = Modifier
|
||||||
lazyListState = lazyListState,
|
.align(Alignment.CenterEnd)
|
||||||
modifier = Modifier.fillMaxSize()
|
.fillMaxHeight()
|
||||||
) {
|
.padding(end = 2.dp)
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
){
|
){
|
||||||
stickyHeader {
|
LazyColumn(
|
||||||
Surface(
|
state = listState,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
modifier = Modifier.fillMaxSize()
|
||||||
modifier = Modifier.fillParentMaxWidth()
|
) {
|
||||||
) {
|
stickyHeader {
|
||||||
Text(
|
DrawerHeaderSticky(
|
||||||
"Liste des solfa disponibles",
|
title = "Solfa disponibles",
|
||||||
style = MaterialTheme.typography.titleLarge
|
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")) {
|
if (internalExpanded) {
|
||||||
isFfpm = true
|
items(internalList) { item ->
|
||||||
} else if(title.startsWith("ews")) {
|
val isSelected = item.path == activePath
|
||||||
isEws = true
|
NavigationDrawerItem(
|
||||||
} else {
|
label = { DrawerItemLabel(item, sharedScreenModel) },
|
||||||
isFF = true
|
selected = isSelected,
|
||||||
}
|
onClick = {
|
||||||
NavigationDrawerItem(
|
scope.launch { drawerState.close() }
|
||||||
label = {
|
sharedScreenModel.reset()
|
||||||
Text(item.title)
|
solfaScreenModel.loadFromFile(item.path)
|
||||||
},
|
onSongSelected(midi)
|
||||||
icon = {
|
},
|
||||||
val isIcon = when {
|
shape = RoundedCornerShape(5.dp),
|
||||||
isFfpm -> Icons.AutoMirrored.Filled.MenuBook
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||||
isEws -> Icons.Filled.MusicNote
|
colors = NavigationDrawerItemDefaults.colors(
|
||||||
isFF -> Icons.Filled.Book
|
selectedContainerColor = Color(0, 157, 255).copy(alpha = 0.1f),
|
||||||
else -> Icons.Filled.Menu
|
selectedTextColor = Color(0, 157, 255)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
}
|
||||||
modifier = Modifier
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
.fillMaxWidth()
|
Row(
|
||||||
.align(Alignment.BottomCenter)
|
modifier = Modifier
|
||||||
.padding(16.dp),
|
.fillMaxWidth()
|
||||||
contentAlignment = Alignment.Center
|
.padding(16.dp),
|
||||||
) {
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
Column(
|
) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
Button(onClick = {
|
||||||
) {
|
scope.launch { drawerState.close() }
|
||||||
Row (
|
}) {
|
||||||
|
Text("Scanner")
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
solfaScreenModel.loadCustomFile()
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
}) {
|
||||||
|
Text("Importer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
) {
|
@Composable
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
fun DrawerAccordionSection(
|
||||||
Button(
|
title: String,
|
||||||
onClick = {
|
items: List<DrawerItem>,
|
||||||
onScannerButtonClick()
|
icon: ImageVector,
|
||||||
}
|
iconColor: Color,
|
||||||
) {
|
activePath: String,
|
||||||
Text("Scanner")
|
scope: CoroutineScope,
|
||||||
}
|
solfaScreenModel: SolfaScreenModel,
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
onItemClick: (DrawerItem) -> Unit
|
||||||
Button(
|
) {
|
||||||
onClick = {
|
var expanded by remember { mutableStateOf(true) }
|
||||||
scope.launch {
|
|
||||||
drawerState.close()
|
val rotationState by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
|
||||||
}
|
|
||||||
solfaScreenModel.loadCustomFile()
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
}
|
Row(
|
||||||
) {
|
modifier = Modifier
|
||||||
Text("Importer")
|
.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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -18,10 +20,11 @@ import mg.dot.feufaro.DeepLinkHandler
|
||||||
import mg.dot.feufaro.FileRepository
|
import mg.dot.feufaro.FileRepository
|
||||||
import mg.dot.feufaro.data.DrawerItem
|
import mg.dot.feufaro.data.DrawerItem
|
||||||
import mg.dot.feufaro.data.GridTUOData
|
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.solfa.TimeUnitObject
|
||||||
import mg.dot.feufaro.midi.FMediaPlayer
|
import mg.dot.feufaro.midi.FMediaPlayer
|
||||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel {
|
class SharedScreenModel(private val fileRepository: FileRepository) : ScreenModel {
|
||||||
private val _nextLabel = MutableStateFlow<String>("Next ...")
|
private val _nextLabel = MutableStateFlow<String>("Next ...")
|
||||||
|
|
@ -68,13 +71,127 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
private val _drawerItems = MutableStateFlow<List<DrawerItem>>(emptyList())
|
||||||
val drawerItems: StateFlow<List<DrawerItem>> = _drawerItems.asStateFlow()
|
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 {
|
init {
|
||||||
loadItems()
|
loadItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadItems() {
|
fun loadItems() {
|
||||||
screenModelScope.launch {
|
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) {
|
override suspend fun saveFile(filePath: String, data: ByteArray) {
|
||||||
val userHome = System.getProperty("user.home")
|
val userHome = System.getProperty("user.home")
|
||||||
val file = File("$userHome/$filePath")
|
val file = File("$userHome/$filePath")
|
||||||
file.writeBytes(data) // Extension Kotlin très efficace
|
file.writeBytes(data)
|
||||||
}
|
}
|
||||||
override suspend fun saveLocalFile(filePath: String, data: ByteArray) {
|
override suspend fun saveLocalFile(filePath: String, data: ByteArray) {
|
||||||
val file = if (filePath.startsWith("/")) {
|
val file = if (filePath.startsWith("/")) {
|
||||||
File(filePath) // chemin absolu → utilisé directement
|
File(filePath)
|
||||||
} else {
|
} 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.parentFile?.mkdirs()
|
||||||
file.writeBytes(data) // Extension Kotlin très efficace
|
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 {
|
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.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.rememberScrollbarAdapter
|
import androidx.compose.foundation.rememberScrollbarAdapter
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -14,15 +15,26 @@ import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun MyVerticalScrollbar (
|
actual fun MyVerticalScrollbar (
|
||||||
scrollState: ScrollState,
|
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
scrollState: ScrollState?,
|
||||||
|
lazyListState: LazyListState?,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box ( modifier = Modifier.fillMaxSize()){
|
Box ( modifier = Modifier.fillMaxSize()){
|
||||||
VerticalScrollbar(
|
content()
|
||||||
adapter = rememberScrollbarAdapter(scrollState = scrollState),
|
|
||||||
modifier = modifier.fillMaxHeight(0.5f).background(Color.Green)
|
val adapter = when {
|
||||||
)
|
lazyListState != null -> rememberScrollbarAdapter(lazyListState)
|
||||||
//content()
|
scrollState != null -> rememberScrollbarAdapter(scrollState)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adapter != null) {
|
||||||
|
VerticalScrollbar(
|
||||||
|
adapter = adapter,
|
||||||
|
modifier = modifier.fillMaxHeight(0.5f).background(Color.Green.copy(0.75f))
|
||||||
|
)
|
||||||
|
//content()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue