Compare commits

...
Sign in to create a new pull request.

8 commits
master ... tags

43 changed files with 1227 additions and 621 deletions

View file

@ -1,25 +1,31 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.sqlDelight)
}
kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
compilations.all {
kotlinOptions {
jvmTarget = "21"
}
}
}
jvm("desktop")
sqldelight{
databases {
create("PolyculeDatabase") {
packageName = "com.menagerie.ophelia"
}
}
}
sourceSets {
val desktopMain by getting
@ -28,6 +34,7 @@ kotlin {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material3.android)
implementation(libs.sqldelight.android)
}
commonMain.dependencies {
implementation(compose.runtime)
@ -43,11 +50,13 @@ kotlin {
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha07")
api(libs.datastore.preferences)
api(libs.datastore)
implementation(libs.sqldelight.coroutines)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.androidx.material3.desktop)
implementation(libs.sqldelight.jvm)
}
}
}
@ -74,8 +83,8 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
dependencies {

View file

@ -0,0 +1,17 @@
import android.content.Context
import com.menagerie.ophelia.AppModule
import com.menagerie.ophelia.DatabaseDriverFactory
import com.menagerie.ophelia.PolyculeDatabase
import com.menagerie.ophelia.data.user.UserDataSource
class AndroidAppModule(
private val context: Context,
) : AppModule {
private val db by lazy {
PolyculeDatabase(
driver = DatabaseDriverFactory(context).create()
)
}
override fun provideUserDataSource() = UserDataSource(db)
}

View file

@ -0,0 +1,10 @@
package com.menagerie.ophelia
import android.content.Context
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
actual class DatabaseDriverFactory(private val context: Context) {
actual fun create(): SqlDriver =
AndroidSqliteDriver(PolyculeDatabase.Schema, context, "POLYCULE_DATABASE")
}

View file

@ -12,13 +12,12 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
MainView(
MainView(
prefs = remember {
createDataStore(application)
}
)
)
}
}
}

View file

@ -1,3 +1,4 @@
<resources>
<string name="app_name">Ophelia</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Calligraffitti"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

View file

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="back">Back</string>
<string name="app_tag">A Polycule Pocket-dex</string>
<string name="welcome">Get Started</string>
<string name="welcome_back">Welcome Back</string>
##Routes
<string name="route_polyculeHome">polyculeHome</string>
<string name="route_home">home</string>
##Bio
<string name="add_bio">Add Bio</string>
<string name="edit_bio">Edit Bio</string>
<string name="finish">Finish</string>
<string name="bio_load">Bio Loaded</string>
<string name="open_context_menu">Open context menu</string>
<string name="name">Name</string>
<string name="bio_add_toast">New Bio Added!</string>
<string name="bio_edit_toast">Bio Edited!</string>
<string name="bio_finish_toast">Welcome Aboard!</string>
##Tags
<string name="mono">Monogamous</string>
<string name="plural">Plural</string>
<string name="furry">Furry</string>
<string name="therian">Therian</string>
<string name="bdsm">BDSM</string>
<string name="alcohol">Alcohol</string>
<string name="weed">Weed</string>
<string name="asexual">Asexual</string>
<string name="trans">Transgender</string>
##Ophelia
<string name = "ophelia_says">Ophelia Says</string>
<string name = "final_check_in">What do you think; looks like you?</string>
<string-array name="welcome_blurb">
<item>Hi!</item>
<item>I'm Ophelia, a Polycule companion system designed to help you keep track of your metas, reference a charter, and find things in common with your polycule.</item>
<item>I'm meant to work dynamically with how polyamory works for you, so what all those words mean will be something we work out together later.</item>
<item>First, I need to get to know "you".</item>
<item>Click Go to get started.</item>
</string-array>
<string-array name="whats_your_name">
<item>I'm Ophelia, what's your name?</item>
<item>Your Name is how you'll be addressed in the app, and is the primary way you'll be referred to in polycules.</item>
<item>But don't worry; your name, like everything about you, can be changed at any time!</item>
</string-array>
<string name="introduction_with_name">"Nice to meet you,%1$s!</string>
<string-array name="introduction_to_pronouns">
<item>Personal Pronouns are words used to refer to a person without always using their name.</item>
<item>Since Pronouns are a personal choice, I want to leave them as open as possible for you!</item>
<item>We're dealing with four Personal Pronouns, and using mine as an example: She (subjective) / Her (objective) / Hers (possessive) / Herself (reflexive)</item>
<item>Your Pronouns will be used when referring to you in specific contexts, and are displayed alongside your name in most contexts.</item>
</string-array>
<string-array name="introduction_to_tags">
<item>Tags are a way to highlight important things about yourself.</item>
<item>They also lead to more detailed parts of you bio, where you can flesh out specific parts of your bio.</item>
</string-array>
<string-array name="introduction_to_polycules">
<item>Great!</item>
<item>Now, there's just one more thing! We need to create a polycule for you. A "polycule" for our purposes is defined as a group of people with a shared relationship process.</item>
<item>A person may be in a polycule with any number of people, including just themselves, and in any number of polycules.</item>
<item>All we need to get started is a name:</item>
</string-array>
##Helpers
<string name = "go">Go</string>
</resources>

