From 375ee332bb9a09ef5053a4fd83ba94bda5c2c015 Mon Sep 17 00:00:00 2001 From: Comeza Date: Tue, 20 May 2025 22:24:19 +0200 Subject: [PATCH 01/11] Create items endpoint (#13) * Create insertItem muation and items query --- sapling/src/product.rs | 30 ++++++++++++++++----- sapling/src/queries/fetch_stocks.sql | 1 + sapling/src/queries/insert_item.sql | 3 +++ sapling/src/queries/mod.rs | 2 ++ sapling/src/queries/setup/create_tables.sql | 11 ++++---- sapling/src/schema/product.rs | 25 ++++++++++++++--- 6 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 sapling/src/queries/fetch_stocks.sql create mode 100644 sapling/src/queries/insert_item.sql diff --git a/sapling/src/product.rs b/sapling/src/product.rs index 686598c..8680ab8 100644 --- a/sapling/src/product.rs +++ b/sapling/src/product.rs @@ -38,13 +38,12 @@ pub struct Brand { wikipedia: Option, } -#[derive(FromRow)] -pub struct Stock { - stock_id: Id, - product_id: Id, - amount: u32, +#[derive(FromRow, SimpleObject)] +#[graphql(complex)] +pub struct Item { + item_id: Id, + ean: Ean, created_at: DateTime, - updated_at: Option>, cost: Option, } @@ -65,6 +64,25 @@ impl Product { Ok(tags) } + + async fn items<'a>(&self, ctx: &Context<'a>) -> async_graphql::Result> { + let pool = ctx.data::()?; + let rows = sqlx::query("SELECT * FROM item WHERE ean = ?;").bind(self.ean).fetch_all(pool).await?; + let items = rows.iter().map(Item::from_row).collect::, _>>()?; + + Ok(items) + } +} + +#[ComplexObject] +impl Item { + async fn product<'a>(&self, ctx: &Context<'a>) -> async_graphql::Result { + let pool = ctx.data::()?; + let row = sqlx::query(queries::SQL_FETCH_PRODUCT).bind(self.ean).fetch_one(pool).await?; + let product = Product::from_row(&row)?; + + Ok(product) + } } #[derive(sqlx::Type, Debug, Serialize, Deserialize, Clone, Copy)] diff --git a/sapling/src/queries/fetch_stocks.sql b/sapling/src/queries/fetch_stocks.sql new file mode 100644 index 0000000..a6ea11e --- /dev/null +++ b/sapling/src/queries/fetch_stocks.sql @@ -0,0 +1 @@ +SELECT * FROM item where item_id = ?; diff --git a/sapling/src/queries/insert_item.sql b/sapling/src/queries/insert_item.sql new file mode 100644 index 0000000..3cd8dc9 --- /dev/null +++ b/sapling/src/queries/insert_item.sql @@ -0,0 +1,3 @@ +INSERT INTO item (ean, cost) +VALUES (?, ?) +RETURNING *; diff --git a/sapling/src/queries/mod.rs b/sapling/src/queries/mod.rs index 106cf97..84ca5dd 100644 --- a/sapling/src/queries/mod.rs +++ b/sapling/src/queries/mod.rs @@ -5,6 +5,8 @@ pub const SQL_INSERT_SESSION: &str = include_str!("auth/insert_user_session.sql" pub const SQL_FETCH_PRODUCT: &str = include_str!("fetch_product.sql"); pub const SQL_INSERT_PRODUCT: &str = include_str!("insert_product.sql"); +pub const SQL_INSERT_ITEM: &str = include_str!("insert_item.sql"); + pub const SQL_CREATE_AUTH_TABLES: &str = include_str!("setup/create_auth_tables.sql"); pub const SQL_CREATE_TABLES: &str = include_str!("setup/create_tables.sql"); diff --git a/sapling/src/queries/setup/create_tables.sql b/sapling/src/queries/setup/create_tables.sql index 615f763..7aef90b 100644 --- a/sapling/src/queries/setup/create_tables.sql +++ b/sapling/src/queries/setup/create_tables.sql @@ -1,7 +1,7 @@ CREATE TABLE brand ( brand_id INTEGER PRIMARY KEY, name VARCHAR NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE tag ( @@ -34,10 +34,9 @@ CREATE TABLE product ( updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE stock ( - stock_id INTEGER PRIMARY KEY, - product_ean INTEGER REFERENCES product(ean), +CREATE TABLE item ( + item_id INTEGER PRIMARY KEY, + ean INTEGER REFERENCES product(ean), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP, cost INTEGER -); \ No newline at end of file +); diff --git a/sapling/src/schema/product.rs b/sapling/src/schema/product.rs index 2dc5b27..175681a 100644 --- a/sapling/src/schema/product.rs +++ b/sapling/src/schema/product.rs @@ -3,7 +3,7 @@ use sqlx::FromRow; use crate::{ Database, - product::{Ean, EanValidator, Product}, + product::{Ean, EanValidator, Product, Item}, queries, }; @@ -19,9 +19,7 @@ impl ProductQuery { ) -> Result> { let pool = ctx.data::()?; let row = sqlx::query(queries::SQL_FETCH_PRODUCT).bind(&ean).fetch_optional(pool).await?; - - let product = match row { - Some(row) => Some(Product::from_row(&row)?), +let product = match row { Some(row) => Some(Product::from_row(&row)?), None => None, }; @@ -39,6 +37,18 @@ impl ProductQuery { Ok(rows) } + + async fn items<'a>(&self, ctx: &Context<'a>) -> Result> { + let pool = ctx.data::()?; + let rows = sqlx::query("SELECT * FROM item;") + .fetch_all(pool) + .await? + .iter() + .map(Item::from_row) + .collect::, _>>()?; + + Ok(rows) + } } #[derive(Default)] @@ -57,4 +67,11 @@ impl ProductMutation { let product = Product::from_row(&row)?; Ok(product) } + + async fn insert_item<'a>(&self, ctx: &'a Context<'a>, ean: Ean, cost: i32) -> Result { + let pool = ctx.data::()?; + let row = sqlx::query(queries::SQL_INSERT_ITEM).bind(ean).bind(cost).fetch_one(pool).await?; + let stock = Item::from_row(&row)?; + Ok(stock) + } } From 6cf3cddb2d5b9cbef874523ec6bd48b5c04ec357 Mon Sep 17 00:00:00 2001 From: isiko Date: Mon, 12 May 2025 23:01:37 +0200 Subject: [PATCH 02/11] Add basic Foliage structure --- .../.kotlin/errors/errors-1747067490401.log | 4 + foliage/app/build.gradle.kts | 7 ++ .../main/java/sapling/foliage/MainActivity.kt | 62 ++++++++----- .../foliage/ui/components/MainNavBar.kt | 93 +++++++++++++++++++ .../foliage/ui/screens/InventoryScreen.kt | 19 ++++ .../foliage/ui/screens/SettingsScreen.kt | 17 ++++ .../foliage/ui/screens/ShoppingScreen.kt | 40 ++++++++ foliage/gradle/libs.versions.toml | 11 ++- 8 files changed, 229 insertions(+), 24 deletions(-) create mode 100644 foliage/.kotlin/errors/errors-1747067490401.log create mode 100644 foliage/app/src/main/java/sapling/foliage/ui/components/MainNavBar.kt create mode 100644 foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt create mode 100644 foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt create mode 100644 foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt diff --git a/foliage/.kotlin/errors/errors-1747067490401.log b/foliage/.kotlin/errors/errors-1747067490401.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/foliage/.kotlin/errors/errors-1747067490401.log @@ -0,0 +1,4 @@ +kotlin version: 2.0.21 +error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: + 1. Kotlin compile daemon is ready + diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts index 05c08fd..7a05be0 100644 --- a/foliage/app/build.gradle.kts +++ b/foliage/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) } android { @@ -40,7 +41,12 @@ android { } dependencies { + val nav_version = "2.9.0" + + implementation(libs.androidx.material) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -56,4 +62,5 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + implementation(libs.kotlinx.serialization.json) } \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt index e426752..c2eedbf 100644 --- a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt +++ b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt @@ -4,44 +4,60 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import kotlinx.serialization.Serializable +import sapling.foliage.ui.components.Inventory +import sapling.foliage.ui.components.MainNavbar +import sapling.foliage.ui.components.Settings +import sapling.foliage.ui.components.ShoppingTourList +import sapling.foliage.ui.screens.InventoryScreen +import sapling.foliage.ui.screens.SettingsScreen +import sapling.foliage.ui.screens.ShoppingScreen import sapling.foliage.ui.theme.FoliageTheme + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { FoliageTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) + val navController = rememberNavController() + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier.fillMaxSize() + ) { + NavHost( + navController = navController, + startDestination = ShoppingTourList, + modifier = Modifier.weight(1f) + ) { + composable { ShoppingScreen() } + composable { InventoryScreen() } + composable { SettingsScreen() } + } + MainNavbar(navController = navController) } } } } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - FoliageTheme { - Greeting("Android") - } } \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ui/components/MainNavBar.kt b/foliage/app/src/main/java/sapling/foliage/ui/components/MainNavBar.kt new file mode 100644 index 0000000..2cebc18 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/ui/components/MainNavBar.kt @@ -0,0 +1,93 @@ +package sapling.foliage.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Kitchen +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material.icons.outlined.Kitchen +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.ShoppingCart +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.currentBackStackEntryAsState +import kotlinx.serialization.Serializable + +data class NavItem( + val label: String, + val selectedIcon: ImageVector, + val defaultIcon: ImageVector, + val route: T +) + +val navItemList = listOf( + NavItem( + label = "Einkäufe", + selectedIcon = Icons.Filled.ShoppingCart, + defaultIcon = Icons.Outlined.ShoppingCart, + route = ShoppingTourList + ), + NavItem( + label = "Vorat", + selectedIcon = Icons.Filled.Kitchen, + defaultIcon = Icons.Outlined.Kitchen, + route = Inventory + ), + NavItem( + label = "Settings", + selectedIcon = Icons.Filled.Settings, + defaultIcon = Icons.Outlined.Settings, + route = Settings + ), +) + +@Serializable object ShoppingTourList +@Serializable object Inventory +@Serializable object Settings + + +@Composable +fun MainNavbar(modifier: Modifier = Modifier, navController: NavController) { + + NavigationBar { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + navItemList.forEach { topLevelRoute -> + val isSelected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true + NavigationBarItem( + icon = { + Icon( + imageVector = if (isSelected) topLevelRoute.selectedIcon else topLevelRoute.defaultIcon, + contentDescription = topLevelRoute.label + ) + }, + label = { Text(topLevelRoute.label) }, + selected = isSelected, + onClick = { + navController.navigate(topLevelRoute.route) { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + // Avoid multiple copies of the same destination when + // reselecting the same item + launchSingleTop = true + // Restore state when reselecting a previously selected item + restoreState = true + } + } + ) + } + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt new file mode 100644 index 0000000..5ecd8f0 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt @@ -0,0 +1,19 @@ +package sapling.foliage.ui.screens + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController +import sapling.foliage.ui.components.MainNavbar + +@Composable +fun InventoryScreen(modifier: Modifier = Modifier) { + val navController = rememberNavController() + Scaffold( + modifier = Modifier.fillMaxSize() + ) { _ -> + Text("Inventory") + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt new file mode 100644 index 0000000..f94c0da --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt @@ -0,0 +1,17 @@ +package sapling.foliage.ui.screens + +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController +import sapling.foliage.ui.components.MainNavbar + +@Composable +fun SettingsScreen(modifier: Modifier = Modifier) { + val navController = rememberNavController() + Scaffold( + ){ _ -> + Text("Settings") + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt new file mode 100644 index 0000000..1edeedf --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt @@ -0,0 +1,40 @@ +package sapling.foliage.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.compose.rememberNavController +import sapling.foliage.ui.components.MainNavbar + +@Composable +fun ShoppingScreen(modifier: Modifier = Modifier) { + val navController = rememberNavController() + Scaffold( + modifier = Modifier.fillMaxSize(), + floatingActionButton = { + FloatingActionButton(onClick = {}) { + Icon(Icons.Filled.Add, "Add new Shopping Tour") + } + }, + ) { _ -> + Text("Shopping") + } +// Scaffold( +// modifier= modifier.fillMaxSize(), +// floatingActionButtonPosition = FabPosition.Start, +// floatingActionButton = { +// FloatingActionButton(onClick = {}) { +// Icon(Icons.Filled.Add, "Add new Shopping Tour") +// } +// }, +// ) { _ -> Text("Shopping") } +} \ No newline at end of file diff --git a/foliage/gradle/libs.versions.toml b/foliage/gradle/libs.versions.toml index 2e88400..fc71df8 100644 --- a/foliage/gradle/libs.versions.toml +++ b/foliage/gradle/libs.versions.toml @@ -5,12 +5,19 @@ coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" +kotlinxSerializationJson = "1.7.3" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.09.00" +material = "1.8.1" +navigationCompose = "2.9.0" +navigationComposeJvmstubs = "2.9.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -24,9 +31,11 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } - +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } From c88337a449aca2fb46aee98c795ff889ba9eb26e Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Fri, 9 May 2025 02:24:51 +0200 Subject: [PATCH 03/11] Foliage simple query --- foliage/app/build.gradle.kts | 20 +- foliage/app/src/main/AndroidManifest.xml | 11 +- .../sapling/foliage/ProductsQuery.graphql | 11 + .../sapling/foliage/service/schema.graphqls | 416 ++++++++++++++++++ .../java/sapling/foliage/GraphQLClient.kt | 23 + .../java/sapling/foliage/GraphQLViewModel.kt | 26 ++ .../java/sapling/foliage/GraphQlScreen.kt | 33 ++ .../main/java/sapling/foliage/MainActivity.kt | 17 +- .../java/sapling/foliage/ViewModelFactory.kt | 19 + .../main/res/xml/network_security_config.xml | 11 + foliage/build.gradle.kts | 6 +- foliage/gradle/libs.versions.toml | 21 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- foliage/graphql.config.yml | 2 + 14 files changed, 589 insertions(+), 29 deletions(-) create mode 100644 foliage/app/src/main/graphql/sapling/foliage/ProductsQuery.graphql create mode 100644 foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls create mode 100644 foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt create mode 100644 foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt create mode 100644 foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt create mode 100644 foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt create mode 100644 foliage/app/src/main/res/xml/network_security_config.xml create mode 100644 foliage/graphql.config.yml diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts index 7a05be0..4a36704 100644 --- a/foliage/app/build.gradle.kts +++ b/foliage/app/build.gradle.kts @@ -2,6 +2,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + + id("com.apollographql.apollo") alias(libs.plugins.kotlin.serialization) } @@ -38,10 +40,20 @@ android { buildFeatures { compose = true } + buildToolsVersion = "36.0.0" +} + +apollo { + service("service") { + packageName.set("sapling.foliage") + schemaFile.set(file("src/main/graphql/sapling/foliage/service/schema.graphqls")) + introspection { + endpointUrl.set("http://localhost:3000/gql") + } + } } dependencies { - val nav_version = "2.9.0" implementation(libs.androidx.material) @@ -49,12 +61,17 @@ dependencies { implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.lifecycle.viewmodel.compose) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.apollo.runtime) + implementation(libs.apollo.api) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -62,5 +79,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) - implementation(libs.kotlinx.serialization.json) } \ No newline at end of file diff --git a/foliage/app/src/main/AndroidManifest.xml b/foliage/app/src/main/AndroidManifest.xml index 4edf54f..bf83b88 100644 --- a/foliage/app/src/main/AndroidManifest.xml +++ b/foliage/app/src/main/AndroidManifest.xml @@ -2,20 +2,26 @@ + + + + + + android:usesCleartextTraffic="true"> + @@ -24,5 +30,4 @@ - \ No newline at end of file diff --git a/foliage/app/src/main/graphql/sapling/foliage/ProductsQuery.graphql b/foliage/app/src/main/graphql/sapling/foliage/ProductsQuery.graphql new file mode 100644 index 0000000..1dfa4dd --- /dev/null +++ b/foliage/app/src/main/graphql/sapling/foliage/ProductsQuery.graphql @@ -0,0 +1,11 @@ +query ProductsQuery { + products { + ean + name + description + groups { + name + } + tags { name } + } +} diff --git a/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls b/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls new file mode 100644 index 0000000..a9617cc --- /dev/null +++ b/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls @@ -0,0 +1,416 @@ +""" +The `Boolean` scalar type represents `true` or `false`. +""" +scalar Boolean + +""" +Implement the DateTime scalar + +The input/output is a string in RFC3339 format. +""" +scalar DateTime + +scalar EAN + +""" +The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +""" +scalar Float + +scalar ID + +""" +The `Int` scalar type represents non-fractional whole numeric values. +""" +scalar Int + +type Product { + ean: EAN! + + name: String! + + brandId: Int + + description: String + + insertedAt: DateTime! + + updatedAt: DateTime! + + tags: [Tag!]! + + groups: [Tag!]! +} + +type RootMutation { + login(username: String!, password: String!): Session! + + register(username: String!, password: String!): User! + + insertProduct(ean: EAN!, name: String!): Product! +} + +type RootQuery { + product(ean: EAN!): Product + + products: [Product!]! +} + +type Session { + token: String! + + created: DateTime! + + user: User! +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar String + +type Tag { + tagId: Int! + + name: String! +} + +type User { + userId: Int! + + username: String! + + created: DateTime! +} + +""" +A Directive provides a way to describe alternate runtime execution and type +validation behavior in a GraphQL document. + +In some cases, you need to provide options to alter GraphQL's execution +behavior in ways field arguments will not suffice, such as conditionally +including or skipping a field. Directives provide this by describing +additional information to the executor. +""" +type __Directive { + name: String! + + description: String + + locations: [__DirectiveLocation!]! + + args(includeDeprecated: Boolean! = false): [__InputValue!]! + + isRepeatable: Boolean! +} + +""" +A Directive can be adjacent to many parts of the GraphQL language, a +__DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a fragment definition. + """ + FRAGMENT_DEFINITION + + """ + Location adjacent to a fragment spread. + """ + FRAGMENT_SPREAD + + """ + Location adjacent to an inline fragment. + """ + INLINE_FRAGMENT + + """ + Location adjacent to a variable definition. + """ + VARIABLE_DEFINITION + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an input object field definition. + """ + INPUT_FIELD_DEFINITION +} + +""" +One possible value for a given Enum. Enum values are unique values, not a +placeholder for a string or numeric value. However an Enum value is returned +in a JSON response as a string. +""" +type __EnumValue { + name: String! + + description: String + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +Object and Interface types are described by a list of Fields, each of which +has a name, potentially a list of arguments, and a return type. +""" +type __Field { + name: String! + + description: String + + args(includeDeprecated: Boolean! = false): [__InputValue!]! + + type: __Type! + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +Arguments provided to Fields or Directives and the input fields of an +InputObject are represented as Input Values which describe their type and +optionally a default value. +""" +type __InputValue { + name: String! + + description: String + + type: __Type! + + defaultValue: String + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes +all available types and directives on the server, as well as the entry +points for query, mutation, and subscription operations. +""" +type __Schema { + """ + description of __Schema for newer graphiql introspection schema + requirements + """ + description: String! + + """ + A list of all types supported by this server. + """ + types: [__Type!]! + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will + be rooted at. + """ + mutationType: __Type + + """ + If this server support subscription, the type that subscription + operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all directives supported by this server. + """ + directives: [__Directive!]! +} + +""" +The fundamental unit of any GraphQL Schema is the type. There are many kinds +of types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about +that type. Scalar types provide no information beyond a name and +description, while Enum types provide their values. Object and Interface +types provide the fields they describe. Abstract types, Union and Interface, +provide the Object types possible at runtime. List and NonNull types compose +other types. +""" +type __Type { + kind: __TypeKind! + + name: String + + description: String + + fields(includeDeprecated: Boolean! = false): [__Field!] + + interfaces: [__Type!] + + possibleTypes: [__Type!] + + enumValues(includeDeprecated: Boolean! = false): [__EnumValue!] + + inputFields(includeDeprecated: Boolean! = false): [__InputValue!] + + ofType: __Type + + specifiedByURL: String + + isOneOf: Boolean +} + +""" +An enum describing what kind of type a given `__Type` is. +""" +enum __TypeKind { + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid + fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields` and `possibleTypes` are + valid fields. + """ + INTERFACE + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + + """ + Indicates this type is an enum. `enumValues` is a valid field. + """ + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """ + Indicates this type is a list. `ofType` is a valid field. + """ + LIST + + """ + Indicates this type is a non-null. `ofType` is a valid field. + """ + NON_NULL +} + +""" +Marks an element of a GraphQL schema as no longer supported. +""" +directive @deprecated ("A reason for why it is deprecated, formatted using Markdown syntax" reason: String = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|INPUT_FIELD_DEFINITION|ENUM_VALUE + +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include ("Included when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT + +""" +Indicates that an Input Object is a OneOf Input Object (and thus requires + exactly one of its field be provided) +""" +directive @oneOf on INPUT_OBJECT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip ("Skipped when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT + +""" +Provides a scalar specification URL for specifying the behavior of custom scalar types. +""" +directive @specifiedBy ("URL that specifies the behavior of this scalar." url: String!) on SCALAR + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. +""" +schema { + query: RootQuery + mutation: RootMutation +} diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt b/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt new file mode 100644 index 0000000..bc905fe --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt @@ -0,0 +1,23 @@ +package sapling.foliage + +import android.content.Context +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.network.okHttpClient +import okhttp3.OkHttpClient + +class GraphQLClient(private val url: String) { + + fun create(context: Context): ApolloClient { + val okHttpClient = OkHttpClient.Builder().addInterceptor { chain -> + val req = + chain.request().newBuilder().addHeader("Content-Type", "application/json").build() + chain.proceed(req) + }.build() + + return ApolloClient.Builder().serverUrl(url).okHttpClient(okHttpClient).build() + } + + companion object { + private const val BASE_URL = "http://localhost:3000/gql" + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt b/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt new file mode 100644 index 0000000..673dda5 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt @@ -0,0 +1,26 @@ +package sapling.foliage + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.apollographql.apollo.ApolloClient +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class GraphQLViewModel(private val apolloClient: ApolloClient) : ViewModel() { + private val _uiState = MutableStateFlow("Loading...") + val uiState: StateFlow = _uiState; + + fun fetchData() { + viewModelScope.launch { + try { + val response = apolloClient.query(ProductsQuery()).execute() + _uiState.value = response.dataOrThrow().products.toString() + } catch (e: Exception) { + Log.e("GraphQLViewModel", "Error fetching data", e) + _uiState.value = "Error: ${e.message}" + } + } + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt b/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt new file mode 100644 index 0000000..94ff285 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt @@ -0,0 +1,33 @@ +package sapling.foliage + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun GraphQLScreen(viewModel: GraphQLViewModel = viewModel(factory = ViewModelFactory(LocalContext.current))) { + val uiState by viewModel.uiState.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + modifier = Modifier.padding(16.dp), + onClick = { viewModel.fetchData() }) { Text("Fetch Data") } + + Text(text = uiState, modifier = Modifier.padding(16.dp)) + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt index c2eedbf..9f5f4f1 100644 --- a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt +++ b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt @@ -7,24 +7,10 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable import sapling.foliage.ui.components.Inventory import sapling.foliage.ui.components.MainNavbar import sapling.foliage.ui.components.Settings @@ -34,7 +20,6 @@ import sapling.foliage.ui.screens.SettingsScreen import sapling.foliage.ui.screens.ShoppingScreen import sapling.foliage.ui.theme.FoliageTheme - class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -53,7 +38,7 @@ class MainActivity : ComponentActivity() { ) { composable { ShoppingScreen() } composable { InventoryScreen() } - composable { SettingsScreen() } + composable { GraphQLScreen() } } MainNavbar(navController = navController) } diff --git a/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt b/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt new file mode 100644 index 0000000..4f2da79 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt @@ -0,0 +1,19 @@ +package sapling.foliage + +import android.content.Context +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(GraphQLViewModel::class.java)) { + val apolloClient = GraphQLClient("http://10.2.3.67:3000/gql").create(context) + + @Suppress("UNCHECKED_CAST") + return GraphQLViewModel(apolloClient) as T + } + + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/foliage/app/src/main/res/xml/network_security_config.xml b/foliage/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..11fed13 --- /dev/null +++ b/foliage/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,11 @@ + + + + + + + 10.2.3.67 + + + + \ No newline at end of file diff --git a/foliage/build.gradle.kts b/foliage/build.gradle.kts index 952b930..d492648 100644 --- a/foliage/build.gradle.kts +++ b/foliage/build.gradle.kts @@ -3,4 +3,8 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false -} \ No newline at end of file + + id("com.apollographql.apollo") version "4.2.0" apply false +} + + diff --git a/foliage/gradle/libs.versions.toml b/foliage/gradle/libs.versions.toml index fc71df8..22740f6 100644 --- a/foliage/gradle/libs.versions.toml +++ b/foliage/gradle/libs.versions.toml @@ -1,20 +1,26 @@ [versions] agp = "8.10.0" +apolloRuntime = "4.2.0" kotlin = "2.0.21" -coreKtx = "1.10.1" +coreKtx = "1.16.0" junit = "4.13.2" -junitVersion = "1.1.5" -espressoCore = "3.5.1" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +kotlinxCoroutinesAndroid = "1.9.0" +lifecycleRuntimeKtx = "2.9.0" +activityCompose = "1.10.1" +composeBom = "2025.05.00" +lifecycleViewmodelCompose = "2.9.0" +mediationTestSuite = "3.0.0" kotlinxSerializationJson = "1.7.3" -lifecycleRuntimeKtx = "2.6.1" -activityCompose = "1.8.0" -composeBom = "2024.09.00" material = "1.8.1" navigationCompose = "2.9.0" navigationComposeJvmstubs = "2.9.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +apollo-api = { module = "com.apollographql.apollo3:apollo-api", version = "4.0.0-beta.7" } +apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apolloRuntime" } androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } @@ -31,6 +37,9 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } +lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +mediation-test-suite = { group = "com.google.android.ads", name = "mediation-test-suite", version.ref = "mediationTestSuite" } androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } diff --git a/foliage/gradle/wrapper/gradle-wrapper.properties b/foliage/gradle/wrapper/gradle-wrapper.properties index e78e7bc..ff58940 100644 --- a/foliage/gradle/wrapper/gradle-wrapper.properties +++ b/foliage/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu May 08 20:06:13 CEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/foliage/graphql.config.yml b/foliage/graphql.config.yml new file mode 100644 index 0000000..ad14d4c --- /dev/null +++ b/foliage/graphql.config.yml @@ -0,0 +1,2 @@ +schema: "http://localhost:3000/gql" +documents: '**/*.graphql' \ No newline at end of file From 53772e6f50784e6f136685474f73954ee1573102 Mon Sep 17 00:00:00 2001 From: isiko Date: Tue, 13 May 2025 19:03:04 +0200 Subject: [PATCH 04/11] Add more Content to Shopping Tour Screen --- .../foliage/ui/screens/ShoppingScreen.kt | 263 ++++++++++++++++-- 1 file changed, 245 insertions(+), 18 deletions(-) diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt index 1edeedf..355c622 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt @@ -1,40 +1,267 @@ package sapling.foliage.ui.screens -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.FabPosition +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.compose.rememberNavController -import sapling.foliage.ui.components.MainNavbar +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle.FULL +data class ShoppingTour( + val date: LocalDate, + val market: String, + val title: String?, + val price: Float, + val autoGenerated: Boolean, + val commited: Boolean, + val entries: List +) + +data class ShoppingTourEntry( + val product: String, + val amount: Int, +) + +val tempIngredientList = listOf( + ShoppingTourEntry("Mate", 20), + ShoppingTourEntry("Milch", 1), + ShoppingTourEntry("Erdnussbutter", 8), + ShoppingTourEntry("Nudeln", 5), + ShoppingTourEntry("Tortelini", 17), + ShoppingTourEntry("Eier", 23) +) + +val tempTours = listOf( + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = true, + price = 1.00f, + title = "Test Titel", + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = true, + price = 1.00f, + title = "Test Titel", + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = false, + price = 1.00f, + title = "Test Titel", + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = false, + price = 1.00f, + title = "Test Titel", + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = true, + price = 1.00f, + title = "Test Titel", + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = true, + price = 1.00f, + title = "Test Titel", + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = false, + price = 1.00f, + title = "Test Titel", + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = false, + price = 1.00f, + title = "Test Titel", + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = true, + price = 1.00f, + title = null, + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = true, + price = 1.00f, + title = null, + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = false, + price = 1.00f, + title = null, + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = false, + commited = false, + price = 1.00f, + title = null, + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = true, + price = 1.00f, + title = null, + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = true, + price = 1.00f, + title = null, + entries = listOf(), + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = false, + price = 1.00f, + title = null, + entries = tempIngredientList, + ), + ShoppingTour( + date = LocalDate.parse("2000-12-13"), + market = "Rewe", + autoGenerated = true, + commited = false, + price = 1.00f, + title = null, + entries = listOf(), + ), +) + +@Composable +fun ShoppingTourEntry(modifier: Modifier = Modifier, shoppingTour: ShoppingTour) { + ListItem( + leadingContent = { + Icon( + imageVector = Icons.Filled.ShoppingCart, + contentDescription = "Leading Icon", + tint = MaterialTheme.colorScheme.onSecondary, + modifier = Modifier + .background(MaterialTheme.colorScheme.secondary, shape = CircleShape) + .padding(8.dp) + ) + }, + overlineContent = { if (shoppingTour.autoGenerated) Text("AUTO-IMPORT") else null }, + headlineContent = { + Text( + text = shoppingTour.title ?: DateTimeFormatter + .ofLocalizedDate(FULL) + .format(shoppingTour.date) + ) + }, + supportingContent = { + val itemString = + shoppingTour.entries.joinToString(", ") { e -> "${e.amount}x ${e.product}" } + Text( + text = if (itemString.isNotEmpty()) itemString else "Nothing was bought", + maxLines = 2, + overflow = TextOverflow.Ellipsis + + ) + }, + trailingContent = { + Text( + text = "%.2f€".format(shoppingTour.price), + modifier = Modifier.fillMaxHeight(), + fontSize = 20.sp + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview @Composable fun ShoppingScreen(modifier: Modifier = Modifier) { - val navController = rememberNavController() Scaffold( - modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(title = { + Text("Shopping Tours") + }) + }, floatingActionButton = { FloatingActionButton(onClick = {}) { Icon(Icons.Filled.Add, "Add new Shopping Tour") } }, - ) { _ -> - Text("Shopping") + ) { innerPadding -> + LazyColumn( + contentPadding = innerPadding, + modifier = Modifier.fillMaxHeight(), + ) { + items(tempTours) { tour -> + ShoppingTourEntry(shoppingTour = tour) + } + } } -// Scaffold( -// modifier= modifier.fillMaxSize(), -// floatingActionButtonPosition = FabPosition.Start, -// floatingActionButton = { -// FloatingActionButton(onClick = {}) { -// Icon(Icons.Filled.Add, "Add new Shopping Tour") -// } -// }, -// ) { _ -> Text("Shopping") } } \ No newline at end of file From 3bb46ec6f6474d729f825eb29fd00af40a396d77 Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Fri, 23 May 2025 18:01:27 +0200 Subject: [PATCH 05/11] Add settings dependency --- foliage/app/build.gradle.kts | 4 +- .../main/java/sapling/foliage/MainActivity.kt | 6 ++- .../foliage/{ => gql}/GraphQLClient.kt | 2 +- .../foliage/{ => gql}/GraphQLViewModel.kt | 3 +- .../foliage/{ => gql}/GraphQlScreen.kt | 2 +- .../foliage/{ => gql}/ViewModelFactory.kt | 3 +- .../foliage/ui/screens/SettingsScreen.kt | 38 +++++++++++++++++-- foliage/gradle/libs.versions.toml | 4 ++ 8 files changed, 50 insertions(+), 12 deletions(-) rename foliage/app/src/main/java/sapling/foliage/{ => gql}/GraphQLClient.kt (96%) rename foliage/app/src/main/java/sapling/foliage/{ => gql}/GraphQLViewModel.kt (93%) rename foliage/app/src/main/java/sapling/foliage/{ => gql}/GraphQlScreen.kt (97%) rename foliage/app/src/main/java/sapling/foliage/{ => gql}/ViewModelFactory.kt (92%) diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts index 4a36704..8f03b91 100644 --- a/foliage/app/build.gradle.kts +++ b/foliage/app/build.gradle.kts @@ -54,8 +54,8 @@ apollo { } dependencies { - - + implementation(libs.zhanghai.preferences) + implementation(libs.androidx.preference.ktx) implementation(libs.androidx.material) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.material.icons.extended) diff --git a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt index 9f5f4f1..c96c75d 100644 --- a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt +++ b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt @@ -11,6 +11,10 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.preference.Preference +import me.zhanghai.compose.preference.Preference +import me.zhanghai.compose.preference.Preferences +import sapling.foliage.gql.GraphQLScreen import sapling.foliage.ui.components.Inventory import sapling.foliage.ui.components.MainNavbar import sapling.foliage.ui.components.Settings @@ -38,7 +42,7 @@ class MainActivity : ComponentActivity() { ) { composable { ShoppingScreen() } composable { InventoryScreen() } - composable { GraphQLScreen() } + composable { SettingsScreen() } } MainNavbar(navController = navController) } diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt similarity index 96% rename from foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt rename to foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt index bc905fe..904b36f 100644 --- a/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt +++ b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt @@ -1,4 +1,4 @@ -package sapling.foliage +package sapling.foliage.gql import android.content.Context import com.apollographql.apollo.ApolloClient diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt similarity index 93% rename from foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt rename to foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt index 673dda5..5831fa4 100644 --- a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt +++ b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt @@ -1,4 +1,4 @@ -package sapling.foliage +package sapling.foliage.gql import android.util.Log import androidx.lifecycle.ViewModel @@ -7,6 +7,7 @@ import com.apollographql.apollo.ApolloClient import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import sapling.foliage.ProductsQuery class GraphQLViewModel(private val apolloClient: ApolloClient) : ViewModel() { private val _uiState = MutableStateFlow("Loading...") diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt similarity index 97% rename from foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt rename to foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt index 94ff285..d68fba6 100644 --- a/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt @@ -1,4 +1,4 @@ -package sapling.foliage +package sapling.foliage.gql import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize diff --git a/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt b/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt similarity index 92% rename from foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt rename to foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt index 4f2da79..45bc4ef 100644 --- a/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt +++ b/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt @@ -1,7 +1,6 @@ -package sapling.foliage +package sapling.foliage.gql import android.content.Context -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt index f94c0da..748fee6 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt @@ -1,17 +1,47 @@ package sapling.foliage.ui.screens +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Adb +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.rememberNavController -import sapling.foliage.ui.components.MainNavbar +import me.zhanghai.compose.preference.ProvidePreferenceLocals +import me.zhanghai.compose.preference.switchPreference +import me.zhanghai.compose.preference.textFieldPreference @Composable fun SettingsScreen(modifier: Modifier = Modifier) { val navController = rememberNavController() Scaffold( - ){ _ -> - Text("Settings") + ) { padding -> + ProvidePreferenceLocals { + LazyColumn(modifier = modifier, contentPadding = padding) { + switchPreference( + key = "use_debug_server", + defaultValue = false, + title = { Text(text = "Use debug server") }, + summary = { Text(text = if (it) "On" else "Off") }) + + textFieldPreference( + key = "gql_endpoint", + title = { Text(text = "GQL Endpoint") }, + defaultValue = "https://sapling.geigr.dev/gql", + summary = { Text(text = it) }, + textToValue = { x -> x }) + + + textFieldPreference( + key = "debug_gql_endpoint", + title = { Text(text = "GQL Debug Endpoint") }, + defaultValue = "http://sapling.local:3000/gql", + summary = { Text(text = it) }, + textToValue = { x -> x }) + } + } } -} \ No newline at end of file +} diff --git a/foliage/gradle/libs.versions.toml b/foliage/gradle/libs.versions.toml index 22740f6..9643e8e 100644 --- a/foliage/gradle/libs.versions.toml +++ b/foliage/gradle/libs.versions.toml @@ -7,6 +7,7 @@ junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" kotlinxCoroutinesAndroid = "1.9.0" +zhanghai-preferences = "1.1.1" lifecycleRuntimeKtx = "2.9.0" activityCompose = "1.10.1" composeBom = "2025.05.00" @@ -16,9 +17,11 @@ kotlinxSerializationJson = "1.7.3" material = "1.8.1" navigationCompose = "2.9.0" navigationComposeJvmstubs = "2.9.0" +preferenceKtx = "1.2.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } apollo-api = { module = "com.apollographql.apollo3:apollo-api", version = "4.0.0-beta.7" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apolloRuntime" } androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } @@ -38,6 +41,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } +zhanghai-preferences = { module = "me.zhanghai.compose.preference:library", version.ref = "zhanghai-preferences" } lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } mediation-test-suite = { group = "com.google.android.ads", name = "mediation-test-suite", version.ref = "mediationTestSuite" } androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" } From a10845973c16b3bf22c80dbdd0fc4f2b1c6845fb Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Sun, 25 May 2025 19:12:38 +0200 Subject: [PATCH 06/11] Add settings screen --- foliage/app/build.gradle.kts | 28 ++++++----- .../src/main/java/sapling/foliage/Foliage.kt | 43 ++++++++++++++++ .../main/java/sapling/foliage/MainActivity.kt | 38 +------------- .../java/sapling/foliage/gql/GraphQLClient.kt | 23 --------- .../sapling/foliage/gql/GraphQLViewModel.kt | 18 ++++--- .../sapling/foliage/gql/ViewModelFactory.kt | 4 +- .../foliage/ui/screens/SettingsScreen.kt | 49 +++++++------------ foliage/gradle/libs.versions.toml | 10 ++-- 8 files changed, 98 insertions(+), 115 deletions(-) create mode 100644 foliage/app/src/main/java/sapling/foliage/Foliage.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts index 8f03b91..714e92e 100644 --- a/foliage/app/build.gradle.kts +++ b/foliage/app/build.gradle.kts @@ -48,29 +48,31 @@ apollo { packageName.set("sapling.foliage") schemaFile.set(file("src/main/graphql/sapling/foliage/service/schema.graphqls")) introspection { - endpointUrl.set("http://localhost:3000/gql") + // TODO: Use setting or env to control schema source + endpointUrl.set("https://sapling.geigr.dev/gql") } } } dependencies { - implementation(libs.zhanghai.preferences) - implementation(libs.androidx.preference.ktx) - implementation(libs.androidx.material) - implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.lifecycle.viewmodel.compose) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.material) + implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.preference.ktx) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.apollo.runtime) implementation(libs.apollo.api) + implementation(libs.apollo.runtime) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.zhanghai.preferences) + implementation(platform(libs.androidx.compose.bom)) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) @@ -79,4 +81,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/foliage/app/src/main/java/sapling/foliage/Foliage.kt b/foliage/app/src/main/java/sapling/foliage/Foliage.kt new file mode 100644 index 0000000..7f370ca --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/Foliage.kt @@ -0,0 +1,43 @@ +package sapling.foliage + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import me.zhanghai.compose.preference.ProvidePreferenceLocals +import sapling.foliage.ui.components.Inventory +import sapling.foliage.ui.components.MainNavbar +import sapling.foliage.ui.components.Settings +import sapling.foliage.ui.components.ShoppingTourList +import sapling.foliage.ui.screens.InventoryScreen +import sapling.foliage.ui.screens.SettingsScreen +import sapling.foliage.ui.screens.ShoppingScreen +import sapling.foliage.ui.theme.FoliageTheme + +@Composable +fun FoliageApp() { + FoliageTheme { + ProvidePreferenceLocals { + val navController = rememberNavController() + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier.fillMaxSize() + ) { + NavHost( + navController = navController, + startDestination = ShoppingTourList, + modifier = Modifier.weight(1f) + ) { + composable { ShoppingScreen() } + composable { InventoryScreen() } + composable { SettingsScreen() } + } + MainNavbar(navController = navController) + } + } + } +} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt index c96c75d..21037df 100644 --- a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt +++ b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt @@ -4,49 +4,13 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.preference.Preference -import me.zhanghai.compose.preference.Preference -import me.zhanghai.compose.preference.Preferences -import sapling.foliage.gql.GraphQLScreen -import sapling.foliage.ui.components.Inventory -import sapling.foliage.ui.components.MainNavbar -import sapling.foliage.ui.components.Settings -import sapling.foliage.ui.components.ShoppingTourList -import sapling.foliage.ui.screens.InventoryScreen -import sapling.foliage.ui.screens.SettingsScreen -import sapling.foliage.ui.screens.ShoppingScreen -import sapling.foliage.ui.theme.FoliageTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - FoliageTheme { - val navController = rememberNavController() - Column( - verticalArrangement = Arrangement.Bottom, - modifier = Modifier.fillMaxSize() - ) { - NavHost( - navController = navController, - startDestination = ShoppingTourList, - modifier = Modifier.weight(1f) - ) { - composable { ShoppingScreen() } - composable { InventoryScreen() } - composable { SettingsScreen() } - } - MainNavbar(navController = navController) - } - } + FoliageApp() } } } \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt deleted file mode 100644 index 904b36f..0000000 --- a/foliage/app/src/main/java/sapling/foliage/gql/GraphQLClient.kt +++ /dev/null @@ -1,23 +0,0 @@ -package sapling.foliage.gql - -import android.content.Context -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.okHttpClient -import okhttp3.OkHttpClient - -class GraphQLClient(private val url: String) { - - fun create(context: Context): ApolloClient { - val okHttpClient = OkHttpClient.Builder().addInterceptor { chain -> - val req = - chain.request().newBuilder().addHeader("Content-Type", "application/json").build() - chain.proceed(req) - }.build() - - return ApolloClient.Builder().serverUrl(url).okHttpClient(okHttpClient).build() - } - - companion object { - private const val BASE_URL = "http://localhost:3000/gql" - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt index 5831fa4..37fbe5e 100644 --- a/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt +++ b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt @@ -4,23 +4,27 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Query import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import sapling.foliage.ProductsQuery -class GraphQLViewModel(private val apolloClient: ApolloClient) : ViewModel() { - private val _uiState = MutableStateFlow("Loading...") - val uiState: StateFlow = _uiState; - fun fetchData() { +class GraphQLViewModel, D : Query.Data>(private val apolloClient: ApolloClient) : + ViewModel() { + private val _uiState = MutableStateFlow(null) + val uiState: StateFlow = _uiState; + + fun fetch(query: T) { viewModelScope.launch { + ProductsQuery try { - val response = apolloClient.query(ProductsQuery()).execute() - _uiState.value = response.dataOrThrow().products.toString() + val response = apolloClient.query(query).execute() + val data = response.dataOrThrow() + _uiState.value = data; } catch (e: Exception) { Log.e("GraphQLViewModel", "Error fetching data", e) - _uiState.value = "Error: ${e.message}" } } } diff --git a/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt b/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt index 45bc4ef..4d9d221 100644 --- a/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt +++ b/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt @@ -3,11 +3,13 @@ package sapling.foliage.gql import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.apollographql.apollo.ApolloClient class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(GraphQLViewModel::class.java)) { - val apolloClient = GraphQLClient("http://10.2.3.67:3000/gql").create(context) + val apolloClient = + ApolloClient.Builder().serverUrl("https://sapling.geigr.dev/gql").build() @Suppress("UNCHECKED_CAST") return GraphQLViewModel(apolloClient) as T diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt index 748fee6..b0065e7 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt @@ -1,47 +1,34 @@ package sapling.foliage.ui.screens +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Adb -import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.filled.Home import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.navigation.compose.rememberNavController -import me.zhanghai.compose.preference.ProvidePreferenceLocals -import me.zhanghai.compose.preference.switchPreference +import androidx.preference.PreferenceScreen import me.zhanghai.compose.preference.textFieldPreference @Composable fun SettingsScreen(modifier: Modifier = Modifier) { - val navController = rememberNavController() - Scaffold( - ) { padding -> - ProvidePreferenceLocals { - LazyColumn(modifier = modifier, contentPadding = padding) { - switchPreference( - key = "use_debug_server", - defaultValue = false, - title = { Text(text = "Use debug server") }, - summary = { Text(text = if (it) "On" else "Off") }) - - textFieldPreference( - key = "gql_endpoint", - title = { Text(text = "GQL Endpoint") }, - defaultValue = "https://sapling.geigr.dev/gql", - summary = { Text(text = it) }, - textToValue = { x -> x }) - - - textFieldPreference( - key = "debug_gql_endpoint", - title = { Text(text = "GQL Debug Endpoint") }, - defaultValue = "http://sapling.local:3000/gql", - summary = { Text(text = it) }, - textToValue = { x -> x }) - } + Scaffold(modifier) { contentPadding -> + LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = contentPadding) { + textFieldPreference( + key = "home_server_url", + defaultValue = "https://sapling.geigr.dev/gql", + icon = { + Icon( + imageVector = Icons.Filled.Home, + contentDescription = null + ) + }, + title = { Text(text = "Home Server URL") }, + summary = { Text(it) }, + textToValue = { it } + ) } } } diff --git a/foliage/gradle/libs.versions.toml b/foliage/gradle/libs.versions.toml index 9643e8e..f26dc46 100644 --- a/foliage/gradle/libs.versions.toml +++ b/foliage/gradle/libs.versions.toml @@ -1,26 +1,30 @@ [versions] agp = "8.10.0" apolloRuntime = "4.2.0" +datastorePreferences = "1.1.7" kotlin = "2.0.21" coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" -kotlinxCoroutinesAndroid = "1.9.0" +kotlinxCoroutinesAndroid = "1.10.2" +lifecycleRuntimeCompose = "2.9.0" zhanghai-preferences = "1.1.1" lifecycleRuntimeKtx = "2.9.0" activityCompose = "1.10.1" -composeBom = "2025.05.00" +composeBom = "2025.05.01" lifecycleViewmodelCompose = "2.9.0" mediationTestSuite = "3.0.0" kotlinxSerializationJson = "1.7.3" -material = "1.8.1" +material = "1.8.2" navigationCompose = "2.9.0" navigationComposeJvmstubs = "2.9.0" preferenceKtx = "1.2.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } apollo-api = { module = "com.apollographql.apollo3:apollo-api", version = "4.0.0-beta.7" } apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apolloRuntime" } From 4a1d9aa7695b7e9aeafc2faa8d0996e507067aae Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Sun, 1 Jun 2025 19:48:39 +0200 Subject: [PATCH 07/11] Fetch data from live server --- foliage/app/build.gradle.kts | 4 +- .../sapling/foliage/InventoryQuery.graphql | 11 ++ .../sapling/foliage/service/schema.graphqls | 18 +++ .../src/main/java/sapling/foliage/Apollo.kt | 6 + .../src/main/java/sapling/foliage/Foliage.kt | 3 + .../sapling/foliage/gql/GraphQLViewModel.kt | 31 ------ .../java/sapling/foliage/gql/GraphQlScreen.kt | 33 ------ .../sapling/foliage/gql/ViewModelFactory.kt | 20 ---- .../foliage/ui/screens/InventoryScreen.kt | 103 +++++++++++++++++- .../foliage/ui/screens/SettingsScreen.kt | 1 - foliage/gradle/libs.versions.toml | 35 +++--- foliage/graphql.config.yml | 2 +- 12 files changed, 158 insertions(+), 109 deletions(-) create mode 100644 foliage/app/src/main/graphql/sapling/foliage/InventoryQuery.graphql create mode 100644 foliage/app/src/main/java/sapling/foliage/Apollo.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts index 714e92e..62beada 100644 --- a/foliage/app/build.gradle.kts +++ b/foliage/app/build.gradle.kts @@ -3,8 +3,8 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) - id("com.apollographql.apollo") alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.apollo) } android { @@ -45,7 +45,7 @@ android { apollo { service("service") { - packageName.set("sapling.foliage") + packageName.set("sapling.foliage.gql") schemaFile.set(file("src/main/graphql/sapling/foliage/service/schema.graphqls")) introspection { // TODO: Use setting or env to control schema source diff --git a/foliage/app/src/main/graphql/sapling/foliage/InventoryQuery.graphql b/foliage/app/src/main/graphql/sapling/foliage/InventoryQuery.graphql new file mode 100644 index 0000000..e882e46 --- /dev/null +++ b/foliage/app/src/main/graphql/sapling/foliage/InventoryQuery.graphql @@ -0,0 +1,11 @@ +query InventoryQuery { + items { + product { + name + ean + } + + cost + createdAt + } +} \ No newline at end of file diff --git a/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls b/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls index a9617cc..a4dc2fa 100644 --- a/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls +++ b/foliage/app/src/main/graphql/sapling/foliage/service/schema.graphqls @@ -24,6 +24,18 @@ The `Int` scalar type represents non-fractional whole numeric values. """ scalar Int +type Item { + itemId: Int! + + ean: EAN! + + createdAt: DateTime! + + cost: Int + + product: Product! +} + type Product { ean: EAN! @@ -40,6 +52,8 @@ type Product { tags: [Tag!]! groups: [Tag!]! + + items: [Item!]! } type RootMutation { @@ -48,12 +62,16 @@ type RootMutation { register(username: String!, password: String!): User! insertProduct(ean: EAN!, name: String!): Product! + + insertItem(ean: EAN!, cost: Int!): Item! } type RootQuery { product(ean: EAN!): Product products: [Product!]! + + items: [Item!]! } type Session { diff --git a/foliage/app/src/main/java/sapling/foliage/Apollo.kt b/foliage/app/src/main/java/sapling/foliage/Apollo.kt new file mode 100644 index 0000000..1f9d228 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/Apollo.kt @@ -0,0 +1,6 @@ +package sapling.foliage + +import com.apollographql.apollo.ApolloClient + +val apolloClient = ApolloClient.Builder().serverUrl("https://sapling.geigr.dev/gql") + .build() diff --git a/foliage/app/src/main/java/sapling/foliage/Foliage.kt b/foliage/app/src/main/java/sapling/foliage/Foliage.kt index 7f370ca..2934937 100644 --- a/foliage/app/src/main/java/sapling/foliage/Foliage.kt +++ b/foliage/app/src/main/java/sapling/foliage/Foliage.kt @@ -1,5 +1,6 @@ package sapling.foliage +import android.content.SharedPreferences import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -8,7 +9,9 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.flow.asStateFlow import me.zhanghai.compose.preference.ProvidePreferenceLocals +import me.zhanghai.compose.preference.defaultPreferenceFlow import sapling.foliage.ui.components.Inventory import sapling.foliage.ui.components.MainNavbar import sapling.foliage.ui.components.Settings diff --git a/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt deleted file mode 100644 index 37fbe5e..0000000 --- a/foliage/app/src/main/java/sapling/foliage/gql/GraphQLViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package sapling.foliage.gql - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.api.Query -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import sapling.foliage.ProductsQuery - - -class GraphQLViewModel, D : Query.Data>(private val apolloClient: ApolloClient) : - ViewModel() { - private val _uiState = MutableStateFlow(null) - val uiState: StateFlow = _uiState; - - fun fetch(query: T) { - viewModelScope.launch { - ProductsQuery - try { - val response = apolloClient.query(query).execute() - val data = response.dataOrThrow() - _uiState.value = data; - } catch (e: Exception) { - Log.e("GraphQLViewModel", "Error fetching data", e) - } - } - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt b/foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt deleted file mode 100644 index d68fba6..0000000 --- a/foliage/app/src/main/java/sapling/foliage/gql/GraphQlScreen.kt +++ /dev/null @@ -1,33 +0,0 @@ -package sapling.foliage.gql - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel - -@Composable -fun GraphQLScreen(viewModel: GraphQLViewModel = viewModel(factory = ViewModelFactory(LocalContext.current))) { - val uiState by viewModel.uiState.collectAsState() - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - modifier = Modifier.padding(16.dp), - onClick = { viewModel.fetchData() }) { Text("Fetch Data") } - - Text(text = uiState, modifier = Modifier.padding(16.dp)) - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt b/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt deleted file mode 100644 index 4d9d221..0000000 --- a/foliage/app/src/main/java/sapling/foliage/gql/ViewModelFactory.kt +++ /dev/null @@ -1,20 +0,0 @@ -package sapling.foliage.gql - -import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.apollographql.apollo.ApolloClient - -class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(GraphQLViewModel::class.java)) { - val apolloClient = - ApolloClient.Builder().serverUrl("https://sapling.geigr.dev/gql").build() - - @Suppress("UNCHECKED_CAST") - return GraphQLViewModel(apolloClient) as T - } - - throw IllegalArgumentException("Unknown ViewModel class") - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt index 5ecd8f0..3155737 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt @@ -1,19 +1,114 @@ package sapling.foliage.ui.screens import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController -import sapling.foliage.ui.components.MainNavbar +import kotlinx.coroutines.launch +import sapling.foliage.apolloClient +import sapling.foliage.gql.InventoryQuery +import java.time.Instant +@OptIn(ExperimentalMaterial3Api::class) @Composable +@Preview fun InventoryScreen(modifier: Modifier = Modifier) { val navController = rememberNavController() + + var itemList by remember { mutableStateOf(emptyList()) } + var isRefreshing by remember { mutableStateOf(false) } + val state = rememberPullToRefreshState() + val coroutineScope = rememberCoroutineScope() + + fun fetchData() { + isRefreshing = true + coroutineScope.launch { + val response = apolloClient.query(InventoryQuery()).execute() + itemList = response.data?.items ?: emptyList() + isRefreshing = false + } + } + + LaunchedEffect(apolloClient) { + fetchData() + } + Scaffold( - modifier = Modifier.fillMaxSize() - ) { _ -> - Text("Inventory") + modifier = Modifier.pullToRefresh( + state = state, + isRefreshing = isRefreshing, + onRefresh = { fetchData() } + ), + floatingActionButton = { + FloatingActionButton(onClick = {}) { + Icon(Icons.Filled.Add, "Insert Items") + } + }, + topBar = { TopAppBar(title = { Text("Inventory") }) } + ) { + ItemList( + modifier = Modifier + .fillMaxSize() + .padding(it), + items = itemList, + isRefreshing = isRefreshing, + onRefresh = { fetchData() }, + ) } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ItemList( + items: List, + modifier: Modifier = Modifier, + isRefreshing: Boolean = false, + onRefresh: () -> Unit = {}, +) { + PullToRefreshBox( + modifier = modifier, + onRefresh = onRefresh, + isRefreshing = isRefreshing, + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + InventoryItem(it) + } + } + } +} + +@Composable +fun InventoryItem( + item: InventoryQuery.Item, + modifier: Modifier = Modifier +) { + val date = Instant.parse(item.createdAt as String) + ListItem( + modifier = modifier, + headlineContent = { Text(item.product.name) }, + supportingContent = { Text(item.product.ean.toString()) }, + trailingContent = { Text(date.toString()) }) } \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt index b0065e7..01bdf3c 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.preference.PreferenceScreen import me.zhanghai.compose.preference.textFieldPreference @Composable diff --git a/foliage/gradle/libs.versions.toml b/foliage/gradle/libs.versions.toml index f26dc46..99423bf 100644 --- a/foliage/gradle/libs.versions.toml +++ b/foliage/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -agp = "8.10.0" -apolloRuntime = "4.2.0" +agp = "8.10.1" +apollo = "4.2.0" datastorePreferences = "1.1.7" kotlin = "2.0.21" coreKtx = "1.16.0" @@ -22,37 +22,38 @@ navigationComposeJvmstubs = "2.9.0" preferenceKtx = "1.2.1" [libraries] +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } -androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } -apollo-api = { module = "com.apollographql.apollo3:apollo-api", version = "4.0.0-beta.7" } -apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apolloRuntime" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" } +androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +apollo-api = { module = "com.apollographql.apollo3:apollo-api", version = "4.0.0-beta.7" } +apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } +junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } -zhanghai-preferences = { module = "me.zhanghai.compose.preference:library", version.ref = "zhanghai-preferences" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } mediation-test-suite = { group = "com.google.android.ads", name = "mediation-test-suite", version.ref = "mediationTestSuite" } -androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +zhanghai-preferences = { module = "me.zhanghai.compose.preference:library", version.ref = "zhanghai-preferences" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +apollo = { id = "com.apollographql.apollo", version.ref = "apollo" } \ No newline at end of file diff --git a/foliage/graphql.config.yml b/foliage/graphql.config.yml index ad14d4c..c6debfc 100644 --- a/foliage/graphql.config.yml +++ b/foliage/graphql.config.yml @@ -1,2 +1,2 @@ -schema: "http://localhost:3000/gql" +schema: "https://sapling.geigr.dev/gql" documents: '**/*.graphql' \ No newline at end of file From 6efd5233fcbd2185e7c15c0b63a696f9388504a3 Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Sun, 1 Jun 2025 19:53:27 +0200 Subject: [PATCH 08/11] Remove kotlin error logs (again) --- foliage/.gitignore | 1 + foliage/.kotlin/errors/errors-1747067490401.log | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 foliage/.kotlin/errors/errors-1747067490401.log diff --git a/foliage/.gitignore b/foliage/.gitignore index aa724b7..facb128 100644 --- a/foliage/.gitignore +++ b/foliage/.gitignore @@ -1,6 +1,7 @@ *.iml .gradle /local.properties +/.kotlin /.idea/caches /.idea/libraries /.idea/modules.xml diff --git a/foliage/.kotlin/errors/errors-1747067490401.log b/foliage/.kotlin/errors/errors-1747067490401.log deleted file mode 100644 index 1219b50..0000000 --- a/foliage/.kotlin/errors/errors-1747067490401.log +++ /dev/null @@ -1,4 +0,0 @@ -kotlin version: 2.0.21 -error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output: - 1. Kotlin compile daemon is ready - From f770c39999ae0e63e71b12487275386356559ff4 Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Sat, 7 Jun 2025 00:20:54 +0200 Subject: [PATCH 09/11] Fix merge issues --- foliage/app/build.gradle.kts | 16 ++++++++-------- .../java/sapling/foliage/GraphQLViewModel.kt | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts index 15a3187..cef5a65 100644 --- a/foliage/app/build.gradle.kts +++ b/foliage/app/build.gradle.kts @@ -59,27 +59,27 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.material) + implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.material3) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.ui) implementation(libs.apollo.api) implementation(libs.apollo.runtime) implementation(libs.kotlinx.coroutines.android) implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.play.services.code.scanner) implementation(libs.zhanghai.preferences) implementation(platform(libs.androidx.compose.bom)) - implementation(libs.play.services.code.scanner) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) + // testImplementation(libs.junit) + // androidTestImplementation(libs.androidx.junit) + // androidTestImplementation(libs.androidx.espresso.core) + // androidTestImplementation(platform(libs.androidx.compose.bom)) + // androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) } diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt b/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt index 673dda5..481560f 100644 --- a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt +++ b/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt @@ -7,6 +7,7 @@ import com.apollographql.apollo.ApolloClient import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import sapling.foliage.gql.ProductsQuery class GraphQLViewModel(private val apolloClient: ApolloClient) : ViewModel() { private val _uiState = MutableStateFlow("Loading...") From f0d73116f2513b5eb0555209f2a022fd707d1738 Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Sat, 7 Jun 2025 00:23:33 +0200 Subject: [PATCH 10/11] Removed readded files --- .../java/sapling/foliage/GraphQLClient.kt | 23 ------------- .../java/sapling/foliage/GraphQLViewModel.kt | 27 --------------- .../java/sapling/foliage/GraphQlScreen.kt | 33 ------------------- .../java/sapling/foliage/ViewModelFactory.kt | 19 ----------- 4 files changed, 102 deletions(-) delete mode 100644 foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt delete mode 100644 foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt b/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt deleted file mode 100644 index bc905fe..0000000 --- a/foliage/app/src/main/java/sapling/foliage/GraphQLClient.kt +++ /dev/null @@ -1,23 +0,0 @@ -package sapling.foliage - -import android.content.Context -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.okHttpClient -import okhttp3.OkHttpClient - -class GraphQLClient(private val url: String) { - - fun create(context: Context): ApolloClient { - val okHttpClient = OkHttpClient.Builder().addInterceptor { chain -> - val req = - chain.request().newBuilder().addHeader("Content-Type", "application/json").build() - chain.proceed(req) - }.build() - - return ApolloClient.Builder().serverUrl(url).okHttpClient(okHttpClient).build() - } - - companion object { - private const val BASE_URL = "http://localhost:3000/gql" - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt b/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt deleted file mode 100644 index 481560f..0000000 --- a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package sapling.foliage - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.apollographql.apollo.ApolloClient -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import sapling.foliage.gql.ProductsQuery - -class GraphQLViewModel(private val apolloClient: ApolloClient) : ViewModel() { - private val _uiState = MutableStateFlow("Loading...") - val uiState: StateFlow = _uiState; - - fun fetchData() { - viewModelScope.launch { - try { - val response = apolloClient.query(ProductsQuery()).execute() - _uiState.value = response.dataOrThrow().products.toString() - } catch (e: Exception) { - Log.e("GraphQLViewModel", "Error fetching data", e) - _uiState.value = "Error: ${e.message}" - } - } - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt b/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt deleted file mode 100644 index 94ff285..0000000 --- a/foliage/app/src/main/java/sapling/foliage/GraphQlScreen.kt +++ /dev/null @@ -1,33 +0,0 @@ -package sapling.foliage - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel - -@Composable -fun GraphQLScreen(viewModel: GraphQLViewModel = viewModel(factory = ViewModelFactory(LocalContext.current))) { - val uiState by viewModel.uiState.collectAsState() - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - modifier = Modifier.padding(16.dp), - onClick = { viewModel.fetchData() }) { Text("Fetch Data") } - - Text(text = uiState, modifier = Modifier.padding(16.dp)) - } -} \ No newline at end of file diff --git a/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt b/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt deleted file mode 100644 index 4f2da79..0000000 --- a/foliage/app/src/main/java/sapling/foliage/ViewModelFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package sapling.foliage - -import android.content.Context -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(GraphQLViewModel::class.java)) { - val apolloClient = GraphQLClient("http://10.2.3.67:3000/gql").create(context) - - @Suppress("UNCHECKED_CAST") - return GraphQLViewModel(apolloClient) as T - } - - throw IllegalArgumentException("Unknown ViewModel class") - } -} \ No newline at end of file From 8bfcf3cddc387c31a6286e65bdc395f7dd8eb2d7 Mon Sep 17 00:00:00 2001 From: Aaron Geiger Date: Sat, 7 Jun 2025 00:35:52 +0200 Subject: [PATCH 11/11] Use home server url for apolloClient --- foliage/app/src/main/java/sapling/foliage/Apollo.kt | 8 ++++++-- .../java/sapling/foliage/ui/screens/InventoryScreen.kt | 8 ++++++-- .../java/sapling/foliage/ui/screens/SettingsScreen.kt | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/foliage/app/src/main/java/sapling/foliage/Apollo.kt b/foliage/app/src/main/java/sapling/foliage/Apollo.kt index 1f9d228..63401c4 100644 --- a/foliage/app/src/main/java/sapling/foliage/Apollo.kt +++ b/foliage/app/src/main/java/sapling/foliage/Apollo.kt @@ -1,6 +1,10 @@ package sapling.foliage +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import com.apollographql.apollo.ApolloClient +import me.zhanghai.compose.preference.defaultPreferenceFlow -val apolloClient = ApolloClient.Builder().serverUrl("https://sapling.geigr.dev/gql") - .build() +fun apolloClient(serverUrl: String?): ApolloClient = + ApolloClient.Builder().serverUrl(serverUrl ?: "https://sapling.geigr.dev/gql") + .build() diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt index 3155737..465aea5 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController import kotlinx.coroutines.launch +import me.zhanghai.compose.preference.defaultPreferenceFlow import sapling.foliage.apolloClient import sapling.foliage.gql.InventoryQuery import java.time.Instant @@ -41,17 +43,19 @@ fun InventoryScreen(modifier: Modifier = Modifier) { var isRefreshing by remember { mutableStateOf(false) } val state = rememberPullToRefreshState() val coroutineScope = rememberCoroutineScope() + val homeServer by defaultPreferenceFlow().collectAsState() fun fetchData() { isRefreshing = true coroutineScope.launch { - val response = apolloClient.query(InventoryQuery()).execute() + val response = + apolloClient(homeServer["home_server_url"]).query(InventoryQuery()).execute() itemList = response.data?.items ?: emptyList() isRefreshing = false } } - LaunchedEffect(apolloClient) { + LaunchedEffect(homeServer) { fetchData() } diff --git a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt index 02b6d20..389bf42 100644 --- a/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt +++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/SettingsScreen.kt @@ -9,12 +9,16 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import me.zhanghai.compose.preference.defaultPreferenceFlow import me.zhanghai.compose.preference.textFieldPreference @Composable fun SettingsScreen(modifier: Modifier = Modifier) { + Scaffold(modifier) { contentPadding -> LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = contentPadding) { textFieldPreference(