├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [](https://github.com/BrianJr03/Issa-Recipe-App/releases/latest)
7 |
8 | # Issa Recipe App
9 |
10 |
11 |
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 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/release/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/release/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #212121
4 |
--------------------------------------------------------------------------------
/app/src/test/java/jr/brian/issarecipeapp/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package jr.brian.issarecipeapp
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | dependencies {
4 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.45'
5 | classpath 'com.google.gms:google-services:4.4.0'
6 | }
7 | }
8 |
9 | plugins {
10 | id 'com.android.application' version '8.1.4' apply false
11 | id 'com.android.library' version '8.1.4' apply false
12 | id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/docs/assets/icons8-cookbook-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/docs/assets/icons8-cookbook-512.png
--------------------------------------------------------------------------------
/docs/assets/recipe_app_ss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/docs/assets/recipe_app_ss.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed May 03 22:47:27 EDT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/recipe-app-demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BrianJr03/Issa-Recipe-App/a47163d5b1f5169f0cc6c1227de4f319a29c0fef/recipe-app-demo.mp4
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Issa Recipe App"
16 | include ':app'
17 |
--------------------------------------------------------------------------------