View file

@ -2,7 +2,9 @@ package com.menagerie.ophelia
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material3.Scaffold
@ -18,14 +20,19 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.menagerie.ophelia.HomeScreen.HomeScreen
import com.menagerie.ophelia.HomeScreen.MainMenu
import com.menagerie.ophelia.HomeScreen.Settings
import com.menagerie.ophelia.model.Recipe
import com.menagerie.ophelia.model.recipesList
import com.menagerie.ophelia.recipesdetails.RecipeDetails
import com.menagerie.ophelia.recipeslist.RecipesListScreen
import com.menagerie.ophelia.sensor.SensorManager
import com.menagerie.ophelia.view.HomeScreen.HomeScreen
import com.menagerie.ophelia.view.HomeScreen.MainMenu
import com.menagerie.ophelia.view.HomeScreen.Settings
import com.menagerie.ophelia.view.newUser.AddNameScreen
import com.menagerie.ophelia.view.recipe.model.Recipe
import com.menagerie.ophelia.view.recipe.model.recipesList
import com.menagerie.ophelia.view.recipe.recipesdetails.RecipeDetails
import com.menagerie.ophelia.view.recipe.recipeslist.RecipesListScreen
import com.menagerie.ophelia.view.tags.model.Tag
import com.menagerie.ophelia.view.tags.tagsdetails.TagsDetails
import com.menagerie.ophelia.view.tags.tagslist.TagsListScreen
import com.menagerie.ophelia.view.newUser.NewUserStartScreen
enum class DetailListAppScreen {
List,
@ -47,8 +54,6 @@ fun App(
sensorManager = sensorManager,
prefs = prefs
)
}
@Composable
@ -59,38 +64,58 @@ fun OpheliaNavHost(
prefs: DataStore<Preferences>
){
val items by remember { mutableStateOf(recipesList) }
var show by remember { mutableStateOf(false) }
var showBar by remember { mutableStateOf(false) }
Scaffold {
Column {
//TODO : replace this with a composable task bar
if (show) {
Row {
Button(
onClick = {
navController.popBackStack()
//TODO : replace row with a lazyGrid for better mobile support
if (showBar) {
val listState = rememberLazyGridState()
LazyVerticalGrid(
state = listState, columns = GridCells.Fixed(if (isLarge) 3 else 1)
) {
item {
Button(
onClick = {
navController.popBackStack()
}
) {
Text(text = "Back - TEMP")
}
) {
Text(text = "Back - TEMP")
}
Button(
onClick = {
navController.navigate("mainMenu")
item {
Button(
onClick = {
navController.navigate("mainMenu")
}
) {
Text(text = "Home - TEMP")
}
) {
Text(text = "Home - TEMP")
}
Button(
onClick = {
navController.navigate("settings")
item {
Button(
onClick = {
navController.navigate("settings")
}
) {
Text(text = "Settings - TEMP")
}
) {
Text(text = "Settings - TEMP")
}
Button(onClick = {
//TODO : This should take the user to their bio page
}){
Text(text = "You - TEMP")
item {
Button(onClick = {
//TODO : This should take the user to their bio page
}) {
Text(text = "You - TEMP")
}
}
item {
Button(onClick = {
navController.navigate("dex")
}) {
Text(text = "Encyclopedia - TEMP")
}
}
}
}
@ -106,15 +131,17 @@ fun OpheliaNavHost(
isLarge = isLarge,
sensorManager = sensorManager
)
welcomeGraph(navController)
composable("home") {
showBar = false
HomeScreen {
show = true
navController.navigate("mainMenu")
navController.navigate("newUserStart")
}
}
composable("mainMenu") {
showBar = true
MainMenu(
onPolyClick = {
//TODO : This should take the user to their polycule's main page
@ -137,13 +164,14 @@ fun OpheliaNavHost(
}
}
//Graph for new user startup
fun NavGraphBuilder.welcomeGraph(navController: NavHostController) {
// composable("newUserStart") {
// NewUserStartScreen { navController.navigate("newUserName") }
// }
// composable("newUserName") {
// AddNameScreen { navController.navigate("newUserPronouns") }
// }
composable("newUserStart") {
NewUserStartScreen { navController.navigate("mainMenu") }
}
composable("newUserName") {
AddNameScreen { navController.navigate("newUserPronouns") }
}
// composable("newUserPronouns") {
// AddPronounsScreen { navController.navigate("newUserTags") }
// }
@ -161,6 +189,7 @@ fun NavGraphBuilder.welcomeGraph(navController: NavHostController) {
// }
}
//Graph for Polycule navigation
fun NavGraphBuilder.polyculeGraph(navController: NavHostController) {
// composable("polyculeHome") {
// PolyculeHomeView(
@ -197,6 +226,7 @@ fun NavGraphBuilder.polyculeGraph(navController: NavHostController) {
// }
}
//Graph for your Cookbook
fun NavGraphBuilder.recipeGraph(
navController: NavHostController,
items: List<Recipe>,
@ -220,7 +250,37 @@ fun NavGraphBuilder.recipeGraph(
isLarge = isLarge,
sensorManager = sensorManager,
recipe = currentRecipe,
goBack = { navController.popBackStack() }
)
}
}
//Graph for the Tags
fun NavGraphBuilder.tagGraph(
navController: NavHostController,
items: List<Tag>,
isLarge: Boolean,
sensorManager: SensorManager?,
) {
var currentTag = items.first()
composable(route = DetailListAppScreen.List.name){
TagsListScreen(
isLarge = isLarge,
items = items,
onClick = { tag ->
currentTag = tag
navController.navigate(DetailListAppScreen.Details.name)
}
)
}
composable(route = DetailListAppScreen.Details.name) {
TagsDetails(
isLarge = isLarge,
sensorManager = sensorManager,
tag = currentTag,
)
}
}

View file

@ -0,0 +1,7 @@
package com.menagerie.ophelia
import com.menagerie.ophelia.data.user.UserDataSource
interface AppModule {
fun provideUserDataSource(): UserDataSource
}

View file

@ -0,0 +1,7 @@
package com.menagerie.ophelia
import app.cash.sqldelight.db.SqlDriver
expect class DatabaseDriverFactory {
fun create(): SqlDriver
}

View file

@ -0,0 +1,24 @@
package com.menagerie.ophelia.data.user
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import com.menagerie.ophelia.PolyculeDatabase
import kotlinx.coroutines.Dispatchers
class UserDataSource(db: PolyculeDatabase) {
private val queries = db.userQueries
fun insert(id: Long?, name: String) {
queries.insert(id = id, name = name)
}
fun getAll() = queries.getAll().asFlow().mapToList(Dispatchers.IO)
fun update(id: Long, name: String) {
queries.updateName(id = id, name = name)
}
fun delete(id: Long) {
queries.delete(id = id)
}
}

View file

@ -1,270 +0,0 @@
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.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
import com.menagerie.ophelia.ui.theme.orangeDark
import com.menagerie.ophelia.ui.theme.sugar
import com.menagerie.ophelia.ui.theme.yellow
@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

@ -1,252 +0,0 @@
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.ui.theme.sugar
import com.menagerie.ophelia.ui.theme.yellow
import com.menagerie.ophelia.ui.theme.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

@ -1,4 +1,4 @@
package com.menagerie.ophelia.HomeScreen
package com.menagerie.ophelia.view.HomeScreen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.menagerie.ophelia.ui.theme.OpheliaTheme
@Composable
fun HomeScreen(
@ -56,6 +55,7 @@ fun HeaderText(
) {
Text(text = text)
}
Spacer(modifier = Modifier.weight(1f))
}
}
}

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.HomeScreen
package com.menagerie.ophelia.view.HomeScreen
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.HomeScreen
package com.menagerie.ophelia.view.HomeScreen
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button

View file

@ -0,0 +1,73 @@
package com.menagerie.ophelia.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import ophelia.composeapp.generated.resources.Res
import ophelia.composeapp.generated.resources.header_font
import ophelia.composeapp.generated.resources.ophelia
import ophelia.composeapp.generated.resources.welcome_blurb
import org.jetbrains.compose.resources.StringArrayResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringArrayResource
//@Composable
//fun OpheliaSpanStyle(): SpanStyle {
// val fontFamily = (Res.font.header_font)
//
// return SpanStyle(
// letterSpacing = MaterialTheme.typography.bodyLarge.letterSpacing,
// fontFamily = fontFamily,
// fontSize = MaterialTheme.typography.bodyLarge.fontSize)
//}
@Composable
fun OpheliaFace(
modifier: Modifier
) {
Image(painter = painterResource(Res.drawable.ophelia),
contentDescription = null,
modifier = modifier
)
}
@Composable
fun OpheliaWelcome(
modifier: Modifier
) {
Box(
modifier = modifier
.fillMaxSize()
.padding(24.dp)
) {
Text(
text = OpheliaSaysArray(Res.array.welcome_blurb)
)
}
}
@Composable
fun OpheliaSaysArray(
res: StringArrayResource,
separator: String = "\n\n"
): AnnotatedString {
return buildAnnotatedString {
//pushStyle(OpheliaSpanStyle())
append(stringArrayResource(res).joinToString(separator = separator))
toAnnotatedString()
}
}

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipeslist
package com.menagerie.ophelia.view.common.list
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -14,7 +14,7 @@ import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
@Composable
fun RecipeImage(imageBitmap: DrawableResource, modifier: Modifier){
fun ListImage(imageBitmap: DrawableResource, modifier: Modifier){
Box(modifier = modifier) {
Box(
modifier = modifier

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipeslist
package com.menagerie.ophelia.view.common.list
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
@ -18,14 +18,14 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
@Composable
fun RecipeListItemImageWrapper(
fun ListItemImageWrapper(
modifier: Modifier,
child: @Composable () -> Unit,
) {
val animationDuration = 700
val scale = remember { Animatable(0.3f) }
val rotation = remember {Animatable(20f)}
val offset = remember {Animatable(0f)}
val rotation = remember { Animatable(20f) }
val offset = remember { Animatable(0f) }
LaunchedEffect(Unit) {
scale.animateTo(
@ -47,13 +47,13 @@ fun RecipeListItemImageWrapper(
}
LaunchedEffect(Unit) {
offset.animateTo(
60f,
animationSpec = tween(
durationMillis = animationDuration / 2,
easing = FastOutSlowInEasing
offset.animateTo(
60f,
animationSpec = tween(
durationMillis = animationDuration / 2,
easing = FastOutSlowInEasing
)
)
)
offset.animateTo(
targetValue = 0f,
animationSpec = spring(

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipeslist
package com.menagerie.ophelia.view.common.list
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
@ -11,13 +11,13 @@ 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
//Wrapper for animating cards
@Composable
fun RecipeListItemWrapper(
fun ListItemWrapper(
child: @Composable () -> Unit,
scrollDirection: Boolean
scrollDirection: Boolean,
) {
val scaleAnimatable = remember { Animatable(initialValue = 0.75f) }
val rotateXAnimatable =
@ -66,4 +66,5 @@ fun RecipeListItemWrapper(
{
child()
}
}

View file

@ -0,0 +1,11 @@
package com.menagerie.ophelia.view.newUser
import androidx.compose.runtime.Composable
@Composable
fun AddNameScreen(
onGo: () -> Unit,
) {
//val inputViewModel: BioListViewModel.InputBioViewModel()
}

View file

@ -0,0 +1,46 @@
package com.menagerie.ophelia.view.newUser
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.menagerie.ophelia.view.OpheliaFace
import com.menagerie.ophelia.view.OpheliaWelcome
import ophelia.composeapp.generated.resources.Res
import ophelia.composeapp.generated.resources.go
import org.jetbrains.compose.resources.stringResource
@Composable
fun NewUserStartScreen(
onGo: () -> Unit = {},
) {
Scaffold {
Column {
OpheliaFace(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 12.dp)
)
OpheliaWelcome(
modifier = Modifier.weight(1f)
)
Button(
onClick = onGo,
modifier = Modifier
.padding(12.dp)
.align(Alignment.End)
) {
Text(stringResource(Res.string.go))
}
Spacer(
modifier = Modifier.padding(12.dp)
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.model
package com.menagerie.ophelia.view.recipe.model
import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.DrawableResource

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.model
package com.menagerie.ophelia.view.recipe.model
import androidx.compose.ui.graphics.Color
import com.menagerie.ophelia.ui.theme.green

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipesdetails
package com.menagerie.ophelia.view.recipe.recipesdetails
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -20,7 +20,7 @@ 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 com.menagerie.ophelia.view.recipe.model.Recipe
import ophelia.composeapp.generated.resources.Res
import ophelia.composeapp.generated.resources.ophelia
import org.jetbrains.compose.resources.ExperimentalResourceApi

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipesdetails
package com.menagerie.ophelia.view.recipe.recipesdetails
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -22,7 +22,7 @@ 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
import com.menagerie.ophelia.view.recipe.model.Recipe
@Composable
fun InstructionItem(recipe: Recipe, index: Int) {

View file

@ -1,28 +1,21 @@
package com.menagerie.ophelia.recipesdetails
package com.menagerie.ophelia.view.recipe.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.view.recipe.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,238 @@
package com.menagerie.ophelia.view.recipe.recipesdetails
import androidx.compose.animation.core.animateIntOffsetAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.material3.Icon
import androidx.compose.material3.Text
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.view.recipe.model.Recipe
import com.menagerie.ophelia.sensor.Listener
import com.menagerie.ophelia.sensor.SensorData
import com.menagerie.ophelia.sensor.SensorManager
import com.menagerie.ophelia.ui.theme.orangeDark
import com.menagerie.ophelia.ui.theme.sugar
import com.menagerie.ophelia.ui.theme.yellow
import org.jetbrains.compose.resources.painterResource
import kotlin.math.PI
@Composable
fun RecipeDetailsLarge(
recipe: Recipe,
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
)
}
}
}
}
}
}

View file

@ -0,0 +1,210 @@
package com.menagerie.ophelia.view.recipe.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.view.recipe.model.Recipe
import com.menagerie.ophelia.ui.theme.sugar
import com.menagerie.ophelia.ui.theme.yellow
import com.menagerie.ophelia.ui.theme.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,
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(
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
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipesdetails
package com.menagerie.ophelia.view.recipe.recipesdetails
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
@ -8,7 +8,7 @@ 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
import com.menagerie.ophelia.view.recipe.model.Recipe
internal fun LazyListScope.StepsAndDetails(
@ -39,7 +39,7 @@ internal fun LazyListScope.StepsAndDetails(
}
itemsIndexed(recipe.ingredients) { index, value ->
itemsIndexed(recipe.ingredients) { _, value ->
IngredientItem(recipe, value)
}

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipeslist
package com.menagerie.ophelia.view.recipe.recipeslist
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -22,7 +22,9 @@ 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
import com.menagerie.ophelia.view.common.list.ListImage
import com.menagerie.ophelia.view.common.list.ListItemImageWrapper
import com.menagerie.ophelia.view.recipe.model.Recipe
@Composable
@ -73,9 +75,9 @@ fun RecipeListItem(
Spacer(modifier = Modifier.weight(1f))
}
}
RecipeListItemImageWrapper(modifier = Modifier.align(Alignment.BottomEnd)
ListItemImageWrapper(modifier = Modifier.align(Alignment.BottomEnd)
.fillMaxWidth(0.45f).aspectRatio(1f), child = {
RecipeImage(
ListImage(
imageBitmap = recipe.image, modifier = Modifier
)
})

View file

@ -1,4 +1,4 @@
package com.menagerie.ophelia.recipeslist
package com.menagerie.ophelia.view.recipe.recipeslist
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
@ -17,7 +17,8 @@ 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.view.common.list.ListItemWrapper
import com.menagerie.ophelia.view.recipe.model.Recipe
import com.menagerie.ophelia.ui.theme.sugar
@Composable
@ -39,7 +40,7 @@ fun RecipesListScreen(
}
items(items.size) { item ->
val recipe = items[item]
RecipeListItemWrapper(
ListItemWrapper(
scrollDirection = listState.isScrollingUp(),
child = {
RecipeListItem(

View file

@ -0,0 +1,15 @@
package com.menagerie.ophelia.view.tags.model
import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
data class Tag @OptIn(ExperimentalResourceApi::class) constructor(
val id: Int,
val title: String,
val description: String,
val image: DrawableResource,
val bgImage: DrawableResource? = null,
val bgImageLarge: DrawableResource? = null,
val bgColor: Color
)

View file

@ -0,0 +1,71 @@
package com.menagerie.ophelia.view.tags.model
import com.menagerie.ophelia.ui.theme.sugar
import ophelia.composeapp.generated.resources.Res
import ophelia.composeapp.generated.resources.ophelia
val tagList = listOf(
Tag(
id = 1,
title = "Alcohol",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 2,
title = "Asexual",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 3,
title = "BDSM",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 4,
title = "Furry",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 5,
title = "Monogamous",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 6,
title = "Plural",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 7,
title = "Therian",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 8,
title = "Transgender",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
Tag(
id = 9,
title = "Weed",
description = "",
image = Res.drawable.ophelia,
bgColor = sugar
),
)

View file

@ -0,0 +1,14 @@
package com.menagerie.ophelia.view.tags.tagsdetails
import androidx.compose.runtime.Composable
import com.menagerie.ophelia.sensor.SensorManager
import com.menagerie.ophelia.view.tags.model.Tag
@Composable
fun TagsDetails(
sensorManager: SensorManager?,
tag: Tag,
isLarge: Boolean
) {
}

View file

@ -0,0 +1,89 @@
package com.menagerie.ophelia.view.tags.tagslist
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.material3.MaterialTheme
import androidx.compose.material3.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.view.common.list.ListImage
import com.menagerie.ophelia.view.common.list.ListItemImageWrapper
import com.menagerie.ophelia.view.tags.model.Tag
//The Actual Content of the Card
@Composable
fun TagListItem(
tag: Tag,
onClick: (tag: Tag) -> 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 = tag.bgColor, shape = RoundedCornerShape(35.dp)).fillMaxHeight().clickable {
onClick(tag)
}) {
Card(
backgroundColor = tag.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 = tag.title,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
)
Text(
tag.description,
style = MaterialTheme.typography.displayMedium,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(top = 8.dp),
)
}
Spacer(modifier = Modifier.weight(1f))
}
}
ListItemImageWrapper(modifier = Modifier.align(Alignment.BottomEnd)
.fillMaxWidth(0.45f).aspectRatio(1f), child = {
ListImage(
imageBitmap = tag.image, modifier = Modifier
)
})
}
}
}
}

View file

@ -0,0 +1,81 @@
package com.menagerie.ophelia.view.tags.tagslist
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.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
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.runtime.setValue
import androidx.compose.ui.Modifier
import com.menagerie.ophelia.view.common.list.ListItemWrapper
import com.menagerie.ophelia.view.tags.model.Tag
import com.menagerie.ophelia.ui.theme.sugar
@Composable
fun TagsListScreen(
items: List<Tag>,
onClick: (tag: Tag) -> Unit,
isLarge: Boolean,
) {
//wrap the whole screen in a box (or scaffold)
Box(
modifier = Modifier
.fillMaxSize()
.background(sugar)
){
val listState = rememberLazyGridState() //keep track of how the cells are arranged
LazyVerticalGrid(
state = listState,
columns = GridCells.Fixed(if (isLarge) 3 else 1) //TODO : replace check with enum values?
) {
if(isLarge.not())
item {
Spacer(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars))
} //add a top spacer on mobile for the system bars
items(items.size) { item ->
val tag = items[item]
ListItemWrapper(
scrollDirection = listState.isScrollingUp(),
child = {
TagListItem(
tag = tag,
onClick = onClick
)
}
)
} //grid items list of tags
} // grid
} // Box
} // screen
//TODO : Move this to its own helper file
@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,20 @@
CREATE TABLE User (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
insert:
INSERT OR REPLACE INTO User(id, name)
VALUES(?,?);
getAll:
SELECT * FROM User;
updateName:
UPDATE User
SET name = :name
WHERE id IS :id;
delete:
DELETE FROM User
WHERE id IS :id;

View file

@ -0,0 +1,12 @@
package com.menagerie.ophelia
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
actual class DatabaseDriverFactory {
actual fun create(): SqlDriver {
val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
PolyculeDatabase.Schema.create(driver)
return driver
}
}

View file

@ -0,0 +1,13 @@
package com.menagerie.ophelia
import com.menagerie.ophelia.data.user.UserDataSource
class DesktopAppModule : AppModule {
private val db by lazy {
PolyculeDatabase(
driver = DatabaseDriverFactory().create()
)
}
override fun provideUserDataSource() = UserDataSource(db)
}

View file

@ -3,8 +3,10 @@ package com.menagerie.ophelia
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import androidx.datastore.preferences.core.stringPreferencesKey
import com.menagerie.ophelia.ui.theme.OpheliaTheme
import kotlinx.coroutines.flow.map
@ -28,6 +30,7 @@ fun main() {
Window(
onCloseRequest = ::exitApplication,
title = "Ophelia",
state = rememberWindowState(height = 900.dp, width = 1200.dp)
) {
val currentThemeString by prefs

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="com_google_android_gms_fonts_certs">
<item>@array/com_google_android_gms_fonts_certs_dev</item>
<item>@array/com_google_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_google_android_gms_fonts_certs_dev">
<item>
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</item>
</string-array>
<string-array name="com_google_android_gms_fonts_certs_prod">
<item>
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
</item>
</string-array>
</resources>

View file

@ -19,6 +19,7 @@ material3Android = "1.3.1"
material3Desktop = "1.3.1"
uiTextGoogleFonts = "1.7.5"
core = "1.15.0"
sqldelight = "2.0.2"
navigation-compose = "2.7.0-alpha07"
datastore = "1.1.1"
@ -39,6 +40,9 @@ kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-co
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
androidx-material3-desktop = { group = "androidx.compose.material3", name = "material3-desktop", version.ref = "material3Desktop" }
androidx-ui-text-google-fonts = { group = "androidx.compose.ui", name = "ui-text-google-fonts", version.ref = "uiTextGoogleFonts" }
sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
androidx-core = { group = "androidx.core", name = "core", version.ref = "core" }
datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
@ -48,4 +52,5 @@ androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }