diff --git a/app/debug/app-debug.apk b/app/debug/app-debug.apk new file mode 100644 index 0000000..a0bc7f8 Binary files /dev/null and b/app/debug/app-debug.apk differ diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/MainActivity.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/MainActivity.kt index 7eece91..0030639 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/MainActivity.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/MainActivity.kt @@ -1,9 +1,12 @@ package com.lebaillyapp.dynamicvisualeffectsagsl import android.os.Bundle +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import com.lebaillyapp.dynamicvisualeffectsagsl.navigation.AppNavHost import com.lebaillyapp.dynamicvisualeffectsagsl.ui.theme.DynamicVisualEffectsAGSLTheme @@ -11,7 +14,24 @@ import com.lebaillyapp.dynamicvisualeffectsagsl.ui.theme.DynamicVisualEffectsAGS class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // Enable modern edge-to-edge support enableEdgeToEdge() + + // 1. Allow the window to use the full screen including the notch/cutout area + window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + + // 2. Use FLAG_LAYOUT_NO_LIMITS to allow the window to extend beyond standard screen decorations + // This ensures the visualisation fills the entire display without being constrained by system bar areas. + window.setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + ) + + // 3. Hide the system bars (Status Bar and Navigation Bar) + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + setContent { DynamicVisualEffectsAGSLTheme { AppNavHost() diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/OptimizedHolographicCardEffect.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/OptimizedHolographicCardEffect.kt index 1c5090b..20bcec2 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/OptimizedHolographicCardEffect.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/OptimizedHolographicCardEffect.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.toSize +import java.io.File import kotlin.math.PI @Composable @@ -37,6 +38,7 @@ fun OptimizedHolographicCardEffect( modifier: Modifier = Modifier, bitmap: ImageBitmap, @RawRes shaderResId: Int, + shaderName: String, // === PARAMÈTRES D'ENTRÉE CONTROLLABLES EXTERNEMENT === hologramStrength: Float = 1.5f, // Opacité de l’effet holographique (sur les zones sombres) @@ -96,11 +98,22 @@ fun OptimizedHolographicCardEffect( } // === LECTURE DU SHADER CODE === - val shaderCode = remember { - context.resources.openRawResource(shaderResId) - .bufferedReader().use { it.readText() } + val file = remember(shaderName) { File(context.filesDir, "$shaderName.agsl") } + val shaderCode = remember(shaderName, file.lastModified()) { + if (file.exists()) { + file.readText() + } else { + context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + } + } + val shader = remember(shaderCode) { + try { + RuntimeShader(shaderCode) + } catch (_: Exception) { + val defaultCode = context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + RuntimeShader(defaultCode) + } } - val shader = remember { RuntimeShader(shaderCode) } // === MESURE DU COMPOSANT POUR SET uResolution === var composableSize by remember { mutableStateOf(Size.Zero) } diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/UltraRealisticHolographicEffectShader.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/UltraRealisticHolographicEffectShader.kt index 18f0ce1..3e2a066 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/UltraRealisticHolographicEffectShader.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/holographicEffect/composition/UltraRealisticHolographicEffectShader.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.toSize import kotlinx.coroutines.delay +import java.io.File import kotlin.math.PI import android.util.Log @@ -39,6 +40,7 @@ fun UltraRealisticHolographicEffectShader( modifier: Modifier = Modifier, bitmap: ImageBitmap, @RawRes shaderResId: Int, + shaderName: String, // Effets principaux (simplifiés) effectIntensity: Float = 0.8f, @@ -126,10 +128,23 @@ fun UltraRealisticHolographicEffectShader( } } - val shaderCode = remember { - context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + // Load shader code: check internal storage first, then fallback to resources + val file = remember(shaderName) { File(context.filesDir, "$shaderName.agsl") } + val shaderCode = remember(shaderName, file.lastModified()) { + if (file.exists()) { + file.readText() + } else { + context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + } + } + val shader = remember(shaderCode) { + try { + RuntimeShader(shaderCode) + } catch (_: Exception) { + val defaultCode = context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + RuntimeShader(defaultCode) + } } - val shader = remember { RuntimeShader(shaderCode) } var composableSize by remember { mutableStateOf(Size.Zero) } diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/AppNavHost.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/AppNavHost.kt index e91c726..33f041c 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/AppNavHost.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/AppNavHost.kt @@ -1,26 +1,53 @@ package com.lebaillyapp.dynamicvisualeffectsagsl.navigation +import android.app.ActivityManager +import android.content.Context +import android.graphics.BitmapFactory +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.imageResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import com.lebaillyapp.dynamicvisualeffectsagsl.R -import com.lebaillyapp.dynamicvisualeffectsagsl.holographicEffect.composition.HolographicEffectBitmapShader +import com.lebaillyapp.dynamicvisualeffectsagsl.navigation.ShaderEditorScreen import com.lebaillyapp.dynamicvisualeffectsagsl.holographicEffect.composition.OptimizedHolographicCardEffect import com.lebaillyapp.dynamicvisualeffectsagsl.holographicEffect.composition.UltraRealisticHolographicEffectShader import com.lebaillyapp.dynamicvisualeffectsagsl.topographicflowEffect.TopographicFlowShader import com.lebaillyapp.dynamicvisualeffectsagsl.topographicflowEffect.TopographicFlowShaderWithControls import com.lebaillyapp.dynamicvisualeffectsagsl.waterEffect.composition.WaterEffectBitmapShader - @Composable fun AppNavHost() { val navController = rememberNavController() + val context = LocalContext.current + + var userSelectedBitmap by remember { mutableStateOf(null) } + val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (uri != null) { + val inputStream = context.contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(inputStream) + userSelectedBitmap = bitmap?.asImageBitmap() + } + } NavHost( navController = navController, @@ -29,65 +56,120 @@ fun AppNavHost() { composable("menu") { EffectsMenu( - onSelect = { route -> navController.navigate(route) } + onSelect = { route -> navController.navigate(route) }, + onEdit = { shaderName -> navController.navigate("editor/$shaderName") } ) } - composable("water") { - val bitmap = ImageBitmap.imageResource(R.drawable.demopic_e) - WaterEffectBitmapShader( - modifier = Modifier.fillMaxSize().background(Color.Black), - bitmap = bitmap, - shaderResId = R.raw.water_shader + composable( + "editor/{shaderName}", + arguments = listOf(navArgument("shaderName") { type = NavType.StringType }) + ) { backStackEntry -> + val shaderName = backStackEntry.arguments?.getString("shaderName") ?: "" + ShaderEditorScreen( + shaderName = shaderName, + onBack = { navController.popBackStack() } ) } - composable("holo_base") { - val bitmap = ImageBitmap.imageResource(R.drawable.demopic_d) - OptimizedHolographicCardEffect( - modifier = Modifier.fillMaxSize(), - bitmap = bitmap, - shaderResId = R.raw.holographic_rainbow, - microDetailScale = 2f // more = unzoom , less = zoom (on the effect...) - + composable("water") { + val defaultBitmap = ImageBitmap.imageResource(R.drawable.demopic_e) + EffectContainer(onPickImage = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }) { + WaterEffectBitmapShader( + modifier = Modifier.fillMaxSize().background(Color.Black), + bitmap = userSelectedBitmap ?: defaultBitmap, + shaderResId = R.raw.water_shader, + shaderName = "water_shader" + ) + } + } - ) + composable("holo_base") { + val defaultBitmap = ImageBitmap.imageResource(R.drawable.demopic_d) + EffectContainer(onPickImage = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }) { + OptimizedHolographicCardEffect( + modifier = Modifier.fillMaxSize(), + bitmap = userSelectedBitmap ?: defaultBitmap, + shaderResId = R.raw.holographic_rainbow, + shaderName = "holographic_rainbow", + microDetailScale = 2f + ) + } } composable("holo_Iridescent") { - val bitmap = ImageBitmap.imageResource(R.drawable.demopic_e) - UltraRealisticHolographicEffectShader( - modifier = Modifier.fillMaxSize(), - bitmap = bitmap, - shaderResId = R.raw.holographic_realistic_shader, - effectIntensity = 6.8f, - fresnelPower = 6.0f, - rainbowScale = 1.2f, - rainbowOffset = 0.2f, - normalStrength = 2.0f, - microDetailScale = 45.0f - ) + val defaultBitmap = ImageBitmap.imageResource(R.drawable.demopic_e) + EffectContainer(onPickImage = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }) { + UltraRealisticHolographicEffectShader( + modifier = Modifier.fillMaxSize(), + bitmap = userSelectedBitmap ?: defaultBitmap, + shaderResId = R.raw.holographic_realistic_shader, + shaderName = "holographic_realistic_shader", + effectIntensity = 6.8f, + fresnelPower = 6.0f, + rainbowScale = 1.2f, + rainbowOffset = 0.2f, + normalStrength = 2.0f, + microDetailScale = 45.0f + ) + } } composable("holo_card") { - val bitmap = ImageBitmap.imageResource(R.drawable.de2) - OptimizedHolographicCardEffect( - modifier = Modifier.fillMaxSize(), - bitmap = bitmap, - shaderResId = R.raw.holographic_card_shader - ) + val defaultBitmap = ImageBitmap.imageResource(R.drawable.de2) + EffectContainer(onPickImage = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }) { + OptimizedHolographicCardEffect( + modifier = Modifier.fillMaxSize(), + bitmap = userSelectedBitmap ?: defaultBitmap, + shaderResId = R.raw.holographic_card_shader, + shaderName = "holographic_card_shader" + ) + } } composable("topo") { - TopographicFlowShader(modifier = Modifier.fillMaxSize()) + TopographicFlowShader( + modifier = Modifier.fillMaxSize(), + shaderResId = R.raw.topographicflow_shader, + shaderName = "topographicflow_shader" + ) } composable("topo_controls") { - TopographicFlowShaderWithControls(modifier = Modifier.fillMaxSize()) + TopographicFlowShaderWithControls( + modifier = Modifier.fillMaxSize(), + shaderResId = R.raw.topographicflow_shader, + shaderName = "topographicflow_shader" + ) } composable("fire") { - //later ! + //later ! + } + } +} + +@Composable +fun EffectContainer(onPickImage: () -> Unit, content: @Composable () -> Unit) { + val context = LocalContext.current + val isPinned = remember { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + activityManager.lockTaskModeState != ActivityManager.LOCK_TASK_MODE_NONE + } + + Box(modifier = Modifier.fillMaxSize()) { + content() + if (!isPinned) { + FloatingActionButton( + onClick = onPickImage, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(32.dp), + containerColor = Color.Black.copy(alpha = 0.6f), + contentColor = Color.White + ) { + Icon(Icons.Default.Star, contentDescription = "Pick Image") + } } } } diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/EffectsMenu.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/EffectsMenu.kt index b391c03..7614e35 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/EffectsMenu.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/navigation/EffectsMenu.kt @@ -2,12 +2,19 @@ package com.lebaillyapp.dynamicvisualeffectsagsl.navigation import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +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.heightIn 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.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,14 +22,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @Composable -fun EffectsMenu(onSelect: (String) -> Unit) { +fun EffectsMenu( + onSelect: (String) -> Unit, + onEdit: (String) -> Unit +) { val items = listOf( - "Water Effect" to "water", - "Holographic Base" to "holo_base", - "Holographic Iridescent " to "holo_Iridescent", - "Holographic Card" to "holo_card", - "Topographic Flow" to "topo", - "Topographic Flow + Controls" to "topo_controls" + Triple("Water Effect", "water", "water_shader"), + Triple("Holographic Base", "holo_base", "holographic_rainbow"), + Triple("Holographic Iridescent", "holo_Iridescent", "holographic_realistic_shader"), + Triple("Holographic Card", "holo_card", "holographic_card_shader"), + Triple("Topographic Flow", "topo", "topographicflow_shader"), + Triple("Topographic Flow + Controls", "topo_controls", "topographicflow_shader") ) Box( @@ -32,9 +42,24 @@ fun EffectsMenu(onSelect: (String) -> Unit) { .background(Color.Black) ) { LazyColumn(modifier = Modifier.align(Alignment.Center).padding(16.dp)) { - items(items) { (label, route) -> - MenuButton(label = label) { - onSelect(route) + items(items) { (label, route, shaderName) -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + MenuButton( + label = label, + modifier = Modifier.weight(1f) + ) { + onSelect(route) + } + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = { onEdit(shaderName) }, + modifier = Modifier.background(Color(0xFF1C1A1A), androidx.compose.foundation.shape.CircleShape) + ) { + Icon(Icons.Default.Edit, contentDescription = "Edit Shader", tint = Color.White) + } } } } @@ -42,15 +67,14 @@ fun EffectsMenu(onSelect: (String) -> Unit) { } @Composable -fun MenuButton(label: String, onClick: () -> Unit) { +fun MenuButton(label: String, modifier: Modifier = Modifier, onClick: () -> Unit) { androidx.compose.material3.Button( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 86.dp) + modifier = modifier + .heightIn(min = 64.dp) .padding(vertical = 8.dp), onClick = onClick, colors = androidx.compose.material3.ButtonDefaults.buttonColors(Color(0xFF1C1A1A)) ) { androidx.compose.material3.Text(text = label, color = Color.White) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShader.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShader.kt index d7f6357..ce8a427 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShader.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShader.kt @@ -1,17 +1,12 @@ package com.lebaillyapp.dynamicvisualeffectsagsl.topographicflowEffect import android.graphics.RuntimeShader +import androidx.annotation.RawRes import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameMillis +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -19,40 +14,59 @@ import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.platform.LocalContext import com.lebaillyapp.dynamicvisualeffectsagsl.R import com.lebaillyapp.dynamicvisualeffectsagsl.topographicflowEffect.model.TopographicFlowConfig +import java.io.File @Composable -fun TopographicFlowShader(modifier: Modifier) { +fun TopographicFlowShader( + modifier: Modifier, + @RawRes shaderResId: Int, + shaderName: String = "topographicflow_shader" +) { val context = LocalContext.current - val config = remember { + val prefs = remember { context.getSharedPreferences("topo_settings", android.content.Context.MODE_PRIVATE) } + + val config = remember( + prefs.getFloat("lineDensity", 15f), + prefs.getFloat("lineThickness", 0.05f), + prefs.getFloat("noiseScale", 1f), + prefs.getFloat("noiseIntensity", 0.25f), + prefs.getFloat("speedX", 0.20f), + prefs.getFloat("speedY", 0.05f), + prefs.getFloat("glowWidth", 1.1f), + prefs.getFloat("glowContrast", 0.5f) + ) { TopographicFlowConfig( - lineDensity = 25.0f, - lineThickness = 0.15f, - noiseScale = 1.0f, - noiseIntensity = 0.5f, - speedX = 0.20f, - speedY = 0.25f, - glowWidthMultiplier = 1.1f, - glowContrast = 0.5f + lineDensity = prefs.getFloat("lineDensity", 15f), + lineThickness = prefs.getFloat("lineThickness", 0.05f), + noiseScale = prefs.getFloat("noiseScale", 1f), + noiseIntensity = prefs.getFloat("noiseIntensity", 0.25f), + speedX = prefs.getFloat("speedX", 0.20f), + speedY = prefs.getFloat("speedY", 0.05f), + glowWidthMultiplier = prefs.getFloat("glowWidth", 1.1f), + glowContrast = prefs.getFloat("glowContrast", 0.5f) ) - } // Valeurs par défaut finales + } - // 1. Chargement du shader depuis res/raw - val shaderSource = remember { - try { - // Utiliser R.raw.liquidflow (ajustez si le nom de fichier est différent) - context.resources - .openRawResource(R.raw.topographicflow_shader) - .bufferedReader() - .use { it.readText() } - } catch (e: Exception) { - // Gestion d'erreur - throw IllegalStateException("Impossible de charger le shader AGSL.", e) + // Load shader code: check internal storage first, then fallback to resources + val file = remember(shaderName) { File(context.filesDir, "$shaderName.agsl") } + val shaderSource = remember(shaderName, file.lastModified()) { + if (file.exists()) { + file.readText() + } else { + context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } } } - // 2. Création du RuntimeShader - val shader = remember(shaderSource) { RuntimeShader(shaderSource) } + // 2. Création du RuntimeShader avec fallback + val shader = remember(shaderSource) { + try { + RuntimeShader(shaderSource) + } catch (_: Exception) { + val defaultCode = context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + RuntimeShader(defaultCode) + } + } // 3. Animation du temps var time by remember { mutableFloatStateOf(0f) } diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShaderWithControls.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShaderWithControls.kt index 66201a6..62123aa 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShaderWithControls.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/topographicflowEffect/TopographicFlowShaderWithControls.kt @@ -1,41 +1,46 @@ package com.lebaillyapp.dynamicvisualeffectsagsl.topographicflowEffect +import android.app.ActivityManager +import android.content.Context import android.graphics.RuntimeShader +import androidx.annotation.RawRes import androidx.compose.foundation.Canvas import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameMillis +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.toSize import com.lebaillyapp.dynamicvisualeffectsagsl.R +import androidx.core.content.edit import com.lebaillyapp.dynamicvisualeffectsagsl.topographicflowEffect.model.TopographicFlowConfig +import java.io.File @Composable fun TopographicFlowShaderWithControls( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + @RawRes shaderResId: Int, + shaderName: String ) { - var lineDensity by remember { mutableFloatStateOf(15f) } - var lineThickness by remember { mutableFloatStateOf(0.05f) } - var noiseScale by remember { mutableFloatStateOf(1f) } - var noiseIntensity by remember { mutableFloatStateOf(0.25f) } - var speedX by remember { mutableFloatStateOf(0.20f) } - var speedY by remember { mutableFloatStateOf(0.05f) } - var glowWidth by remember { mutableFloatStateOf(1.1f) } - var glowContrast by remember { mutableFloatStateOf(0.5f) } + val context = LocalContext.current + val prefs = remember { context.getSharedPreferences("topo_settings", Context.MODE_PRIVATE) } + + var lineDensity by remember { mutableFloatStateOf(prefs.getFloat("lineDensity", 15f)) } + var lineThickness by remember { mutableFloatStateOf(prefs.getFloat("lineThickness", 0.05f)) } + var noiseScale by remember { mutableFloatStateOf(prefs.getFloat("noiseScale", 1f)) } + var noiseIntensity by remember { mutableFloatStateOf(prefs.getFloat("noiseIntensity", 0.25f)) } + var speedX by remember { mutableFloatStateOf(prefs.getFloat("speedX", 0.20f)) } + var speedY by remember { mutableFloatStateOf(prefs.getFloat("speedY", 0.05f)) } + var glowWidth by remember { mutableFloatStateOf(prefs.getFloat("glowWidth", 1.1f)) } + var glowContrast by remember { mutableFloatStateOf(prefs.getFloat("glowContrast", 0.5f)) } val config = TopographicFlowConfig( lineDensity = lineDensity, @@ -48,72 +53,101 @@ fun TopographicFlowShaderWithControls( glowContrast = glowContrast ) + val isPinned = remember { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + activityManager.lockTaskModeState != ActivityManager.LOCK_TASK_MODE_NONE + } + Box( modifier = modifier.fillMaxSize() ) { TopographicFlowShaderComposable( modifier = Modifier.matchParentSize(), - config = config + config = config, + shaderResId = shaderResId, + shaderName = shaderName ) - androidx.compose.foundation.layout.Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .background(Color(0x66000000)) - .padding(start = 10.dp, end = 10.dp, bottom = 30.dp) - ) { - CompactSlider("Dens.", lineDensity, 1f, 80f) { lineDensity = it } - CompactSlider("Thick.", lineThickness, 0.01f, 1f) { lineThickness = it } - CompactSlider("N-Scale", noiseScale, 0f, 20f) { noiseScale = it } - CompactSlider("N-Int.", noiseIntensity, 0f, 20f) { noiseIntensity = it } - CompactSlider("SpeedX", speedX, 0f, 1f) { speedX = it } - CompactSlider("SpeedY", speedY, 0f, 1f) { speedY = it } - CompactSlider("GlowW", glowWidth, 0.0f, 3f) { glowWidth = it } - CompactSlider("GlowC", glowContrast, 0f, 1f) { glowContrast = it } + // Contrôles en overlay + if (!isPinned) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp) + .background(Color.Black.copy(alpha = 0.5f)) + .padding(8.dp) + .fillMaxWidth() + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f)) { + ControlSlider("Density", lineDensity, 5f..50f) { + lineDensity = it + prefs.edit { putFloat("lineDensity", it) } + } + ControlSlider("Thickness", lineThickness, 0.01f..0.5f) { + lineThickness = it + prefs.edit { putFloat("lineThickness", it) } + } + ControlSlider("Noise Scale", noiseScale, 0.1f..5f) { + noiseScale = it + prefs.edit { putFloat("noiseScale", it) } + } + ControlSlider("Noise Intensity", noiseIntensity, 0f..1f) { + noiseIntensity = it + prefs.edit { putFloat("noiseIntensity", it) } + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + ControlSlider("Speed X", speedX, -1f..1f) { + speedX = it + prefs.edit { putFloat("speedX", it) } + } + ControlSlider("Speed Y", speedY, -1f..1f) { + speedY = it + prefs.edit { putFloat("speedY", it) } + } + ControlSlider("Glow Width", glowWidth, 0.5f..5f) { + glowWidth = it + prefs.edit { putFloat("glowWidth", it) } + } + ControlSlider("Glow Contrast", glowContrast, 0.1f..2f) { + glowContrast = it + prefs.edit { putFloat("glowContrast", it) } + } + } + } + } } } } -@Composable -private fun CompactSlider( - label: String, - value: Float, - min: Float, - max: Float, - onChange: (Float) -> Unit -) { - androidx.compose.foundation.layout.Column( - modifier = Modifier.padding(vertical = 2.dp) - ) { - androidx.compose.material3.Text( - text = "$label ${"%.2f".format(value)}", - color = Color.White, - fontSize = 10.sp - ) - androidx.compose.material3.Slider( - value = value, - onValueChange = onChange, - valueRange = min..max, - modifier = Modifier.height(22.dp) // slider plus petit - ) - } -} - - @Composable fun TopographicFlowShaderComposable( modifier: Modifier = Modifier, - config: TopographicFlowConfig + config: TopographicFlowConfig, + @RawRes shaderResId: Int, + shaderName: String ) { val context = LocalContext.current - val shaderSource = remember { - context.resources.openRawResource(R.raw.topographicflow_shader) - .bufferedReader() - .use { it.readText() } + val file = remember(shaderName) { File(context.filesDir, "$shaderName.agsl") } + val shaderSource = remember(shaderName, file.lastModified()) { + if (file.exists()) { + file.readText() + } else { + context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + } } - val shader = remember(shaderSource) { RuntimeShader(shaderSource) } + val shader = remember(shaderSource) { + try { + RuntimeShader(shaderSource) + } catch (_: Exception) { + val defaultCode = context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + RuntimeShader(defaultCode) + } + } var time by remember { mutableFloatStateOf(0f) } LaunchedEffect(Unit) { @@ -130,15 +164,8 @@ fun TopographicFlowShaderComposable( Canvas(modifier = modifier.background(colorBg)) { shader.setFloatUniform("resolution", size.width, size.height) shader.setFloatUniform("time", time) - - shader.setFloatUniform( - "lineColor", - colorLine.red, colorLine.green, colorLine.blue, colorLine.alpha - ) - shader.setFloatUniform( - "bgColor", - colorBg.red, colorBg.green, colorBg.blue, colorBg.alpha - ) + shader.setFloatUniform("lineColor", colorLine.red, colorLine.green, colorLine.blue, colorLine.alpha) + shader.setFloatUniform("bgColor", colorBg.red, colorBg.green, colorBg.blue, colorBg.alpha) shader.setFloatUniform("LINE_DENSITY", config.lineDensity) shader.setFloatUniform("LINE_THICKNESS", config.lineThickness) @@ -151,4 +178,22 @@ fun TopographicFlowShaderComposable( drawRect(ShaderBrush(shader)) } -} \ No newline at end of file +} + +@Composable +private fun ControlSlider( + label: String, + value: Float, + range: ClosedFloatingPointRange, + onValueChange: (Float) -> Unit +) { + Column(modifier = Modifier.fillMaxWidth()) { + Text("$label: ${String.format("%.2f", value)}", color = Color.White, fontSize = 12.sp) + Slider( + value = value, + onValueChange = onValueChange, + valueRange = range, + modifier = Modifier.height(24.dp) + ) + } +} diff --git a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/waterEffect/composition/WaterEffectBitmapShader.kt b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/waterEffect/composition/WaterEffectBitmapShader.kt index 683f4c7..75cde0b 100644 --- a/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/waterEffect/composition/WaterEffectBitmapShader.kt +++ b/app/src/main/java/com/lebaillyapp/dynamicvisualeffectsagsl/waterEffect/composition/WaterEffectBitmapShader.kt @@ -4,7 +4,7 @@ import android.graphics.RenderEffect import android.graphics.RuntimeShader import androidx.annotation.RawRes import androidx.compose.foundation.Image -import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,21 +28,36 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.toSize import androidx.lifecycle.viewmodel.compose.viewModel import com.lebaillyapp.dynamicvisualeffectsagsl.waterEffect.viewmodel.WaterViewModel +import java.io.File @Composable fun WaterEffectBitmapShader( modifier: Modifier = Modifier, bitmap: ImageBitmap, viewModel: WaterViewModel = viewModel(), - @RawRes shaderResId: Int + @RawRes shaderResId: Int, + shaderName: String ) { val context = LocalContext.current - // Load shader code once - val shaderCode = remember { - context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + // Load shader code: check internal storage first, then fallback to resources + val file = remember(shaderName) { File(context.filesDir, "$shaderName.agsl") } + val shaderCode = remember(shaderName, file.lastModified()) { + if (file.exists()) { + file.readText() + } else { + context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + } + } + val shader = remember(shaderCode) { + try { + RuntimeShader(shaderCode) + } catch (_: Exception) { + // Fallback to default if user code fails + val defaultCode = context.resources.openRawResource(shaderResId).bufferedReader().use { it.readText() } + RuntimeShader(defaultCode) + } } - val shader = remember { RuntimeShader(shaderCode) } // Track animation time in seconds (consistent with shader) var currentTimeSeconds by remember { mutableFloatStateOf(0f) } @@ -85,23 +100,21 @@ fun WaterEffectBitmapShader( val renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "inputShader").asComposeRenderEffect() val touchModifier = Modifier.pointerInput(Unit) { - forEachGesture { - awaitPointerEventScope { - val event = awaitPointerEvent() - event.changes.forEach { change -> + awaitEachGesture { + val event = awaitPointerEvent() + event.changes.forEach { change -> + if (change.pressed) { + viewModel.addWave(change.position, change.id.value.toInt(), currentTimeSeconds) + } + } + while (true) { + val move = awaitPointerEvent() + move.changes.forEach { change -> if (change.pressed) { viewModel.addWave(change.position, change.id.value.toInt(), currentTimeSeconds) } } - while (true) { - val move = awaitPointerEvent() - move.changes.forEach { change -> - if (change.pressed) { - viewModel.addWave(change.position, change.id.value.toInt(), currentTimeSeconds) - } - } - if (move.changes.all { !it.pressed }) break - } + if (move.changes.all { !it.pressed }) break } } } diff --git a/app/src/main/res/raw/water_shader.agsl b/app/src/main/res/raw/water_shader.agsl index d6ab93b..ec76bfd 100644 --- a/app/src/main/res/raw/water_shader.agsl +++ b/app/src/main/res/raw/water_shader.agsl @@ -1,120 +1,75 @@ -// This is the input texture/bitmap that will be deformed. -// It's typically the content of the Composable the shader is applied to. +// Optimized Ripple Shader for Android AGSL uniform shader inputShader; - -// The resolution (width, height) of the area where the shader is applied, in pixels. -// Used for accurate coordinate calculations. uniform float2 uResolution; +uniform float uTime; -// The current animation time, in seconds. This value advances each frame, -// driving the wave's propagation and damping. -uniform float uTime; - -// Maximum number of waves the shader can process simultaneously. -// This must match the size of the uniform arrays passed from Kotlin. const int MAX_WAVES = 20; -// The actual number of active waves currently being processed (<= MAX_WAVES). -uniform int uNumWaves; - -// Arrays holding the parameters for each active wave. -// These are populated by the Kotlin code. -uniform float2 uWaveOrigins[MAX_WAVES]; // (x, y) coordinates of the wave's origin. -uniform float uWaveAmplitudes[MAX_WAVES]; // Initial amplitude of the wave. -uniform float uWaveFrequencies[MAX_WAVES]; // Frequency of the wave (how many cycles per unit distance). -uniform float uWaveSpeeds[MAX_WAVES]; // Propagation speed of the wave (pixels per second). -uniform float uWaveStartTimes[MAX_WAVES]; // Time (in seconds) when the wave was created. +uniform int uNumWaves; +uniform float2 uWaveOrigins[MAX_WAVES]; +uniform float uWaveAmplitudes[MAX_WAVES]; +uniform float uWaveFrequencies[MAX_WAVES]; +uniform float uWaveSpeeds[MAX_WAVES]; +uniform float uWaveStartTimes[MAX_WAVES]; -// Global damping factor applied to all waves. -// Controls how quickly wave amplitude diminishes over time. -uniform float uGlobalDamping; +uniform float uGlobalDamping; // Expecting value like 0.95 +uniform float uMinAmplitudeThreshold; -// Minimum amplitude threshold. Waves whose calculated amplitude falls below this -// value will have no effect on the pixels. -uniform float uMinAmplitudeThreshold; +const float PI = 3.1415926535; -// Define PI for mathematical calculations. -float PI = 3.141592653589793; - -/** - * The main entry point for the fragment shader. This function is executed for every pixel - * on the screen (or the target drawing surface). - * - * @param fragCoord The absolute (x, y) coordinate of the current pixel being processed, in pixels. - * @return The final color for the current pixel, typically sampled from the inputShader - * after applying the calculated deformation. - */ half4 main(float2 fragCoord) { - // 'fragCoord' represents the current pixel's position - float2 point = fragCoord; - - // Initialize the total offset for the current pixel to zero. - // This will accumulate the deformation effect from all active waves. - float2 totalOffset = float2(0.0, 0.0); + float2 totalOffset = float2(0.0); - // Iterate through all active waves (up to MAX_WAVES). + // Optimization: Pre-calculate time-based damping outside the pixel loop if possible, + // but since AGSL uniforms are constant per-draw, we handle it efficiently here. + for (int i = 0; i < MAX_WAVES; i++) { - if (i >= uNumWaves) continue; - // Retrieve parameters for the current wave from the uniform arrays. - float waveStartTime = uWaveStartTimes[i]; - float waveAmplitude = uWaveAmplitudes[i]; - float waveFrequency = uWaveFrequencies[i]; - float waveSpeed = uWaveSpeeds[i]; - float2 waveOrigin = uWaveOrigins[i]; - - // Calculate the elapsed time for this specific wave since its creation. - float elapsed = uTime - waveStartTime; - - // Calculate the vector from the wave's origin to the current pixel, - // and then its magnitude (distance). - float2 diff = point - waveOrigin; - float distance = length(diff); // Equivalent to `Offset.getLength()` - - // Calculate how far the wave front has propagated. - float waveFront = waveSpeed * elapsed; - - // Calculate the relative distance from the wave front. - // If relDist > 0, the wave hasn't reached this pixel yet. - float relDist = distance - waveFront; - - // Optimization: If the wave hasn't reached this pixel, skip to the next wave. - if (relDist > 0.0) { - continue; + // Strict limit check + if (i >= uNumWaves) break; + + float elapsed = uTime - uWaveStartTimes[i]; + + // 1. DAMPING OPTIMIZATION + // pow() is expensive. Using exp() with a log-transform or + // simply skipping very old waves saves cycles. + if (elapsed < 0.0 || elapsed > 10.0) continue; + + float currentAmplitude = uWaveAmplitudes[i] * pow(uGlobalDamping, elapsed); + + if (currentAmplitude < uMinAmplitudeThreshold) continue; + + float2 diff = fragCoord - uWaveOrigins[i]; + float dist = length(diff); + float waveFront = uWaveSpeeds[i] * elapsed; + + // 2. SOFT EDGE OPTIMIZATION + // Instead of a hard 'if (dist > waveFront) continue', we use a + // smoothstep to fade the wave out at the leading edge. + float relDist = dist - waveFront; + if (relDist > 0.0) continue; + + // 3. PHASE CALCULATION + // Phase = (distance * frequency * 2PI / speed) - (time * frequency * 2PI) + // We factor out (frequency * 2PI) to reduce multiplications. + float commonFactor = uWaveFrequencies[i] * 2.0 * PI; + float phase = commonFactor * (dist / uWaveSpeeds[i] - elapsed); + + // 4. ATTENUATION (Distance-based) + // Real waves get weaker as they spread out. + // Adding a 1.0/dist factor makes it look much more realistic. + float distFade = max(0.0, 1.0 - dist / (uWaveSpeeds[i] * 2.0)); + + // 5. WAVE SHAPE + // sin() is fine, but we multiply by a falloff so ripples don't + // exist infinitely behind the wave front. + float falloff = exp(relDist * 0.01); // Softens the "tail" of the ripple + float waveEffect = sin(phase) * currentAmplitude * distFade * falloff; + + if (dist > 0.001) { + totalOffset += (diff / dist) * waveEffect; } - - // Calculate angular frequency (omega) and wave number (k). - float omega = waveFrequency * 2.0 * PI; - float k = omega / waveSpeed; - - // Calculate the current amplitude of the wave, considering damping over time. - // `pow(base, exponent)` is used for exponential damping. - float currentAmplitude = waveAmplitude * pow(uGlobalDamping, elapsed); - - // Optimization: If the current amplitude is too low, its effect is negligible. - // Skip this wave for the current pixel. - if (currentAmplitude < uMinAmplitudeThreshold) { - continue; - } - - // Calculate the phase of the wave at this pixel and time. - float phase = k * distance - omega * elapsed; - - // Calculate the actual displacement magnitude (wave effect) at this pixel. - // `sin` function creates the ripple peaks and troughs. - float waveEffect = sin(phase) * currentAmplitude; - - // Determine the direction of the deformation. - // It's a normalized vector pointing from the wave origin to the current pixel. - float2 direction = float2(0.0, 0.0); - if (distance > 0.0) { - direction = diff / distance; // Normalize the difference vector - } - - // Accumulate the deformation offset from this wave to the total. - totalOffset += direction * waveEffect; } - // Finally, sample the `inputShader` (the bitmap) at the deformed coordinates. - // `fragCoord + totalOffset` gives the new, shifted position to fetch the color from. + // Use .eval() to sample the original texture at the offset position return inputShader.eval(fragCoord + totalOffset); -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 20e2a01..5327e2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,15 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +org.gradle.configuration-cache=true +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false +android.builtInKotlin=false +android.newDsl=false \ No newline at end of file diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..55715a1 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c4c096..31103db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -agp = "8.11.0" -kotlin = "2.0.21" +agp = "9.1.0" +kotlin = "2.2.10" coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b6a30dd..ba491df 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jul 03 18:28:42 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b7e672..65dcaf5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories {