diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index ebb96d8..c4f11fb 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -29,6 +29,7 @@ kotlin { implementation(compose.preview) implementation(libs.androidx.activity.compose) implementation(libs.koin.android) // Koin Android-specific extensions + implementation(libs.koin.androidx.compose) } commonMain.dependencies { @@ -48,6 +49,11 @@ kotlin { implementation(libs.serialization) api(libs.kmp.observableviewmodel.core) implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.material3) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.compose.viewmodel.navigation) + } commonTest.dependencies { implementation(libs.kotlin.test) @@ -56,7 +62,6 @@ kotlin { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) } - //androidMain.dependsOn(androidAndJvmMain) } } diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 26403a7..c0af1cb 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -2,6 +2,7 @@ = withContext(Dispatchers.IO) { - try { - when { - filePath.startsWith("assets://") -> { - readAssetFileLines(filePath) - } - - else -> { - File(filePath).readLines() - } - } - } catch (e: IOException) { - throw IOException("Failed to read file or asset '$filePath'") - } - - } - override suspend fun readFileContent(filePath: String): String = withContext(Dispatchers.IO) { "" } - private fun readAssetFileLines(assetFileName: String): List { - return context.assets.open(assetFileName).use { inputStream -> - BufferedReader(InputStreamReader(inputStream)).useLines { it.toList() } - } - } - private fun readAssetFileContent(assetFileName: String): String { - - return context.assets.open(assetFileName).use { inputStream -> - // BufferedReader(InputStreamReader(inputStream)) permet de lire le texte ligne par ligne. - BufferedReader(InputStreamReader(inputStream)).use { reader -> - reader.readText() // Lit tout le contenu en une seule chaîne. - } - } - } -} diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt index 67eae5d..5e7ec2c 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/MainActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import org.koin.androidx.compose.koinViewModel class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -13,7 +14,8 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - App() + val sharedViewModel: SharedViewModel = koinViewModel() + App(sharedViewModel = sharedViewModel) } } } diff --git a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt index 57b8e57..b1339a2 100644 --- a/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt +++ b/composeApp/src/androidMain/kotlin/mg/dot/feufaro/di/AndroidModule.kt @@ -1,7 +1,6 @@ package mg.dot.feufaro.di import android.content.Context -import mg.dot.feufaro.AndroidFileRepository // Import the actual Android implementation import mg.dot.feufaro.config.AppConfig import org.koin.core.module.dsl.singleOf // Import for Koin DSL import org.koin.core.module.dsl.bind // Import for Koin DSL @@ -12,15 +11,10 @@ import org.koin.android.ext.koin.androidContext // Import for androidContext() val androidModule = module { // Koin will automatically provide the Android Context because you called androidContext() in AndroidApp. // It then uses this Context to construct AndroidFileRepository. - singleOf(::AndroidFileRepository) { bind() } single { AppConfig( transposeto = "C", transposeasif = "C", - buttonContainerColorHex = "#ff00f0", - buttonContentColorHex = "#ffffff", - buttonDisabledContainerColorHex = "#999999", - buttonDisabledContentColorHex = "#ffffff", ) } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt index 4553ad3..59a779b 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/App.kt @@ -3,6 +3,7 @@ package mg.dot.feufaro import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow @@ -26,6 +27,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -41,7 +43,10 @@ import kotlinx.coroutines.launch import mg.dot.feufaro.solfa.LazyVerticalGridTUO import mg.dot.feufaro.solfa.Solfa import org.koin.compose.koinInject - +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.window.Popup +import kotlin.math.roundToInt @Composable @Preview @@ -53,6 +58,8 @@ fun App(sharedViewModel: SharedViewModel = SharedViewModel() val currentDisplayConfig by displayConfigManager.displayConfig.collectAsState() // Load Configurations val configScope = CoroutineScope(Dispatchers.Default) + var showContextualMenu by remember { mutableStateOf(false)} + var menuPosition by remember { mutableStateOf(Offset.Zero)} LaunchedEffect(Unit) { configScope.launch { @@ -82,7 +89,34 @@ fun App(sharedViewModel: SharedViewModel = SharedViewModel() MaterialTheme { var showContent by remember { mutableStateOf(false) } var gridWidthPx by remember { mutableStateOf(0) } - Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + Column( + Modifier.fillMaxSize() + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + offset -> + menuPosition = offset + showContextualMenu = true + } + ) + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (showContextualMenu) { + Popup( + alignment = Alignment.TopStart, + offset = IntOffset(menuPosition.x.roundToInt(), + menuPosition.y.roundToInt()), + onDismissRequest = { + showContextualMenu = false + } + ) { + ContextualMenu(onMenuItemClick = { item -> + println("Clicked: $item") + showContextualMenu = false + }) + } + } Column( modifier = Modifier .fillMaxWidth() diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt new file mode 100644 index 0000000..a312d1a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/ContextualMenu.kt @@ -0,0 +1,40 @@ +package mg.dot.feufaro + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.List +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.material3.IconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.ui.unit.TextUnitType +@Composable +fun ContextualMenu (onMenuItemClick: (String) -> Unit){ + Column( + modifier = Modifier + .background(Color.DarkGray.copy(alpha = 0.8f), RoundedCornerShape(8.dp)) + .padding(8.dp) + ) { + MenuItem(icon = Icons.Default.Add, text = "+") { onMenuItemClick("Ajouter")} + MenuItem(icon = Icons.Default.Edit, text = "!") { onMenuItemClick("Modifier")} + MenuItem(icon = Icons.Default.List, text = "-") { onMenuItemClick("Liste")} + } +} + +@Composable +fun MenuItem(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String, onClick: () -> Unit) { + IconButton(onClick = onClick) { + Column(horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally) { + Icon(icon, contentDescription = text, tint = Color.White) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt index 77d63dc..a7af635 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/FileRepository.kt @@ -1,13 +1,50 @@ package mg.dot.feufaro import java.io.IOException // Java.io.IOException est généralement partagée sur JVM/Android +import feufaro.composeapp.generated.resources.Res +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File // 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. -expect interface FileRepository { +interface FileRepository { // Lecture de fichier ligne par ligne suspend fun readFileLines(filePath: String): List // Lecture de fichier entier en tant que String suspend fun readFileContent(filePath: String): String +} + +// This is just a regular class that implements the common 'FileRepository' interface. +// It is NOT an 'actual' declaration of 'FileRepository'. +class CommonFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE commonMain 'FileRepository' interface + override suspend fun readFileLines(filePath: String): List = withContext(Dispatchers.IO) { + try { + when { + filePath.startsWith("assets://") -> { + readAssetFileLines(filePath) + } + + else -> { + File(filePath).readLines() + } + } + } catch (e: IOException) { + throw IOException("Failed to read file or asset '$filePath'") + } + } + override suspend fun readFileContent(filePath: String): String = withContext(Dispatchers.IO) { + val lines = readFileLines(filePath) + lines.joinToString("\n") { it } + } + private suspend fun readAssetFileLines(assetFileName: String): List { + return try { + Res.readBytes("files/"+assetFileName.removePrefix("assets://")).decodeToString().split("\n") + } catch (e: IOException) { + println("Could not read /"+assetFileName.removePrefix("assets://")) + throw IOException("Could not read asset file: $assetFileName", e) + } + + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/SharedViewModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/SharedViewModel.kt index 575b86e..4fa50fe 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/SharedViewModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/SharedViewModel.kt @@ -29,6 +29,8 @@ open class SharedViewModel(): ViewModel() { val tuoList: StateFlow> = _tuoList.asStateFlow() private var _playlist = MutableStateFlow>(viewModelScope, emptyList()) val playlist: StateFlow> = _playlist.asStateFlow() + private var _nextPlayed = MutableStateFlow(viewModelScope, -1) + val nextPlayed: StateFlow = _nextPlayed.asStateFlow() private val tempTimeUnitObjectList = mutableListOf() var _hasMarker = MutableStateFlow(viewModelScope, false) val hasMarker: StateFlow = _hasMarker.asStateFlow() @@ -83,4 +85,12 @@ open class SharedViewModel(): ViewModel() { _hasMarker.value = theHasMarker } + fun playNext() { + val nextIndex = (_nextPlayed.value + 1) % _playlist.value.size + _nextPlayed.value = nextIndex + } + fun currentPlayed(): String { + val playlistIndex = _nextPlayed.value + return _playlist.value[playlistIndex] + } } diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt index d754ea0..a8c2b5b 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/di/AppModule.kt @@ -1,18 +1,22 @@ package mg.dot.feufaro.di +import mg.dot.feufaro.CommonFileRepository import mg.dot.feufaro.FileRepository +import mg.dot.feufaro.SharedViewModel import mg.dot.feufaro.DisplayConfigManager // Importez DisplayConfigManager import mg.dot.feufaro.config.AppConfig import mg.dot.feufaro.musicXML.MusicXML import org.koin.dsl.module import org.koin.core.module.dsl.singleOf import org.koin.core.module.Module +import org.koin.core.module.dsl.viewModel val commonModule = module { // Déclarez FileRepository comme un singleton. // L'implémentation concrète (actual) sera résolue par Koin en fonction de la plateforme. // Pour Android, Koin injectera le Context que vous avez fourni via androidContext(). singleOf(::MusicXML) + single { CommonFileRepository() } single { DisplayConfigManager(fileRepository = get())} - + viewModel { SharedViewModel() } } \ 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 a5760c4..afe0f98 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -36,7 +36,6 @@ class Solfa(val sharedViewModel: SharedViewModel, private val fileRepository: Fi var nextLIndex: Int = -1 var inGroup: Boolean = false var templateString: String = "" - var nextPlayed = -1 private val meta: MutableMap = mutableMapOf() private val lyricsComment: MutableList = mutableListOf() @@ -94,15 +93,16 @@ class Solfa(val sharedViewModel: SharedViewModel, private val fileRepository: Fi } nextTimeUnitObject() } - fun loadSolfa(sourceFileName: String) { + fun loadSolfa() { + val sourceFileName: String = sharedViewModel.currentPlayed() sharedViewModel.reset() parse(sourceFileName) } fun loadNextInPlaylist() { - val playlist = sharedViewModel?.playlist?.value ?: listOf() + val playlist = sharedViewModel.playlist.value if (playlist.isNotEmpty()) { - nextPlayed = (nextPlayed + 1) % playlist.size - loadSolfa(playlist[nextPlayed]) + sharedViewModel.playNext() + loadSolfa() } } fun parse(sourceFile: String) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index 4fabf47..3748407 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -6,9 +6,9 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.Text import androidx.compose.ui.Modifier +import androidx.compose.material3.Text +import androidx.compose.material3.LocalTextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.runtime.LaunchedEffect diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/DesktopFileRepository.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/DesktopFileRepository.kt deleted file mode 100644 index db97e10..0000000 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/DesktopFileRepository.kt +++ /dev/null @@ -1,41 +0,0 @@ -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.IOException - - -// This is just a regular class that implements the common 'FileRepository' interface. -// It is NOT an 'actual' declaration of 'FileRepository'. -class DesktopFileRepository : FileRepository { // IMPORTS AND IMPLEMENTS THE commonMain 'FileRepository' interface - override suspend fun readFileLines(filePath: String): List = withContext(Dispatchers.IO) { - try { - when { - filePath.startsWith("assets://") -> { - readAssetFileLines(filePath) - } - - else -> { - File(filePath).readLines() - } - } - } catch (e: IOException) { - throw IOException("Failed to read file or asset '$filePath'") - } - } - override suspend fun readFileContent(filePath: String): String = withContext(Dispatchers.IO) { - val lines = readFileLines(filePath) - lines.joinToString("\n") { it } - } - private suspend fun readAssetFileLines(assetFileName: String): List { - return try { - Res.readBytes("files/"+assetFileName.removePrefix("assets://")).decodeToString().split("\n") - } catch (e: IOException) { - println("Could not read /"+assetFileName.removePrefix("assets://")) - throw IOException("Could not read asset file: $assetFileName", e) - } - - } -} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/FileRepository.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/FileRepository.kt deleted file mode 100644 index 87661b2..0000000 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/FileRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package mg.dot.feufaro - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import mg.dot.feufaro.FileRepository - - -actual interface FileRepository { - actual suspend fun readFileLines(filePath: String): List - actual suspend fun readFileContent(filePath: String): String -} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt index 313378d..5b43312 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/di/DesktopModule.kt @@ -1,6 +1,5 @@ package mg.dot.feufaro.di -import mg.dot.feufaro.DesktopFileRepository // Import the actual desktop implementation import org.koin.core.module.dsl.singleOf // Import for Koin DSL import org.koin.core.module.dsl.bind // Import for Koin DSL import org.koin.dsl.module @@ -10,11 +9,10 @@ import mg.dot.feufaro.config.AppConfig val desktopModule = module { // When Koin is initialized on Desktop, it will use DesktopFileRepository // as the implementation for the FileRepository interface. - singleOf(::DesktopFileRepository) { bind() } single { AppConfig( transposeto = "C", - transposeasif = "C" + transposeasif = "C" ) } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt index 2e527f8..8ad306c 100644 --- a/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt +++ b/composeApp/src/desktopMain/kotlin/mg/dot/feufaro/main.kt @@ -9,6 +9,7 @@ import org.koin.core.context.KoinContext import org.koin.core.logger.Level import org.koin.compose.KoinContext + fun main() = application { startKoin { printLogger(Level.INFO) // Mettez Level.INFO pour voir les logs de Koin @@ -24,7 +25,8 @@ fun main() = application { title = "Feufaro", ) { KoinContext { - App() + val sharedViewModel: SharedViewModel = SharedViewModel() + App(sharedViewModel = sharedViewModel) } } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b80f210..642a78b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.3" +agp = "8.10.1" android-compileSdk = "35" android-minSdk = "24" android-targetSdk = "35" @@ -20,8 +20,11 @@ koin = "4.0.4" core = "0.91.1" kmpObservableviewmodelCore = "1.0.0-BETA-3" kotlinxSerializationJson = "1.8.1" +material3 = "1.3.2" [libraries] +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } +koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -39,13 +42,15 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-core-viewmodel = { module = "io.insert-koin:koin-core-viewmodel", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } -#20250704 +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } + core = { module = "io.github.pdvrieze.xmlutil:core", version.ref = "core" } #core-android = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "core" } #core-jdk = { module = "io.github.pdvrieze.xmlutil:core-jdk", version.ref = "core" } serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "core" } kmp-observableviewmodel-core = { module = "com.rickclephas.kmp:kmp-observableviewmodel-core", version.ref = "kmpObservableviewmodelCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME