diff --git a/foliage/.gitignore b/foliage/.gitignore index 28a82e3..52405f7 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/app/build.gradle.kts b/foliage/app/build.gradle.kts index fe0dad4..cef5a65 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,37 +45,41 @@ 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 { - 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.play.services.code.scanner) - 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.play.services.code.scanner) + implementation(libs.zhanghai.preferences) + implementation(platform(libs.androidx.compose.bom)) - 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) -} \ No newline at end of file +} 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..63401c4 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/Apollo.kt @@ -0,0 +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 + +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/Foliage.kt b/foliage/app/src/main/java/sapling/foliage/Foliage.kt new file mode 100644 index 0000000..2934937 --- /dev/null +++ b/foliage/app/src/main/java/sapling/foliage/Foliage.kt @@ -0,0 +1,46 @@ +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 +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 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 +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/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 673dda5..0000000 --- a/foliage/app/src/main/java/sapling/foliage/GraphQLViewModel.kt +++ /dev/null @@ -1,26 +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 - -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/MainActivity.kt b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt index 9f5f4f1..698787f 100644 --- a/foliage/app/src/main/java/sapling/foliage/MainActivity.kt +++ b/foliage/app/src/main/java/sapling/foliage/MainActivity.kt @@ -4,45 +4,14 @@ 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 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 { GraphQLScreen() } - } - MainNavbar(navController = navController) - } - } + FoliageApp() } } } \ 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 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 2bc1158..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 @@ -1,28 +1,118 @@ 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.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.collectAsState +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.BarcodeScanner -import sapling.foliage.ui.components.MainNavbar +import kotlinx.coroutines.launch +import me.zhanghai.compose.preference.defaultPreferenceFlow +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() + val homeServer by defaultPreferenceFlow().collectAsState() + + fun fetchData() { + isRefreshing = true + coroutineScope.launch { + val response = + apolloClient(homeServer["home_server_url"]).query(InventoryQuery()).execute() + itemList = response.data?.items ?: emptyList() + isRefreshing = false + } + } + + LaunchedEffect(homeServer) { + fetchData() + } + Scaffold( - modifier = Modifier.fillMaxSize() - ) { _ -> - Text("Inventory") - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxSize() - ) { - BarcodeScanner() + 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 f94c0da..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 @@ -1,17 +1,39 @@ 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.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.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.navigation.compose.rememberNavController -import sapling.foliage.ui.components.MainNavbar +import me.zhanghai.compose.preference.defaultPreferenceFlow + +import me.zhanghai.compose.preference.textFieldPreference @Composable fun SettingsScreen(modifier: Modifier = Modifier) { - val navController = rememberNavController() - Scaffold( - ){ _ -> - Text("Settings") + + 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 } + ) + } } -} \ No newline at end of file +} diff --git a/foliage/gradle/libs.versions.toml b/foliage/gradle/libs.versions.toml index bb8f6b3..00d260e 100644 --- a/foliage/gradle/libs.versions.toml +++ b/foliage/gradle/libs.versions.toml @@ -1,52 +1,57 @@ [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" -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" playServicesCodeScanner = "16.1.0" [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" } -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-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } +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" } + +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +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" } 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" } +lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } play-services-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "playServicesCodeScanner" } +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" } 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 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) + } }