ophelia-recipe #1

Merged
Azea_Avenbright merged 3 commits from ophelia-recipe into master 2024-11-16 18:02:03 -05:00
38 changed files with 1809 additions and 51 deletions

View file

@ -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"

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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<SensorData> = 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) {
}
}

View file

@ -0,0 +1,9 @@
package com.menagerie.ophelia.sensor
class SensorManagerImpl : SensorManager {
var listener: Listener? = null
override fun registerListener(listener: Listener) {
this.listener = listener
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15.73,3H8.27L3,8.27v7.46L8.27,21h7.46L21,15.73V8.27L15.73,3zM17,15.74L15.74,17 12,13.26 8.26,17 7,15.74 10.74,12 7,8.26 8.26,7 12,10.74 15.74,7 17,8.26 13.26,12 17,15.74z"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10.1,15.9l1.42,-1.42C8.79,12.05 7,10.41 7,8.85C7,7.8 7.8,7 8.85,7c1.11,0 1.54,0.65 2.68,2h0.93c1.12,-1.31 1.53,-2 2.68,-2c0.87,0 1.55,0.54 1.77,1.32c0.35,-0.04 0.68,-0.06 1,-0.06c0.36,0 0.7,0.03 1.03,0.08C18.7,6.43 17.13,5 15.15,5c-0.12,0 -0.23,0.03 -0.35,0.04C14.92,4.71 15,4.37 15,4c0,-1.66 -1.34,-3 -3,-3S9,2.34 9,4c0,0.37 0.08,0.71 0.2,1.04C9.08,5.03 8.97,5 8.85,5C6.69,5 5,6.69 5,8.85C5,11.27 7.04,13.16 10.1,15.9z"/>
<path android:fillColor="@android:color/white" android:pathData="M22.5,16.24c-0.32,-0.18 -0.66,-0.29 -1,-0.35c0.07,-0.1 0.15,-0.18 0.21,-0.28c1.08,-1.87 0.46,-4.18 -1.41,-5.26c-2.09,-1.21 -4.76,-0.39 -8.65,0.9l0.52,1.94c3.47,-1.14 5.79,-1.88 7.14,-1.1c0.91,0.53 1.2,1.61 0.68,2.53c-0.56,0.96 -1.33,1 -3.07,1.32l-0.47,0.81c0.58,1.62 0.97,2.33 0.39,3.32c-0.53,0.91 -1.61,1.2 -2.53,0.68c-0.06,-0.03 -0.11,-0.09 -0.17,-0.13c-0.3,0.67 -0.64,1.24 -1.03,1.73c0.07,0.04 0.13,0.09 0.2,0.14c1.87,1.08 4.18,0.46 5.26,-1.41c0.06,-0.1 0.09,-0.21 0.14,-0.32c0.22,0.27 0.48,0.51 0.8,0.69c1.43,0.83 3.27,0.34 4.1,-1.1S23.93,17.06 22.5,16.24z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.32,14.01c-0.74,3.58 -1.27,5.95 -2.62,6.73c-0.91,0.53 -2,0.24 -2.53,-0.68c-0.56,-0.96 -0.2,-1.66 0.39,-3.32L7.1,15.93c-1.7,-0.31 -2.5,-0.33 -3.07,-1.32c-0.53,-0.91 -0.24,-2 0.68,-2.53c0.09,-0.05 0.19,-0.08 0.29,-0.11c-0.35,-0.56 -0.64,-1.17 -0.82,-1.85c-0.16,0.07 -0.32,0.14 -0.48,0.23c-1.87,1.08 -2.49,3.39 -1.41,5.26c0.06,0.1 0.14,0.18 0.21,0.28c-0.34,0.06 -0.68,0.17 -1,0.35c-1.43,0.83 -1.93,2.66 -1.1,4.1s2.66,1.93 4.1,1.1c0.32,-0.18 0.58,-0.42 0.8,-0.69c0.05,0.11 0.08,0.22 0.14,0.32c1.08,1.87 3.39,2.49 5.26,1.41c2.09,-1.21 2.71,-3.93 3.55,-7.94L12.32,14.01z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/purple_200"/>
</item>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_app_logo"/>
</item>
</layer-list>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/purple_200"/>
</item>
<item
android:width="140dp"
android:height="180dp"
android:drawable="@drawable/ic_app_logo"
android:gravity="center" />
</layer-list>

View file

@ -0,0 +1,9 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4.5,9.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
<path android:fillColor="@android:color/white" android:pathData="M9,5.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
<path android:fillColor="@android:color/white" android:pathData="M15,5.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
<path android:fillColor="@android:color/white" android:pathData="M19.5,9.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
<path android:fillColor="@android:color/white" android:pathData="M17.34,14.86c-0.87,-1.02 -1.6,-1.89 -2.48,-2.91 -0.46,-0.54 -1.05,-1.08 -1.75,-1.32 -0.11,-0.04 -0.22,-0.07 -0.33,-0.09 -0.25,-0.04 -0.52,-0.04 -0.78,-0.04s-0.53,0 -0.79,0.05c-0.11,0.02 -0.22,0.05 -0.33,0.09 -0.7,0.24 -1.28,0.78 -1.75,1.32 -0.87,1.02 -1.6,1.89 -2.48,2.91 -1.31,1.31 -2.92,2.76 -2.62,4.79 0.29,1.02 1.02,2.03 2.33,2.32 0.73,0.15 3.06,-0.44 5.54,-0.44h0.18c2.48,0 4.81,0.58 5.54,0.44 1.31,-0.29 2.04,-1.31 2.33,-2.32 0.31,-2.04 -1.3,-3.49 -2.61,-4.8z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,14c0,1.3 0.84,2.4 2,2.82V20H3v2h6v-2H7v-3.18C8.16,16.4 9,15.3 9,14V6H3V14zM5,8h2v3H5V8z"/>
<path android:fillColor="@android:color/white" android:pathData="M20.63,8.54l-0.95,-0.32C19.28,8.09 19,7.71 19,7.28V3c0,-0.55 -0.45,-1 -1,-1h-3c-0.55,0 -1,0.45 -1,1v4.28c0,0.43 -0.28,0.81 -0.68,0.95l-0.95,0.32C11.55,8.82 11,9.58 11,10.44V20c0,1.1 0.9,2 2,2h7c1.1,0 2,-0.9 2,-2v-9.56C22,9.58 21.45,8.82 20.63,8.54zM16,4h1v1h-1V4zM13,10.44l0.95,-0.32C15.18,9.72 16,8.57 16,7.28V7h1v0.28c0,1.29 0.82,2.44 2.05,2.85L20,10.44V12h-7V10.44zM20,20h-7v-2h7V20z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,12c0,-1.1 -0.9,-2 -2,-2V7c0,-1.1 -0.9,-2 -2,-2H8C6.9,5 6,5.9 6,7v3c-1.1,0 -2,0.9 -2,2v5h1.33L6,19h1l0.67,-2h8.67L17,19h1l0.67,-2H20V12zM16,10h-3V7h3V10zM8,7h3v3H8V7zM6,12h12v3H6V12z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#7dae7d"
android:pathData="M0,0h108v108h-108z" />
</vector>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M22,9l0,-2l-2,0l0,2l-2,0l0,2l2,0l0,2l2,0l0,-2l2,0l0,-2z"/>
<path android:fillColor="@android:color/white" android:pathData="M8,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4S4,5.79 4,8S5.79,12 8,12z"/>
<path android:fillColor="@android:color/white" android:pathData="M8,13c-2.67,0 -8,1.34 -8,4v3h16v-3C16,14.34 10.67,13 8,13z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.51,4.05C13.43,5.11 14,6.49 14,8s-0.57,2.89 -1.49,3.95C14.47,11.7 16,10.04 16,8S14.47,4.3 12.51,4.05z"/>
<path android:fillColor="@android:color/white" android:pathData="M16.53,13.83C17.42,14.66 18,15.7 18,17v3h2v-3C20,15.55 18.41,14.49 16.53,13.83z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,7.77L18.39,18H5.61L12,7.77M12,4L2,20h20L12,4z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.93,0 3.5,1.57 3.5,3.5S13.93,15 12,15s-3.5,-1.57 -3.5,-3.5S10.07,8 12,8zM16.53,8.38l3.97,-3.96V7h2V1h-6v2h2.58l-3.97,3.97C14.23,6.36 13.16,6 12,6c-1.16,0 -2.23,0.36 -3.11,0.97L8.24,6.32l1.41,-1.41L8.24,3.49L6.82,4.9L4.92,3H7.5V1h-6v6h2V4.42l1.91,1.9L3.99,7.74l1.41,1.41l1.41,-1.41l0.65,0.65C6.86,9.27 6.5,10.34 6.5,11.5c0,2.7 1.94,4.94 4.5,5.41L11,19H9v2h2v2h2v-2h2v-2h-2l0,-2.09c2.56,-0.47 4.5,-2.71 4.5,-5.41C17.5,10.34 17.14,9.27 16.53,8.38z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,20H2v-2h5.75l0,0C7.02,15.19 4.81,12.99 2,12.26C2.64,12.1 3.31,12 4,12C8.42,12 12,15.58 12,20zM22,12.26C21.36,12.1 20.69,12 20,12c-2.93,0 -5.48,1.58 -6.88,3.93c0.29,0.66 0.53,1.35 0.67,2.07c0.13,0.65 0.2,1.32 0.2,2h2h6v-2h-5.75C16.98,15.19 19.19,12.99 22,12.26zM15.64,11.02c0.78,-2.09 2.23,-3.84 4.09,-5C15.44,6.16 12,9.67 12,14c0,0.01 0,0.02 0,0.02C12.95,12.75 14.2,11.72 15.64,11.02zM11.42,8.85C10.58,6.66 8.88,4.89 6.7,4C8.14,5.86 9,8.18 9,10.71c0,0.21 -0.03,0.41 -0.04,0.61c0.43,0.24 0.83,0.52 1.22,0.82C10.39,10.96 10.83,9.85 11.42,8.85z"/>
</vector>

View file

@ -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")
// }
// }
// }
// }
}
}
}

View file

@ -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<String>,
val instructions: List<String>,
val image: DrawableResource,
val bgImage: DrawableResource? = null,
val bgImageLarge: DrawableResource? = null,
val bgColor: Color
)

View file

@ -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 dont 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 910 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 youll 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,
)
)

View file

@ -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)
)
}
}
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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
)
}

View file

@ -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
)
}
}
}

View file

@ -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)
)
}
}
}

View file

@ -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)
}
}

View file

@ -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)
)
}
}

View file

@ -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
)
})
}
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}

View file

@ -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<Recipe>,
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
}

View file

@ -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,
)

View file

@ -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

View file

@ -8,6 +8,6 @@ fun main() = application {
onCloseRequest = ::exitApplication,
title = "Ophelia",
) {
App()
App(null, true)
}
}

View file

@ -7,6 +7,6 @@ import kotlinx.browser.document
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport(document.body!!) {
App()
App(null, true)
}
}

View file

@ -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" }