diff --git a/vertex-ai-friendly-meals/android/.gitignore b/vertex-ai-friendly-meals/android/.gitignore new file mode 100644 index 0000000..0b37fa0 --- /dev/null +++ b/vertex-ai-friendly-meals/android/.gitignore @@ -0,0 +1,20 @@ +*.iml +*.apk +*.aar +*.zip +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +.vscode +.project +.settings +.classpath +google-services.json +.idea/ +.idea/* +local.properties \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/.gitignore b/vertex-ai-friendly-meals/android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/build.gradle.kts b/vertex-ai-friendly-meals/android/app/build.gradle.kts new file mode 100644 index 0000000..cbccbf5 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.google.services) + alias(libs.plugins.google.ksp) + alias(libs.plugins.google.hilt) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.google.firebase.example.friendlymeals" + compileSdk = 35 + + defaultConfig { + applicationId = "com.google.firebase.example.friendlymeals" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + 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.androidx.navigation.compose) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.constraintlayout.compose) + implementation(libs.kotlinx.serialization.json) + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) + + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.vertexai) + + //Library to handle Markdown in Compose + implementation(libs.richtext.commonmark) + + 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/vertex-ai-friendly-meals/android/app/proguard-rules.pro b/vertex-ai-friendly-meals/android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/androidTest/java/com/google/firebase/example/friendlymeals/ExampleInstrumentedTest.kt b/vertex-ai-friendly-meals/android/app/src/androidTest/java/com/google/firebase/example/friendlymeals/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d5d0fe9 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/androidTest/java/com/google/firebase/example/friendlymeals/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.google.firebase.example.friendlymeals + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.google.firebase.example.friendlymeals", appContext.packageName) + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/AndroidManifest.xml b/vertex-ai-friendly-meals/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4fd22b3 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt new file mode 100644 index 0000000..5d1d51c --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/MainActivity.kt @@ -0,0 +1,61 @@ +package com.google.firebase.example.friendlymeals + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.google.firebase.example.friendlymeals.ui.home.HomeRoute +import com.google.firebase.example.friendlymeals.ui.home.HomeScreen +import com.google.firebase.example.friendlymeals.ui.theme.FriendlyMealsTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setSoftInputMode() + + setContent { + val snackbarHostState = remember { SnackbarHostState() } + val navController = rememberNavController() + + FriendlyMealsTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = HomeRoute, + modifier = Modifier.padding(innerPadding) + ) { + composable { HomeScreen() } + } + } + } + } + } + } + + private fun setSoftInputMode() { + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/MainViewModel.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/MainViewModel.kt new file mode 100644 index 0000000..c528afd --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/MainViewModel.kt @@ -0,0 +1,18 @@ +package com.google.firebase.example.friendlymeals + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +open class MainViewModel : ViewModel() { + fun launchCatching(block: suspend CoroutineScope.() -> Unit) = + viewModelScope.launch( + CoroutineExceptionHandler { _, throwable -> + Log.e("MainViewModel", throwable.message ?: "Unknown error") + }, + block = block + ) +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt new file mode 100644 index 0000000..b48caa7 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/datasource/AIRemoteDataSource.kt @@ -0,0 +1,28 @@ +package com.google.firebase.example.friendlymeals.data.datasource + +import android.graphics.Bitmap +import com.google.firebase.vertexai.GenerativeModel +import com.google.firebase.vertexai.ImagenModel +import com.google.firebase.vertexai.type.PublicPreviewAPI +import javax.inject.Inject + +@OptIn(PublicPreviewAPI::class) +class AIRemoteDataSource @Inject constructor( + private val generativeModel: GenerativeModel, + private val imagenModel: ImagenModel, +) { + suspend fun generateRecipe(ingredients: String, notes: String): String { + var prompt = "Based on this ingredients list: $ingredients, please give me one recipe." + if (notes.isNotBlank()) { + prompt += "Please take in consideration these notes: $notes." + } + val response = generativeModel.generateContent(prompt) + return response.text.orEmpty() + } + + @OptIn(PublicPreviewAPI::class) + suspend fun generateRecipeImage(recipe: String): Bitmap { + val imageResponse = imagenModel.generateImages(recipe) + return imageResponse.images.first().asBitmap() + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/injection/FirebaseHiltModule.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/injection/FirebaseHiltModule.kt new file mode 100644 index 0000000..5613663 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/injection/FirebaseHiltModule.kt @@ -0,0 +1,46 @@ +package com.google.firebase.example.friendlymeals.data.injection + +import com.google.firebase.Firebase +import com.google.firebase.vertexai.GenerativeModel +import com.google.firebase.vertexai.ImagenModel +import com.google.firebase.vertexai.type.ImagenAspectRatio +import com.google.firebase.vertexai.type.ImagenImageFormat +import com.google.firebase.vertexai.type.ImagenPersonFilterLevel +import com.google.firebase.vertexai.type.ImagenSafetyFilterLevel +import com.google.firebase.vertexai.type.ImagenSafetySettings +import com.google.firebase.vertexai.type.PublicPreviewAPI +import com.google.firebase.vertexai.type.imagenGenerationConfig +import com.google.firebase.vertexai.vertexAI +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object FirebaseHiltModule { + @Provides fun generativeModel(): GenerativeModel { + return Firebase.vertexAI.generativeModel( + modelName = "gemini-2.0-flash" + ) + } + + @OptIn(PublicPreviewAPI::class) + @Provides fun imagenModel(): ImagenModel { + val generationConfig = imagenGenerationConfig { + numberOfImages = 1 + aspectRatio = ImagenAspectRatio.SQUARE_1x1 + imageFormat = ImagenImageFormat.png() + } + + val safetySettings = ImagenSafetySettings( + safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE, + personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL + ) + return Firebase.vertexAI.imagenModel( + modelName = "imagen-3.0-generate-002", + generationConfig = generationConfig, + safetySettings = safetySettings + ) + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/injection/FriendlyMealsHiltApp.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/injection/FriendlyMealsHiltApp.kt new file mode 100644 index 0000000..b913ac9 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/injection/FriendlyMealsHiltApp.kt @@ -0,0 +1,7 @@ +package com.google.firebase.example.friendlymeals.data.injection + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class FriendlyMealsHiltApp: Application() \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/model/Recipe.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/model/Recipe.kt new file mode 100644 index 0000000..4863e2a --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/model/Recipe.kt @@ -0,0 +1,11 @@ +package com.google.firebase.example.friendlymeals.data.model + +import android.graphics.Bitmap + +data class Recipe( + val title: String = "", + val description: String = "", + val ingredients: List = listOf(), + val steps: List = listOf(), + val image: Bitmap? = null +) \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt new file mode 100644 index 0000000..8adf79e --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/data/repository/AIRepository.kt @@ -0,0 +1,17 @@ +package com.google.firebase.example.friendlymeals.data.repository + +import android.graphics.Bitmap +import com.google.firebase.example.friendlymeals.data.datasource.AIRemoteDataSource +import javax.inject.Inject + +class AIRepository @Inject constructor( + private val aiRemoteDataSource: AIRemoteDataSource +) { + suspend fun generateRecipe(ingredients: String, notes: String): String { + return aiRemoteDataSource.generateRecipe(ingredients, notes) + } + + suspend fun generateRecipeImage(recipe: String): Bitmap { + return aiRemoteDataSource.generateRecipeImage(recipe) + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/home/HomeScreen.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/home/HomeScreen.kt new file mode 100644 index 0000000..8c616e9 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/home/HomeScreen.kt @@ -0,0 +1,224 @@ +package com.google.firebase.example.friendlymeals.ui.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.firebase.example.friendlymeals.R +import com.google.firebase.example.friendlymeals.data.model.Recipe +import com.google.firebase.example.friendlymeals.ui.shared.LoadingIndicator +import com.google.firebase.example.friendlymeals.ui.theme.DarkFirebaseYellow +import com.google.firebase.example.friendlymeals.ui.theme.FriendlyMealsTheme +import com.google.firebase.example.friendlymeals.ui.theme.LightFirebaseYellow +import com.google.firebase.example.friendlymeals.ui.theme.MediumFirebaseYellow +import com.halilibo.richtext.commonmark.Markdown +import com.halilibo.richtext.ui.BasicRichText +import kotlinx.serialization.Serializable + +@Serializable +object HomeRoute + +@Composable +fun HomeScreen( + viewModel: HomeViewModel = hiltViewModel() +) { + val recipe = viewModel.recipe.collectAsStateWithLifecycle() + val loading = viewModel.loading.collectAsStateWithLifecycle() + + HomeScreenContent( + onGenerateClick = viewModel::generateRecipe, + recipe = recipe.value, + loading = loading.value + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreenContent( + modifier: Modifier = Modifier, + onGenerateClick: (String, String) -> Unit, + recipe: Recipe?, + loading: Boolean +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) { innerPadding -> + Column( + modifier = modifier + .fillMaxWidth() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + ) { + IngredientsBox(onGenerateClick = onGenerateClick) + + Spacer(modifier = Modifier.size(16.dp)) + + if (loading) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(RoundedCornerShape(48f)) + .border(2.dp, DarkFirebaseYellow, RoundedCornerShape(48f)) + .background(MediumFirebaseYellow) + .padding(16.dp) + ) { + LoadingIndicator() + } + } + + if (recipe != null) RecipeBox(recipe = recipe) + } + } +} + +@Composable +fun IngredientsBox( + modifier: Modifier = Modifier, + onGenerateClick: (String, String) -> Unit, +) { + var ingredients by remember { mutableStateOf("") } + var notes by remember { mutableStateOf("") } + + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(RoundedCornerShape(48f)) + .border(2.dp, DarkFirebaseYellow, RoundedCornerShape(48f)) + .background(MediumFirebaseYellow) + .padding(16.dp) + ) { + Column(modifier = Modifier.fillMaxWidth()) { + TextField( + value = ingredients, + onValueChange = { ingredients = it }, + modifier = Modifier + .fillMaxWidth().height(128.dp), + shape = RoundedCornerShape(24.dp), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + focusedContainerColor = LightFirebaseYellow, + unfocusedContainerColor = LightFirebaseYellow + ), + placeholder = { + Text(text = stringResource(R.string.ingredients_hint)) + } + ) + + Spacer(modifier = Modifier.size(16.dp)) + + TextField( + value = notes, + onValueChange = { notes = it }, + modifier = Modifier + .fillMaxWidth().height(128.dp), + shape = RoundedCornerShape(24.dp), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + focusedContainerColor = LightFirebaseYellow, + unfocusedContainerColor = LightFirebaseYellow + ), + placeholder = { + Text(text = stringResource(R.string.notes_hint)) + } + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = DarkFirebaseYellow), + onClick = { onGenerateClick(ingredients, notes) } + ) { + Text(stringResource(R.string.generate_recipe_button), fontSize = 16.sp) + } + } + } +} + +@Composable +fun RecipeBox( + modifier: Modifier = Modifier, + recipe: Recipe +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .clip(RoundedCornerShape(48f)) + .border(2.dp, DarkFirebaseYellow, RoundedCornerShape(48f)) + .background(MediumFirebaseYellow) + .padding(16.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val image = recipe.image?.asImageBitmap() + + if (image != null) { + Image(bitmap = image, "Recipe image") + } + + Spacer(modifier = Modifier.size(16.dp)) + + BasicRichText { + Markdown(recipe.description) + } + } + } +} + +@Composable +@Preview(showSystemUi = true) +fun HomeScreenPreview() { + FriendlyMealsTheme(darkTheme = true) { + HomeScreenContent( + onGenerateClick = { _, _ -> }, + recipe = Recipe(), + loading = false + ) + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/home/HomeViewModel.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..c74c2a9 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/home/HomeViewModel.kt @@ -0,0 +1,37 @@ +package com.google.firebase.example.friendlymeals.ui.home + +import com.google.firebase.example.friendlymeals.MainViewModel +import com.google.firebase.example.friendlymeals.data.model.Recipe +import com.google.firebase.example.friendlymeals.data.repository.AIRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val aiRepository: AIRepository +) : MainViewModel() { + private val _recipe = MutableStateFlow(null) + val recipe: StateFlow + get() = _recipe.asStateFlow() + + private val _loading = MutableStateFlow(false) + val loading: StateFlow + get() = _loading.asStateFlow() + + fun generateRecipe(ingredients: String, notes: String) { + launchCatching { + _loading.value = true + val generatedRecipe = aiRepository.generateRecipe(ingredients, notes) + val recipeImage = aiRepository.generateRecipeImage(generatedRecipe) + + _loading.value = false + _recipe.value = Recipe( + description = generatedRecipe, + image = recipeImage + ) + } + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/shared/LoadingIndicator.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/shared/LoadingIndicator.kt new file mode 100644 index 0000000..5f001b1 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/shared/LoadingIndicator.kt @@ -0,0 +1,26 @@ +package com.google.firebase.example.friendlymeals.ui.shared + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.firebase.example.friendlymeals.ui.theme.DarkFirebaseYellow +import com.google.firebase.example.friendlymeals.ui.theme.LightFirebaseYellow + +@Composable +fun LoadingIndicator() { + Box(modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp)) { + CircularProgressIndicator( + modifier = Modifier + .width(64.dp) + .align(Alignment.Center), + color = DarkFirebaseYellow, + trackColor = LightFirebaseYellow, + ) + } +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Color.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Color.kt new file mode 100644 index 0000000..57f222a --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Color.kt @@ -0,0 +1,15 @@ +package com.google.firebase.example.friendlymeals.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val LightFirebaseYellow = Color(0xFFFFFDF4) +val MediumFirebaseYellow = Color(0xFFFFF3CC) +val DarkFirebaseYellow = Color(0xFFFFC400) \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Theme.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Theme.kt new file mode 100644 index 0000000..b4c341e --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.google.firebase.example.friendlymeals.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun FriendlyMealsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Type.kt b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Type.kt new file mode 100644 index 0000000..6880d71 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/java/com/google/firebase/example/friendlymeals/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.google.firebase.example.friendlymeals.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/drawable/ic_launcher_background.xml b/vertex-ai-friendly-meals/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/vertex-ai-friendly-meals/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..22fa52c Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..efd783b Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..22fa52c Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..efd783b Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..22fa52c Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..efd783b Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..22fa52c Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..efd783b Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..22fa52c Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..efd783b Binary files /dev/null and b/vertex-ai-friendly-meals/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/values/colors.xml b/vertex-ai-friendly-meals/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/values/strings.xml b/vertex-ai-friendly-meals/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..03f942a --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + Friendly Meals + Enter your list of ingredients + Any notes or preferred cuisines? + Generate recipe + \ No newline at end of file diff --git a/vertex-ai-friendly-meals/android/app/src/main/res/values/themes.xml b/vertex-ai-friendly-meals/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..14b58ef --- /dev/null +++ b/vertex-ai-friendly-meals/android/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +