├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── jr │ │ └── brian │ │ └── issarecipeapp │ │ └── ExampleInstrumentedTest.kt │ ├── debug │ ├── ic_launcher-playstore.png │ └── res │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── ic_launcher_background.xml │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── jr │ │ │ └── brian │ │ │ └── issarecipeapp │ │ │ ├── MainActivity.kt │ │ │ ├── MyApp.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── model │ │ │ ├── local │ │ │ │ ├── AppDatabase.kt │ │ │ │ ├── Chat.kt │ │ │ │ ├── Meal.kt │ │ │ │ ├── MyDataStore.kt │ │ │ │ ├── Recipe.kt │ │ │ │ ├── RecipeDao.kt │ │ │ │ └── RecipeFolder.kt │ │ │ ├── remote │ │ │ │ ├── ApiService.kt │ │ │ │ ├── CachedChatBot.kt │ │ │ │ ├── ChatBot.kt │ │ │ │ ├── Firebase.kt │ │ │ │ └── OpenAIImage.kt │ │ │ └── repository │ │ │ │ ├── RepoImpl.kt │ │ │ │ └── Repository.kt │ │ │ ├── util │ │ │ ├── constants.kt │ │ │ ├── extensions.kt │ │ │ └── util.kt │ │ │ ├── view │ │ │ └── ui │ │ │ │ ├── components │ │ │ │ ├── ChatComponents.kt │ │ │ │ ├── DialogComponents.kt │ │ │ │ ├── LottieComponents.kt │ │ │ │ ├── TextFieldComponents.kt │ │ │ │ └── swipe_cards │ │ │ │ │ ├── InfiniteList.kt │ │ │ │ │ ├── Modifier.kt │ │ │ │ │ ├── RecipeCard.kt │ │ │ │ │ ├── RecipeStack.kt │ │ │ │ │ ├── StackBackgroundCard.kt │ │ │ │ │ ├── StackForegroundCard.kt │ │ │ │ │ ├── SwipeControls.kt │ │ │ │ │ ├── SwipeDirection.kt │ │ │ │ │ ├── SwipeOverlay.kt │ │ │ │ │ └── SwipeableState.kt │ │ │ │ ├── pages │ │ │ │ ├── AskPage.kt │ │ │ │ ├── ConvoContextPage.kt │ │ │ │ ├── FavRecipesPage.kt │ │ │ │ ├── GenerateRecipePage.kt │ │ │ │ ├── HomePage.kt │ │ │ │ ├── RecipeSwipePage.kt │ │ │ │ └── SettingsPage.kt │ │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Dimens.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── viewmodel │ │ │ └── MainViewModel.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── baseline_arrow_back_24.xml │ │ ├── baseline_check_24.xml │ │ ├── baseline_delete_forever_24.xml │ │ ├── baseline_edit_24.xml │ │ ├── baseline_fastfood_24.xml │ │ ├── baseline_favorite_24.xml │ │ ├── baseline_history_24.xml │ │ ├── baseline_info_24.xml │ │ ├── baseline_menu_24.xml │ │ ├── baseline_mic_24.xml │ │ ├── baseline_more_24.xml │ │ ├── baseline_open_in_new_24.xml │ │ ├── baseline_send_24.xml │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ ├── foodbowl.json │ │ ├── loading.json │ │ ├── loadingpink.json │ │ └── recipe.json │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ ├── release │ ├── ic_launcher-playstore.png │ └── res │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── ic_launcher_background.xml │ └── test │ └── java │ └── jr │ └── brian │ └── issarecipeapp │ └── ExampleUnitTest.kt ├── build.gradle ├── docs └── assets │ ├── icons8-cookbook-512.png │ └── recipe_app_ss.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── recipe-app-demo.mp4 └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | google-services.json 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Issa Recipe App -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 |
5 | 6 | [![Github All Releases](https://img.shields.io/github/downloads/BrianJr03/Issa-Recipe-App/total.svg)](https://github.com/BrianJr03/Issa-Recipe-App/releases/latest) 7 | 8 | # Issa Recipe App 9 | 10 | 11 |
12 | 13 | Get it on Google Play 14 | 15 | 16 | 17 | Get it on Github 18 | 19 |
20 | 21 | ## About the Project 22 | 23 | A simple [recipe app](https://youtu.be/8a2mytF2KMw) powered by ChatGPT. Swipe or Generate your own recipes and find your next favorite dish! 24 | 25 | `Note`: An OpenAI [API Key](https://platform.openai.com/account/api-keys) is needed to use this app 26 | 27 |
28 | 29 |
30 | 31 | ## Features 32 | 33 | - Ask 34 | - Ask an AI powered Chef anything culinary related. You will be provided with great anwsers! 35 | 36 | - Swipe 37 | - Browse through randomized recipes and experience Love at first Swipe 38 | - `breakfast`, `lunch`, and `dinner` recipes will be accessible based on your device's local time 39 | 40 | - Generate or Randomize with Image Generation 41 | - Create your own recipes based on 42 | - Party Size 43 | - Ingredients 44 | - Occasion 45 | - Dietary Restrictions 46 | - Food Allergies 47 | - Additional Info (if needed) 48 | - *An image based on your recipe will be generated* 49 | 50 | - Favorites 51 | - Filter via search 52 | - Rename any recipe 53 | - Organize in folders (coming soon) 54 | 55 | - Settings 56 | - Save Custom Dietary Restrictions 57 | - Save Custom Food Allergies 58 | - Save OpenAI API Key 59 | - Enable Image Generation 60 | 61 | ## Prerequisites 62 | 63 | - [Android Studio](https://developer.android.com/studio) 64 | - Android SDK 65 | - OpenAI API Key can be found at 66 | 67 | ## Installation 68 | 69 | Feel free to download the latest release from one of the sources above. 70 | If you want to build it yourself, follow the steps below. 71 | 72 | 1. Clone the repo 73 | 74 | ```sh 75 | git clone https://github.com/BrianJr03/Issa-Recipe-App.git 76 | ``` 77 | 78 | 2. Open in Android Studio 79 | 3. Run on emulator or device 80 | 81 | ## Tech Stack 82 | 83 | - Kotlin 84 | - Jetpack Compose 85 | - MVVM with Repository 86 | - Coroutines 87 | - RoomDB 88 | - DaggerHilt 89 | 90 | App Icon: https://icons8.com/icon/O3dWhsYC0M0n/cookbook 91 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | google-services.json -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-parcelize' 5 | id 'dagger.hilt.android.plugin' 6 | id 'kotlin-kapt' 7 | id 'com.google.gms.google-services' 8 | } 9 | 10 | android { 11 | namespace 'jr.brian.issarecipeapp' 12 | compileSdk 34 13 | 14 | defaultConfig { 15 | applicationId "jr.brian.issarecipeapp" 16 | minSdk 26 17 | targetSdk 34 18 | versionCode 1 19 | versionName "1.6" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | vectorDrawables { 23 | useSupportLibrary true 24 | } 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_17 35 | targetCompatibility JavaVersion.VERSION_17 36 | } 37 | kotlinOptions { 38 | jvmTarget = '17' 39 | } 40 | buildFeatures { 41 | compose true 42 | } 43 | composeOptions { 44 | kotlinCompilerExtensionVersion '1.4.7' 45 | } 46 | packagingOptions { 47 | resources { 48 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | // Navigation 55 | // noinspection GradleDependency 56 | implementation "androidx.navigation:navigation-compose:2.6.0-rc01" 57 | 58 | // View Pager 59 | implementation "com.google.accompanist:accompanist-pager:0.23.1" 60 | implementation "com.google.accompanist:accompanist-pager-indicators:0.23.1" 61 | 62 | // Dagger - Hilt 63 | implementation "com.google.dagger:hilt-android:2.48" 64 | implementation 'com.google.firebase:firebase-database-ktx:20.3.0' 65 | kapt "com.google.dagger:hilt-android-compiler:2.45" 66 | kapt "androidx.hilt:hilt-compiler:1.1.0" 67 | implementation 'androidx.hilt:hilt-navigation-compose:1.1.0' 68 | 69 | // OkHttp 70 | implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2") 71 | implementation("com.google.code.gson:gson:2.10.1") 72 | 73 | // Room DB 74 | def roomVersion = "2.6.1" 75 | implementation("androidx.room:room-runtime:$roomVersion") 76 | //noinspection KaptUsageInsteadOfKsp 77 | kapt("androidx.room:room-compiler:$roomVersion") 78 | 79 | // Lottie 80 | implementation ("com.airbnb.android:lottie-compose:6.0.0") 81 | 82 | // Jetpack DataStore 83 | implementation "androidx.datastore:datastore-preferences:1.0.0" 84 | 85 | // Glide 86 | implementation "com.github.bumptech.glide:compose:1.0.0-beta01" 87 | 88 | implementation 'androidx.core:core-ktx:1.12.0' 89 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' 90 | implementation 'androidx.activity:activity-compose:1.8.2' 91 | implementation platform('androidx.compose:compose-bom:2022.10.00') 92 | implementation 'androidx.compose.ui:ui' 93 | implementation 'androidx.compose.ui:ui-graphics' 94 | implementation 'androidx.compose.ui:ui-tooling-preview' 95 | implementation 'androidx.compose.material3:material3' 96 | testImplementation 'junit:junit:4.13.2' 97 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 98 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 99 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') 100 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 101 | debugImplementation 'androidx.compose.ui:ui-tooling' 102 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 103 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/jr/brian/issarecipeapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("jr.brian.issarecipeapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/debug/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #212121 4 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.annotation.RequiresApi 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.collectAsState 13 | import androidx.compose.ui.Modifier 14 | import androidx.navigation.compose.NavHost 15 | import androidx.navigation.compose.composable 16 | import androidx.navigation.compose.rememberNavController 17 | import dagger.hilt.android.AndroidEntryPoint 18 | import jr.brian.issarecipeapp.model.local.AppDataStore 19 | import jr.brian.issarecipeapp.model.local.RecipeDao 20 | import jr.brian.issarecipeapp.model.remote.ApiService 21 | import jr.brian.issarecipeapp.util.ASK_CONTEXT_ROUTE 22 | import jr.brian.issarecipeapp.util.ASK_ROUTE 23 | import jr.brian.issarecipeapp.util.SWIPE_RECIPES_ROUTE 24 | import jr.brian.issarecipeapp.util.FAV_RECIPES_ROUTE 25 | import jr.brian.issarecipeapp.util.GPT_3_5_TURBO 26 | import jr.brian.issarecipeapp.util.HOME_ROUTE 27 | import jr.brian.issarecipeapp.util.MEAL_DETAILS_ROUTE 28 | import jr.brian.issarecipeapp.util.SETTINGS_ROUTE 29 | import jr.brian.issarecipeapp.view.ui.pages.AskContextPage 30 | import jr.brian.issarecipeapp.view.ui.pages.AskPage 31 | import jr.brian.issarecipeapp.view.ui.pages.FavRecipesPage 32 | import jr.brian.issarecipeapp.view.ui.pages.HomePage 33 | import jr.brian.issarecipeapp.view.ui.pages.GenerateRecipePage 34 | import jr.brian.issarecipeapp.view.ui.pages.RecipeSwipe 35 | import jr.brian.issarecipeapp.view.ui.pages.SettingsPage 36 | import jr.brian.issarecipeapp.view.ui.theme.IssaRecipeAppTheme 37 | import javax.inject.Inject 38 | 39 | @AndroidEntryPoint 40 | class MainActivity : ComponentActivity() { 41 | var dao: RecipeDao? = null 42 | @Inject set 43 | 44 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | 48 | val dataStore = AppDataStore(this) 49 | 50 | setContent { 51 | IssaRecipeAppTheme { 52 | Surface( 53 | modifier = Modifier.fillMaxSize(), 54 | color = MaterialTheme.colorScheme.background 55 | ) { 56 | val storedApiKey = dataStore.getApiKey.collectAsState(initial = "").value 57 | ?: "" 58 | val dietaryRestrictions = 59 | dataStore.getDietaryRestrictions.collectAsState(initial = "none").value 60 | ?: "none" 61 | val foodAllergies = 62 | dataStore.getFoodAllergies.collectAsState(initial = "none").value 63 | ?: "none" 64 | val gptModel = 65 | dataStore.getGptModel.collectAsState(initial = GPT_3_5_TURBO).value 66 | ?: GPT_3_5_TURBO 67 | val askContext = 68 | dataStore.getAskContext.collectAsState(initial = "").value 69 | ?: "" 70 | val isImageGenerationEnabled = 71 | dataStore.getIsImageGenerationEnabled.collectAsState(initial = "false").value 72 | ?: "" 73 | 74 | ApiService.ApiKey.userApiKey = storedApiKey 75 | 76 | dao?.let { 77 | AppUI( 78 | dao = it, 79 | savedApiKey = storedApiKey, 80 | savedDietaryRestrictions = dietaryRestrictions, 81 | savedFoodAllergies = foodAllergies, 82 | savedGptModel = gptModel, 83 | storedAskContext = askContext, 84 | isImageGenerationEnabled = isImageGenerationEnabled, 85 | dataStore = dataStore 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | @Composable 95 | fun AppUI( 96 | dao: RecipeDao, 97 | savedApiKey: String, 98 | savedDietaryRestrictions: String, 99 | savedFoodAllergies: String, 100 | savedGptModel: String, 101 | storedAskContext: String, 102 | isImageGenerationEnabled: String, 103 | dataStore: AppDataStore 104 | ) { 105 | val navController = rememberNavController() 106 | NavHost(navController = navController, startDestination = HOME_ROUTE, builder = { 107 | composable(HOME_ROUTE, content = { 108 | HomePage( 109 | onNavToAsk = { 110 | navController.navigate(ASK_ROUTE) { 111 | launchSingleTop = true 112 | } 113 | }, 114 | onNavToMealDetails = { 115 | navController.navigate(MEAL_DETAILS_ROUTE) { 116 | launchSingleTop = true 117 | } 118 | }, onNavToFavRecipes = { 119 | navController.navigate(FAV_RECIPES_ROUTE) { 120 | launchSingleTop = true 121 | } 122 | }, onNavToSwipe = { 123 | navController.navigate(SWIPE_RECIPES_ROUTE) { 124 | launchSingleTop = true 125 | } 126 | }, 127 | onNavToSettings = { 128 | navController.navigate(SETTINGS_ROUTE) { 129 | launchSingleTop = true 130 | } 131 | } 132 | ) 133 | }) 134 | composable(ASK_ROUTE, content = { 135 | AskPage( 136 | dao = dao, 137 | savedApiKey = savedApiKey, 138 | savedAskContext = storedAskContext, 139 | savedModel = savedGptModel, 140 | onNavToAskContext = { 141 | navController.navigate(ASK_CONTEXT_ROUTE) { 142 | launchSingleTop = true 143 | } 144 | }, 145 | onNavToSettings = { 146 | navController.navigate(SETTINGS_ROUTE) { 147 | launchSingleTop = true 148 | } 149 | } 150 | ) 151 | }) 152 | composable(ASK_CONTEXT_ROUTE, content = { 153 | AskContextPage(storedContext = storedAskContext, dataStore = dataStore) 154 | }) 155 | composable(MEAL_DETAILS_ROUTE, content = { 156 | GenerateRecipePage( 157 | dao = dao, 158 | dataStore = dataStore, 159 | dietaryRestrictions = savedDietaryRestrictions, 160 | foodAllergies = savedFoodAllergies, 161 | gptModel = savedGptModel, 162 | isImageGenerationEnabled = isImageGenerationEnabled, 163 | onNavToSettings = { 164 | navController.navigate(SETTINGS_ROUTE) { 165 | launchSingleTop = true 166 | } 167 | } 168 | ) 169 | }) 170 | composable(FAV_RECIPES_ROUTE, content = { 171 | FavRecipesPage(dao = dao) 172 | }) 173 | composable(SWIPE_RECIPES_ROUTE, content = { 174 | RecipeSwipe(dao = dao) 175 | }) 176 | composable(SETTINGS_ROUTE, content = { 177 | SettingsPage( 178 | dao = dao, 179 | apiKey = savedApiKey, 180 | dietaryRestrictions = savedDietaryRestrictions, 181 | foodAllergies = savedFoodAllergies, 182 | gptModel = savedGptModel, 183 | isImageGenerationEnabled = isImageGenerationEnabled, 184 | dataStore = dataStore, 185 | ) 186 | }) 187 | }) 188 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/MyApp.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MyApp : Application() -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import jr.brian.issarecipeapp.model.local.AppDatabase 11 | import jr.brian.issarecipeapp.model.local.RecipeDao 12 | import jr.brian.issarecipeapp.model.repository.RepoImpl 13 | import jr.brian.issarecipeapp.model.repository.Repository 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object AppModule { 19 | @Provides 20 | @Singleton 21 | fun provideRepository(): Repository = RepoImpl() 22 | 23 | @Provides 24 | @Singleton 25 | fun provideDao(appDatabase: AppDatabase): RecipeDao = appDatabase.dao() 26 | 27 | @Provides 28 | @Singleton 29 | fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase { 30 | return Room.databaseBuilder( 31 | appContext, 32 | AppDatabase::class.java, 33 | "recipes" 34 | ) 35 | .allowMainThreadQueries() 36 | .fallbackToDestructiveMigration() 37 | .build() 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database( 7 | entities = [Recipe::class, Chat::class, RecipeFolder::class], 8 | version = 5, 9 | exportSchema = false 10 | ) 11 | abstract class AppDatabase : RoomDatabase() { 12 | abstract fun dao(): RecipeDao 13 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/Chat.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "chats") 7 | data class Chat( 8 | @PrimaryKey val fullTimeStamp: String, 9 | val text: String, 10 | val dateSent: String, 11 | val timeSent: String, 12 | val senderLabel: String, 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/Meal.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class Meal( 8 | val name: String 9 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/MyDataStore.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.edit 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | import javax.inject.Inject 12 | 13 | class AppDataStore @Inject constructor(private val context: Context) { 14 | companion object { 15 | private val Context.dataStore: 16 | DataStore by preferencesDataStore("app-data-store") 17 | val API_KEY = stringPreferencesKey("user-api-key") 18 | val DIETARY_RESTRICTIONS = stringPreferencesKey("dietary-restrictions") 19 | val FOOD_ALLERGIES = stringPreferencesKey("food-allergies") 20 | val GPT_MODEL = stringPreferencesKey("gpt-model") 21 | val ASK_CONTEXT = stringPreferencesKey("ask-context") 22 | val IS_IMAGE_GENERATION_ENABLED = stringPreferencesKey("is-image-generation-enabled") 23 | } 24 | 25 | val getApiKey: Flow = context.dataStore.data.map { preferences -> 26 | preferences[API_KEY] 27 | } 28 | 29 | suspend fun saveApiKey(value: String) { 30 | context.dataStore.edit { preferences -> 31 | preferences[API_KEY] = value 32 | } 33 | } 34 | 35 | val getAskContext: Flow = context.dataStore.data.map { preferences -> 36 | preferences[ASK_CONTEXT] 37 | } 38 | 39 | suspend fun saveAskContext(askContext: String) { 40 | context.dataStore.edit { preferences -> 41 | preferences[ASK_CONTEXT] = askContext 42 | } 43 | } 44 | 45 | val getIsImageGenerationEnabled: Flow = context.dataStore.data.map { preferences -> 46 | preferences[IS_IMAGE_GENERATION_ENABLED] 47 | } 48 | 49 | suspend fun saveIsImageGenerationEnabled(isEnabled: String) { 50 | context.dataStore.edit { preferences -> 51 | preferences[IS_IMAGE_GENERATION_ENABLED] = isEnabled 52 | } 53 | } 54 | 55 | val getDietaryRestrictions: Flow = context.dataStore.data.map { preferences -> 56 | preferences[DIETARY_RESTRICTIONS] 57 | } 58 | 59 | suspend fun saveDietaryRestrictions(value: String) { 60 | context.dataStore.edit { preferences -> 61 | preferences[DIETARY_RESTRICTIONS] = value 62 | } 63 | } 64 | 65 | val getFoodAllergies: Flow = context.dataStore.data.map { preferences -> 66 | preferences[FOOD_ALLERGIES] 67 | } 68 | 69 | suspend fun saveFoodAllergies(value: String) { 70 | context.dataStore.edit { preferences -> 71 | preferences[FOOD_ALLERGIES] = value 72 | } 73 | } 74 | 75 | val getGptModel: Flow = context.dataStore.data.map { preferences -> 76 | preferences[GPT_MODEL] 77 | } 78 | 79 | suspend fun saveGptModel(value: String) { 80 | context.dataStore.edit { preferences -> 81 | preferences[GPT_MODEL] = value 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/Recipe.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "recipes") 7 | data class Recipe( 8 | @PrimaryKey @JvmField val recipe: String, 9 | @JvmField var name: String, 10 | @JvmField var imageUrl: String = "" 11 | ) 12 | 13 | fun getRandomRecipes(recipes: List, count: Int): List { 14 | require(count <= recipes.size) { "Count should not exceed the size of the recipe list." } 15 | 16 | val selectedIndices = mutableSetOf() 17 | val randomRecipes = mutableListOf() 18 | 19 | while (randomRecipes.size < count) { 20 | val availableIndices = (recipes.indices).filter { !selectedIndices.contains(it) } 21 | if (availableIndices.isEmpty()) { 22 | break 23 | } 24 | val randomIndex = availableIndices.random() 25 | selectedIndices.add(randomIndex) 26 | randomRecipes.add(recipes[randomIndex]) 27 | } 28 | 29 | return randomRecipes 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/RecipeDao.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import androidx.room.* 4 | 5 | @Dao 6 | interface RecipeDao { 7 | @Insert(onConflict = OnConflictStrategy.REPLACE) 8 | fun insertRecipe(recipe: Recipe) 9 | 10 | @Update 11 | fun updateRecipe(recipe: Recipe) 12 | 13 | @Query("SELECT * FROM recipes") 14 | fun getRecipes(): List 15 | 16 | @Delete 17 | fun removeRecipe(recipe: Recipe) 18 | 19 | @Query("DELETE FROM recipes") 20 | fun removeAllRecipes() 21 | 22 | // ---- FOLDERS ---- // 23 | 24 | @Insert(onConflict = OnConflictStrategy.REPLACE) 25 | fun insertFolder(folder: RecipeFolder) 26 | 27 | @Update 28 | fun updateFolder(folder: RecipeFolder) 29 | 30 | @Query("SELECT * FROM folders") 31 | fun getFolders(): List 32 | 33 | @Delete 34 | fun removeFolder(folder: RecipeFolder) 35 | 36 | @Query("DELETE FROM folders") 37 | fun removeAllFolders() 38 | 39 | // ---- CHAT ---- // 40 | 41 | @Insert(onConflict = OnConflictStrategy.REPLACE) 42 | fun insertChat(chat: Chat) 43 | 44 | @Query("SELECT * FROM chats") 45 | fun getChats(): List 46 | 47 | @Delete 48 | fun removeChat(chat: Chat) 49 | 50 | @Query("DELETE FROM chats") 51 | fun removeAllChats() 52 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/local/RecipeFolder.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.local 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import androidx.room.TypeConverter 6 | import androidx.room.TypeConverters 7 | import com.google.gson.Gson 8 | import com.google.gson.reflect.TypeToken 9 | 10 | @Entity(tableName = "folders") 11 | @TypeConverters(RecipeFolderTypeConverters::class) 12 | data class RecipeFolder( 13 | @PrimaryKey val name: String, 14 | val recipes: List 15 | ) 16 | 17 | class RecipeFolderTypeConverters { 18 | @TypeConverter 19 | fun fromRecipeList(recipeList: List): String { 20 | return Gson().toJson(recipeList) 21 | } 22 | 23 | @TypeConverter 24 | fun toRecipeList(recipeJson: String): List { 25 | val recipeType = object : TypeToken>() {}.type 26 | return Gson().fromJson(recipeJson, recipeType) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/remote/ApiService.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.remote 2 | 3 | import android.util.Log 4 | import jr.brian.issarecipeapp.model.local.RecipeDao 5 | import jr.brian.issarecipeapp.util.DALL_E_3 6 | import jr.brian.issarecipeapp.util.DEFAULT_IMAGE_SIZE 7 | import jr.brian.issarecipeapp.util.GPT_3_5_TURBO 8 | import jr.brian.issarecipeapp.util.NUM_OF_CHATS_USED_FOR_HISTORY 9 | import jr.brian.issarecipeapp.util.STANDARD_IMAGE_QUALITY 10 | import jr.brian.issarecipeapp.util.TITLE_IS_REQUIRED 11 | import jr.brian.issarecipeapp.util.generateAskQuery 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.withContext 14 | import java.net.SocketTimeoutException 15 | import java.net.UnknownHostException 16 | 17 | interface ApiService { 18 | 19 | object ApiKey { 20 | var userApiKey = "" 21 | } 22 | 23 | companion object { 24 | suspend fun getAskResponse( 25 | dao: RecipeDao? = null, 26 | userPrompt: String, 27 | system: String? = null, 28 | model: String = GPT_3_5_TURBO 29 | ): String { 30 | var aiResponse: String 31 | try { 32 | withContext(Dispatchers.IO) { 33 | val key = ApiKey.userApiKey 34 | val request = ChatBot.ChatCompletionRequest( 35 | model = model, 36 | systemContent = generateAskQuery(system = system) 37 | ) 38 | val bot = CachedChatBot( 39 | apiKey = key, 40 | request = request, 41 | prevChats = dao?.getChats()?.takeLast(NUM_OF_CHATS_USED_FOR_HISTORY) 42 | ?: emptyList() 43 | ) 44 | aiResponse = bot.generateResponse(userPrompt) 45 | } 46 | } catch (e: SocketTimeoutException) { 47 | aiResponse = "Connection timed out. Please try again." 48 | } catch (e: java.lang.IllegalArgumentException) { 49 | aiResponse = "ERROR: ${e.message}" 50 | } catch (e: UnknownHostException) { 51 | aiResponse = "ERROR: ${e.message}.\n\n" + 52 | "This could indicate no/very poor internet connection. " + 53 | "Please check your connection and try again." 54 | } 55 | Log.i("myTag-model", model) 56 | return aiResponse 57 | } 58 | 59 | suspend fun generateImageUrl( 60 | title: String, 61 | ingredients: String? = null, 62 | size: String = DEFAULT_IMAGE_SIZE 63 | ): String { 64 | var imageUrl: String 65 | if (title.isBlank()) { 66 | return TITLE_IS_REQUIRED 67 | } else { 68 | val ingredientsQuery = if (ingredients.isNullOrBlank()) "" 69 | else "Please include $ingredients." 70 | withContext(Dispatchers.IO) { 71 | val openAIImageClient = OpenAIImageClient(apiKey = ApiKey.userApiKey) 72 | val imageGenerationRequest = OpenAIImageClient.ImageGenerationRequest( 73 | model = DALL_E_3, 74 | prompt = "Realistic image of $title. $ingredientsQuery", 75 | size = size, 76 | quality = STANDARD_IMAGE_QUALITY, 77 | n = 1 78 | ) 79 | imageUrl = try { 80 | openAIImageClient.generateImageUrl(imageGenerationRequest) 81 | } catch (e: IllegalArgumentException) { 82 | e.message.toString() 83 | } 84 | } 85 | } 86 | Log.i("myTag-image_url", imageUrl) 87 | return imageUrl 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/remote/CachedChatBot.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.remote 2 | 3 | import jr.brian.issarecipeapp.model.local.Chat 4 | import jr.brian.issarecipeapp.util.DEFAULT_CHAT_ROLE 5 | 6 | /** WRITTEN BY: Collin Barber ~ https://github.com/CJCrafter **/ 7 | 8 | /** 9 | * Caches the initial request to make interactions easier (and String based). 10 | */ 11 | class CachedChatBot( 12 | apiKey: String, 13 | private val request: ChatCompletionRequest, 14 | prevChats: List 15 | ) : ChatBot(apiKey) { 16 | private val previousChats = prevChats 17 | private val chatRole = DEFAULT_CHAT_ROLE 18 | 19 | fun generateResponse( 20 | content: String, 21 | role: String = chatRole 22 | ): String { 23 | request.includeChatHistory() 24 | request.messages.add(ChatMessage(role, content)) 25 | val response = super.generateResponse(request) 26 | val temp = response.choices[0].message 27 | request.messages.add(temp) 28 | return temp.content 29 | } 30 | 31 | private fun ChatCompletionRequest.includeChatHistory() { 32 | previousChats.forEach { 33 | messages.add(ChatMessage(chatRole, it.text)) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/remote/ChatBot.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.remote 2 | 3 | import com.google.gson.GsonBuilder 4 | import com.google.gson.JsonObject 5 | import com.google.gson.JsonParser 6 | import com.google.gson.annotations.SerializedName 7 | import okhttp3.MediaType 8 | import okhttp3.MediaType.Companion.toMediaType 9 | import okhttp3.OkHttpClient 10 | import okhttp3.Request 11 | import okhttp3.RequestBody 12 | import okhttp3.RequestBody.Companion.toRequestBody 13 | import java.lang.IllegalArgumentException 14 | import java.util.concurrent.TimeUnit 15 | 16 | /** WRITTEN BY: Collin Barber ~ https://github.com/CJCrafter **/ 17 | 18 | /** 19 | * The ChatBot class wraps the OpenAI API and lets you send messages and 20 | * receive responses. For more information on how this works, check out 21 | * the [OpenAI Documentation](https://platform.openai.com/docs/api-reference/completions). 22 | * 23 | * @param apiKey Your OpenAI API key that starts with "sk-". 24 | */ 25 | open class ChatBot(private val apiKey: String) { 26 | 27 | private val client = OkHttpClient.Builder() 28 | .connectTimeout(60, TimeUnit.SECONDS) 29 | .readTimeout(60, TimeUnit.SECONDS) 30 | .writeTimeout(60, TimeUnit.SECONDS) 31 | .build() 32 | 33 | private val mediaType: MediaType = "application/json; charset=utf-8".toMediaType() 34 | private val gson = GsonBuilder().create() 35 | 36 | /** 37 | * Blocks the current thread until OpenAI responds to the http request. 38 | * You can access the generated message in [ChatCompletionRequest.messages]. 39 | * 40 | * @param request The data used to control the output. 41 | * @return The returned response. 42 | * @throws IllegalArgumentException If the server returns an error. 43 | */ 44 | fun generateResponse(request: ChatCompletionRequest): ChatCompletionResponse { 45 | val json = gson.toJson(request) 46 | val body: RequestBody = json.toRequestBody(mediaType) 47 | val httpRequest: Request = Request.Builder() 48 | .url("https://api.openai.com/v1/chat/completions") 49 | .addHeader("Content-Type", "application/json") 50 | .addHeader("Authorization", "Bearer $apiKey") 51 | .post(body) 52 | .build() 53 | 54 | // Block the thread and wait for OpenAI's json response 55 | val response = client.newCall(httpRequest).execute() 56 | val jsonResponse = response.body!!.string() 57 | val rootObject = JsonParser.parseString(jsonResponse).asJsonObject 58 | 59 | // Usually happens if you give improper arguments (either an unrecognized argument or bad argument value) 60 | if (rootObject.has("error")) 61 | throw IllegalArgumentException(rootObject["error"].asJsonObject["message"].asString) 62 | 63 | return ChatCompletionResponse(rootObject) 64 | } 65 | 66 | /** 67 | * The ChatGPT API takes a list of 'roles' and 'content'. The role is 68 | * one of 3 options: system, assistant, and user. 'System' is used to 69 | * prompt ChatGPT before the user gives input. 'Assistant' is a message 70 | * from ChatGPT. 'User' is a message from the human. 71 | * 72 | * @param role Who sent the message. 73 | * @param content The raw content of the message. 74 | */ 75 | data class ChatMessage(val role: String, val content: String) { 76 | constructor(json: JsonObject) : this( 77 | json["role"].asString, 78 | json["content"].asString 79 | ) 80 | } 81 | 82 | /** 83 | * These are the arguments that control the result of the output. For more 84 | * information, refer to the [OpenAI Docs](https://platform.openai.com/docs/api-reference/completions/create). 85 | * 86 | * @param model The model used to generate the text. Recommended: "gpt-3.5-turbo." 87 | * @param messages All previous messages from the conversation. 88 | * @param temperature How "creative" the results are. [[0.0, 2.0]]. 89 | * @param topP Controls how "on topic" the tokens are. 90 | * @param n Controls how many responses to generate. Numbers >1 will chew through your tokens. 91 | * @param stream **UNTESTED** recommend keeping this false. 92 | * @param stop The sequence used to stop generating tokens. 93 | * @param maxTokens The maximum number of tokens to use. 94 | * @param presencePenalty Prevent talking about duplicate topics. 95 | * @param frequencyPenalty Prevent repeating the same text. 96 | * @param logitBias Control specific tokens from being used. 97 | * @param user Who send this request (for moderation). 98 | */ 99 | data class ChatCompletionRequest( 100 | val model: String, // recommend: "gpt-3.5-turbo" 101 | val messages: MutableList, 102 | val temperature: Float = 1.0f, 103 | @SerializedName("top_p") val topP: Float = 1.0f, 104 | val n: Int = 1, 105 | val stream: Boolean = false, 106 | val stop: String? = null, 107 | @SerializedName("max_tokens") val maxTokens: Int? = null, // default is 4096 108 | @SerializedName("presence_penalty") val presencePenalty: Float = 0.0f, 109 | @SerializedName("frequency_penalty") val frequencyPenalty: Float = 0.0f, 110 | @SerializedName("logit_bias") val logitBias: JsonObject? = null, 111 | val user: String? = null 112 | ) { 113 | constructor(model: String, systemContent: String) : this( 114 | model, 115 | arrayListOf(ChatMessage("system", systemContent)) 116 | ) 117 | } 118 | 119 | /** 120 | * This is the object returned from the API. You want to access choices[0] 121 | * to get your response. 122 | */ 123 | data class ChatCompletionResponse( 124 | val id: String, 125 | val `object`: String, 126 | val created: Long, 127 | val choices: List, 128 | val usage: ChatCompletionUsage, 129 | ) { 130 | constructor(json: JsonObject) : this( 131 | json["id"].asString, 132 | json["object"].asString, 133 | json["created"].asLong, 134 | json["choices"].asJsonArray.map { ChatCompletionChoice(it.asJsonObject) }, 135 | ChatCompletionUsage(json["usage"].asJsonObject) 136 | ) 137 | } 138 | 139 | /** 140 | * Holds the data for 1 generated text completion. 141 | * 142 | * @param index The index in the array... 0 if n=1. 143 | * @param message The generated text. 144 | * @param finishReason Why did the bot stop generating tokens? 145 | */ 146 | data class ChatCompletionChoice( 147 | val index: Int, 148 | val message: ChatMessage, 149 | val finishReason: String 150 | ) { 151 | constructor(json: JsonObject) : this( 152 | json["index"].asInt, 153 | ChatMessage(json["message"].asJsonObject), 154 | json["finish_reason"].asString 155 | ) 156 | } 157 | 158 | /** 159 | * Holds how many tokens that were used by your API request. Use these 160 | * tokens to calculate how much money you have spent on each request. 161 | * 162 | * @param promptTokens How many tokens the input used. 163 | * @param completionTokens How many tokens the output used. 164 | * @param totalTokens How many tokens in total. 165 | */ 166 | data class ChatCompletionUsage( 167 | val promptTokens: Int, 168 | val completionTokens: Int, 169 | val totalTokens: Int 170 | ) { 171 | constructor(json: JsonObject) : this( 172 | json["prompt_tokens"].asInt, 173 | json["completion_tokens"].asInt, 174 | json["total_tokens"].asInt 175 | ) 176 | } 177 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/remote/Firebase.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.remote 2 | 3 | import com.google.firebase.database.FirebaseDatabase 4 | import jr.brian.issarecipeapp.model.local.Recipe 5 | import com.google.firebase.database.DataSnapshot 6 | import com.google.firebase.database.DatabaseError 7 | import com.google.firebase.database.ValueEventListener 8 | import jr.brian.issarecipeapp.util.getPath 9 | 10 | fun retrieveRecipes( 11 | onSuccess: (List) -> Unit, 12 | onError: (error: DatabaseError) -> Unit 13 | ) { 14 | val database = FirebaseDatabase.getInstance() 15 | val recipesRef = database.getReference(getPath()) 16 | 17 | recipesRef.addListenerForSingleValueEvent(object : ValueEventListener { 18 | override fun onDataChange(dataSnapshot: DataSnapshot) { 19 | val recipeList = mutableListOf() 20 | 21 | for (childSnapshot in dataSnapshot.children) { 22 | val recipeName = childSnapshot 23 | .child("name") 24 | .getValue(String::class.java) 25 | 26 | val recipeContent = childSnapshot 27 | .child("recipe") 28 | .getValue(String::class.java) 29 | 30 | if (recipeName != null && recipeContent != null) { 31 | val recipe = Recipe(name = recipeName, recipe = recipeContent) 32 | recipeList.add(recipe) 33 | } 34 | } 35 | 36 | onSuccess(recipeList) 37 | } 38 | 39 | override fun onCancelled(error: DatabaseError) { 40 | onError(error) 41 | } 42 | }) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/remote/OpenAIImage.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.remote 2 | 3 | import com.google.gson.JsonParser 4 | import okhttp3.MediaType 5 | import okhttp3.MediaType.Companion.toMediaType 6 | import okhttp3.OkHttpClient 7 | import okhttp3.Request 8 | import okhttp3.RequestBody 9 | import okhttp3.RequestBody.Companion.toRequestBody 10 | import java.lang.IllegalArgumentException 11 | import java.util.concurrent.TimeUnit 12 | 13 | class OpenAIImageClient(private val apiKey: String) { 14 | private val client = OkHttpClient.Builder() 15 | .connectTimeout(60, TimeUnit.SECONDS) 16 | .readTimeout(60, TimeUnit.SECONDS) 17 | .writeTimeout(60, TimeUnit.SECONDS) 18 | .build() 19 | 20 | private val mediaType: MediaType = "application/json; charset=utf-8".toMediaType() 21 | 22 | data class ImageGenerationRequest( 23 | val model: String, 24 | val prompt: String, 25 | val size: String, 26 | val quality: String, 27 | val n: Int 28 | ) 29 | 30 | private fun extractImageUrl(jsonResponse: String): String { 31 | val rootObject = JsonParser.parseString(jsonResponse).asJsonObject 32 | if (rootObject.has("data") 33 | && rootObject.getAsJsonArray("data").size() > 0 34 | ) { 35 | val dataArray = rootObject.getAsJsonArray("data") 36 | val firstDataObject = dataArray[0].asJsonObject 37 | if (firstDataObject.has("url")) { 38 | return firstDataObject.getAsJsonPrimitive("url").asString 39 | } 40 | } 41 | return "Empty Image Url" 42 | } 43 | 44 | fun generateImageUrl(request: ImageGenerationRequest): String { 45 | val json = """{ 46 | "model": "${request.model}", 47 | "prompt": "${request.prompt}", 48 | "size": "${request.size}", 49 | "quality": "${request.quality}", 50 | "n": ${request.n} 51 | }""" 52 | 53 | val body: RequestBody = json.toRequestBody(mediaType) 54 | val httpRequest: Request = Request.Builder() 55 | .url("https://api.openai.com/v1/images/generations") 56 | .addHeader("Content-Type", "application/json") 57 | .addHeader("Authorization", "Bearer $apiKey") 58 | .post(body) 59 | .build() 60 | 61 | val response = client.newCall(httpRequest).execute() 62 | val jsonResponse = response.body!!.string() 63 | val rootObject = JsonParser.parseString(jsonResponse).asJsonObject 64 | 65 | if (response.isSuccessful) { 66 | return extractImageUrl(rootObject.toString()) 67 | } else { 68 | throw IllegalArgumentException(rootObject["error"].asJsonObject["message"].asString) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/repository/RepoImpl.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.repository 2 | 3 | import jr.brian.issarecipeapp.model.local.RecipeDao 4 | import jr.brian.issarecipeapp.model.remote.ApiService 5 | 6 | class RepoImpl : Repository { 7 | companion object { 8 | private val apiService = ApiService 9 | } 10 | 11 | override suspend fun getAskResponse( 12 | dao: RecipeDao?, 13 | userPrompt: String, 14 | system: String?, 15 | model: String 16 | ): String { 17 | return apiService.getAskResponse( 18 | userPrompt = userPrompt, 19 | system = system, 20 | model = model, 21 | dao = dao 22 | ) 23 | } 24 | 25 | override suspend fun generateImageUrl( 26 | title: String, 27 | ingredients: String?, 28 | size: String 29 | ): String { 30 | return apiService.generateImageUrl( 31 | title = title, 32 | ingredients = ingredients, 33 | size = size 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/model/repository/Repository.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.model.repository 2 | 3 | import jr.brian.issarecipeapp.model.local.RecipeDao 4 | import jr.brian.issarecipeapp.util.DEFAULT_IMAGE_SIZE 5 | 6 | interface Repository { 7 | suspend fun getAskResponse( 8 | dao: RecipeDao? = null, 9 | userPrompt: String, 10 | system: String? = null, 11 | model: String 12 | ): String 13 | 14 | suspend fun generateImageUrl( 15 | title: String, 16 | ingredients: String? = null, 17 | size: String = DEFAULT_IMAGE_SIZE 18 | ): String 19 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/util/constants.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.util 2 | 3 | const val GPT_3_5_TURBO = "gpt-3.5-turbo" 4 | const val GPT_4 = "gpt-4" 5 | 6 | const val DALL_E_3 = "dall-e-3" 7 | const val DEFAULT_IMAGE_SIZE = "1024x1024" 8 | const val STANDARD_IMAGE_QUALITY = "standard" 9 | 10 | const val GENERATE_API_KEY_URL = "https://platform.openai.com/account/api-keys" 11 | const val API_USAGE_URL = "https://platform.openai.com/usage" 12 | 13 | const val DEFAULT_RECIPE_TITLE = "Food" 14 | 15 | const val DEFAULT_CHAT_ROLE = "user" 16 | 17 | // Routes 18 | const val HOME_ROUTE = "home" 19 | const val ASK_ROUTE = "ask" 20 | const val ASK_CONTEXT_ROUTE = "ask-context" 21 | const val MEAL_DETAILS_ROUTE = "meal-details" 22 | const val FAV_RECIPES_ROUTE = "fav-recipes" 23 | const val SWIPE_RECIPES_ROUTE = "swipe-recipes" 24 | const val SETTINGS_ROUTE = "settings" 25 | // End Routes 26 | 27 | // Max Values 28 | const val MAX_CARDS_IN_STACK = 7 29 | const val PARTY_SIZE_MAX_CHAR_COUNT = 4 30 | const val RECIPE_NAME_MAX_CHAR_COUNT = 40 31 | const val NUM_OF_CHATS_USED_FOR_HISTORY = 10 32 | // End Max Values 33 | 34 | // Meal Hours 35 | const val breakfastStartHour = 5 // 5 AM 36 | const val breakfastEndHour = 10 // 10 AM 37 | const val lunchStartHour = 11 // 11 AM 38 | const val lunchEndHour = 16 // 4 PM 39 | // End Meal Hours 40 | 41 | // Emojis 42 | const val UP_SIDE_DOWN_FACE_EMOJI = "\uD83D\uDE43" 43 | const val SAD_FACE_EMOJI = "\uD83D\uDE14" 44 | const val COOL_FACE_EMOJI = "\uD83D\uDE0E" 45 | // End Emojis 46 | 47 | // Labels 48 | const val API_KEY_LABEL = "OpenAI API Key" 49 | const val PARTY_SIZE_LABEL = "Party Size" 50 | const val DIETARY_RESTRICTIONS_LABEL = "Dietary Restrictions" 51 | const val FOOD_ALLERGY_LABEL = "Food Allergies" 52 | const val GPT_LABEL = "GPT Model" 53 | const val INGREDIENTS_LABEL = "Ingredients *" 54 | const val REJECTED_RECIPES_DIALOG_LABEL = "Rejected Recipes $SAD_FACE_EMOJI" 55 | const val NO_REJECTED_RECIPES_DIALOG_LABEL = "No Rejected Recipes $COOL_FACE_EMOJI" 56 | const val CHEF_GPT_LABEL = "ChefGPT" 57 | const val USER_LABEL = "Me" 58 | const val SWIPE_SCREEN_LABEL = "Love at First Swipe" 59 | // End Labels 60 | 61 | // Error Messages 62 | const val ERROR = "error" 63 | const val API_KEY_REQUIRED = "API Key is required" 64 | const val TITLE_IS_REQUIRED = "Title is required." 65 | const val NO_RESPONSE_MSG = "No response. Please try again." 66 | const val CONNECTION_TIMEOUT_MSG = "Connection timed out. Please try again." 67 | const val NO_RECIPES_TO_SWIPE_MSG = "No recipes to swipe at this time. Please check again later." 68 | const val ERROR_IMAGE_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/" + 69 | "No-Image-Placeholder.svg/1665px-No-Image-Placeholder.svg.png" 70 | // End Error Messages -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/util/extensions.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.util 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import java.net.URL 6 | 7 | fun String.ifBlankUse(value: String): String { 8 | if (isBlank()) { 9 | return value 10 | } 11 | return this 12 | } 13 | 14 | fun String.isUrl(): Boolean { 15 | return try { 16 | URL(this) 17 | true 18 | } catch (e: Exception) { 19 | false 20 | } 21 | } 22 | 23 | fun Context.showToast(text: String, isLongToast: Boolean = false) { 24 | val duration = if (isLongToast) Toast.LENGTH_LONG else Toast.LENGTH_SHORT 25 | Toast.makeText(this, text, duration).show() 26 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/util/util.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.speech.RecognizerIntent 6 | import android.speech.SpeechRecognizer 7 | import android.widget.Toast 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.foundation.text.selection.TextSelectionColors 16 | import androidx.compose.material.Icon 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.text.style.TextAlign 26 | import androidx.compose.ui.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import jr.brian.issarecipeapp.R 29 | import jr.brian.issarecipeapp.model.local.Recipe 30 | import jr.brian.issarecipeapp.model.local.RecipeDao 31 | import jr.brian.issarecipeapp.view.ui.components.RejectedRecipeContentDialog 32 | import jr.brian.issarecipeapp.view.ui.components.RejectedRecipeHistoryDialog 33 | import jr.brian.issarecipeapp.view.ui.pages.RejectedRecipeCache 34 | import jr.brian.issarecipeapp.view.ui.theme.BlueIsh 35 | import java.time.format.DateTimeFormatter 36 | import java.util.Calendar 37 | import java.util.Locale 38 | 39 | val occasionOptions = 40 | listOf( 41 | "breakfast", 42 | "brunch", 43 | "lunch", 44 | "snack", 45 | "dinner", 46 | "dessert", 47 | "any occasion" 48 | ) 49 | 50 | val dietaryOptions = listOf( 51 | "lactose intolerance", 52 | "gluten intolerance", 53 | "vegetarian", 54 | "vegan", 55 | "kosher", 56 | "none" 57 | ) 58 | 59 | val allergyOptions = listOf( 60 | "dairy", 61 | "peanuts", 62 | "fish", 63 | "soy", 64 | "sesame", 65 | "none" 66 | ) 67 | 68 | val modelOptions = listOf( 69 | GPT_3_5_TURBO, 70 | GPT_4 71 | ) 72 | 73 | private val infoExamples = 74 | listOf( 75 | "Include a 3 day meal plan.", 76 | "Target 50 grams of carbs.", 77 | "Target 2000 calories.", 78 | "Organic ingredients only.", 79 | "List stores to visit", 80 | "List healthy alternatives.", 81 | "Respond in Spanish.", 82 | "Include only pizza recipes" 83 | ) 84 | 85 | val randomInfo = infoExamples.random() 86 | val randomMealOccasion = occasionOptions.random() 87 | 88 | val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("h:mm a") 89 | val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("MM.dd.yy") 90 | 91 | fun generateAskQuery( 92 | system: String? = null 93 | ) = "You are a 5 star chef. " + 94 | if (system.isNullOrBlank()) "" else "$system " + 95 | "\nLastly, only respond to questions that are about " + 96 | "preparing food, " + 97 | "cooking food, " + 98 | "providing recipes, " + 99 | "providing culinary advice, " + 100 | "or anything that generally has to do with any aspect of your job." + 101 | "Politely decline anything outside of that list." 102 | 103 | fun generateRecipeQuery( 104 | occasion: String, 105 | partySize: String, 106 | dietaryRestrictions: String, 107 | foodAllergies: String, 108 | ingredients: String, 109 | additionalInfo: String, 110 | ) = "Generate a recipe for $occasion that serves $partySize " + 111 | "using the following ingredients: $ingredients. " + 112 | "Keep in mind the following " + 113 | "dietary restrictions: $dietaryRestrictions. " + 114 | "Also note that I am allergic to $foodAllergies. " + 115 | "Please include the estimated calories, fat, carbs, protein " + 116 | "and preparation / cook time. " + 117 | "Also, title the recipe and surround it in '✨' for easy extraction. " + 118 | if (additionalInfo.isNotBlank()) 119 | "Lastly, here is some additional info for this recipe:" + 120 | " $additionalInfo. Thanks!" 121 | else "" 122 | 123 | val customTextSelectionColors = TextSelectionColors( 124 | handleColor = BlueIsh, 125 | backgroundColor = BlueIsh 126 | ) 127 | 128 | fun isWithinTimeRange(startHour: Int, endHour: Int): Boolean { 129 | val calendar = Calendar.getInstance() 130 | val currentHour = calendar.get(Calendar.HOUR_OF_DAY) 131 | return currentHour in startHour..endHour 132 | } 133 | 134 | val isBreakfastTime = isWithinTimeRange(breakfastStartHour, breakfastEndHour) 135 | val isLunchTime = isWithinTimeRange(lunchStartHour, lunchEndHour) 136 | 137 | fun getPath(): String { 138 | return if (isBreakfastTime) { 139 | "breakfast" 140 | } else if (isLunchTime) { 141 | "lunch" 142 | } else { 143 | "dinner" 144 | } 145 | } 146 | 147 | @Composable 148 | fun SwipeHeaderLabel(dao: RecipeDao) { 149 | val isHistoryDialogShowing = remember { 150 | mutableStateOf(false) 151 | } 152 | 153 | val isContentDialogShowing = remember { 154 | mutableStateOf(false) 155 | } 156 | 157 | val selectedRecipe = remember { 158 | mutableStateOf(Recipe("", "")) 159 | } 160 | 161 | RejectedRecipeHistoryDialog( 162 | isShowing = isHistoryDialogShowing, 163 | recipes = RejectedRecipeCache.cache.distinct(), 164 | onSelectItem = { 165 | isContentDialogShowing.value = true 166 | selectedRecipe.value = it 167 | } 168 | ) 169 | 170 | RejectedRecipeContentDialog( 171 | dao = dao, 172 | recipe = selectedRecipe.value, 173 | isShowing = isContentDialogShowing 174 | ) 175 | 176 | Row( 177 | Modifier.fillMaxWidth(), 178 | horizontalArrangement = Arrangement.Center, 179 | verticalAlignment = Alignment.CenterVertically 180 | ) { 181 | Text( 182 | SWIPE_SCREEN_LABEL, 183 | color = BlueIsh, 184 | textAlign = TextAlign.Center, 185 | fontSize = 20.sp, 186 | fontWeight = FontWeight.Bold, 187 | modifier = Modifier.padding(start = 20.dp) 188 | ) 189 | Spacer(modifier = Modifier.weight(1f)) 190 | Row( 191 | modifier = Modifier.clickable { 192 | isHistoryDialogShowing.value = !isHistoryDialogShowing.value 193 | }, 194 | horizontalArrangement = Arrangement.Center, 195 | verticalAlignment = Alignment.CenterVertically 196 | ) { 197 | Text( 198 | text = "7", color = BlueIsh, 199 | textAlign = TextAlign.Center, 200 | fontSize = 20.sp, 201 | ) 202 | Spacer(modifier = Modifier.width(5.dp)) 203 | Icon( 204 | painter = painterResource(id = R.drawable.baseline_history_24), 205 | contentDescription = "history", 206 | tint = BlueIsh, 207 | modifier = Modifier 208 | .padding(end = 20.dp) 209 | 210 | ) 211 | } 212 | } 213 | } 214 | 215 | fun getSpeechInputIntent(context: Context): Intent? { 216 | if (!SpeechRecognizer.isRecognitionAvailable(context)) { 217 | Toast.makeText(context, "Speech not available", Toast.LENGTH_SHORT).show() 218 | } else { 219 | val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) 220 | intent.putExtra( 221 | RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH 222 | ) 223 | intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault()) 224 | intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak Now") 225 | return intent 226 | } 227 | return null 228 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/ChatComponents.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.LocalIndication 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.combinedClickable 9 | import androidx.compose.foundation.indication 10 | import androidx.compose.foundation.interaction.MutableInteractionSource 11 | import androidx.compose.foundation.layout.Arrangement 12 | import androidx.compose.foundation.layout.Box 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.Row 15 | import androidx.compose.foundation.layout.Spacer 16 | import androidx.compose.foundation.layout.height 17 | import androidx.compose.foundation.layout.padding 18 | import androidx.compose.foundation.layout.size 19 | import androidx.compose.foundation.layout.width 20 | import androidx.compose.foundation.lazy.LazyColumn 21 | import androidx.compose.foundation.lazy.LazyListState 22 | import androidx.compose.foundation.shape.RoundedCornerShape 23 | import androidx.compose.foundation.text.selection.SelectionContainer 24 | import androidx.compose.material.CircularProgressIndicator 25 | import androidx.compose.material.OutlinedTextField 26 | import androidx.compose.material.TextFieldDefaults 27 | import androidx.compose.material3.Icon 28 | import androidx.compose.material3.MaterialTheme 29 | import androidx.compose.material3.Text 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.runtime.CompositionLocalProvider 32 | import androidx.compose.runtime.collectAsState 33 | import androidx.compose.runtime.derivedStateOf 34 | import androidx.compose.runtime.mutableStateOf 35 | import androidx.compose.runtime.remember 36 | import androidx.compose.ui.Alignment 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.draw.clip 39 | import androidx.compose.ui.graphics.Color 40 | import androidx.compose.ui.platform.LocalFocusManager 41 | import androidx.compose.ui.res.painterResource 42 | import androidx.compose.ui.text.TextStyle 43 | import androidx.compose.ui.text.font.FontWeight 44 | import androidx.compose.ui.unit.dp 45 | import androidx.compose.ui.unit.sp 46 | import jr.brian.issarecipeapp.R 47 | import jr.brian.issarecipeapp.model.local.Chat 48 | import jr.brian.issarecipeapp.util.CHEF_GPT_LABEL 49 | import jr.brian.issarecipeapp.view.ui.theme.BlueIsh 50 | import jr.brian.issarecipeapp.viewmodel.MainViewModel 51 | 52 | @Composable 53 | fun ChatHeader( 54 | isChatGptTyping: Boolean, 55 | modifier: Modifier = Modifier, 56 | headerTextModifier: Modifier = Modifier, 57 | onResetAllChats: () -> Unit, 58 | onNavToAskContext: () -> Unit, 59 | ) { 60 | val isDeleteDialogShowing = remember { mutableStateOf(false) } 61 | 62 | DeleteDialog( 63 | title = "Reset this Conversation?", 64 | isShowing = isDeleteDialogShowing, 65 | ) { 66 | onResetAllChats() 67 | } 68 | 69 | Row( 70 | verticalAlignment = Alignment.CenterVertically, 71 | horizontalArrangement = Arrangement.Center, 72 | modifier = modifier 73 | ) { 74 | if (isChatGptTyping) { 75 | Row( 76 | verticalAlignment = Alignment.CenterVertically, 77 | horizontalArrangement = Arrangement.Center 78 | ) { 79 | Spacer(modifier = Modifier.weight(.1f)) 80 | Text( 81 | "ChefGPT is typing", 82 | style = TextStyle( 83 | fontWeight = FontWeight.Bold, 84 | fontSize = 18.sp 85 | ) 86 | ) 87 | // LottieLoading( 88 | // isShowing = isChatGptTyping, 89 | // modifier = Modifier.size(40.dp) 90 | // ) 91 | Spacer(modifier = Modifier.weight(.1f)) 92 | } 93 | } 94 | else { 95 | Row( 96 | verticalAlignment = Alignment.CenterVertically, 97 | horizontalArrangement = Arrangement.SpaceEvenly, 98 | modifier = Modifier.padding(top = 15.dp, bottom = 15.dp) 99 | ) { 100 | Spacer(modifier = Modifier.width(15.dp)) 101 | Text( 102 | "Ask", 103 | color = BlueIsh, 104 | style = TextStyle( 105 | fontWeight = FontWeight.Bold, 106 | fontSize = 20.sp 107 | ), 108 | modifier = headerTextModifier 109 | ) 110 | Spacer(modifier = Modifier.weight(.1f)) 111 | Icon( 112 | painter = painterResource(id = R.drawable.baseline_more_24), 113 | contentDescription = "More", 114 | tint = BlueIsh, 115 | modifier = Modifier 116 | .size(25.dp) 117 | .clickable { 118 | onNavToAskContext() 119 | } 120 | ) 121 | Spacer(modifier = Modifier.width(15.dp)) 122 | Icon( 123 | painter = painterResource(id = R.drawable.baseline_delete_forever_24), 124 | contentDescription = "Reset Conversation", 125 | tint = BlueIsh, 126 | modifier = Modifier 127 | .size(25.dp) 128 | .clickable { 129 | isDeleteDialogShowing.value = !isDeleteDialogShowing.value 130 | } 131 | ) 132 | Spacer(modifier = Modifier.width(15.dp)) 133 | } 134 | } 135 | } 136 | } 137 | 138 | @OptIn(ExperimentalFoundationApi::class) 139 | @Composable 140 | fun ChatSection( 141 | chats: List, 142 | listState: LazyListState, 143 | viewModel: MainViewModel, 144 | modifier: Modifier = Modifier, 145 | onDeleteChat: (chat: Chat) -> Unit 146 | ) { 147 | val isChatGptTyping = viewModel.loading.collectAsState() 148 | val interactionSource = remember { MutableInteractionSource() } 149 | 150 | if (chats.isEmpty()) { 151 | Column( 152 | verticalArrangement = Arrangement.Center, 153 | horizontalAlignment = Alignment.CenterHorizontally, 154 | modifier = Modifier.height(50.dp) 155 | ) { 156 | Text( 157 | "No Chats Recorded", 158 | style = TextStyle(fontSize = 20.sp) 159 | ) 160 | } 161 | } 162 | 163 | LazyColumn(modifier = modifier, state = listState) { 164 | items(chats.size) { index -> 165 | val chat = chats[index] 166 | val isHumanChatBox = chat.senderLabel != CHEF_GPT_LABEL 167 | val isDeleteDialogShowing = remember { mutableStateOf(false) } 168 | 169 | val isShowingLoadingBar = remember { 170 | derivedStateOf { 171 | (isChatGptTyping.value && index == chats.size - 1) 172 | } 173 | } 174 | 175 | DeleteDialog( 176 | title = "Delete this Chat?", 177 | isShowing = isDeleteDialogShowing, 178 | ) { 179 | onDeleteChat(chat) 180 | } 181 | 182 | ChatBox( 183 | text = chat.text, 184 | dateSent = chat.dateSent, 185 | timeSent = chat.timeSent, 186 | senderLabel = chat.senderLabel, 187 | isHumanChatBox = isHumanChatBox, 188 | isChefGptTyping = isShowingLoadingBar.value, 189 | modifier = Modifier 190 | .padding(10.dp) 191 | .indication(interactionSource, LocalIndication.current) 192 | .animateItemPlacement(), 193 | onDeleteChat = { 194 | isDeleteDialogShowing.value = true 195 | } 196 | ) 197 | } 198 | } 199 | } 200 | 201 | @OptIn(ExperimentalFoundationApi::class) 202 | @Composable 203 | private fun ChatBox( 204 | text: String, 205 | dateSent: String, 206 | timeSent: String, 207 | senderLabel: String, 208 | isHumanChatBox: Boolean, 209 | isChefGptTyping: Boolean, 210 | modifier: Modifier = Modifier, 211 | onDeleteChat: () -> Unit 212 | ) { 213 | val focusManager = LocalFocusManager.current 214 | val isChatInfoShowing = remember { mutableStateOf(false) } 215 | 216 | Row( 217 | verticalAlignment = Alignment.CenterVertically, 218 | modifier = modifier 219 | ) { 220 | Column( 221 | modifier = Modifier.weight(.8f), 222 | horizontalAlignment = if (isHumanChatBox) Alignment.End else Alignment.Start 223 | ) { 224 | AnimatedVisibility(visible = isChatInfoShowing.value) { 225 | Column( 226 | horizontalAlignment = if (isHumanChatBox) Alignment.End else Alignment.Start 227 | ) { 228 | Row( 229 | modifier = Modifier.padding( 230 | start = if (isHumanChatBox) 0.dp else 10.dp, 231 | end = if (isHumanChatBox) 10.dp else 0.dp 232 | ), 233 | verticalAlignment = Alignment.CenterVertically 234 | ) { 235 | Text( 236 | senderLabel, 237 | modifier = Modifier 238 | ) 239 | Spacer(Modifier.width(5.dp)) 240 | Text("•") 241 | Spacer(Modifier.width(5.dp)) 242 | Text(dateSent) 243 | Spacer(Modifier.width(5.dp)) 244 | Text("•") 245 | Spacer(Modifier.width(5.dp)) 246 | Text(timeSent) 247 | } 248 | Row( 249 | modifier = Modifier.padding( 250 | start = if (isHumanChatBox) 0.dp else 10.dp, 251 | end = if (isHumanChatBox) 10.dp else 0.dp 252 | ), 253 | verticalAlignment = Alignment.CenterVertically, 254 | ) { 255 | Text( 256 | text = "Delete", 257 | modifier = Modifier.clickable { 258 | onDeleteChat() 259 | isChatInfoShowing.value = false 260 | } 261 | ) 262 | } 263 | } 264 | } 265 | Row(verticalAlignment = Alignment.CenterVertically) { 266 | if (isChefGptTyping) { 267 | CircularProgressIndicator( 268 | color = BlueIsh, 269 | modifier = Modifier.size(30.dp) 270 | ) 271 | } 272 | Box( 273 | modifier = Modifier 274 | .padding(10.dp) 275 | .clip(RoundedCornerShape(10.dp)) 276 | .background(if (isHumanChatBox) BlueIsh else Color.Gray) 277 | .combinedClickable( 278 | onClick = { 279 | focusManager.clearFocus() 280 | isChatInfoShowing.value = !isChatInfoShowing.value 281 | } 282 | ) 283 | ) { 284 | CompositionLocalProvider { 285 | SelectionContainer { 286 | Text( 287 | text, 288 | color = Color.White, 289 | modifier = Modifier.padding(15.dp) 290 | ) 291 | } 292 | } 293 | } 294 | } 295 | } 296 | } 297 | } 298 | 299 | @Composable 300 | fun ChatTextFieldRow( 301 | promptText: String, 302 | textFieldOnValueChange: (String) -> Unit, 303 | modifier: Modifier = Modifier, 304 | sendIconModifier: Modifier = Modifier, 305 | micIconModifier: Modifier = Modifier 306 | ) { 307 | OutlinedTextField( 308 | modifier = modifier, 309 | value = promptText, 310 | onValueChange = textFieldOnValueChange, 311 | label = { 312 | Text( 313 | text = "Enter a prompt", 314 | color = BlueIsh, 315 | style = TextStyle( 316 | fontWeight = FontWeight.Bold 317 | ) 318 | ) 319 | }, 320 | colors = TextFieldDefaults.textFieldColors( 321 | textColor = BlueIsh, 322 | focusedIndicatorColor = MaterialTheme.colorScheme.primary, 323 | unfocusedIndicatorColor = BlueIsh 324 | ), 325 | leadingIcon = { 326 | Icon( 327 | painter = painterResource(id = R.drawable.baseline_mic_24), 328 | tint = BlueIsh, 329 | contentDescription = "Mic", 330 | modifier = micIconModifier 331 | ) 332 | }, 333 | trailingIcon = { 334 | Icon( 335 | painter = painterResource(id = R.drawable.baseline_send_24), 336 | tint = BlueIsh, 337 | contentDescription = "Send Message", 338 | modifier = sendIconModifier 339 | ) 340 | } 341 | ) 342 | 343 | Spacer(Modifier.height(15.dp)) 344 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/LottieComponents.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components 2 | 3 | import androidx.annotation.RawRes 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.runtime.State 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import com.airbnb.lottie.compose.LottieAnimation 12 | import com.airbnb.lottie.compose.LottieCompositionSpec 13 | import com.airbnb.lottie.compose.LottieConstants 14 | import com.airbnb.lottie.compose.animateLottieCompositionAsState 15 | import com.airbnb.lottie.compose.rememberLottieComposition 16 | import jr.brian.issarecipeapp.R 17 | 18 | @Composable 19 | private fun Lottie( 20 | @RawRes lottieRes: Int, 21 | isShowing: MutableState, 22 | modifier: Modifier = Modifier 23 | ) { 24 | val isPlaying by remember { mutableStateOf(isShowing.value) } 25 | val speed by remember { mutableStateOf(1f) } 26 | val composition by rememberLottieComposition( 27 | LottieCompositionSpec.RawRes(lottieRes) 28 | ) 29 | val progress by animateLottieCompositionAsState( 30 | composition, 31 | iterations = LottieConstants.IterateForever, 32 | isPlaying = isPlaying, 33 | speed = speed, 34 | restartOnPlay = false 35 | ) 36 | LottieAnimation( 37 | composition, 38 | { progress }, 39 | modifier = modifier 40 | ) 41 | } 42 | 43 | @Composable 44 | private fun Lottie( 45 | @RawRes lottieRes: Int, 46 | isShowing: State, 47 | modifier: Modifier = Modifier 48 | ) { 49 | val isPlaying by remember { mutableStateOf(isShowing.value) } 50 | val speed by remember { mutableStateOf(1f) } 51 | val composition by rememberLottieComposition( 52 | LottieCompositionSpec.RawRes(lottieRes) 53 | ) 54 | val progress by animateLottieCompositionAsState( 55 | composition, 56 | iterations = LottieConstants.IterateForever, 57 | isPlaying = isPlaying, 58 | speed = speed, 59 | restartOnPlay = false 60 | ) 61 | LottieAnimation( 62 | composition, 63 | { progress }, 64 | modifier = modifier 65 | ) 66 | } 67 | 68 | @Suppress("Unused") 69 | @Composable 70 | fun LottieLoading( 71 | isShowing: MutableState, 72 | modifier: Modifier = Modifier 73 | ) { 74 | Lottie( 75 | lottieRes = R.raw.loadingpink, 76 | isShowing = isShowing, 77 | modifier = modifier 78 | ) 79 | } 80 | 81 | @Suppress("Unused") 82 | @Composable 83 | fun LottieLoading( 84 | isShowing: State, 85 | modifier: Modifier = Modifier 86 | ) { 87 | Lottie( 88 | lottieRes = R.raw.loadingpink, 89 | isShowing = isShowing, 90 | modifier = modifier 91 | ) 92 | } 93 | 94 | @Suppress("Unused") 95 | @Composable 96 | fun LottieFoodBowl( 97 | isShowing: MutableState, 98 | modifier: Modifier = Modifier 99 | ) { 100 | Lottie( 101 | lottieRes = R.raw.foodbowl, 102 | isShowing = isShowing, 103 | modifier = modifier 104 | ) 105 | } 106 | 107 | @Suppress("Unused") 108 | @Composable 109 | fun LottieFoodBowl( 110 | isShowing: State, 111 | modifier: Modifier = Modifier 112 | ) { 113 | Lottie( 114 | lottieRes = R.raw.foodbowl, 115 | isShowing = isShowing, 116 | modifier = modifier 117 | ) 118 | } 119 | 120 | @Composable 121 | fun LottieRecipe( 122 | isShowing: MutableState, 123 | modifier: Modifier = Modifier 124 | ) { 125 | Lottie( 126 | lottieRes = R.raw.recipe, 127 | isShowing = isShowing, 128 | modifier = modifier 129 | ) 130 | } 131 | 132 | @Suppress("Unused") 133 | @Composable 134 | fun LottieRecipe( 135 | isShowing: State, 136 | modifier: Modifier = Modifier 137 | ) { 138 | Lottie( 139 | lottieRes = R.raw.recipe, 140 | isShowing = isShowing, 141 | modifier = modifier 142 | ) 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/TextFieldComponents.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.text.KeyboardActions 5 | import androidx.compose.foundation.text.KeyboardOptions 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.OutlinedTextField 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextFieldDefaults 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.text.input.ImeAction 20 | import androidx.compose.ui.unit.dp 21 | import jr.brian.issarecipeapp.view.ui.theme.BlueIsh 22 | 23 | @OptIn(ExperimentalMaterial3Api::class) 24 | @Composable 25 | fun DefaultTextField( 26 | label: String, 27 | value: String, 28 | modifier: Modifier = Modifier, 29 | maxCount: Int = Int.MAX_VALUE, 30 | readOnly: Boolean = false, 31 | isError: MutableState = mutableStateOf(false), 32 | onValueChange: ((str: String) -> Unit)?, 33 | onDone: (() -> Unit)? = null, 34 | trailingIcon: @Composable (() -> Unit)? = null 35 | ) { 36 | val showErrorColor = remember { 37 | mutableStateOf(false) 38 | } 39 | OutlinedTextField( 40 | modifier = modifier.padding( 41 | start = 15.dp, 42 | end = 15.dp, 43 | bottom = 15.dp 44 | ), 45 | value = value, 46 | isError = isError.value, 47 | readOnly = readOnly, 48 | onValueChange = { str -> 49 | if (str.length <= maxCount) { 50 | if (str.isNotBlank()) { 51 | showErrorColor.value = false 52 | } else if (str.toIntOrNull() != null) { 53 | showErrorColor.value = false 54 | } 55 | } 56 | onValueChange?.invoke(str) 57 | }, 58 | label = { 59 | Text( 60 | text = label, 61 | style = TextStyle( 62 | color = BlueIsh, 63 | fontWeight = FontWeight.Bold 64 | ) 65 | ) 66 | }, 67 | colors = TextFieldDefaults.textFieldColors( 68 | focusedIndicatorColor = if (showErrorColor.value) Color.Red else BlueIsh, 69 | unfocusedIndicatorColor = if (showErrorColor.value) Color.Red 70 | else MaterialTheme.colorScheme.background 71 | ), 72 | trailingIcon = trailingIcon, 73 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), 74 | keyboardActions = KeyboardActions(onDone = { 75 | onDone?.invoke() 76 | }), 77 | ) 78 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/InfiniteList.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.runtime.mutableStateListOf 4 | import androidx.compose.runtime.snapshots.SnapshotStateList 5 | 6 | 7 | // When calling onItemsExhausted, the pointer will reset to 0 no matter what. 8 | // Modify this list for fresh items. Or do nothing if you want it to 9 | // just cycle back to the first item. 10 | class InfiniteList( 11 | itemsInput: List = listOf(), 12 | private val onItemsExhausted: (SnapshotStateList) -> Unit = {} 13 | ){ 14 | private var currentIndex = 0 15 | private var items = mutableStateListOf() 16 | 17 | init{ items.addAll(itemsInput) } 18 | 19 | val current get() = items.getOrNull(currentIndex) 20 | val next get() = items.getOrNull(currentIndex+1) 21 | val size get() = items.size 22 | 23 | fun moveToNext(): Int { 24 | if (currentIndex >= items.size - 1) { 25 | onItemsExhausted(items) 26 | currentIndex = 0 27 | } else { currentIndex++ } 28 | return currentIndex 29 | } 30 | 31 | fun addAll(elements: Collection) { items.addAll(elements) } 32 | 33 | operator fun get(ix: Int) = items.getOrNull(ix) 34 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/Modifier.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.foundation.gestures.detectDragGestures 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.rememberCoroutineScope 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.composed 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.graphics.TransformOrigin 10 | import androidx.compose.ui.graphics.graphicsLayer 11 | import androidx.compose.ui.input.pointer.consumeAllChanges 12 | import androidx.compose.ui.input.pointer.pointerInput 13 | import androidx.compose.ui.layout.boundsInParent 14 | import androidx.compose.ui.layout.onGloballyPositioned 15 | 16 | fun Modifier.swipeable(state: SwipeableState) = composed { with(state) { 17 | 18 | val coroutineScope = rememberCoroutineScope() 19 | 20 | if (!isAnimationRunning) { 21 | 22 | LaunchedEffect(alpha) { animatedAlpha.snapTo(alpha) } 23 | 24 | LaunchedEffect(scale) { animatedScale.snapTo(scale) } 25 | 26 | LaunchedEffect(offset) { animatedShift.snapTo(shift) } 27 | 28 | LaunchedEffect(offset) { animatedRotation.snapTo(rotation) } 29 | 30 | } 31 | 32 | onGloballyPositioned { 33 | rect = it.boundsInParent() 34 | } 35 | .graphicsLayer { 36 | translationX = animatedShift.value 37 | rotationZ = animatedRotation.value 38 | transformOrigin = TransformOrigin(.5f, .75f) 39 | } 40 | .pointerInput(Unit) { 41 | if (!isAnimationRunning) { 42 | detectDragGestures(onDragStart = { 43 | offset = Offset.Zero 44 | }, onDragCancel = { 45 | offset = Offset.Zero 46 | }, onDragEnd = { 47 | handleSwipeAction(coroutineScope, direction, offset) 48 | offset = Offset.Zero 49 | }) { change, dragAmount -> 50 | change.consumeAllChanges() 51 | offset = offset?.plus(dragAmount) 52 | } 53 | } 54 | } 55 | } } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/RecipeCard.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.fillMaxHeight 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.Card 14 | import androidx.compose.material3.CardDefaults 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.text.font.FontStyle 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.text.style.TextAlign 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.dp 25 | import jr.brian.issarecipeapp.view.ui.theme.dp_10 26 | import jr.brian.issarecipeapp.view.ui.theme.dp_15 27 | import jr.brian.issarecipeapp.view.ui.theme.sp_16 28 | 29 | @Composable 30 | fun RecipeCard( 31 | recipe: String, 32 | modifier: Modifier = Modifier, 33 | cardPadding: PaddingValues = PaddingValues(12.dp) 34 | ) { 35 | Card( 36 | modifier = modifier 37 | .fillMaxWidth(0.90f) 38 | .fillMaxHeight(0.80f) 39 | .padding(2.dp), 40 | shape = RoundedCornerShape(dp_10), 41 | border = BorderStroke(0.5.dp, Color.Gray), 42 | elevation = CardDefaults.cardElevation(dp_15), 43 | ) { 44 | LazyColumn( 45 | modifier = Modifier 46 | .fillMaxSize() 47 | .padding(cardPadding), 48 | verticalArrangement = Arrangement.SpaceBetween, 49 | horizontalAlignment = Alignment.CenterHorizontally 50 | ) { 51 | item { 52 | Box( 53 | Modifier 54 | .fillMaxWidth() 55 | .weight(1f), 56 | contentAlignment = Alignment.TopCenter 57 | ) { 58 | Text( 59 | text = recipe, 60 | textAlign = TextAlign.Center, 61 | fontWeight = FontWeight.Bold, 62 | fontStyle = FontStyle.Italic, 63 | fontSize = sp_16, 64 | lineHeight = sp_16 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | @Preview(showBackground = true) 73 | @Composable 74 | private fun QuoteCardPreview() { 75 | Box(contentAlignment = Alignment.Center) { 76 | RecipeCard("Eggs") 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/RecipeStack.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.setValue 8 | import androidx.compose.ui.Modifier 9 | import jr.brian.issarecipeapp.model.local.RecipeDao 10 | 11 | @Composable 12 | fun RecipeStack( 13 | modifier: Modifier = Modifier, 14 | dao: RecipeDao, 15 | items: InfiniteList, 16 | onReject: (T) -> Unit = {}, 17 | onLike: (T) -> Unit = {}, 18 | itemContent: @Composable (T) -> Unit, 19 | ) { 20 | var currentPageNum by remember{ mutableStateOf(0) } 21 | 22 | val swipeState = rememberSwipeableState( 23 | onLeft = { items.current?.let{ 24 | onReject(it) 25 | currentPageNum = items.moveToNext() 26 | } }, 27 | onRight = { items.current?.let{ 28 | onLike(it) 29 | currentPageNum = items.moveToNext() 30 | } } 31 | ) 32 | 33 | items[currentPageNum+1]?.let { profile -> 34 | StackBackgroundCard(modifier, dao, swipeState) { itemContent(profile) } 35 | } 36 | items[currentPageNum]?.let { profile -> 37 | StackForegroundCard(modifier, dao, swipeState) { itemContent(profile) } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/StackBackgroundCard.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Card 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.graphicsLayer 14 | import androidx.compose.ui.unit.dp 15 | import jr.brian.issarecipeapp.model.local.RecipeDao 16 | import jr.brian.issarecipeapp.util.SwipeHeaderLabel 17 | 18 | @Composable 19 | fun StackBackgroundCard( 20 | modifier: Modifier = Modifier, 21 | dao: RecipeDao, 22 | state: SwipeableState, 23 | content: @Composable () -> Unit, 24 | ) = with(state) { 25 | 26 | Column( 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | verticalArrangement = Arrangement.Center 29 | ) { 30 | Spacer(modifier = Modifier.height(20.dp)) 31 | 32 | SwipeHeaderLabel(dao) 33 | 34 | Spacer(modifier = Modifier.height(20.dp)) 35 | 36 | Card( 37 | modifier 38 | .padding(bottom = 10.dp) 39 | .graphicsLayer { 40 | scaleX = animatedScale.value 41 | scaleY = animatedScale.value 42 | }) { 43 | Box( contentAlignment = Alignment.Center) { 44 | content() 45 | } 46 | } 47 | 48 | Spacer(modifier = Modifier.height(10.dp)) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/StackForegroundCard.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.BoxWithConstraints 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.material.Card 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import jr.brian.issarecipeapp.model.local.RecipeDao 15 | import jr.brian.issarecipeapp.util.SwipeHeaderLabel 16 | 17 | @Composable 18 | fun StackForegroundCard( 19 | modifier: Modifier = Modifier, 20 | dao: RecipeDao, 21 | state: SwipeableState, 22 | content: @Composable () -> Unit, 23 | ) { 24 | SwipeOverlay(state) { 25 | BoxWithConstraints(modifier, contentAlignment = Alignment.Center) { 26 | 27 | Column( 28 | horizontalAlignment = Alignment.CenterHorizontally, 29 | verticalArrangement = Arrangement.Center 30 | ) { 31 | Spacer(modifier = Modifier.height(20.dp)) 32 | 33 | SwipeHeaderLabel(dao) 34 | 35 | Spacer(modifier = Modifier.height(20.dp)) 36 | 37 | Card(Modifier.swipeable(state)) { 38 | Box(contentAlignment = Alignment.Center) { 39 | content() 40 | } 41 | } 42 | 43 | SwipeControls(this@BoxWithConstraints.constraints.maxWidth, state) 44 | } 45 | 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/SwipeControls.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.foundation.shape.CircleShape 13 | import androidx.compose.material.Card 14 | import androidx.compose.material.Icon 15 | import androidx.compose.material.Text 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.rounded.Clear 18 | import androidx.compose.material.icons.rounded.Favorite 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.graphics.graphicsLayer 25 | import androidx.compose.ui.unit.dp 26 | import jr.brian.issarecipeapp.view.ui.theme.BlueIsh 27 | 28 | @Composable 29 | fun SwipeControls(width: Int, state: SwipeableState) = with(state) { 30 | 31 | val coroutineScope = rememberCoroutineScope() 32 | 33 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 34 | Row( 35 | Modifier 36 | .width(width.dp) 37 | .padding(top = 25.dp, start = 25.dp, end = 25.dp, bottom = 15.dp) 38 | .graphicsLayer { 39 | alpha = if (isAnimationRunning || shift != 0f) 0f else 1f 40 | }, 41 | horizontalArrangement = Arrangement.SpaceEvenly, 42 | verticalAlignment = Alignment.CenterVertically 43 | ) { 44 | Card( 45 | backgroundColor = BlueIsh, 46 | modifier = Modifier 47 | .clip(CircleShape) 48 | .clickable { 49 | handleControlsAction(coroutineScope, SwipeDirection.LEFT) 50 | }) { 51 | Box(Modifier.size(64.dp), contentAlignment = Alignment.Center) { 52 | Icon( 53 | Icons.Rounded.Clear, 54 | "decline", 55 | Modifier.fillMaxSize(.5f) 56 | ) 57 | } 58 | } 59 | Card( 60 | backgroundColor = BlueIsh, 61 | modifier = Modifier 62 | .clip(CircleShape) 63 | .clickable { 64 | handleControlsAction(coroutineScope, SwipeDirection.RIGHT) 65 | }) { 66 | Box(Modifier.size(64.dp), contentAlignment = Alignment.Center) { 67 | Icon( 68 | Icons.Rounded.Favorite, 69 | "favorite", 70 | Modifier.fillMaxSize(.5f) 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | Text( 77 | "You may see the same recipe more than once.", 78 | color = BlueIsh, 79 | ) 80 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/SwipeDirection.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | enum class SwipeDirection { 4 | NONE, LEFT, RIGHT 5 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/SwipeOverlay.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.Icon 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.rounded.Clear 8 | import androidx.compose.material.icons.rounded.Favorite 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.graphicsLayer 14 | 15 | @Composable 16 | fun SwipeOverlay(state: SwipeableState, content: @Composable () -> Unit) = with(state) { 17 | Box(contentAlignment = Alignment.Center) { 18 | content() 19 | Box(Modifier, contentAlignment = Alignment.Center) { 20 | when { 21 | isSwipingToLeft || direction == SwipeDirection.LEFT -> Icon( 22 | Icons.Rounded.Clear, 23 | "swipe left", 24 | Modifier.fillMaxSize(.25f).graphicsLayer { 25 | alpha = animatedAlpha.value 26 | }, 27 | tint = if (isSwipingToLeft) Color.Red.copy(alpha = .5f) else Color.Unspecified) 28 | isSwipingToRight || direction == SwipeDirection.RIGHT -> Icon( 29 | Icons.Rounded.Favorite, 30 | "swipe right", 31 | Modifier.fillMaxSize(.25f).graphicsLayer { 32 | alpha = animatedAlpha.value 33 | }, 34 | tint = if (isSwipingToRight) Color.Red.copy(alpha = .5f) else Color.Unspecified) 35 | else -> Unit 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/components/swipe_cards/SwipeableState.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.components.swipe_cards 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.derivedStateOf 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.geometry.Rect 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.joinAll 16 | import kotlinx.coroutines.launch 17 | import kotlin.math.abs 18 | 19 | @Composable 20 | fun rememberSwipeableState( 21 | swipeThreshold: Float = .4f, 22 | swipeAngle: Float = 25f, 23 | onLeft: () -> Unit, 24 | onRight: () -> Unit, 25 | ) = remember { SwipeableState(swipeThreshold, swipeAngle, onLeft, onRight) } 26 | 27 | class SwipeableState constructor( 28 | private val swipeThreshold: Float = .4f, 29 | private val swipeAngle: Float = 25f, 30 | private val onLeft: () -> Unit, 31 | private val onRight: () -> Unit, 32 | ) { 33 | 34 | internal var rect: Rect by mutableStateOf(Rect.Zero) 35 | 36 | internal var offset: Offset? by mutableStateOf(null) 37 | 38 | internal val alpha: Float by derivedStateOf { 39 | lerp( 40 | abs(offsetPercentage), 41 | 0f, 42 | swipeThreshold, 43 | 0f, 1f 44 | ).coerceIn(0f, 1f) 45 | } 46 | 47 | internal val shift: Float by derivedStateOf { 48 | offset?.run { x.coerceIn(-rect.width, rect.width) } ?: 0f 49 | } 50 | 51 | internal val scale: Float by derivedStateOf { 52 | lerp( 53 | abs(offsetPercentage), 54 | 0f, 55 | swipeThreshold, 56 | .9f, 1f 57 | ).coerceIn(.9f, 1f) 58 | } 59 | 60 | internal val rotation: Float by derivedStateOf { 61 | offset?.run { (offsetPercentage * swipeAngle).coerceIn(-swipeAngle, swipeAngle) } ?: 0f 62 | } 63 | 64 | internal val direction by derivedStateOf { 65 | offset?.run { 66 | when { 67 | x < 0 -> SwipeDirection.LEFT 68 | x > 0 -> SwipeDirection.RIGHT 69 | else -> SwipeDirection.NONE 70 | } 71 | } ?: SwipeDirection.NONE 72 | } 73 | 74 | internal var isAnimationRunning by mutableStateOf(false) 75 | 76 | internal var isSwipingToLeft by mutableStateOf(false) 77 | 78 | internal var isSwipingToRight by mutableStateOf(false) 79 | 80 | internal fun handleSwipeAction( 81 | coroutineScope: CoroutineScope, 82 | direction: SwipeDirection, 83 | swipeOffset: Offset?, 84 | ) { 85 | coroutineScope.launch { 86 | 87 | val swiped = swipeOffset?.run { swipeAvailable(x) } ?: true 88 | 89 | isSwipingToDefault = !swiped 90 | isSwipingToLeft = swiped && direction == SwipeDirection.LEFT 91 | isSwipingToRight = swiped && direction == SwipeDirection.RIGHT 92 | 93 | isAnimationRunning = true 94 | 95 | joinAll( 96 | launch { 97 | animatedAlpha.animateTo(if (isSwipingToDefault) 1f else 0f, animation) 98 | }, 99 | launch { 100 | animatedScale.animateTo(if (isSwipingToDefault) .9f else 1f, animation) 101 | }, 102 | launch { 103 | animatedShift.animateTo( 104 | if (swiped) when (direction) { 105 | SwipeDirection.LEFT -> rect.width * -1.5f 106 | SwipeDirection.RIGHT -> rect.width * 1.5f 107 | else -> 0f 108 | } else 0f, animation 109 | ) 110 | }, 111 | launch { 112 | animatedRotation.animateTo( 113 | if (swiped) when (direction) { 114 | SwipeDirection.LEFT -> swipeAngle * -1.5f 115 | SwipeDirection.RIGHT -> swipeAngle * 1.5f 116 | else -> 0f 117 | } else 0f, animation 118 | ) 119 | } 120 | ) 121 | 122 | isAnimationRunning = false 123 | 124 | if (swiped) when (direction) { 125 | SwipeDirection.LEFT -> onLeft() 126 | SwipeDirection.RIGHT -> onRight() 127 | else -> Unit 128 | } 129 | 130 | animatedAlpha.snapTo(1f) 131 | animatedScale.snapTo(.9f) 132 | animatedShift.snapTo(0f) 133 | animatedRotation.snapTo(0f) 134 | 135 | isSwipingToDefault = false 136 | isSwipingToLeft = false 137 | isSwipingToRight = false 138 | } 139 | } 140 | 141 | internal fun handleControlsAction(coroutineScope: CoroutineScope, direction: SwipeDirection) = 142 | handleSwipeAction(coroutineScope, direction, null) 143 | 144 | internal val animatedAlpha = Animatable(0f) 145 | 146 | internal val animatedShift = Animatable(0f) 147 | 148 | internal val animatedScale = Animatable(0f) 149 | 150 | internal val animatedRotation = Animatable(0f) 151 | 152 | private val animationDuration = 250 153 | 154 | private val animation = tween(durationMillis = animationDuration, easing = LinearEasing) 155 | 156 | private var isSwipingToDefault by mutableStateOf(false) 157 | 158 | private val offsetPercentage by derivedStateOf { offset?.run { x / rect.width } ?: 0f } 159 | 160 | private fun swipeAvailable(value: Float) = abs(value) / rect.width > swipeThreshold 161 | 162 | private fun lerp( 163 | value: Float, 164 | minValue: Float, 165 | maxValue: Float, 166 | targetMinValue: Float, 167 | targetMaxValue: Float, 168 | ) = 169 | (value - minValue) / (maxValue - minValue) * (targetMaxValue - targetMinValue) + targetMinValue 170 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/pages/AskPage.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.pages 2 | 3 | import android.app.Activity.RESULT_OK 4 | import android.speech.RecognizerIntent 5 | import android.widget.Toast 6 | import androidx.activity.compose.rememberLauncherForActivityResult 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.gestures.Orientation 10 | import androidx.compose.foundation.gestures.scrollable 11 | import androidx.compose.foundation.interaction.MutableInteractionSource 12 | import androidx.compose.foundation.layout.* 13 | import androidx.compose.foundation.lazy.rememberLazyListState 14 | import androidx.compose.foundation.rememberScrollState 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.Scaffold 17 | import androidx.compose.runtime.* 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.focus.onFocusEvent 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.platform.LocalFocusManager 23 | import androidx.compose.ui.unit.dp 24 | import androidx.hilt.navigation.compose.hiltViewModel 25 | import jr.brian.issarecipeapp.model.local.Chat 26 | import kotlinx.coroutines.launch 27 | import java.time.LocalDateTime 28 | import jr.brian.issarecipeapp.model.local.RecipeDao 29 | import jr.brian.issarecipeapp.util.API_KEY_REQUIRED 30 | import jr.brian.issarecipeapp.util.CHEF_GPT_LABEL 31 | import jr.brian.issarecipeapp.util.NO_RESPONSE_MSG 32 | import jr.brian.issarecipeapp.util.USER_LABEL 33 | import jr.brian.issarecipeapp.util.dateFormatter 34 | import jr.brian.issarecipeapp.util.getSpeechInputIntent 35 | import jr.brian.issarecipeapp.util.showToast 36 | import jr.brian.issarecipeapp.util.timeFormatter 37 | import jr.brian.issarecipeapp.view.ui.components.ChatHeader 38 | import jr.brian.issarecipeapp.view.ui.components.ChatSection 39 | import jr.brian.issarecipeapp.view.ui.components.ChatTextFieldRow 40 | import jr.brian.issarecipeapp.view.ui.components.EmptyPromptDialog 41 | import jr.brian.issarecipeapp.viewmodel.MainViewModel 42 | 43 | @OptIn(ExperimentalMaterial3Api::class) 44 | @Composable 45 | fun AskPage( 46 | dao: RecipeDao, 47 | savedApiKey: String, 48 | savedAskContext: String, 49 | savedModel: String, 50 | viewModel: MainViewModel = hiltViewModel(), 51 | onNavToAskContext: () -> Unit, 52 | onNavToSettings: () -> Unit, 53 | ) { 54 | val scope = rememberCoroutineScope() 55 | val context = LocalContext.current 56 | val focusManager = LocalFocusManager.current 57 | 58 | val promptText = remember { mutableStateOf("") } 59 | 60 | val isEmptyPromptDialogShowing = remember { mutableStateOf(false) } 61 | val isChatGptTyping = remember { mutableStateOf(false) } 62 | 63 | val interactionSource = remember { MutableInteractionSource() } 64 | 65 | val chats = remember { dao.getChats().toMutableStateList() } 66 | 67 | val scrollState = rememberScrollState() 68 | val chatListState = rememberLazyListState() 69 | 70 | LaunchedEffect(key1 = 1, block = { 71 | chatListState.animateScrollToItem(chats.size) 72 | }) 73 | 74 | val speechToText = rememberLauncherForActivityResult( 75 | contract = ActivityResultContracts.StartActivityForResult() 76 | ) { 77 | if (it.resultCode != RESULT_OK) { 78 | return@rememberLauncherForActivityResult 79 | } 80 | focusManager.clearFocus() 81 | val results = it.data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) 82 | promptText.value = 83 | "${promptText.value}${if (promptText.value.isEmpty()) "" else " "}" + results?.get(0) 84 | } 85 | 86 | val sendOnClick = { 87 | focusManager.clearFocus() 88 | if (chats.isEmpty()) { 89 | scope.launch { 90 | chatListState.animateScrollToItem(0) 91 | } 92 | } 93 | if (savedApiKey.isEmpty()) { 94 | onNavToSettings() 95 | context.showToast(API_KEY_REQUIRED) 96 | } else if (promptText.value.isEmpty() || promptText.value.isBlank()) { 97 | isEmptyPromptDialogShowing.value = true 98 | } else { 99 | val prompt = promptText.value 100 | promptText.value = "" 101 | scope.launch { 102 | val myChat = Chat( 103 | fullTimeStamp = LocalDateTime.now().toString(), 104 | text = prompt, 105 | dateSent = LocalDateTime.now().format(dateFormatter), 106 | timeSent = LocalDateTime.now().format(timeFormatter), 107 | senderLabel = USER_LABEL 108 | ) 109 | chats.add(myChat) 110 | dao.insertChat(myChat) 111 | chatListState.animateScrollToItem(chats.size) 112 | viewModel.getAskResponse( 113 | userPrompt = prompt, 114 | context = savedAskContext, 115 | model = savedModel, 116 | dao = dao 117 | ) 118 | val chatGptChat = Chat( 119 | fullTimeStamp = LocalDateTime.now().toString(), 120 | text = viewModel.response.value ?: NO_RESPONSE_MSG, 121 | dateSent = LocalDateTime.now().format(dateFormatter), 122 | timeSent = LocalDateTime.now().format(timeFormatter), 123 | senderLabel = CHEF_GPT_LABEL 124 | ) 125 | chats.add(chatGptChat) 126 | dao.insertChat(chatGptChat) 127 | chatListState.animateScrollToItem(chats.size) 128 | } 129 | 130 | } 131 | } 132 | 133 | EmptyPromptDialog(isShowing = isEmptyPromptDialogShowing) 134 | 135 | Scaffold { 136 | Column( 137 | horizontalAlignment = Alignment.CenterHorizontally, 138 | modifier = Modifier 139 | .scrollable(scrollState, orientation = Orientation.Vertical) 140 | .padding(it) 141 | .navigationBarsPadding() 142 | ) 143 | { 144 | Spacer(Modifier.height(5.dp)) 145 | 146 | ChatHeader( 147 | isChatGptTyping = isChatGptTyping.value, 148 | modifier = Modifier.padding(5.dp), 149 | onResetAllChats = { 150 | chats.clear() 151 | dao.removeAllChats() 152 | Toast.makeText( 153 | context, 154 | "Conversation has been reset.", 155 | Toast.LENGTH_LONG 156 | ).show() 157 | }, 158 | onNavToAskContext = { 159 | onNavToAskContext() 160 | } 161 | ) 162 | 163 | ChatSection( 164 | chats = chats, 165 | listState = chatListState, 166 | viewModel = viewModel, 167 | modifier = Modifier 168 | .weight(.90f) 169 | .clickable( 170 | interactionSource = interactionSource, indication = null 171 | ) { 172 | focusManager.clearFocus() 173 | } 174 | ) { chat -> 175 | chats.remove(chat) 176 | dao.removeChat(chat) 177 | } 178 | 179 | ChatTextFieldRow( 180 | promptText = promptText.value, 181 | textFieldOnValueChange = { text -> promptText.value = text }, 182 | modifier = Modifier 183 | .fillMaxWidth() 184 | .padding(start = 15.dp, end = 15.dp) 185 | .onFocusEvent { event -> 186 | if (event.isFocused) { 187 | scope.launch { 188 | scrollState.animateScrollTo(scrollState.maxValue) 189 | } 190 | } 191 | }, 192 | sendIconModifier = Modifier 193 | .size(30.dp) 194 | .clickable { sendOnClick() }, 195 | micIconModifier = Modifier 196 | .size(25.dp) 197 | .clickable { 198 | speechToText.launch(getSpeechInputIntent(context)) 199 | } 200 | ) 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/pages/ConvoContextPage.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.pages 2 | 3 | import androidx.compose.foundation.gestures.Orientation 4 | import androidx.compose.foundation.gestures.scrollable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.focus.onFocusEvent 14 | import androidx.compose.ui.unit.dp 15 | import jr.brian.issarecipeapp.model.local.AppDataStore 16 | import jr.brian.issarecipeapp.view.ui.components.DefaultTextField 17 | import kotlinx.coroutines.launch 18 | 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | @Composable 21 | fun AskContextPage( 22 | storedContext: String, 23 | dataStore: AppDataStore 24 | ) { 25 | val scope = rememberCoroutineScope() 26 | val scrollState = rememberScrollState() 27 | 28 | val conversationalContextText = remember { mutableStateOf("") } 29 | conversationalContextText.value = storedContext 30 | 31 | Scaffold { 32 | Spacer(Modifier.height(5.dp)) 33 | Column( 34 | modifier = Modifier 35 | .scrollable(scrollState, orientation = Orientation.Vertical) 36 | .padding(it), 37 | horizontalAlignment = Alignment.CenterHorizontally 38 | ) { 39 | LazyColumn( 40 | horizontalAlignment = Alignment.CenterHorizontally, 41 | modifier = Modifier 42 | .padding(20.dp) 43 | ) { 44 | items(1) { 45 | DefaultTextField( 46 | value = conversationalContextText.value, 47 | onValueChange = { text -> 48 | conversationalContextText.value = text 49 | scope.launch { 50 | dataStore.saveAskContext(text) 51 | } 52 | }, 53 | label = "Provide Context", 54 | modifier = Modifier 55 | .padding(start = 16.dp, bottom = 16.dp, end = 16.dp) 56 | .onFocusEvent { event -> 57 | if (event.isFocused) { 58 | scope.launch { 59 | scrollState.animateScrollTo(scrollState.maxValue) 60 | } 61 | } 62 | } 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/pages/HomePage.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.pages 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.lazy.LazyColumn 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.Scaffold 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.rememberCoroutineScope 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import jr.brian.issarecipeapp.R 26 | import jr.brian.issarecipeapp.view.ui.components.LottieRecipe 27 | import jr.brian.issarecipeapp.view.ui.theme.BlueIsh 28 | import kotlinx.coroutines.launch 29 | 30 | @OptIn(ExperimentalMaterial3Api::class) 31 | @Composable 32 | fun HomePage( 33 | onNavToAsk: () -> Unit, 34 | onNavToMealDetails: () -> Unit, 35 | onNavToFavRecipes: () -> Unit, 36 | onNavToSwipe: () -> Unit, 37 | onNavToSettings: () -> Unit 38 | ) { 39 | val scope = rememberCoroutineScope() 40 | 41 | val isMenuShowing = remember { 42 | mutableStateOf(true) 43 | } 44 | 45 | Scaffold { 46 | Spacer(modifier = Modifier.height(15.dp)) 47 | 48 | LazyColumn( 49 | modifier = Modifier 50 | .padding(it) 51 | .fillMaxSize(), 52 | horizontalAlignment = Alignment.CenterHorizontally, 53 | verticalArrangement = Arrangement.Center 54 | ) { 55 | item { 56 | Text( 57 | fontSize = 20.sp, 58 | color = BlueIsh, 59 | text = stringResource(id = R.string.app_name), 60 | ) 61 | 62 | Spacer(modifier = Modifier.height(20.dp)) 63 | 64 | LottieRecipe( 65 | isShowing = remember { 66 | mutableStateOf(true) 67 | }, modifier = Modifier.size(250.dp) 68 | ) 69 | 70 | AnimatedVisibility(visible = isMenuShowing.value) { 71 | Column( 72 | modifier = Modifier 73 | .padding(it), 74 | horizontalAlignment = Alignment.CenterHorizontally, 75 | verticalArrangement = Arrangement.Center 76 | ) { 77 | Text( 78 | fontSize = 30.sp, 79 | color = BlueIsh, 80 | text = "Ask", 81 | modifier = Modifier.clickable { 82 | isMenuShowing.value = false 83 | scope.launch { 84 | onNavToAsk() 85 | } 86 | } 87 | ) 88 | 89 | Spacer(modifier = Modifier.height(25.dp)) 90 | 91 | // Text( 92 | // fontSize = 30.sp, 93 | // color = BlueIsh, 94 | // text = "Swipe", 95 | // modifier = Modifier.clickable { 96 | // isMenuShowing.value = false 97 | // scope.launch { 98 | // onNavToSwipe() 99 | // } 100 | // } 101 | // ) 102 | // 103 | // Spacer(modifier = Modifier.height(25.dp)) 104 | 105 | Text( 106 | fontSize = 30.sp, 107 | color = BlueIsh, 108 | text = "Generate", 109 | modifier = Modifier.clickable { 110 | isMenuShowing.value = false 111 | scope.launch { 112 | onNavToMealDetails() 113 | } 114 | }) 115 | 116 | Spacer(modifier = Modifier.height(25.dp)) 117 | 118 | Text( 119 | fontSize = 30.sp, 120 | color = BlueIsh, 121 | text = "Favorites", 122 | modifier = Modifier.clickable { 123 | isMenuShowing.value = false 124 | scope.launch { 125 | onNavToFavRecipes() 126 | } 127 | } 128 | ) 129 | 130 | Spacer(modifier = Modifier.height(25.dp)) 131 | 132 | Text( 133 | fontSize = 30.sp, 134 | color = BlueIsh, 135 | text = "Settings", 136 | modifier = Modifier.clickable { 137 | isMenuShowing.value = false 138 | scope.launch { 139 | onNavToSettings() 140 | } 141 | } 142 | ) 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/pages/RecipeSwipePage.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.pages 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.material3.CircularProgressIndicator 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.collectAsState 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.hilt.navigation.compose.hiltViewModel 17 | import jr.brian.issarecipeapp.model.local.Recipe 18 | import jr.brian.issarecipeapp.model.local.RecipeDao 19 | import jr.brian.issarecipeapp.view.ui.components.swipe_cards.RecipeCard 20 | import jr.brian.issarecipeapp.view.ui.components.swipe_cards.RecipeStack 21 | import jr.brian.issarecipeapp.view.ui.theme.BlueIsh 22 | import jr.brian.issarecipeapp.view.ui.theme.dp_20 23 | import jr.brian.issarecipeapp.view.ui.theme.sp_16 24 | import jr.brian.issarecipeapp.viewmodel.MainViewModel 25 | 26 | @Composable 27 | fun RecipeSwipe( 28 | dao: RecipeDao, 29 | viewModel: MainViewModel = hiltViewModel() 30 | ) { 31 | val recipes = remember { viewModel.swipeRecipes } 32 | 33 | val loading = viewModel.swipeLoading.collectAsState() 34 | 35 | if (loading.value) { 36 | Column( 37 | horizontalAlignment = Alignment.CenterHorizontally, 38 | verticalArrangement = Arrangement.Center, 39 | modifier = Modifier.fillMaxSize() 40 | ) { 41 | Text( 42 | "Loading Recipes!", 43 | color = BlueIsh, 44 | style = TextStyle(fontSize = sp_16) 45 | ) 46 | Spacer(modifier = Modifier.height(dp_20)) 47 | CircularProgressIndicator(color = BlueIsh) 48 | } 49 | } else { 50 | RecipeStack( 51 | dao = dao, 52 | items = recipes, 53 | onLike = { 54 | dao.insertRecipe(recipe = it) 55 | }, onReject = { 56 | with(RejectedRecipeCache.cache) { 57 | if (size == 7) { 58 | remove(first()) 59 | add(it) 60 | } else { 61 | add(it) 62 | } 63 | } 64 | }) { data -> 65 | RecipeCard(data.recipe) 66 | } 67 | } 68 | } 69 | 70 | object RejectedRecipeCache { 71 | val cache = mutableListOf() 72 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) 12 | 13 | //val PinkIsh = Color(0xFFAF7C7B) 14 | val BlueIsh = Color(0xFF7BAFB0) 15 | val Crimson = Color(0xFFDC143C) -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/theme/Dimens.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.theme 2 | 3 | import androidx.compose.ui.unit.Dp 4 | import androidx.compose.ui.unit.dp 5 | import androidx.compose.ui.unit.sp 6 | 7 | val default: Dp = 0.dp 8 | val spaceXXSmall: Dp = 2.dp 9 | val spaceExtraSmall: Dp = 4.dp 10 | val spaceSmall: Dp = 8.dp 11 | val spaceMedium: Dp = 16.dp 12 | val spaceLarge: Dp = 32.dp 13 | val spaceExtraLarge: Dp = 64.dp 14 | val spaceXXLarge: Dp = 128.dp 15 | val spaceXXXLarge: Dp = 256.dp 16 | 17 | 18 | val sp_10 = 10.sp 19 | val sp_12 = 12.sp 20 | val sp_14 = 14.sp 21 | val sp_16 = 16.sp 22 | val sp_32 = 32.sp 23 | val sp_36 = 36.sp 24 | 25 | 26 | val dp_0: Dp = 0.dp 27 | val dp_1: Dp = 1.dp 28 | val dp_2: Dp = 2.dp 29 | val dp_3: Dp = 3.dp 30 | val dp_4: Dp = 4.dp 31 | val dp_5: Dp = 5.dp 32 | val dp_8: Dp = 8.dp 33 | val dp_10: Dp = 10.dp 34 | val dp_15: Dp = 15.dp 35 | val dp_16: Dp = 16.dp 36 | val dp_20: Dp = 20.dp 37 | val dp_24: Dp = 24.dp 38 | val dp_50: Dp = 50.dp 39 | val dp_80: Dp = 80.dp 40 | val dp_100: Dp = 100.dp 41 | val dp_150: Dp = 150.dp 42 | val dp_200:Dp = 200.dp 43 | -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun IssaRecipeAppTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/view/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.view.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/jr/brian/issarecipeapp/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package jr.brian.issarecipeapp.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import javax.inject.Inject 8 | import androidx.compose.runtime.mutableStateListOf 9 | import androidx.lifecycle.viewModelScope 10 | import jr.brian.issarecipeapp.model.local.Recipe 11 | import jr.brian.issarecipeapp.model.local.RecipeDao 12 | import jr.brian.issarecipeapp.model.local.getRandomRecipes 13 | import jr.brian.issarecipeapp.model.remote.retrieveRecipes 14 | import jr.brian.issarecipeapp.model.repository.Repository 15 | import jr.brian.issarecipeapp.util.CONNECTION_TIMEOUT_MSG 16 | import jr.brian.issarecipeapp.util.DEFAULT_RECIPE_TITLE 17 | import jr.brian.issarecipeapp.util.ERROR 18 | import jr.brian.issarecipeapp.util.MAX_CARDS_IN_STACK 19 | import jr.brian.issarecipeapp.util.NO_RECIPES_TO_SWIPE_MSG 20 | import jr.brian.issarecipeapp.util.UP_SIDE_DOWN_FACE_EMOJI 21 | import jr.brian.issarecipeapp.view.ui.components.swipe_cards.InfiniteList 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.delay 24 | import kotlinx.coroutines.launch 25 | import kotlinx.coroutines.withContext 26 | 27 | @HiltViewModel 28 | class MainViewModel @Inject constructor(private val repository: Repository) : ViewModel() { 29 | private val _response = MutableStateFlow(null) 30 | val response = _response.asStateFlow() 31 | 32 | private val _recipeTitle = MutableStateFlow(null) 33 | val recipeTitle = _recipeTitle.asStateFlow() 34 | 35 | private val _imageUrlResponse = MutableStateFlow(null) 36 | val imageUrlResponse = _imageUrlResponse.asStateFlow() 37 | 38 | private val _loading = MutableStateFlow(false) 39 | val loading = _loading.asStateFlow() 40 | 41 | private val _imageLoading = MutableStateFlow(false) 42 | val imageLoading = _imageLoading.asStateFlow() 43 | 44 | private val _swipeLoading = MutableStateFlow(false) 45 | val swipeLoading = _swipeLoading.asStateFlow() 46 | 47 | private val currentSwipeRecipes = mutableStateListOf() 48 | private val newSwipeRecipes = mutableListOf() 49 | 50 | private var initialized = false 51 | 52 | suspend fun generateImageUrl( 53 | title: String, 54 | ingredients: String? = null 55 | ) { 56 | _imageLoading.emit(true) 57 | _imageUrlResponse.emit( 58 | repository.generateImageUrl(title, ingredients) 59 | ) 60 | _imageLoading.emit(false) 61 | } 62 | 63 | suspend fun getAskResponse( 64 | dao: RecipeDao? = null, 65 | userPrompt: String, 66 | context: String? = null, 67 | model: String 68 | ) { 69 | _loading.emit(true) 70 | _response.emit( 71 | repository.getAskResponse( 72 | userPrompt = userPrompt, 73 | system = context, 74 | model = model, 75 | dao = dao 76 | ) 77 | ) 78 | _recipeTitle.emit( 79 | _response.value?.let { extractRecipeTitle(it) } 80 | ) 81 | _loading.emit(false) 82 | } 83 | 84 | val swipeRecipes = InfiniteList { 85 | viewModelScope.launch { 86 | _swipeLoading.emit(true) 87 | delay(2000) 88 | _swipeLoading.emit(false) 89 | useNewRecipes() 90 | it.clear() 91 | it.addAll(currentSwipeRecipes) 92 | } 93 | } 94 | 95 | init { 96 | viewModelScope.launch { 97 | withContext(Dispatchers.IO) { 98 | refreshRecipes() 99 | } 100 | } 101 | } 102 | 103 | private fun extractRecipeTitle(input: String): String { 104 | val regex = Regex("""✨(.*?)✨""") 105 | val matchResult = regex.find(input) 106 | return matchResult?.groupValues?.get(1)?.trim() ?: DEFAULT_RECIPE_TITLE 107 | } 108 | 109 | private suspend fun refreshRecipes() { 110 | newSwipeRecipes.clear() 111 | retrieveRecipes( 112 | onSuccess = { recipes -> 113 | if (recipes.isEmpty() || recipes.size < MAX_CARDS_IN_STACK) { 114 | newSwipeRecipes.add( 115 | Recipe( 116 | recipe = NO_RECIPES_TO_SWIPE_MSG, 117 | name = UP_SIDE_DOWN_FACE_EMOJI 118 | ) 119 | ) 120 | } else { 121 | val randomRecipes = getRandomRecipes(recipes, MAX_CARDS_IN_STACK) 122 | newSwipeRecipes.addAll(randomRecipes.filter { 123 | it.recipe.lowercase() != CONNECTION_TIMEOUT_MSG 124 | && it.recipe.lowercase().substring(0, 5) != ERROR 125 | }) 126 | } 127 | viewModelScope.launch { 128 | withContext(Dispatchers.IO) { 129 | onRecipesRetrieved() 130 | } 131 | } 132 | }, 133 | onError = { error -> 134 | newSwipeRecipes.add( 135 | Recipe( 136 | recipe = error.message, 137 | name = "Error Code: ${error.code}" 138 | ) 139 | ) 140 | } 141 | ) 142 | } 143 | 144 | private suspend fun onRecipesRetrieved() { 145 | _swipeLoading.emit(false) 146 | if (!initialized) { 147 | initialized = true 148 | useNewRecipes() 149 | swipeRecipes.addAll(currentSwipeRecipes) 150 | } 151 | } 152 | 153 | private fun useNewRecipes() { 154 | currentSwipeRecipes.clear() 155 | currentSwipeRecipes.addAll(newSwipeRecipes) 156 | viewModelScope.launch { 157 | withContext(Dispatchers.IO) { 158 | refreshRecipes() 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_check_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_delete_forever_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_edit_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_fastfood_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_history_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_info_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_menu_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_mic_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_more_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_open_in_new_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_send_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/raw/loading.json: -------------------------------------------------------------------------------- 1 | {"v":"5.6.5","fr":60,"ip":0,"op":105,"w":340,"h":340,"nm":"food_loading 2","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[84.731,56.983,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.46,-3.36],[0,0],[0,0],[-3.05,-0.55],[0,0],[-1.87,-2.66],[0,0],[0,0],[-3.7,-1.41],[0,0],[-0.14,-4.62],[0,0],[0,0],[0,0],[0,0],[3.45,1.44],[2.59,-2.54],[0,0],[0,0],[0,0],[3.02,0.64],[2.41,-1.76],[0,0],[0,0],[0,0],[3.75,-0.52],[0.97,-3.64],[0,0],[0,0],[0,0],[3.1,-1.59],[0.11,-3.43],[0,0],[0,0],[0,0],[0,0],[-3.87,1.99],[-3.23,-1.74],[0,0],[0,0],[-4.02,0.67]],"o":[[4.15,-0.58],[0,0],[0,0],[2.64,-1.61],[0,0],[3.17,0.68],[0,0],[0,0],[3.09,-2.4],[0,0],[4.21,1.76],[0,0],[0,0],[0,0],[0,0],[0,-3.83],[-3.34,-1.39],[0,0],[0,0],[0,0],[-1.5,-2.78],[-2.9,-0.62],[0,0],[0,0],[0,0],[-1.82,-3.42],[-3.66,0.51],[0,0],[0,0],[0,0],[-2.77,-2.13],[-3.01,1.55],[0,0],[0,0],[0,0],[0,0],[-0.01,-4.43],[3.33,-1.71],[0,0],[0,0],[1.46,-3.81],[0,0]],"v":[[-10.367,-9.341],[0.323,-4.771],[0.553,-4.451],[0.793,-4.591],[9.623,-6.241],[10.003,-6.161],[17.833,-0.961],[17.893,-0.861],[18.123,-1.041],[29.023,-2.681],[29.343,-2.551],[36.473,7.959],[36.473,8.299],[36.473,9.449],[34.183,9.449],[34.183,8.299],[28.473,-0.421],[18.723,1.479],[18.483,1.719],[17.393,2.859],[16.643,1.479],[9.533,-3.901],[1.213,-2.111],[0.923,-1.891],[-0.147,-1.041],[-0.797,-2.251],[-10.047,-7.061],[-17.667,-0.211],[-17.747,0.109],[-18.147,1.849],[-19.557,0.759],[-29.127,-0.131],[-34.177,7.959],[-34.187,8.299],[-34.187,9.449],[-36.477,9.449],[-36.477,8.299],[-30.167,-2.181],[-19.717,-2.081],[-19.507,-1.961],[-19.487,-2.011],[-10.687,-9.291]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[4]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[-6]},{"t":75,"s":[0]}],"ix":10},"p":{"a":0,"k":[84.73,129.421,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-14.01,-7.46],[0,0],[0,0],[0,0],[-0.2,16.18]],"o":[[0,0],[0,0],[0,16.21],[0,0],[0,0],[0,0],[14.12,-7.29],[0,0]],"v":[[42.365,-20.02],[-42.365,-20.02],[-42.365,-18.87],[-19.555,19.64],[-18.835,20.02],[18.835,20.02],[19.085,19.89],[42.365,-18.32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[12.78,-6.82],[0,0],[0,0],[0,0],[0.56,14.71],[0,0]],"o":[[0,0],[-0.56,14.71],[0,0],[0,0],[0,0],[-12.78,-6.82],[0,0],[0,0]],"v":[[40.06,-17.714],[40.04,-17.244],[18.5,17.596],[18.28,17.716],[-18.28,17.716],[-18.5,17.596],[-40.04,-17.244],[-40.06,-17.714]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[100]},{"t":86,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[89.147,20.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":70,"s":[100,100,100]},{"t":86,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[4.416,4.528],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":31,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":62,"s":[100]},{"t":78,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[68.536,69.048,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":31,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":62,"s":[100,100,100]},{"t":78,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[4.416,4.528],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[100]},{"t":68,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[30.261,29.806,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":19,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":52,"s":[100,100,100]},{"t":68,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[4.416,4.528],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[11]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[-29]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[119.376,57.07,0],"ix":2},"a":{"a":0,"k":[-8,25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[2.121,-12.743],[4.341,-12.163],[-2.119,12.747],[-4.339,12.157]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[17]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[-12]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[134.278,52.95,0],"ix":2},"a":{"a":0,"k":[-17,27,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.196,-13.475],[8.196,-12.345],[-6.194,13.475],[-8.194,12.345]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"▽ foodIcon 2","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[171.052,174.657,0],"ix":2},"a":{"a":0,"k":[85,85,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":170,"h":170,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"loading","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[248]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[488]},{"t":105,"s":[720]}],"ix":10},"p":{"a":0,"k":[170,170,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[140,140],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.007843137719,0.600000023842,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":250,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":200,"ix":2}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"loading","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /app/src/main/res/raw/loadingpink.json: -------------------------------------------------------------------------------- 1 | {"v":"5.6.5","fr":60,"ip":0,"op":105,"w":340,"h":340,"nm":"food_loading 2","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[84.731,56.983,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-2.46,-3.36],[0,0],[0,0],[-3.05,-0.55],[0,0],[-1.87,-2.66],[0,0],[0,0],[-3.7,-1.41],[0,0],[-0.14,-4.62],[0,0],[0,0],[0,0],[0,0],[3.45,1.44],[2.59,-2.54],[0,0],[0,0],[0,0],[3.02,0.64],[2.41,-1.76],[0,0],[0,0],[0,0],[3.75,-0.52],[0.97,-3.64],[0,0],[0,0],[0,0],[3.1,-1.59],[0.11,-3.43],[0,0],[0,0],[0,0],[0,0],[-3.87,1.99],[-3.23,-1.74],[0,0],[0,0],[-4.02,0.67]],"o":[[4.15,-0.58],[0,0],[0,0],[2.64,-1.61],[0,0],[3.17,0.68],[0,0],[0,0],[3.09,-2.4],[0,0],[4.21,1.76],[0,0],[0,0],[0,0],[0,0],[0,-3.83],[-3.34,-1.39],[0,0],[0,0],[0,0],[-1.5,-2.78],[-2.9,-0.62],[0,0],[0,0],[0,0],[-1.82,-3.42],[-3.66,0.51],[0,0],[0,0],[0,0],[-2.77,-2.13],[-3.01,1.55],[0,0],[0,0],[0,0],[0,0],[-0.01,-4.43],[3.33,-1.71],[0,0],[0,0],[1.46,-3.81],[0,0]],"v":[[-10.367,-9.341],[0.323,-4.771],[0.553,-4.451],[0.793,-4.591],[9.623,-6.241],[10.003,-6.161],[17.833,-0.961],[17.893,-0.861],[18.123,-1.041],[29.023,-2.681],[29.343,-2.551],[36.473,7.959],[36.473,8.299],[36.473,9.449],[34.183,9.449],[34.183,8.299],[28.473,-0.421],[18.723,1.479],[18.483,1.719],[17.393,2.859],[16.643,1.479],[9.533,-3.901],[1.213,-2.111],[0.923,-1.891],[-0.147,-1.041],[-0.797,-2.251],[-10.047,-7.061],[-17.667,-0.211],[-17.747,0.109],[-18.147,1.849],[-19.557,0.759],[-29.127,-0.131],[-34.177,7.959],[-34.187,8.299],[-34.187,9.449],[-36.477,9.449],[-36.477,8.299],[-30.167,-2.181],[-19.717,-2.081],[-19.507,-1.961],[-19.487,-2.011],[-10.687,-9.291]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[4]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[-6]},{"t":75,"s":[0]}],"ix":10},"p":{"a":0,"k":[84.73,129.421,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-14.01,-7.46],[0,0],[0,0],[0,0],[-0.2,16.18]],"o":[[0,0],[0,0],[0,16.21],[0,0],[0,0],[0,0],[14.12,-7.29],[0,0]],"v":[[42.365,-20.02],[-42.365,-20.02],[-42.365,-18.87],[-19.555,19.64],[-18.835,20.02],[18.835,20.02],[19.085,19.89],[42.365,-18.32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[12.78,-6.82],[0,0],[0,0],[0,0],[0.56,14.71],[0,0]],"o":[[0,0],[-0.56,14.71],[0,0],[0,0],[0,0],[-12.78,-6.82],[0,0],[0,0]],"v":[[40.06,-17.714],[40.04,-17.244],[18.5,17.596],[18.28,17.716],[-18.28,17.716],[-18.5,17.596],[-40.04,-17.244],[-40.06,-17.714]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":24,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[100]},{"t":86,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[89.147,20.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":24,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":70,"s":[100,100,100]},{"t":86,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[4.416,4.528],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":31,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":62,"s":[100]},{"t":78,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[68.536,69.048,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":31,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":62,"s":[100,100,100]},{"t":78,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[4.416,4.528],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":19,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":52,"s":[100]},{"t":68,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[30.261,29.806,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":19,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":52,"s":[100,100,100]},{"t":68,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[4.416,4.528],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[11]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[-29]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[119.376,57.07,0],"ix":2},"a":{"a":0,"k":[-8,25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[2.121,-12.743],[4.341,-12.163],[-2.119,12.747],[-4.339,12.157]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Path","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[17]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[-12]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[134.278,52.95,0],"ix":2},"a":{"a":0,"k":[-17,27,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.196,-13.475],[8.196,-12.345],[-6.194,13.475],[-8.194,12.345]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"▽ foodIcon 2","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[171.052,174.657,0],"ix":2},"a":{"a":0,"k":[85,85,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":170,"h":170,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"loading","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[248]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":81,"s":[488]},{"t":105,"s":[720]}],"ix":10},"p":{"a":0,"k":[170,170,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[140,140],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.9568627450980393,0.058823529411764705,0.27450980392156865,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":250,"ix":1}},{"n":"g","nm":"gap","v":{"a":0,"k":200,"ix":2}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"loading","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #212121 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Issa Recipe App 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |