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