diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 548fe43..3a85434 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -60,6 +60,7 @@ kotlin { implementation(libs.androidx.lifecycle.runtime.compose) implementation("org.jetbrains.compose.material3:material3:1.7.0") // Or latest version implementation("org.jetbrains.compose.material3:material3-window-size-class:1.7.0") // For window size classes + implementation("org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha07") } desktopMain.dependencies { implementation(compose.desktop.currentOs) @@ -71,7 +72,7 @@ kotlin { android { namespace = "com.menagerie.ophelia" - compileSdk = 35 + compileSdk = libs.versions.android.compileSdk.get().toInt() defaultConfig { applicationId = "com.menagerie.ophelia" diff --git a/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/MainActivity.kt index 1675909..2248ff6 100644 --- a/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/MainActivity.kt @@ -1,23 +1,26 @@ package com.menagerie.ophelia +import android.app.Activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.tooling.preview.Preview +import androidx.core.view.WindowCompat import com.menagerie.ophelia.ui.theme.OpheliaTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { - App() + MainView() } } } - -@Preview -@Composable -fun AppAndroidPreview() { - App() -} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/main.android.kt b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/main.android.kt new file mode 100644 index 0000000..5805ed7 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/main.android.kt @@ -0,0 +1,40 @@ +package com.menagerie.ophelia + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import com.menagerie.ophelia.sensor.SensorDataManager +import com.menagerie.ophelia.sensor.SensorManagerImpl +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@Composable +fun MainView(isLargeScreen: Boolean = false) { + + val sensorManager = SensorManagerImpl() + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + DisposableEffect(Unit) { + val dataManager = SensorDataManager(context) + dataManager.init() + + val job = scope.launch { + dataManager.data + .receiveAsFlow() + .onEach {sensorManager.listener?.onUpdate(it)} + .collect() + } + + onDispose { + dataManager.cancel() + job.cancel() + } + } + + App(sensorManager, isLargeScreen) +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/sensor/SensorDataManager.kt b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/sensor/SensorDataManager.kt new file mode 100644 index 0000000..446fafa --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/sensor/SensorDataManager.kt @@ -0,0 +1,66 @@ +package com.menagerie.ophelia.sensor + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import kotlinx.coroutines.channels.Channel +import kotlin.math.PI + +class SensorDataManager(context: Context) : SensorEventListener { + + private val sensorManager by lazy { + context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + } + + fun init() { + val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + } + + private var gravity : FloatArray? = null + private var geomagnetic : FloatArray? = null + + val data: Channel = Channel(Channel.UNLIMITED) + + override fun onSensorChanged(event: SensorEvent?) { + if(event?.sensor?.type == Sensor.TYPE_GRAVITY) + gravity = event.values + + if(event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) + geomagnetic = event.values + + if(gravity != null && geomagnetic != null) { + var r = FloatArray(9) + var i = FloatArray(9) + + if(SensorManager.getRotationMatrix(r, i, gravity, geomagnetic)) { + var orientation = FloatArray(3) + SensorManager.getOrientation(r, orientation) + val adjustedPitch = orientation[1] - (PI.toFloat() / 2) + + Log.d( + "Sensor Values", + "Sensor values are ${orientation[2]} and pitch is ${orientation[1] - 1.50}" + ) + + data.trySend( + SensorData( + roll = orientation[2], + pitch = orientation[1], + ) + ) + } + } + } + + fun cancel() { + sensorManager.unregisterListener(this) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/sensor/SensorManagerImpl.kt b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/sensor/SensorManagerImpl.kt new file mode 100644 index 0000000..df42055 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/menagerie/ophelia/sensor/SensorManagerImpl.kt @@ -0,0 +1,9 @@ +package com.menagerie.ophelia.sensor + +class SensorManagerImpl : SensorManager { + var listener: Listener? = null + + override fun registerListener(listener: Listener) { + this.listener = listener + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/asexual_tag.xml b/composeApp/src/commonMain/composeResources/drawable/asexual_tag.xml new file mode 100644 index 0000000..ef6bfa4 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/asexual_tag.xml @@ -0,0 +1,5 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/bdsm_tag.xml b/composeApp/src/commonMain/composeResources/drawable/bdsm_tag.xml new file mode 100644 index 0000000..f059ad3 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/bdsm_tag.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/bg_splash.xml b/composeApp/src/commonMain/composeResources/drawable/bg_splash.xml new file mode 100644 index 0000000..2bf4a4a --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/bg_splash.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/bg_splash_12.xml b/composeApp/src/commonMain/composeResources/drawable/bg_splash_12.xml new file mode 100644 index 0000000..ee68be4 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/bg_splash_12.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/furry_tag.xml b/composeApp/src/commonMain/composeResources/drawable/furry_tag.xml new file mode 100644 index 0000000..df83859 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/furry_tag.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_app_logo.png b/composeApp/src/commonMain/composeResources/drawable/ic_app_logo.png new file mode 100644 index 0000000..a1ef349 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/ic_app_logo.png differ diff --git a/composeApp/src/commonMain/composeResources/drawable/liqour_tag.xml b/composeApp/src/commonMain/composeResources/drawable/liqour_tag.xml new file mode 100644 index 0000000..086654f --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/liqour_tag.xml @@ -0,0 +1,6 @@ + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/mono_tag.xml b/composeApp/src/commonMain/composeResources/drawable/mono_tag.xml new file mode 100644 index 0000000..0a14708 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/mono_tag.xml @@ -0,0 +1,5 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ophelia_background.xml b/composeApp/src/commonMain/composeResources/drawable/ophelia_background.xml new file mode 100644 index 0000000..7c97e64 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ophelia_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ophelia_foreground.xml b/composeApp/src/commonMain/composeResources/drawable/ophelia_foreground.xml new file mode 100644 index 0000000..359649e --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ophelia_foreground.xml @@ -0,0 +1,5 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/plural_tag.xml b/composeApp/src/commonMain/composeResources/drawable/plural_tag.xml new file mode 100644 index 0000000..8038fe7 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/plural_tag.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/therian_tag.xml b/composeApp/src/commonMain/composeResources/drawable/therian_tag.xml new file mode 100644 index 0000000..d96049f --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/therian_tag.xml @@ -0,0 +1,5 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/trans_tag.xml b/composeApp/src/commonMain/composeResources/drawable/trans_tag.xml new file mode 100644 index 0000000..7e45dca --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/trans_tag.xml @@ -0,0 +1,5 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/weed_tag.xml b/composeApp/src/commonMain/composeResources/drawable/weed_tag.xml new file mode 100644 index 0000000..23ead7b --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/weed_tag.xml @@ -0,0 +1,5 @@ + + + diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/App.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/App.kt index bac3170..3fa0ac6 100644 --- a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/App.kt +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/App.kt @@ -1,24 +1,24 @@ package com.menagerie.ophelia -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import com.menagerie.ophelia.ui.theme.OpheliaTheme -import ophelia.composeapp.generated.resources.Res -import ophelia.composeapp.generated.resources.ophelia -import org.jetbrains.compose.resources.painterResource +import com.menagerie.ophelia.sensor.SensorManager +import com.menagerie.ophelia.model.recipesList +import com.menagerie.ophelia.recipesdetails.RecipeDetails +import com.menagerie.ophelia.recipeslist.RecipesListScreen enum class Theme { Auto, @@ -26,9 +26,16 @@ enum class Theme { Dark } -@OptIn(ExperimentalMaterial3Api::class) +enum class DetailListAppScreen { + List, + Details, +} + +@OptIn(ExperimentalSharedTransitionApi::class) @Composable -fun App() { +fun App(sensorManager: SensorManager?, isLarge: Boolean = false) { + + val navController = rememberNavController() var currentTheme by rememberSaveable { mutableStateOf(Theme.Light)} val isDarkTheme: Boolean? = when (currentTheme) { @@ -45,31 +52,65 @@ fun App() { } } - OpheliaTheme(isDarkTheme ?: isSystemInDarkTheme()) { - Scaffold { - var showContent by remember { mutableStateOf(false) } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Row { - Button(onClick = { showContent = !showContent }) { - Text("Click Me!") - } - Button(onClick = onThemeToggle){ - val text : String = currentTheme.name - Text(text) - } + + //TODO : Replace/Duplicate This and use it to show Polycule Members instead. + //TODO : Move this to its own page + val items by remember {mutableStateOf(recipesList)} + var currentRecipe = items.first() + + SharedTransitionLayout { + val sharedTransitionScope = this + NavHost( + navController = navController, + startDestination = DetailListAppScreen.List.name, + modifier = Modifier.fillMaxSize() + ) { + composable(route = DetailListAppScreen.List.name) { + RecipesListScreen(animatedVisibilityScope = this, + sharedTransactionScope = sharedTransitionScope, + isLarge = isLarge, + items = items, + onClick = { recipe -> + currentRecipe = recipe + navController.navigate(DetailListAppScreen.Details.name) + }) } - AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image(painterResource(Res.drawable.ophelia), null) - Text("Compose: $greeting") - } + composable(route = DetailListAppScreen.Details.name) { + RecipeDetails( + isLarge = isLarge, + sensorManager = sensorManager, + recipe = currentRecipe, + goBack = {navController.popBackStack()} + ) } + } } + +// Scaffold { +// var showContent by remember { mutableStateOf(false) } +// Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { +// Row { +// Button(onClick = { showContent = !showContent }) { +// Text("Click Me!") +// } +// Button(onClick = onThemeToggle){ +// val text : String = currentTheme.name +// Text(text) +// } +// } +// AnimatedVisibility(showContent) { +// val greeting = remember { Greeting().greet() } +// Column( +// Modifier.fillMaxWidth(), +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// Image(painterResource(Res.drawable.ophelia), null) +// Text("Compose: $greeting") +// } +// } +// } +// } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/model/Recipe.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/model/Recipe.kt new file mode 100644 index 0000000..d8245dd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/model/Recipe.kt @@ -0,0 +1,22 @@ +package com.menagerie.ophelia.model + +import androidx.compose.ui.graphics.Color +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.ExperimentalResourceApi + + +/** + * Created by abdulbasit on 18/06/2023. + */ + +data class Recipe @OptIn(ExperimentalResourceApi::class) constructor( + val id: Int, + val title: String, + val description: String, + val ingredients: List, + val instructions: List, + val image: DrawableResource, + val bgImage: DrawableResource? = null, + val bgImageLarge: DrawableResource? = null, + val bgColor: Color +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/model/exampleRecipes.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/model/exampleRecipes.kt new file mode 100644 index 0000000..c111a95 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/model/exampleRecipes.kt @@ -0,0 +1,351 @@ +package com.menagerie.ophelia.model + +import androidx.compose.ui.graphics.Color +import ophelia.composeapp.generated.resources.Res +import ophelia.composeapp.generated.resources.ophelia +import org.jetbrains.compose.resources.ExperimentalResourceApi + + +val primary = Color(0xff7AC5C1); +val primaryLighter = Color(0xffE6F9FF); +val white = Color(0xffffffff); +val realBlack = Color(0xff000000); +val text = Color(0xff0F1E31); +val black = Color(0xff0F1E31); +val blackLight = Color(0xff1B2C41); +val orangeDark = Color(0xffCE5A01); +val yellow = Color(0xffFFEF7D); +val sugar = Color(0xffFBF5E9); +val honey = Color(0xffDA7C16); +val pinkLight = Color(0xffF9B7B6); +val green = Color(0xffADBE56); +val red = Color(0xffCF252F); + +/** + * Created by abdulbasit on 18/06/2023. + */ + +@OptIn(ExperimentalResourceApi::class) +val recipesList = listOf( + Recipe( + id = 1, + title = "Lemon Cheesecake", + description = "Tart Lemon Cheesecake sits atop an almond-graham cracker crust to add a delightful nuttiness to the traditional graham cracker crust. Finish the cheesecake with lemon curd for double the tart pucker!", + ingredients = listOf( + "110 g digestive biscuits ", + "50 g butter", + "25 g light brown soft sugar ", + "350g mascarpone", + "75 g caster sugar ", + "1 lemon, zested", + "2 - 3 lemons, juiced(about 90 ml)" + ), + instructions = listOf( + " Crush the digestive biscuits in a food bag with a rolling pin or in the food processor . Melt the butter in a saucepan, take off heat and stir in the brown sugar and biscuit crumbs .", + " Line the base of a 20 cm loose bottomed cake tin with baking parchment.Press the biscuit into the bottom of the tin and chill in the fridge while making the topping.", + " Beat together the mascarpone, caster sugar, lemon zest and juice, until smooth and creamy . Spread over the base and chillfor a couple of hours .", + ), + image = Res.drawable.ophelia, + bgImage = Res.drawable.ophelia, + bgImageLarge = Res.drawable.ophelia, + bgColor = Color(0xffFFEF7D), + ), + Recipe( + id = 2, + title = "Macaroons", + description = + "Soft and chewy on the inside, crisp and golden on the outside — these are the perfect macaroons.", + ingredients = listOf( + "1 ¾ cups powdered sugar(210 g)", + "1 cup almond flour(95 g), finely ground", + "1 teaspoon salt, divided", + "3 egg whites, at room temperature", + "¼ cup granulated sugar(50 g)", + "½ teaspoon vanilla extract", + "2 drops pink gel food coloring", + ), + instructions = listOf( + "Make the macarons= In the bowl of a food processor, combine the powdered sugar, almond flour, and ½ teaspoon of salt, and process on low speed, until extra fine. Sift the almond flour mixture through a fine-mesh sieve into a large bowl.", + "In a separate large bowl, beat the egg whites and the remaining ½ teaspoon of salt with an electric hand mixer until soft peaks form. Gradually add the granulated sugar until fully incorporated. Continue to beat until stiff peaks form (you should be able to turn the bowl upside down without anything falling out).", + "Add the vanilla and beat until incorporated. Add the food coloring and beat until just combined.", + "Add about ⅓ of the sifted almond flour mixture at a time to the beaten egg whites and use a spatula to gently fold until combined. After the last addition of almond flour, continue to fold slowly until the batter falls into ribbons and you can make a figure 8 while holding the spatula up.", + "Transfer the macaron batter into a piping bag fitted with a round tip.", + "Place 4 dots of the batter in each corner of a rimmed baking sheet, and place a piece of parchment paper over it, using the batter to help adhere the parchment to the baking sheet.", + "Pipe the macarons onto the parchment paper in 1½-inch (3-cm) circles, spacing at least 1-inch (2-cm) apart.", + "Tap the baking sheet on a flat surface 5 times to release any air bubbles.", + "Let the macarons sit at room temperature for 30 minutes to 1 hour, until dry to the touch.", + "Preheat the oven to 300˚F (150˚C).", + "Bake the macarons for 17 minutes, until the feet are well-risen and the macarons don’t stick to the parchment paper.", + "Transfer the macarons to a wire rack to cool completely before filling.", + "Make the buttercream= In a large bowl, add the butter and beat with a mixer for 1 minute until light and fluffy. Sift in the powdered sugar and beat until fully incorporated. Add the vanilla and beat to combine. Add the cream, 1 tablespoon at a time, and beat to combine, until desired consistency is reached.", + "Transfer the buttercream to a piping bag fitted with a round tip.", + "Add a dollop of buttercream to one macaron shell. Top it with another macaron shell to create a sandwich. Repeat with remaining macaron shells and buttercream.", + "Place in an airtight container for 24 hours to “bloom”.", + ), + image = Res.drawable.ophelia, + bgImage = null, + bgColor = primary, + ), + Recipe( + id = 3, + title = "Cream Cupcakes", + description = + "Bake these easy vanilla cupcakes in just 35 minutes. Perfect for birthdays, picnics or whenever you fancy a sweet treat, they're sure to be a crowd-pleaser", + ingredients = listOf( + "", + ), + instructions = listOf(), + image = Res.drawable.ophelia, + bgColor = pinkLight, + ), + Recipe( + id = 4, + title = "Chocolate Cheesecake", + description = + "Treat family and friends to this decadent chocolate dessert. It's an indulgent end to a dinner party or weekend family meal", + ingredients = listOf( + "150g digestive biscuits (about 10)", + "1 tbsp caster sugar", + "45g butter, melted, plus extra for the tin", + "150g dark chocolate", + "120ml double cream", + "2 tsp cocoa powder", + "200g full-fat cream cheese", + "115g caster sugar" + ), + instructions = listOf( + "To make the biscuit base, crush the digestive biscuits with a rolling pin or blitz in a food processor, then tip into a bowl with the sugar and butter and stir to combine. Butter and line an 18cm springform tin and tip in the biscuit mixture, pushing it down with the back of a spoon. Chill in the fridge for 30 mins.", + "To make the cheesecake, melt the chocolate in short bursts in the microwave, then leave to cool slightly. Whip the cream in a large bowl using an electric whisk until soft peaks form, then fold in the cocoa powder. Beat the cream cheese and sugar together, then fold in the cream mixture and the cooled chocolate.", + "Spoon the cheesecake mixture over the biscuit base, levelling it out with the back of a spoon. Transfer to the freezer and freeze for 2 hrs, or until set. Remove from the tin and leave at room temperature to soften for about 20 mins before serving.", + ), + image = Res.drawable.ophelia, + bgColor = orangeDark, + ), + Recipe( + id = 5, + title = "Fruit Plate", + description = "Melons - they're firmer so make a great base for the softer berries and fruits.Tropical fruit -the top of a pineapple can be included for height,while dragonfruit looks vibrant.", + ingredients = listOf(""), + instructions = listOf(""), + image = Res.drawable.ophelia, + bgColor = green, + ), + Recipe( + id = 6, + title = "Chocolate Donuts", + description = + "Moist and fluffy donuts that are baked, not fried, and full of chocolate. Covered in a thick chocolate glaze, these are perfect for any chocoholic who wants a chocolate version of this classic treat.", + ingredients = listOf( + "1 cup (140g) all-purpose flour", + "1/4 cup (25g) unsweetened cocoa powder", + "1/2 teaspoon baking powder", + "1/2 teaspoon baking soda", + "1/8 teaspoon salt", + "1 large egg", + "1/2 cup (100g) granulated sugar", + "1/3 cup (80 ml) milk", + "1/4 cup (60 ml) yogurt", + "2 tablespoons (30g) unsalted butter, melted", + "1/2 teaspoon vanilla extract", + ), + instructions = listOf( + "Preheat oven to 350°F/180°. Grease a donut pan with oil or butter. Set aside.", + "Make the donuts= Whisk together the flour, cocoa powder, baking powder, baking soda, and salt in a large bowl. Set aside.", + "In a medium bowl whisk egg with sugar until well combined. Add milk, yogurt, melted butter and vanilla extract, and whisk until combined. Pour into the flour mixture and mix until just combined. The batter will be thick.", + "Fill donut cavities with batter ¾ way full using a spoon or a piping bag (much easier). Cut a corner off the bottom of the bag and pipe the batter into each donut cup.", + "Bake for 9–10 minutes or until a toothpick inserted into the center of the donuts comes out clean. Allow to cool for 5 minutes in pan, then remove donuts from pan and transfer to a wire rack. Allow to cool completely before glazing.", + "Make the chocolate glaze= Melt the chocolate, heavy cream, and butter gently in the microwave (in 30-second intervals, stirring in between) or a double boiler until smooth. Dip the tops of the donuts into the chocolate glaze, and place on a cooling rack to set.", + "Donuts are best eaten the same day or keep them for up to 3 days in the refrigerator.", + ), + image = Res.drawable.ophelia, + bgImage = null, + bgColor = sugar, + ), + Recipe( + id = 7, + title = "Strawberry Cake", + description = + "Jam-packed with fresh strawberries, this strawberry cake is one of the simplest, most delicious cakes you’ll ever make.", + ingredients = listOf( + "", + ), + instructions = listOf( + "", + ), + image = Res.drawable.ophelia, + bgColor = red, + ), + Recipe( + id = 8, + title = "Fluffy Cake", + description = + "This is a very good everyday cake leavened with baking powder. It's relatively light — it isn't loaded with butter, and it calls for only 2 eggs and 2 percent milk. Mine was perfectly baked after 30 minutes. After 10 minutes on the cooling rack, the cake released from the pans easily.", + ingredients = listOf( + "1/2 cup (1 stick) unsalted butter, cut into 2-tablespoon pieces and softened; plus more for coating pans", + "2 1/4 cups all-purpose flour, plus more for coating pans", + "1 1/3 cups granulated sugar", + "1 tablespoon baking powder", + "1/2 teaspoon salt", + "1 tablespoon vanilla extract", + "1 cup 2 percent milk, room temperature", + "2 large eggs, room temperature", + ), + instructions = listOf( + "Gather the ingredients. Preheat the oven to 350 F.", + "Butter and flour two 9-inch cake pans. If desired, line the bottom with a circle of parchment", + "Combine the sugar, flour, baking powder, and salt in the bowl of a stand mixer fitted with the paddle attachment. Mix until the dry ingredients are combined.", + "With the mixer on the lowest speed, add the butter one chunk at a time and blend until the mixture looks sandy, between 30 seconds and 1 minute. Scrape down the bowl and paddle with a rubber spatula.", + "Add the vanilla extract and, with the mixer on low, pour in the milk. Stop and scrape, then mix for another minute.", + "Add the first egg and mix on medium-low until completely incorporated. Add the second egg and do the same. Scrape down the bowl and mix until fluffy on medium speed, about 30 seconds.", + "Pour the batter into the prepared pans and give each one a couple of solid taps on the countertop to release any air bubbles. Transfer the pans to the preheated oven.", + "Bake for about 30 minutes, or until a toothpick inserted into the center comes out clean or with a crumb or two attached. The tops will be golden brown, the edges will pull away from the sides of the pan, and the cakes will spring back when you touch them.", + "Cool the cakes in their pans on a wire rack for 10 minutes, then loosen the edges by running a knife along the sides of the pan. Turn the cakes out onto the racks and cool for at least 1 hour before frosting.", + "Frost with your choice of frosting and enjoy.", + ), + image = Res.drawable.ophelia, + bgImage = null, + bgColor = orangeDark, + ), + Recipe( + id = 9, + title = "White Cream Cake", + description = + "This White Chocolate Cake is both decadent and delicious! White chocolate is incorporated into the cake layers, the frosting, and the drip for a stunning monochrome effect.", + ingredients = listOf( + "2 ½ cups all-purpose flour", + "1 teaspoon baking soda", + "½ teaspoon baking powder", + "½ teaspoon salt", + "6 (1 ounce) squares white chocolate, chopped", + "½ cup hot water", + "1 cup butter, softened", + "1 ½ cups white sugar", + "3 eggs", + "1 cup buttermilk", + "6 (1 ounce) squares white chocolate, chopped", + "2 ½ tablespoons all-purpose flour", + "1 cup milk", + "1 cup butter, softened", + "1 cup white sugar", + "1 teaspoon vanilla extract", + ), + instructions = listOf( + "Preheat oven to 350 degrees F (175 degrees C). Sift together the 2 1/2 cups flour, baking soda, baking powder and salt. Set aside.", + "In small saucepan, melt 6 ounces white chocolate and hot water over low heat. Stir until smooth, and allow to cool to room temperature.", + "In a large bowl, cream 1 cup butter and 1 1/2 cup sugar until light and fluffy. Add eggs one at a time, beating well with each addition. Stir in flour mixture alternately with buttermilk. Mix in melted white chocolate.", + "Pour batter into two 9 inch round cake pans. Bake for 30 to 35 minutes in the preheated oven, until a toothpick inserted into the center of the cake comes out clean.", + "To make Frosting= In a medium bowl, combine 6 ounces white chocolate, 2 1/2 tablespoons flour and 1 cup milk. Cook over medium heat, stirring constantly, until mixture is very thick. Cool completely.", + "In large bowl, cream 1 cup butter, 1 cup sugar and 1 teaspoon vanilla; beat until light and fluffy. Gradually add cooled white chocolate mixture. Beat at high speed until it is the consistency of whipped cream. Spread between layers, on top and sides of cake.", + ), + image = Res.drawable.ophelia, + bgImage = null, + bgColor = sugar, + ), + Recipe( + id = 10, + title = "Fruit Pie", + description = + "Bake a hearty fruit pie for dessert. Our collection of year-round pastry classics includes apple & blackberry, summer berries, lemon meringue and mince pies.", + ingredients = listOf( + "", + ), + instructions = listOf( + "", + ), + image = Res.drawable.ophelia, + bgImage = null, + bgColor = yellow, + ), + Recipe( + id = 11, + title = "Honey Cake", + description = + "The secret to this cake ' s fantastic flavor is the tiny amount of bitterness from burnt honey.The slightly tangy whipped cream frosting provides a bit of acidity and lovely light texture, and unlike other frostings, it' s not too sweet", + ingredients = listOf( + "¾ cup wildflower honey", + "3 tablespoons cold water", + "1 cup white sugar", + "14 tablespoons unsalted butter, cut into slices", + "¾ cup wildflower honey", + "2 ½ teaspoons baking soda", + "1 teaspoon ground cinnamon", + "¾ teaspoon fine salt", + "6 large cold eggs", + "3 ¾ cups all-purpose flour", + ), + instructions = listOf( + "Pour 3/4 cup wildflower honey into a deep saucepan over medium heat. Boil until a shade darker and caramel-like in aroma, about 10 minutes. Turn off heat and whisk in cold water.", + "Preheat the oven to 375 degrees F (190 degrees C). Line a baking sheet with a silicone baking mat. Place a mixing bowl and whisk in the refrigerator.", + "Place a large metal bowl over the lowest heat setting on the stovetop. Add sugar, butter, 3/4 cup wildflower honey, and 1/4 cup burnt honey. Let sit until butter melts, 5 to 7 minutes. Reserve remaining burnt honey for the frosting.", + "Meanwhile, combine baking soda, cinnamon, and salt in a small bowl.", + "Whisk butter mixture and let sit until very warm to the touch. Whisk in eggs. Keep mixture over low heat until it warms up again, then whisk in baking soda mixture. Remove from heat. Sift in flour in 2 or 3 additions, stirring well after each, until batter is easily spreadable.", + "Transfer about 1/2 cup batter onto the prepared baking sheet. Spread into an 8- or 9-inch circle using an offset spatula. Shake and tap the pan to knock out any air bubbles.", + "Bake in the preheated oven until lightly browned, 6 to 7 minutes. Remove liner from the pan and let cake layer continue cooling until firm enough to remove, 6 to 7 minutes. Invert cake onto a round of parchment paper.", + "Repeat Steps 6 and 7 until you have 8 cake layers, letting each cool on an individual parchment round. Trim edges using a pizza wheel to ensure they are the same size; save scraps for crumb mixture.", + "Spread any remaining batter onto the lined baking sheet. Bake in the preheated oven until edges are dry, about 10 minutes. Remove from the oven and cut into small pieces; toss with reserved cake scraps.", + "Return scraps to the oven and bake until browned, 7 to 10 minutes more. Let cool completely, 15 to 20 minutes. Transfer to a resealable bag and beat into fairly fine crumbs using a rolling pin. Set aside.", + "Remove the bowl and whisk from the refrigerator. Pour in heavy cream and whisk until soft peaks form. Add sour cream and remaining burnt honey; continue whisking until stiff peaks form.", + "Place a cake layer on a parchment paper round on a pizza pan or serving plate. Spread a cup of frosting evenly on top, almost to the edge. Repeat with cake layers and frosting, pressing layers in smooth-side down. Place last cake layer smooth-side up. Frost top and sides of cake. Cover with crumbs; clean any excess crumbs around base.", + "Cover with plastic wrap and refrigerate for at least 8 hours to overnight. Transfer to a cake stand using 2 spatulas. Cut and serve.", + ), + image = Res.drawable.ophelia, + bgImage = null, + bgColor = honey, + ), + Recipe( + id = 12, + title = "Powdered Cake", + description = + "Heavy on the butter and nutmeg, this cake has all the flavors of your favorite cake donut in a convenient square shape.", + ingredients = listOf( + "", + ), + instructions = listOf( + "", + ), + image = Res.drawable.ophelia, + bgColor = sugar, + ), + Recipe( + id = 13, + title = "Strawberries", + description = + "We' ll admit it = we go a little crazy during strawberry season.Though easy to grow, these sweet berries just taste better when you get them in season, as opposed to buying them at other times of the year.", + ingredients = listOf( + "", + ), + instructions = listOf( + "", + ), + image = Res.drawable.ophelia, + bgColor = red, + ), + Recipe( + id = 14, + title = "Chocolate Cake", + description = + "The Best Chocolate Cake Recipe – A one bowl chocolate cake recipe that is quick, easy, and delicious! Updated with gluten-free, dairy-free, and egg-free options!", + ingredients = listOf( + "", + ), + instructions = listOf( + "", + ), + image = Res.drawable.ophelia, + bgColor = orangeDark, + ), + Recipe( + id = 15, + title = "Apple Pie", + description = + "This was my grandmother' s apple pie recipe.I have never seen another one quite like it.It will always be my favorite and has won me several first place prizes in local competitions.", + ingredients = listOf( + "", + ), + instructions = listOf( + "", + ), + image = Res.drawable.ophelia, + bgColor = sugar, + ) +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/IngredientItem.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/IngredientItem.kt new file mode 100644 index 0000000..9d96849 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/IngredientItem.kt @@ -0,0 +1,76 @@ +package com.menagerie.ophelia.recipesdetails + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.menagerie.ophelia.model.Recipe +import ophelia.composeapp.generated.resources.Res +import ophelia.composeapp.generated.resources.ophelia +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun IngredientItem(recipe: Recipe, ingredient: String) { + Box(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp)) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .border( + width = 2.dp, + color = recipe.bgColor, + shape = RoundedCornerShape(35.dp) + ) + ) { + Text( + text = ingredient, + style = MaterialTheme.typography.body2, + modifier = Modifier + .fillMaxWidth() + .padding(start = 55.dp, end = 8.dp, top = 16.dp, bottom = 16.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Box( + modifier = Modifier.padding(start = 4.dp) + ) { + Box( + modifier = Modifier + .size(45.dp) + .shadow(elevation = 10.dp, shape = CircleShape) + .background( + recipe.bgColor, + CircleShape + ), + ) { + Image( + painter = painterResource(Res.drawable.ophelia), + contentDescription = null, + modifier = Modifier.padding(12.dp).rotate(-30f), + colorFilter = ColorFilter.tint(if (recipe.bgColor.luminance() > 0.3) Color.Companion.Black else Color.White) + ) + + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/InstructionItem.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/InstructionItem.kt new file mode 100644 index 0000000..fa0aaad --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/InstructionItem.kt @@ -0,0 +1,81 @@ +package com.menagerie.ophelia.recipesdetails + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.menagerie.ophelia.model.Recipe + +@Composable +fun InstructionItem(recipe: Recipe, index: Int) { + Box(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp)) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .border( + width = 2.dp, + color = recipe.bgColor, + shape = RoundedCornerShape(35.dp) + ) + ) { + Text( + text = recipe.instructions[index], + style = MaterialTheme.typography.body1.copy( + letterSpacing = 1.2.sp, + ), + modifier = Modifier + .fillMaxWidth().fillMaxHeight() + .padding(start = 70.dp, end = 20.dp, top = 20.dp, bottom = 20.dp), + ) + } + + Box( + modifier = Modifier + ) { + Box( + modifier = Modifier + .size(50.dp) + .shadow(elevation = 10.dp, shape = CircleShape) + .background( + recipe.bgColor, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "${index + 1}", + style = MaterialTheme.typography.h5.copy( + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) + ), + color = Color.Black, + fontWeight = FontWeight.W600, + modifier = Modifier.padding(5.dp).rotate(-30f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetails.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetails.kt new file mode 100644 index 0000000..cc8173c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetails.kt @@ -0,0 +1,28 @@ +package com.menagerie.ophelia.recipesdetails + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.runtime.Composable +import com.menagerie.ophelia.model.Recipe +import com.menagerie.ophelia.sensor.SensorManager + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun RecipeDetails( + recipe: Recipe, + goBack: () -> Unit, + sensorManager: SensorManager?, + isLarge: Boolean, +) { + if (isLarge) RecipeDetailsLarge( + recipe = recipe, + goBack = goBack, + sensorManager = sensorManager + ) + else RecipeDetailsSmall( + recipe = recipe, + goBack = goBack, + sensorManager = sensorManager + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetailsLarge.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetailsLarge.kt new file mode 100644 index 0000000..5228c78 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetailsLarge.kt @@ -0,0 +1,270 @@ +package com.menagerie.ophelia.recipesdetails + +import androidx.compose.animation.core.animateIntOffsetAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Card +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.menagerie.ophelia.model.Recipe +import com.menagerie.ophelia.model.orangeDark +import com.menagerie.ophelia.model.sugar +import com.menagerie.ophelia.model.yellow +import com.menagerie.ophelia.sensor.Listener +import com.menagerie.ophelia.sensor.SensorData +import com.menagerie.ophelia.sensor.SensorManager +import org.jetbrains.compose.resources.painterResource +import kotlin.math.PI +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text + +@Composable +fun RecipeDetailsLarge( + recipe: Recipe, + goBack: () -> Unit, + sensorManager: SensorManager?, +) { + val imageRotation = remember { mutableStateOf(0) } + val sensorDataLive = remember { mutableStateOf(SensorData(0.0f, 0.0f)) } + val roll by derivedStateOf { (sensorDataLive.value.roll * 20).coerceIn(-4f, 4f) } + val pitch by derivedStateOf { (sensorDataLive.value.pitch * 20).coerceIn(-4f, 4f) } + + val tweenDuration = 300 + + sensorManager?.registerListener(object : Listener { + override fun onUpdate(sensorData: SensorData) { + sensorDataLive.value = sensorData + } + }) + + val backgroundShadowOffset = animateIntOffsetAsState( + targetValue = IntOffset((roll * 6f).toInt(), (pitch * 6f).toInt()), + animationSpec = tween(tweenDuration) + ) + val backgroundImageOffset = animateIntOffsetAsState( + targetValue = IntOffset(-roll.toInt(), pitch.toInt()), animationSpec = tween(tweenDuration) + ) + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, source: NestedScrollSource + ): Offset { + imageRotation.value += (available.y * 0.5).toInt() + return Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, available: Offset, source: NestedScrollSource + ): Offset { + val delta = available.y + imageRotation.value += ((delta * PI / 180) * 10).toInt() + return super.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + imageRotation.value += available.y.toInt() + return super.onPreFling(available) + } + } + } + + Box( + modifier = Modifier.fillMaxSize() + .background(if (recipe.bgColor == sugar) yellow else sugar) + ) { + val size = mutableStateOf(IntSize(0, 0)) + Row { + Box(modifier = Modifier.fillMaxSize().onGloballyPositioned { + size.value = it.size + }.weight(1f).pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + // on every relayout Compose will send synthetic Move event, + // so we skip it to avoid event spam + if (event.type == PointerEventType.Move) { + val position = event.changes.first().position + sensorDataLive.value = SensorData( + roll = position.x - size.value.height / 4, + pitch = (position.y - size.value.width / 4) + ) + } + } + + } + }) { + Card( + modifier = Modifier + .clip(RoundedCornerShape(topEnd = 35.dp, bottomEnd = 35.dp)), + shape = RoundedCornerShape( + topEnd = 35.dp, + bottomEnd = 35.dp, + ), + ) { + // background image + its shadow + Box(modifier = Modifier.fillMaxSize().background(recipe.bgColor)) { + if (recipe.bgImageLarge != null) { + val painter = painterResource(recipe.bgImageLarge) + Image(painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.offset { + backgroundShadowOffset.value + }.graphicsLayer { + scaleX = 1.050f + scaleY = 1.050f + }.blur(radius = 8.dp), + colorFilter = ColorFilter.tint( + orangeDark.copy(alpha = 0.3f) + ) + ) + + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.background( + Color.Transparent, + RoundedCornerShape( + bottomEnd = 35.dp, bottomStart = 35.dp + ), + ).offset { + backgroundImageOffset.value + }.graphicsLayer { + shadowElevation = 8f + scaleX = 1.050f + scaleY = 1.050f + }, + ) + } + + // image shadows and image + Box( + modifier = Modifier.aspectRatio(1f).padding(32.dp) + .align(Alignment.Center) + ) { + Box(modifier = Modifier.padding(32.dp)) { + Image( + painter = painterResource(recipe.image), + contentDescription = null, + modifier = Modifier.aspectRatio(1f).align(Alignment.Center) + .padding(16.dp).rotate(imageRotation.value.toFloat()) + ) + } + } + } + } + } + + Box( + modifier = Modifier.fillMaxSize() + .background(if (recipe.bgColor == sugar) yellow else sugar) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Scroll) { + val position = event.changes.first().position + // on every relayout Compose will send synthetic Move event, + // so we skip it to avoid event spam + imageRotation.value = + (imageRotation.value + position.getDistance() + .toInt() * 0.010).toInt() + } + } + } + }.weight(1f) + ) { + val listState = rememberLazyListState() + + Box( + modifier = Modifier.fillMaxSize() + ) { + LazyColumn( + contentPadding = PaddingValues(64.dp), + userScrollEnabled = true, + verticalArrangement = Arrangement.Absolute.spacedBy(16.dp), + modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollConnection), + state = listState + ) { + StepsAndDetails( + recipe = recipe + ) + } + } + } + } + + BackButton(goBack) + + } +} + +@Composable +fun BackButton(goBack: () -> Unit) { + Box( + modifier = Modifier.padding(start = 32.dp, top = 16.dp).clip( + RoundedCornerShape(50) + ).clickable { + goBack() + }.background( + color = Color.Black, shape = RoundedCornerShape(50) + ).padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 16.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(20.dp) + ) + Spacer(Modifier.padding(start = 8.dp)) + Text( + text = "Back to Recipes", + color = Color.White + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetailsSmall.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetailsSmall.kt new file mode 100644 index 0000000..0ef813e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/RecipeDetailsSmall.kt @@ -0,0 +1,252 @@ +package com.menagerie.ophelia.recipesdetails + +import androidx.compose.animation.core.animateIntOffsetAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.menagerie.ophelia.model.Recipe +import com.menagerie.ophelia.model.sugar +import com.menagerie.ophelia.model.yellow +import com.menagerie.ophelia.model.orangeDark +import com.menagerie.ophelia.sensor.Listener +import com.menagerie.ophelia.sensor.SensorData +import com.menagerie.ophelia.sensor.SensorManager +import kotlin.math.PI +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.graphicsLayer +import org.jetbrains.compose.resources.painterResource +import androidx.compose.ui.draw.* +import androidx.compose.material3.Icon +import androidx.compose.ui.layout.ContentScale + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RecipeDetailsSmall( + recipe: Recipe, + goBack: () -> Unit, + sensorManager: SensorManager?, +) { + val imageRotation = remember { mutableStateOf(0) } + val sensorDataLive = remember { mutableStateOf(SensorData(0.0f, 0.0f)) } + val roll by derivedStateOf { (sensorDataLive.value.roll).coerceIn(-3f, 3f) } + val pitch by derivedStateOf { (sensorDataLive.value.pitch).coerceIn(-2f, 2f) } + + val tweenDuration = 300 + + sensorManager?.registerListener(object : Listener { + override fun onUpdate(sensorData: SensorData) { + sensorDataLive.value = sensorData + } + }) + + val backgroundShadowOffset = animateIntOffsetAsState( + targetValue = IntOffset((roll * 6f).toInt(), (pitch * 6f).toInt()), + animationSpec = tween(tweenDuration) + ) + val backgroundImageOffset = animateIntOffsetAsState( + targetValue = IntOffset(-roll.toInt(), pitch.toInt()), animationSpec = tween(tweenDuration) + ) + + val toolbarOffsetHeightPx = remember { mutableStateOf(340f) } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, source: NestedScrollSource + ): Offset { + val delta = available.y + val newOffset = toolbarOffsetHeightPx.value + delta + toolbarOffsetHeightPx.value = newOffset.coerceIn(0f, 340f) + imageRotation.value += (available.y * 0.5).toInt() + return Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, available: Offset, source: NestedScrollSource + ): Offset { + val delta = available.y + imageRotation.value += ((delta * PI / 180) * 10).toInt() + return super.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + imageRotation.value += available.y.toInt() + return super.onPreFling(available) + } + } + } + + val candidateHeight = maxOf(toolbarOffsetHeightPx.value, 300f) + val listState = rememberLazyListState() + val (fraction, setFraction) = remember { mutableStateOf(1f) } + + + Box( + modifier = Modifier.fillMaxSize() + .background(color = if (recipe.bgColor == sugar) yellow else sugar) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollConnection), + state = listState + ) { + + stickyHeader { + Box( + modifier = Modifier.shadow( + elevation = if (fraction < 0.05) { + ((1 - fraction) * 16).dp + } else 0.dp, + clip = false, + ambientColor = Color(0xffCE5A01).copy(if (fraction < 0.1) 1f - fraction else 0f), + spotColor = Color(0xffCE5A01).copy(if (fraction < 0.1) 1f - fraction else 0f) + ).alpha(if (fraction < 0.2) 1f - fraction else 0f).fillMaxWidth() + .background( + recipe.bgColor, + RoundedCornerShape( + bottomEnd = 35.dp, bottomStart = 35.dp + ), + ).clip(RoundedCornerShape(bottomEnd = 35.dp, bottomStart = 35.dp)) + .height(candidateHeight.dp) + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { + + //bg image and shadow + recipe.bgImage?.let { + Image(painter = painterResource(it), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.offset { + backgroundShadowOffset.value + }.graphicsLayer { + scaleX = 1.050f + scaleY = 1.050f + }.blur(radius = 8.dp), + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( + orangeDark.copy(alpha = 0.3f) + ) + ) + Image(painter = painterResource(it), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.background( + Color.Transparent, + RoundedCornerShape( + bottomEnd = 35.dp, bottomStart = 35.dp + ), + ).offset { + backgroundImageOffset.value + }.graphicsLayer { + shadowElevation = 8f + scaleX = 1.050f + scaleY = 1.050f + }, + alpha = 1 - fraction + ) + } + + Box( + modifier = Modifier.aspectRatio(1f).align(Alignment.Center) + ) { + Box { + //image rounded shadow +// Box(modifier = Modifier.offset { +// IntOffset( +// x = (roll * 2).dp.roundToPx(), +// y = -(pitch * 2).dp.roundToPx() +// ) +// }) { +// +// Image( +// painter = painterResource(recipe.image), +// contentDescription = null, +// modifier = Modifier.aspectRatio(1f) +// .align(Alignment.Center).padding(16.dp).shadow( +// elevation = 16.dp, +// shape = CircleShape, +// clip = false, +// ambientColor = Color.Red, +// spotColor = Color.Red, +// ), +// colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( +// orangeDark.copy(alpha = 0.0f) +// ) +// ) +// } + + Image( + painter = painterResource(recipe.image), + contentDescription = null, + modifier = Modifier.aspectRatio(1f).align(Alignment.Center) + .windowInsetsPadding(WindowInsets.systemBars) + .padding(16.dp).rotate(imageRotation.value.toFloat()) + .background( + Color.Transparent, + CircleShape, + ) + ) + } + } + } + } + } + + StepsAndDetails( + recipe = recipe + ) + } + + Box(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).size(50.dp) + .padding(10.dp).alpha( + alpha = if (fraction <= 0) 1f else 0f, + ).background( + color = Color.Black, shape = RoundedCornerShape(50) + ).shadow(elevation = 16.dp).padding(5.dp).clickable { + goBack() + }) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + tint = recipe.bgColor, + modifier = Modifier.size(30.dp) + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/StepsAndDetails.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/StepsAndDetails.kt new file mode 100644 index 0000000..4e81c88 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipesdetails/StepsAndDetails.kt @@ -0,0 +1,58 @@ +package com.menagerie.ophelia.recipesdetails + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.menagerie.ophelia.model.Recipe + + +internal fun LazyListScope.StepsAndDetails( + recipe: Recipe +) { + + item { + + Text( + text = recipe.title, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.W700, + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) + + Text( + text = recipe.description, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) + + Text( + text = "INGREDIENTS", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.W700, + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) + + } + + itemsIndexed(recipe.ingredients) { index, value -> + IngredientItem(recipe, value) + } + + item { + Text( + text = "STEPS", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.W700, + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp) + ) + } + + itemsIndexed(recipe.instructions) { index, _ -> + InstructionItem(recipe, index) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeImage.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeImage.kt new file mode 100644 index 0000000..a50d040 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeImage.kt @@ -0,0 +1,36 @@ +package com.menagerie.ophelia.recipeslist + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource + +@Composable +fun RecipeImage(imageBitmap: DrawableResource, modifier: Modifier){ + Box(modifier = modifier) { + Box( + modifier = modifier + .clip(RoundedCornerShape(50)) + .background( + color = Color.Transparent, + shape = RoundedCornerShape(50) + ) + ) + Image( + painter = painterResource(imageBitmap), + contentDescription = null, + modifier = Modifier.background( + color = Color(0xffCE5A01).copy(alpha = 0.2f), + shape = CircleShape, + ).aspectRatio(1f) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItem.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItem.kt new file mode 100644 index 0000000..2e76acb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItem.kt @@ -0,0 +1,94 @@ +package com.menagerie.ophelia.recipeslist + + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.menagerie.ophelia.model.Recipe + + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun RecipeListItem( + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, + recipe: Recipe, + onClick: (recipe: Recipe) -> Unit, +) { + Box(modifier = Modifier) { + Box(modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth().aspectRatio(1.5f) + .shadow( + elevation = 16.dp, + shape = RoundedCornerShape(35.dp), + clip = true, + ambientColor = Color(0xffCE5A01), + spotColor = Color(0xffCE5A01) + ) + .background(color = recipe.bgColor, shape = RoundedCornerShape(35.dp)).fillMaxHeight().clickable { + onClick(recipe) + }) { +// with(sharedTransitionScope) { + Card( + backgroundColor = recipe.bgColor, + shape = RoundedCornerShape(35.dp), + modifier = Modifier.clip(RoundedCornerShape(35.dp)) + ) { + Box( + modifier = Modifier.fillMaxWidth().aspectRatio(1.5f) + ) { + Row( + modifier = Modifier.fillMaxHeight().padding(16.dp).fillMaxWidth(0.55f), + verticalAlignment = Alignment.Bottom + ) { + Column(modifier = Modifier.align(Alignment.Bottom)) { + Text( + text = recipe.title, + style = MaterialTheme.typography.h4, + modifier = Modifier + ) + + Text( + recipe.description, + style = MaterialTheme.typography.subtitle1, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 8.dp), + ) + } + Spacer(modifier = Modifier.weight(1f)) + } + } + RecipeListItemImageWrapper(modifier = Modifier.align(Alignment.BottomEnd) + .fillMaxWidth(0.45f).aspectRatio(1f), child = { + RecipeImage( + imageBitmap = recipe.image, modifier = Modifier + ) + }) + } + + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItemImageWrapper.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItemImageWrapper.kt new file mode 100644 index 0000000..5a45038 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItemImageWrapper.kt @@ -0,0 +1,75 @@ +package com.menagerie.ophelia.recipeslist + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring.DampingRatioLowBouncy +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp + +@Composable +fun RecipeListItemImageWrapper( + modifier: Modifier, + child: @Composable () -> Unit, +) { + val animationDuration = 700 + val scale = remember { Animatable(0.3f) } + val rotation = remember {Animatable(20f)} + val offset = remember {Animatable(0f)} + + LaunchedEffect(Unit) { + scale.animateTo( + targetValue = 1f, + animationSpec = spring( + dampingRatio = 0.6f, + stiffness = 200f + ) + ) + } + + LaunchedEffect(Unit){ + rotation.animateTo( + targetValue = 0f, + animationSpec = tween( + durationMillis = animationDuration + ) + ) + } + + LaunchedEffect(Unit) { + offset.animateTo( + 60f, + animationSpec = tween( + durationMillis = animationDuration / 2, + easing = FastOutSlowInEasing + ) + ) + offset.animateTo( + targetValue = 0f, + animationSpec = spring( + dampingRatio = DampingRatioLowBouncy, + stiffness = 200f + ) + ) + } + + Box(modifier = modifier.offset(x = offset.value.dp).graphicsLayer { + this.rotationZ = rotation.value + }) { + Box( + modifier = Modifier.wrapContentSize().scale(scale.value).rotate(rotation.value) + ) { + child() + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItemWrapper.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItemWrapper.kt new file mode 100644 index 0000000..3091b95 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipeListItemWrapper.kt @@ -0,0 +1,69 @@ +package com.menagerie.ophelia.recipeslist + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.tween +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.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer + +const val perspectiveValue = 0.004 +const val rotateX = 9f + +@Composable +fun RecipeListItemWrapper( + child: @Composable () -> Unit, + scrollDirection: Boolean +) { + val scaleAnimatable = remember { Animatable(initialValue = 0.75f) } + val rotateXAnimatable = + remember { Animatable(initialValue = if (scrollDirection) rotateX else -rotateX)} + + //Observe changes to scrollDirection and update xRotation accordingly + LaunchedEffect(scrollDirection) { + //0 to (-)60 + rotateXAnimatable.animateTo( + if(scrollDirection) rotateX else -rotateX, + animationSpec = tween( + durationMillis = 100, + easing = CubicBezierEasing(0f, 0.5f, 0.5f, 1f) + ) + ) + //(-)60 to 0 + rotateXAnimatable.animateTo( + targetValue = 0f, + animationSpec = tween( + durationMillis = 500, + easing = CubicBezierEasing(0f, 0.5f, 0.5f, 1f) + ) + ) + } + + //Update + LaunchedEffect(Unit) { + scaleAnimatable.animateTo( + 1f, + animationSpec = tween( + durationMillis = 700, + easing = CubicBezierEasing(0f, 0.5f, 0.5f, 1f) + ) + ) + } + + Box( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = scaleAnimatable.value + scaleY = scaleAnimatable.value + rotationX = rotateXAnimatable.value + } + ) + { + child() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipesList.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipesList.kt new file mode 100644 index 0000000..29d5458 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/recipeslist/RecipesList.kt @@ -0,0 +1,77 @@ +package com.menagerie.ophelia.recipeslist + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import com.menagerie.ophelia.model.Recipe +import com.menagerie.ophelia.model.sugar + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun RecipesListScreen( + items: List, + onClick: (recipe: Recipe) -> Unit, + isLarge: Boolean, + sharedTransactionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope, +) { + Box( + modifier = Modifier.fillMaxSize().background(sugar) + ) { + val listState = rememberLazyGridState() + LazyVerticalGrid( + state = listState, columns = GridCells.Fixed(if (isLarge) 3 else 1) + ) { + if(isLarge.not()) + item { + Spacer(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)) + } + items(items.size) { item -> + val recipe = items[item] + RecipeListItemWrapper( + scrollDirection = listState.isScrollingUp(), + child = { + RecipeListItem( + recipe = recipe, + onClick = onClick, + sharedTransitionScope = sharedTransactionScope, + animatedVisibilityScope = animatedVisibilityScope, + ) + } + ) + } + } + } +} + +@Composable +private fun LazyGridState.isScrollingUp(): Boolean { + var previousIndex by remember(this) {mutableStateOf(firstVisibleItemIndex)} + var previousScrollOffset by remember(this) {mutableStateOf(firstVisibleItemScrollOffset)} + return remember(this) { + derivedStateOf { + if(previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/sensor/SensorCallbackController.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/sensor/SensorCallbackController.kt new file mode 100644 index 0000000..8630c63 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/sensor/SensorCallbackController.kt @@ -0,0 +1,14 @@ +package com.menagerie.ophelia.sensor + +fun interface SensorManager { + fun registerListener(listener: Listener) +} + +interface Listener { + fun onUpdate(sensorData : SensorData) +} + +data class SensorData( + val roll: Float, + val pitch: Float, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/ui/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/ui/theme/Theme.kt index feeff6e..5f732a3 100644 --- a/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/ui/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/com/menagerie/ophelia/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package com.menagerie.ophelia.ui.theme -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme diff --git a/composeApp/src/desktopMain/kotlin/com/menagerie/ophelia/main.kt b/composeApp/src/desktopMain/kotlin/com/menagerie/ophelia/main.kt index 05dddef..f708cc1 100644 --- a/composeApp/src/desktopMain/kotlin/com/menagerie/ophelia/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/menagerie/ophelia/main.kt @@ -8,6 +8,6 @@ fun main() = application { onCloseRequest = ::exitApplication, title = "Ophelia", ) { - App() + App(null, true) } } \ No newline at end of file diff --git a/composeApp/src/wasmJsMain/kotlin/com/menagerie/ophelia/main.kt b/composeApp/src/wasmJsMain/kotlin/com/menagerie/ophelia/main.kt index 22f64f9..76e9f9f 100644 --- a/composeApp/src/wasmJsMain/kotlin/com/menagerie/ophelia/main.kt +++ b/composeApp/src/wasmJsMain/kotlin/com/menagerie/ophelia/main.kt @@ -7,6 +7,6 @@ import kotlinx.browser.document @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(document.body!!) { - App() + App(null, true) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f61b8aa..0846a92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.7.2" -android-compileSdk = "34" +android-compileSdk = "35" android-minSdk = "24" android-targetSdk = "34" androidx-activityCompose = "1.9.3" @@ -19,6 +19,7 @@ material3Android = "1.3.1" material3Desktop = "1.3.1" uiTextGoogleFonts = "1.7.5" core = "1.15.0" +navigation-compose = "2.7.0-alpha07" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }