diff --git a/foliage/.gitignore b/foliage/.gitignore
index aa724b7..28a82e3 100644
--- a/foliage/.gitignore
+++ b/foliage/.gitignore
@@ -13,3 +13,4 @@
.externalNativeBuild
.cxx
local.properties
+/.kotlin
diff --git a/foliage/app/build.gradle.kts b/foliage/app/build.gradle.kts
index 05c08fd..fe0dad4 100644
--- a/foliage/app/build.gradle.kts
+++ b/foliage/app/build.gradle.kts
@@ -2,6 +2,9 @@ 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)
}
android {
@@ -10,7 +13,7 @@ android {
defaultConfig {
applicationId = "sapling.foliage"
- minSdk = 31
+ minSdk = 30
targetSdk = 35
versionCode = 1
versionName = "1.0"
@@ -37,18 +40,37 @@ android {
buildFeatures {
compose = true
}
+ buildToolsVersion = "36.0.0"
}
-dependencies {
+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 {
+ implementation(libs.play.services.code.scanner)
+ 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.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)
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 e426752..9f5f4f1 100644
--- a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt
+++ b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt
@@ -4,13 +4,20 @@ 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.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.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+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() {
@@ -19,29 +26,23 @@ class MainActivity : ComponentActivity() {
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 { GraphQLScreen() }
+ }
+ 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/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/java/sapling/foliage/ui/components/BarcodeScanner.kt b/foliage/app/src/main/java/sapling/foliage/ui/components/BarcodeScanner.kt
new file mode 100644
index 0000000..0d6d0b7
--- /dev/null
+++ b/foliage/app/src/main/java/sapling/foliage/ui/components/BarcodeScanner.kt
@@ -0,0 +1,45 @@
+package sapling.foliage.ui.components
+
+import android.widget.Toast
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.platform.LocalContext
+import com.google.mlkit.vision.barcode.common.Barcode
+import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
+import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
+
+@Composable
+fun BarcodeScanner() {
+ val context = LocalContext.current
+ val scannerOptions = remember {
+ GmsBarcodeScannerOptions.Builder()
+ .setBarcodeFormats(
+ Barcode.FORMAT_EAN_13,
+ )
+ .enableAutoZoom()
+ .build()
+ }
+ val scanner = remember { GmsBarcodeScanning.getClient(context, scannerOptions) }
+ var scannedValue by remember { mutableStateOf(null) }
+
+ Button(onClick = {
+ scanner.startScan()
+ .addOnSuccessListener { barcode ->
+ scannedValue = barcode.rawValue
+ }
+ .addOnCanceledListener {
+ Toast.makeText(context, "Scan canceled", Toast.LENGTH_SHORT).show()
+ }
+ .addOnFailureListener {
+ Toast.makeText(context, "Scan failed: ${it.message}", Toast.LENGTH_SHORT)
+ .show()
+ }
+ }) {
+ Text("Scan Barcode")
+ }
+
+ scannedValue?.let {
+ Text("Scanned value: $it")
+ }
+}
\ 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..2bc1158
--- /dev/null
+++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/InventoryScreen.kt
@@ -0,0 +1,28 @@
+package sapling.foliage.ui.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.BarcodeScanner
+import sapling.foliage.ui.components.MainNavbar
+
+@Composable
+fun InventoryScreen(modifier: Modifier = Modifier) {
+ val navController = rememberNavController()
+ Scaffold(
+ modifier = Modifier.fillMaxSize()
+ ) { _ ->
+ Text("Inventory")
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ BarcodeScanner()
+ }
+ }
+}
\ 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..355c622
--- /dev/null
+++ b/foliage/app/src/main/java/sapling/foliage/ui/screens/ShoppingScreen.kt
@@ -0,0 +1,267 @@
+package sapling.foliage.ui.screens
+
+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.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.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) {
+ Scaffold(
+ topBar = {
+ TopAppBar(title = {
+ Text("Shopping Tours")
+ })
+ },
+ floatingActionButton = {
+ FloatingActionButton(onClick = {}) {
+ Icon(Icons.Filled.Add, "Add new Shopping Tour")
+ }
+ },
+ ) { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ modifier = Modifier.fillMaxHeight(),
+ ) {
+ items(tempTours) { tour ->
+ ShoppingTourEntry(shoppingTour = tour)
+ }
+ }
+ }
+}
\ 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 2e88400..bb8f6b3 100644
--- a/foliage/gradle/libs.versions.toml
+++ b/foliage/gradle/libs.versions.toml
@@ -1,16 +1,30 @@
[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"
-lifecycleRuntimeKtx = "2.6.1"
-activityCompose = "1.8.0"
-composeBom = "2024.09.00"
+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"
+material = "1.8.1"
+navigationCompose = "2.9.0"
+navigationComposeJvmstubs = "2.9.0"
+playServicesCodeScanner = "16.1.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" }
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 +38,15 @@ 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" }
+play-services-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "playServicesCodeScanner" }
[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" }
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