diff --git a/app/build.gradle b/app/build.gradle index c6d2c82..067a5d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,9 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'com.google.devtools.ksp' + id 'dagger.hilt.android.plugin' } android { @@ -15,6 +18,7 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + buildConfigField "String", "BASE_URL", "\"https://api.npoint.io/\"" } buildTypes { @@ -24,21 +28,58 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' + } + kapt { + generateStubs = true + } + dataBinding { + enabled = true } } dependencies { - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + // sdp & ssp library for support all sizes on the screen + implementation "com.intuit.sdp:sdp-android:$sdp_version" + implementation "com.intuit.ssp:ssp-android:$ssp_version" + // media3-exoplayer library for playing videos + implementation "androidx.media3:media3-exoplayer:$media3_version" + implementation "androidx.media3:media3-ui:$media3_version" + // dagger hilt dependencies for dependency injection + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-compiler:$hilt_version" + // datastore for saving data in the local storage + implementation "androidx.datastore:datastore-preferences:$datastore_version" + // mockito for testing purposes + testImplementation "org.mockito:mockito-core:$mockito_version" + // lifecycle scope for observing data + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' + // ktx for kotlin extensions + implementation 'androidx.activity:activity-ktx:1.7.2' + implementation 'androidx.fragment:fragment-ktx:1.6.1' + // navigation component for navigating between fragments + implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' + // retrofit for networking calls + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + // coil library for loading images + implementation "io.coil-kt:coil:$coil_version" + // room database + implementation "androidx.room:room-runtime:$room_version" + implementation "androidx.room:room-ktx:$room_version" + ksp "androidx.room:room-compiler:$room_version" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a05c8b9..5618e00 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,12 @@ + + + + + @@ -21,6 +27,10 @@ + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..c684997 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/moataz/phood/MainActivity.kt b/app/src/main/java/com/moataz/phood/MainActivity.kt deleted file mode 100644 index 0dd7a94..0000000 --- a/app/src/main/java/com/moataz/phood/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.moataz.phood - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/com/moataz/phood/app/PhoodApplication.kt b/app/src/main/java/com/moataz/phood/app/PhoodApplication.kt new file mode 100644 index 0000000..78fdd76 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/PhoodApplication.kt @@ -0,0 +1,7 @@ +package com.moataz.phood.app + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class PhoodApplication : Application() diff --git a/app/src/main/java/com/moataz/phood/app/di/DataSourceModule.kt b/app/src/main/java/com/moataz/phood/app/di/DataSourceModule.kt new file mode 100644 index 0000000..84d963f --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/di/DataSourceModule.kt @@ -0,0 +1,20 @@ +package com.moataz.phood.app.di + +import com.moataz.phood.identity.data.local.IdentityLocalDataSourceImpl +import com.moataz.phood.identity.data.repositories.datasources.IdentityLocalDataSource +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataSourceModule { + + @Singleton + @Binds + abstract fun bindIdentityDataSource( + identityLocalDataSourceImpl: IdentityLocalDataSourceImpl, + ): IdentityLocalDataSource +} diff --git a/app/src/main/java/com/moataz/phood/app/di/DatabaseModule.kt b/app/src/main/java/com/moataz/phood/app/di/DatabaseModule.kt new file mode 100644 index 0000000..9b2c270 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/di/DatabaseModule.kt @@ -0,0 +1,49 @@ +package com.moataz.phood.app.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.room.Room +import com.moataz.phood.app.di.utils.Constant.DATABASE_NAME +import com.moataz.phood.app.di.utils.Constant.PREFERENCE_NAME +import com.moataz.phood.recipes.data.local.RecipesDao +import com.moataz.phood.recipes.data.local.RecipesDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Singleton + @Provides + fun provideDatabase(@ApplicationContext appContext: Context): RecipesDatabase { + return Room.databaseBuilder( + appContext, + RecipesDatabase::class.java, + DATABASE_NAME, + ).build() + } + + @Singleton + @Provides + fun provideRecipesDao(database: RecipesDatabase): RecipesDao { + return database.recipesDao() + } + + @Provides + @Singleton + fun provideIdentityDataStorePreferences( + @ApplicationContext applicationContext: Context, + ): DataStore { + return PreferenceDataStoreFactory.create { + applicationContext.preferencesDataStoreFile(PREFERENCE_NAME) + } + } +} diff --git a/app/src/main/java/com/moataz/phood/app/di/NetworkModule.kt b/app/src/main/java/com/moataz/phood/app/di/NetworkModule.kt new file mode 100644 index 0000000..3ed0d5a --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/di/NetworkModule.kt @@ -0,0 +1,63 @@ +package com.moataz.phood.app.di + +import com.moataz.phood.BuildConfig +import com.moataz.phood.identity.data.remote.IdentityService +import com.moataz.phood.identity.data.remote.IdentityServiceImpl +import com.moataz.phood.recipes.data.remote.RecipesService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun provideIdentityService(): IdentityService { + return IdentityServiceImpl() + } + + @Singleton + @Provides + fun provideRecipesNetworkService( + retrofit: Retrofit, + ): RecipesService { + return retrofit.create(RecipesService::class.java) + } + + @Singleton + @Provides + fun provideOkHttpClient(): OkHttpClient { + val builder = OkHttpClient() + .newBuilder() + .callTimeout(25, TimeUnit.SECONDS) + .connectTimeout(25, TimeUnit.SECONDS) + return builder.build() + } + + @Singleton + @Provides + fun provideGsonConverterFactory(): GsonConverterFactory { + return GsonConverterFactory.create() + } + + @Singleton + @Provides + fun provideRetrofit( + okHttpClient: OkHttpClient, + gsonConverterFactory: GsonConverterFactory, + ): Retrofit { + return Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(okHttpClient) + .addConverterFactory(gsonConverterFactory) + .build() + } +} diff --git a/app/src/main/java/com/moataz/phood/app/di/RepositoryModule.kt b/app/src/main/java/com/moataz/phood/app/di/RepositoryModule.kt new file mode 100644 index 0000000..786046e --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/di/RepositoryModule.kt @@ -0,0 +1,28 @@ +package com.moataz.phood.app.di + +import com.moataz.phood.identity.data.repositories.IdentityRepositoryImpl +import com.moataz.phood.identity.domain.repository.IdentityRepository +import com.moataz.phood.recipes.data.repositories.RecipesRepositoryImpl +import com.moataz.phood.recipes.domain.repository.RecipesRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +abstract class RepositoryModule { + + @ViewModelScoped + @Binds + abstract fun bindIdentityRepository( + identityRepositoryImpl: IdentityRepositoryImpl, + ): IdentityRepository + + @ViewModelScoped + @Binds + abstract fun bindRecipesRepository( + recipesRepositoryImpl: RecipesRepositoryImpl, + ): RecipesRepository +} diff --git a/app/src/main/java/com/moataz/phood/app/di/utils/Constant.kt b/app/src/main/java/com/moataz/phood/app/di/utils/Constant.kt new file mode 100644 index 0000000..c6e51e9 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/di/utils/Constant.kt @@ -0,0 +1,6 @@ +package com.moataz.phood.app.di.utils + +object Constant { + const val DATABASE_NAME = "phood_recipes.db" + const val PREFERENCE_NAME = "com.moataz.phood.app_preferences" +} diff --git a/app/src/main/java/com/moataz/phood/app/main/MainActivity.kt b/app/src/main/java/com/moataz/phood/app/main/MainActivity.kt new file mode 100644 index 0000000..490c5f0 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/app/main/MainActivity.kt @@ -0,0 +1,29 @@ +package com.moataz.phood.app.main + +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import com.moataz.phood.R +import com.moataz.phood.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_main) + initView() + } + + private fun initView() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else { + window.statusBarColor = resources.getColor(R.color.main_color) + } + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/local/IdentityLocalDataSourceImpl.kt b/app/src/main/java/com/moataz/phood/identity/data/local/IdentityLocalDataSourceImpl.kt new file mode 100644 index 0000000..5364e36 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/local/IdentityLocalDataSourceImpl.kt @@ -0,0 +1,33 @@ +package com.moataz.phood.identity.data.local + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import com.moataz.phood.identity.data.local.utils.DataStorePreferencesKeys.LOGIN_STATE +import com.moataz.phood.identity.data.repositories.datasources.IdentityLocalDataSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class IdentityLocalDataSourceImpl @Inject constructor( + private val userDataStore: DataStore, +) : IdentityLocalDataSource { + override suspend fun saveLoggedInStatus(status: Boolean) { + withContext(Dispatchers.IO) { + userDataStore.edit { preferences -> + preferences[booleanPreferencesKey(LOGIN_STATE)] = status + } + } + } + + override suspend fun getLoggedInStatus(): Boolean? { + return withContext(Dispatchers.IO) { + userDataStore.data.map { preferences -> + preferences[booleanPreferencesKey(LOGIN_STATE)] + }.first() + } + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/local/utils/DataStorePreferencesKeys.kt b/app/src/main/java/com/moataz/phood/identity/data/local/utils/DataStorePreferencesKeys.kt new file mode 100644 index 0000000..1487d45 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/local/utils/DataStorePreferencesKeys.kt @@ -0,0 +1,5 @@ +package com.moataz.phood.identity.data.local.utils + +object DataStorePreferencesKeys { + const val LOGIN_STATE = "login_state" +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/remote/IdentityService.kt b/app/src/main/java/com/moataz/phood/identity/data/remote/IdentityService.kt new file mode 100644 index 0000000..30a2f95 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/remote/IdentityService.kt @@ -0,0 +1,7 @@ +package com.moataz.phood.identity.data.remote + +import com.moataz.phood.identity.data.remote.dto.UserDTO + +interface IdentityService { + suspend fun signIn(email: String, password: String): UserDTO +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/remote/IdentityServiceImpl.kt b/app/src/main/java/com/moataz/phood/identity/data/remote/IdentityServiceImpl.kt new file mode 100644 index 0000000..dc17860 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/remote/IdentityServiceImpl.kt @@ -0,0 +1,9 @@ +package com.moataz.phood.identity.data.remote + +import com.moataz.phood.identity.data.remote.dto.UserDTO + +class IdentityServiceImpl : IdentityService { + override suspend fun signIn(email: String, password: String): UserDTO { + return UserDTO() + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/remote/dto/UserDTO.kt b/app/src/main/java/com/moataz/phood/identity/data/remote/dto/UserDTO.kt new file mode 100644 index 0000000..db2efe2 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/remote/dto/UserDTO.kt @@ -0,0 +1,7 @@ +package com.moataz.phood.identity.data.remote.dto + +data class UserDTO( + val id: Long = 525118, + val email: String = "mohamed.salah@phood.com", + val password: String = "Brightskies", +) diff --git a/app/src/main/java/com/moataz/phood/identity/data/repositories/IdentityRepositoryImpl.kt b/app/src/main/java/com/moataz/phood/identity/data/repositories/IdentityRepositoryImpl.kt new file mode 100644 index 0000000..79bcfaf --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/repositories/IdentityRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.moataz.phood.identity.data.repositories + +import com.moataz.phood.identity.data.remote.IdentityService +import com.moataz.phood.identity.data.repositories.datasources.IdentityLocalDataSource +import com.moataz.phood.identity.data.repositories.mapper.toUser +import com.moataz.phood.identity.domain.entities.User +import com.moataz.phood.identity.domain.repository.IdentityRepository +import javax.inject.Inject + +class IdentityRepositoryImpl @Inject constructor( + private val remoteService: IdentityService, + private val localDataSource: IdentityLocalDataSource, +) : IdentityRepository { + override suspend fun signIn(email: String, password: String): User { + return remoteService.signIn(email, password).toUser() + } + + override suspend fun saveLoggedInStatus(status: Boolean) { + localDataSource.saveLoggedInStatus(status) + } + + override suspend fun getLoggedInStatus(): Boolean? { + return localDataSource.getLoggedInStatus() + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/repositories/datasources/IdentityLocalDataSource.kt b/app/src/main/java/com/moataz/phood/identity/data/repositories/datasources/IdentityLocalDataSource.kt new file mode 100644 index 0000000..ac90e01 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/repositories/datasources/IdentityLocalDataSource.kt @@ -0,0 +1,6 @@ +package com.moataz.phood.identity.data.repositories.datasources + +interface IdentityLocalDataSource { + suspend fun saveLoggedInStatus(status: Boolean) + suspend fun getLoggedInStatus(): Boolean? +} diff --git a/app/src/main/java/com/moataz/phood/identity/data/repositories/mapper/IdentityMapper.kt b/app/src/main/java/com/moataz/phood/identity/data/repositories/mapper/IdentityMapper.kt new file mode 100644 index 0000000..3b97d1f --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/data/repositories/mapper/IdentityMapper.kt @@ -0,0 +1,12 @@ +package com.moataz.phood.identity.data.repositories.mapper + +import com.moataz.phood.identity.data.remote.dto.UserDTO +import com.moataz.phood.identity.domain.entities.User + +internal fun UserDTO.toUser(): User { + return User( + id = id, + email = email, + password = password, + ) +} diff --git a/app/src/main/java/com/moataz/phood/identity/domain/entities/User.kt b/app/src/main/java/com/moataz/phood/identity/domain/entities/User.kt new file mode 100644 index 0000000..957b8df --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/domain/entities/User.kt @@ -0,0 +1,7 @@ +package com.moataz.phood.identity.domain.entities + +data class User( + val id: Long, + val email: String, + val password: String, +) diff --git a/app/src/main/java/com/moataz/phood/identity/domain/repository/IdentityRepository.kt b/app/src/main/java/com/moataz/phood/identity/domain/repository/IdentityRepository.kt new file mode 100644 index 0000000..12ddf27 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/domain/repository/IdentityRepository.kt @@ -0,0 +1,9 @@ +package com.moataz.phood.identity.domain.repository + +import com.moataz.phood.identity.domain.entities.User + +interface IdentityRepository { + suspend fun signIn(email: String, password: String): User + suspend fun saveLoggedInStatus(status: Boolean) + suspend fun getLoggedInStatus(): Boolean? +} diff --git a/app/src/main/java/com/moataz/phood/identity/domain/usecases/LoggedInStatus.kt b/app/src/main/java/com/moataz/phood/identity/domain/usecases/LoggedInStatus.kt new file mode 100644 index 0000000..dc4936c --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/domain/usecases/LoggedInStatus.kt @@ -0,0 +1,12 @@ +package com.moataz.phood.identity.domain.usecases + +import com.moataz.phood.identity.domain.repository.IdentityRepository +import javax.inject.Inject + +class LoggedInStatus @Inject constructor( + private val identityRepository: IdentityRepository, +) { + suspend operator fun invoke(): Boolean { + return identityRepository.getLoggedInStatus() ?: false + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/domain/usecases/SignInUseCase.kt b/app/src/main/java/com/moataz/phood/identity/domain/usecases/SignInUseCase.kt new file mode 100644 index 0000000..fdc4a94 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/domain/usecases/SignInUseCase.kt @@ -0,0 +1,18 @@ +package com.moataz.phood.identity.domain.usecases + +import com.moataz.phood.identity.domain.repository.IdentityRepository +import javax.inject.Inject + +class SignInUseCase @Inject constructor( + private val identityRepository: IdentityRepository, +) { + suspend operator fun invoke(email: String, password: String): Boolean { + val user = identityRepository.signIn(email, password) + return if (user.email == email && user.password == password) { + identityRepository.saveLoggedInStatus(true) + true + } else { + false + } + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/ui/view/LoginActivity.kt b/app/src/main/java/com/moataz/phood/identity/ui/view/LoginActivity.kt new file mode 100644 index 0000000..941d426 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/ui/view/LoginActivity.kt @@ -0,0 +1,87 @@ +package com.moataz.phood.identity.ui.view + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.moataz.phood.R +import com.moataz.phood.app.main.MainActivity +import com.moataz.phood.databinding.ActivityLoginBinding +import com.moataz.phood.identity.ui.view.utils.Constants.INTRO_VIDEO_URL +import com.moataz.phood.identity.ui.view.utils.CustomPlayerManager +import com.moataz.phood.identity.ui.viewmodel.LoginViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class LoginActivity : AppCompatActivity() { + private lateinit var binding: ActivityLoginBinding + private lateinit var playerManager: CustomPlayerManager + private val viewModel: LoginViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_login) + binding.lifecycleOwner = this + binding.viewModel = viewModel + initStateBar() + initMediaPlayer() + observeEvents() + } + + private fun initStateBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else { + window.statusBarColor = resources.getColor(R.color.main_color) + } + } + + private fun initMediaPlayer() { + playerManager = CustomPlayerManager(this, binding.playerView) + playerManager.setMediaItem(INTRO_VIDEO_URL) + playerManager.displayCustomViewBeforePlayVideo(binding.loginImageBackground) + playerManager.displayVideoPlayerAfterVideoPlay(binding.loginImageBackground) + } + + private fun observeEvents() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.isUserLoggedIn.collect { isUserLoggedIn -> + if (isUserLoggedIn) { + startActivity( + Intent(this@LoginActivity, MainActivity::class.java), + ) + finish() + } + } + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.isLoginFailed.collect {isLoginFailed -> + if (isLoginFailed) { + Toast.makeText( + this@LoginActivity, + "Login Failed", + Toast.LENGTH_SHORT, + ).show() + } + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + playerManager.release() + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/ui/view/databinding/ValidationDataBinding.kt b/app/src/main/java/com/moataz/phood/identity/ui/view/databinding/ValidationDataBinding.kt new file mode 100644 index 0000000..d5c874e --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/ui/view/databinding/ValidationDataBinding.kt @@ -0,0 +1,9 @@ +package com.moataz.phood.identity.ui.view.databinding + +import androidx.databinding.BindingAdapter +import com.google.android.material.textfield.TextInputLayout + +@BindingAdapter("app:isFieldValid", "app:errorMessage") +fun setErrorText(textInputLayout: TextInputLayout, isValid: Boolean, errorMessage: String) { + textInputLayout.error = if (isValid) null else errorMessage +} diff --git a/app/src/main/java/com/moataz/phood/identity/ui/view/utils/Constants.kt b/app/src/main/java/com/moataz/phood/identity/ui/view/utils/Constants.kt new file mode 100644 index 0000000..89aae4c --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/ui/view/utils/Constants.kt @@ -0,0 +1,5 @@ +package com.moataz.phood.identity.ui.view.utils + +object Constants { + const val INTRO_VIDEO_URL = "https://github-production-user-asset-6210df.s3.amazonaws.com/63272288/259910193-32a4a5c7-4844-4a49-837e-375965048de8.mp4" +} diff --git a/app/src/main/java/com/moataz/phood/identity/ui/view/utils/CustomPlayerManager.kt b/app/src/main/java/com/moataz/phood/identity/ui/view/utils/CustomPlayerManager.kt new file mode 100644 index 0000000..b5a66fd --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/ui/view/utils/CustomPlayerManager.kt @@ -0,0 +1,56 @@ +package com.moataz.phood.identity.ui.view.utils + +import android.content.Context +import android.view.View +import android.view.View.GONE +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView + +class CustomPlayerManager( + private val context: Context, + private val playerView: PlayerView, +) { + private lateinit var videoPlayer: ExoPlayer + + init { + initPlayer() + } + + private fun initPlayer() { + videoPlayer = ExoPlayer.Builder(context).build() + playerView.player = videoPlayer + videoPlayer.prepare() + videoPlayer.playWhenReady = true + videoPlayer.repeatMode = Player.REPEAT_MODE_ALL + } + + fun setMediaItem(uri: String) { + val mediaItem = MediaItem.Builder().setUri(uri).build() + videoPlayer.setMediaItem(mediaItem) + videoPlayer.prepare() + } + + fun displayCustomViewBeforePlayVideo(view: View) { + playerView.visibility = GONE + view.visibility = VISIBLE + } + + fun displayVideoPlayerAfterVideoPlay(view: View) { + videoPlayer.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == ExoPlayer.STATE_READY) { + playerView.visibility = VISIBLE + view.visibility = INVISIBLE + } + } + }) + } + + fun release() { + videoPlayer.release() + } +} diff --git a/app/src/main/java/com/moataz/phood/identity/ui/viewmodel/LoginViewModel.kt b/app/src/main/java/com/moataz/phood/identity/ui/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000..fa57092 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/identity/ui/viewmodel/LoginViewModel.kt @@ -0,0 +1,67 @@ +package com.moataz.phood.identity.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.moataz.phood.identity.domain.usecases.LoggedInStatus +import com.moataz.phood.identity.domain.usecases.SignInUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val signInUseCase: SignInUseCase, + private val loggedInStatusUseCase: LoggedInStatus, +) : ViewModel() { + + val email = MutableStateFlow("mohamed.salah@phood.com") + val password = MutableStateFlow("Brightskies") + + private val _isEmailValid = MutableStateFlow(true) + val isEmailValid = _isEmailValid + + private val _isPasswordValid = MutableStateFlow(true) + val isPasswordValid = _isPasswordValid + + private val _isUserLoggedIn = Channel() + val isUserLoggedIn get() = _isUserLoggedIn.receiveAsFlow() + + private val _isLoginFailed = Channel() + val isLoginFailed get() = _isLoginFailed.receiveAsFlow() + + init { + checkIfUserLoggedIn() + } + + private fun checkIfUserLoggedIn() { + viewModelScope.launch { + val isLoggedIn = loggedInStatusUseCase() + if (isLoggedIn) { + _isUserLoggedIn.send(true) + } + } + } + + fun onLoginClicked() { + if (email.value.isNotEmpty() && password.value.isNotEmpty()) { + viewModelScope.launch { + val isUserLoggedIn = signInUseCase(email.value, password.value) + if (isUserLoggedIn) { + checkIfUserLoggedIn() + } else { + _isLoginFailed.send(true) + } + } + } else { + if (email.value.isEmpty()) { + _isEmailValid.value = false + } + if (password.value.isEmpty()) { + _isPasswordValid.value = false + } + } + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/local/RecipesDao.kt b/app/src/main/java/com/moataz/phood/recipes/data/local/RecipesDao.kt new file mode 100644 index 0000000..297c175 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/local/RecipesDao.kt @@ -0,0 +1,26 @@ +package com.moataz.phood.recipes.data.local + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.moataz.phood.recipes.data.local.entity.RecipeEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface RecipesDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertRecipe(vararg recipeEntity: RecipeEntity) + + @Query("DELETE FROM RECIPE_TABLE") + suspend fun deleteRecipe() + + @Query("SELECT * FROM RECIPE_TABLE WHERE recipeType = :recipeType") + fun getRecipesByCategory(recipeType: String): Flow> + + @Query("SELECT * FROM RECIPE_TABLE") + fun getAllRecipes(): Flow> + + @Query("SELECT * FROM RECIPE_TABLE") + suspend fun getRecipes(): List +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/local/RecipesDatabase.kt b/app/src/main/java/com/moataz/phood/recipes/data/local/RecipesDatabase.kt new file mode 100644 index 0000000..3d12264 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/local/RecipesDatabase.kt @@ -0,0 +1,16 @@ +package com.moataz.phood.recipes.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.moataz.phood.recipes.data.local.entity.RecipeEntity +import com.moataz.phood.recipes.data.local.utils.Converters + +@Database( + entities = [RecipeEntity::class], + version = 1, +) +@TypeConverters(Converters::class) +abstract class RecipesDatabase : RoomDatabase() { + abstract fun recipesDao(): RecipesDao +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/local/entity/RecipeEntity.kt b/app/src/main/java/com/moataz/phood/recipes/data/local/entity/RecipeEntity.kt new file mode 100644 index 0000000..ebd10a9 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/local/entity/RecipeEntity.kt @@ -0,0 +1,23 @@ +package com.moataz.phood.recipes.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.moataz.phood.recipes.data.local.utils.EntityConstant.RECIPE_TABLE + +@Entity(tableName = RECIPE_TABLE) +data class RecipeEntity( + @PrimaryKey + val id: String, + val name: String, + val image: String, + val difficulty: Int, + val headline: String, + val fats: String, + val carbos: String, + val calories: String, + val proteins: String, + val description: String, + val time: String, + val ingredients: List, + val recipeType: String, +) diff --git a/app/src/main/java/com/moataz/phood/recipes/data/local/utils/Converters.kt b/app/src/main/java/com/moataz/phood/recipes/data/local/utils/Converters.kt new file mode 100644 index 0000000..a590cf5 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/local/utils/Converters.kt @@ -0,0 +1,15 @@ +package com.moataz.phood.recipes.data.local.utils + +import androidx.room.TypeConverter + +class Converters { + @TypeConverter + fun fromList(list: List): String { + return list.joinToString(",") + } + + @TypeConverter + fun toList(data: String): List { + return data.split(",") + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/local/utils/EntityConstant.kt b/app/src/main/java/com/moataz/phood/recipes/data/local/utils/EntityConstant.kt new file mode 100644 index 0000000..01643da --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/local/utils/EntityConstant.kt @@ -0,0 +1,5 @@ +package com.moataz.phood.recipes.data.local.utils + +object EntityConstant { + const val RECIPE_TABLE = "RECIPE_TABLE" +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/remote/RecipesService.kt b/app/src/main/java/com/moataz/phood/recipes/data/remote/RecipesService.kt new file mode 100644 index 0000000..b8fe981 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/remote/RecipesService.kt @@ -0,0 +1,9 @@ +package com.moataz.phood.recipes.data.remote + +import com.moataz.phood.recipes.data.remote.dto.RecipeDTO +import retrofit2.http.GET + +interface RecipesService { + @GET("43427003d33f1f6b51cc") + suspend fun getRecipes(): List +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/remote/dto/RecipeDTO.kt b/app/src/main/java/com/moataz/phood/recipes/data/remote/dto/RecipeDTO.kt new file mode 100644 index 0000000..e8ff248 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/remote/dto/RecipeDTO.kt @@ -0,0 +1,32 @@ +package com.moataz.phood.recipes.data.remote.dto + +import com.google.gson.annotations.SerializedName + +data class RecipeDTO( + @SerializedName("id") + val id: String?, + @SerializedName("name") + val name: String?, + @SerializedName("image") + val image: String?, + @SerializedName("difficulty") + val difficulty: Int?, + @SerializedName("headline") + val headline: String?, + @SerializedName("fats") + val fats: String?, + @SerializedName("carbos") + val carbos: String?, + @SerializedName("calories") + val calories: String?, + @SerializedName("proteins") + val proteins: String?, + @SerializedName("description") + val description: String?, + @SerializedName("time") + val time: String?, + @SerializedName("ingredients") + val ingredients: List?, + @SerializedName("products") + val products: List?, +) diff --git a/app/src/main/java/com/moataz/phood/recipes/data/repositories/RecipesRepositoryImpl.kt b/app/src/main/java/com/moataz/phood/recipes/data/repositories/RecipesRepositoryImpl.kt new file mode 100644 index 0000000..2a364db --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/repositories/RecipesRepositoryImpl.kt @@ -0,0 +1,56 @@ +package com.moataz.phood.recipes.data.repositories + +import com.moataz.phood.recipes.data.local.RecipesDao +import com.moataz.phood.recipes.data.remote.RecipesService +import com.moataz.phood.recipes.data.repositories.mapper.toRecipeEntity +import com.moataz.phood.recipes.data.repositories.mapper.toRecipesLocal +import com.moataz.phood.recipes.data.repositories.mapper.toRecipesRemote +import com.moataz.phood.recipes.data.repositories.utils.NetworkHelper +import com.moataz.phood.recipes.domain.entities.Recipe +import com.moataz.phood.recipes.domain.repository.RecipesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class RecipesRepositoryImpl @Inject constructor( + private val recipesRemoteService: RecipesService, + private val recipesLocalDatabase: RecipesDao, + private val networkHelper: NetworkHelper, +) : RecipesRepository { + + override suspend fun getRecipesFromRemote(): List { + return recipesRemoteService.getRecipes().toRecipesRemote() + } + + override suspend fun insertRecipe() { + recipesRemoteService.getRecipes().map { recipeDTO -> + recipesLocalDatabase.insertRecipe(recipeDTO.toRecipeEntity()) + } + } + + override suspend fun deleteRecipe() { + recipesLocalDatabase.deleteRecipe() + } + + override suspend fun cachingRecipes() { + if (recipesLocalDatabase.getRecipes().isEmpty()) { + if (networkHelper.isInternetAvailable()) { + getRecipesFromRemote() + deleteRecipe() + insertRecipe() + } + } + } + + override fun getRecipesByCategoryFromLocal(recipeType: String): Flow> { + return recipesLocalDatabase.getRecipesByCategory(recipeType).map { recipesEntities -> + recipesEntities.toRecipesLocal() + } + } + + override fun getAllRecipesTypesFromLocal(): Flow> { + return recipesLocalDatabase.getAllRecipes().map { recipesEntities -> + recipesEntities.toRecipesLocal() + } + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/repositories/mapper/RecipesMapper.kt b/app/src/main/java/com/moataz/phood/recipes/data/repositories/mapper/RecipesMapper.kt new file mode 100644 index 0000000..6da5a40 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/repositories/mapper/RecipesMapper.kt @@ -0,0 +1,67 @@ +package com.moataz.phood.recipes.data.repositories.mapper + +import com.moataz.phood.recipes.data.local.entity.RecipeEntity +import com.moataz.phood.recipes.data.remote.dto.RecipeDTO +import com.moataz.phood.recipes.domain.entities.Recipe + +internal fun List.toRecipesRemote(): List { + return map { recipeDTO -> recipeDTO.toRecipe() } +} + +internal fun RecipeDTO.toRecipe(): Recipe { + return Recipe( + id = id ?: "", + name = name ?: "", + image = image ?: "", + difficulty = difficulty ?: 0, + headline = headline ?: "", + fats = fats ?: "", + carbos = carbos ?: "", + calories = calories ?: "", + proteins = proteins ?: "", + description = description ?: "", + time = time ?: "", + ingredients = ingredients ?: emptyList(), + products = products ?: emptyList(), + ) +} + +internal fun List.toRecipesLocal(): List { + return map { recipeDTO -> recipeDTO.toRecipe() } +} + +internal fun RecipeEntity.toRecipe(): Recipe { + return Recipe( + id = id, + name = name, + image = image, + difficulty = difficulty, + headline = headline, + fats = fats, + carbos = carbos, + calories = calories, + proteins = proteins, + description = description, + time = time, + ingredients = ingredients, + products = listOf(recipeType), + ) +} + +internal fun RecipeDTO.toRecipeEntity(): RecipeEntity { + return RecipeEntity( + id = id ?: "", + name = name ?: "", + image = image ?: "", + difficulty = difficulty ?: 0, + headline = headline ?: "", + fats = fats ?: "", + carbos = carbos ?: "", + calories = calories ?: "", + proteins = proteins ?: "", + description = description ?: "", + time = time ?: "", + ingredients = ingredients ?: emptyList(), + recipeType = products?.first() ?: "", + ) +} diff --git a/app/src/main/java/com/moataz/phood/recipes/data/repositories/utils/NetworkHelper.kt b/app/src/main/java/com/moataz/phood/recipes/data/repositories/utils/NetworkHelper.kt new file mode 100644 index 0000000..ce730ab --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/data/repositories/utils/NetworkHelper.kt @@ -0,0 +1,28 @@ +package com.moataz.phood.recipes.data.repositories.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkHelper @Inject constructor(@ApplicationContext private val context: Context) { + @SuppressLint("MissingPermission") + fun isInternetAvailable(): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + val networkInfo = connectivityManager.activeNetworkInfo + return networkInfo?.isConnectedOrConnecting == true + } + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/domain/entities/Recipe.kt b/app/src/main/java/com/moataz/phood/recipes/domain/entities/Recipe.kt new file mode 100644 index 0000000..df45f06 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/domain/entities/Recipe.kt @@ -0,0 +1,17 @@ +package com.moataz.phood.recipes.domain.entities + +data class Recipe( + val id: String, + val name: String, + val image: String, + val difficulty: Int, + val headline: String, + val fats: String, + val carbos: String, + val calories: String, + val proteins: String, + val description: String, + val time: String, + val ingredients: List, + val products: List, +) diff --git a/app/src/main/java/com/moataz/phood/recipes/domain/repository/RecipesRepository.kt b/app/src/main/java/com/moataz/phood/recipes/domain/repository/RecipesRepository.kt new file mode 100644 index 0000000..246016e --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/domain/repository/RecipesRepository.kt @@ -0,0 +1,13 @@ +package com.moataz.phood.recipes.domain.repository + +import com.moataz.phood.recipes.domain.entities.Recipe +import kotlinx.coroutines.flow.Flow + +interface RecipesRepository { + suspend fun getRecipesFromRemote(): List + suspend fun insertRecipe() + suspend fun deleteRecipe() + suspend fun cachingRecipes() + fun getRecipesByCategoryFromLocal(recipeType: String): Flow> + fun getAllRecipesTypesFromLocal(): Flow> +} diff --git a/app/src/main/java/com/moataz/phood/recipes/domain/usecases/GetRecipesByCategoriesUseCase.kt b/app/src/main/java/com/moataz/phood/recipes/domain/usecases/GetRecipesByCategoriesUseCase.kt new file mode 100644 index 0000000..8efce3b --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/domain/usecases/GetRecipesByCategoriesUseCase.kt @@ -0,0 +1,23 @@ +package com.moataz.phood.recipes.domain.usecases + +import com.moataz.phood.recipes.domain.entities.Recipe +import com.moataz.phood.recipes.domain.repository.RecipesRepository +import com.moataz.phood.recipes.domain.usecases.enums.RecipesTypes +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class GetRecipesByCategoriesUseCase @Inject constructor( + private val recipesRepository: RecipesRepository, +) { + suspend operator fun invoke(category: String): Flow> { + recipesRepository.cachingRecipes() + return if (category == RecipesTypes.ALL.pathName) { + recipesRepository.getAllRecipesTypesFromLocal() + } else { + recipesRepository.getRecipesByCategoryFromLocal(category).map { recipes -> + recipes.filter { recipe -> recipe.products.first() == category } + } + } + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/domain/usecases/GetRecipesUseCase.kt b/app/src/main/java/com/moataz/phood/recipes/domain/usecases/GetRecipesUseCase.kt new file mode 100644 index 0000000..68febb7 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/domain/usecases/GetRecipesUseCase.kt @@ -0,0 +1,13 @@ +package com.moataz.phood.recipes.domain.usecases + +import com.moataz.phood.recipes.domain.entities.Recipe +import com.moataz.phood.recipes.domain.repository.RecipesRepository +import javax.inject.Inject + +class GetRecipesUseCase @Inject constructor( + private val recipesRepository: RecipesRepository, +) { +// suspend operator fun invoke(): List { +// return recipesRepository.getRecipesByCategory() +// } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/domain/usecases/enums/RecipesTypes.kt b/app/src/main/java/com/moataz/phood/recipes/domain/usecases/enums/RecipesTypes.kt new file mode 100644 index 0000000..fe74b75 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/domain/usecases/enums/RecipesTypes.kt @@ -0,0 +1,5 @@ +package com.moataz.phood.recipes.domain.usecases.enums + +enum class RecipesTypes(val pathName: String) { + ALL("all"), +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/view/adapters/RecipesAdapter.kt b/app/src/main/java/com/moataz/phood/recipes/ui/view/adapters/RecipesAdapter.kt new file mode 100644 index 0000000..433662c --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/view/adapters/RecipesAdapter.kt @@ -0,0 +1,48 @@ +package com.moataz.phood.recipes.ui.view.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.moataz.phood.R +import com.moataz.phood.databinding.ItemRecipeBinding +import com.moataz.phood.recipes.ui.viewmodel.model.RecipeUI + +class RecipesAdapter( + private var recipes: List, +) : RecyclerView.Adapter() { + + fun setItems(newItems: List) { + val diffUtilResult = + DiffUtil.calculateDiff(RecipesDiffUtil(recipes, newItems, ::areContentsTheSame)) + recipes = newItems + diffUtilResult.dispatchUpdatesTo(this) + } + + private fun areContentsTheSame(oldItem: RecipeUI, newItem: RecipeUI): Boolean { + return oldItem == newItem + } + + override fun getItemCount(): Int = recipes.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipesViewHolder { + return RecipesViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_recipe, + parent, + false, + ), + ) + } + + override fun onBindViewHolder(holder: RecipesViewHolder, position: Int) { + holder.binding.run { + recipe = recipes[position] + } + } + + inner class RecipesViewHolder(val binding: ItemRecipeBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/view/adapters/RecipesDiffUtil.kt b/app/src/main/java/com/moataz/phood/recipes/ui/view/adapters/RecipesDiffUtil.kt new file mode 100644 index 0000000..1d5893f --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/view/adapters/RecipesDiffUtil.kt @@ -0,0 +1,27 @@ +package com.moataz.phood.recipes.ui.view.adapters + +import androidx.recyclerview.widget.DiffUtil +import com.moataz.phood.recipes.ui.viewmodel.model.RecipeUI + +class RecipesDiffUtil( + private val oldList: List, + private val newList: List, + private val areContentsTheSame: (RecipeUI, RecipeUI) -> Boolean, +) : DiffUtil.Callback() { + + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return areContentsTheSame(oldList[oldItemPosition], newList[newItemPosition]) + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/view/databinding/LoadImageDataBinding.kt b/app/src/main/java/com/moataz/phood/recipes/ui/view/databinding/LoadImageDataBinding.kt new file mode 100644 index 0000000..8d24139 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/view/databinding/LoadImageDataBinding.kt @@ -0,0 +1,12 @@ +package com.moataz.phood.recipes.ui.view.databinding + +import android.view.View +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import coil.load + +@BindingAdapter("imageUrl") +fun loadImage(view: View, imageUrl: String?) { + val image: ImageView = view as ImageView + image.load(imageUrl) +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/view/databinding/ValidationDataBinding.kt b/app/src/main/java/com/moataz/phood/recipes/ui/view/databinding/ValidationDataBinding.kt new file mode 100644 index 0000000..00adf71 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/view/databinding/ValidationDataBinding.kt @@ -0,0 +1,10 @@ +package com.moataz.phood.recipes.ui.view.databinding + +import android.view.View +import androidx.core.view.isVisible +import androidx.databinding.BindingAdapter + +@BindingAdapter("app:showIfTrue") +fun showIfTrue(view: View, condition: Boolean) { + view.isVisible = condition +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/view/screens/RecipesFragment.kt b/app/src/main/java/com/moataz/phood/recipes/ui/view/screens/RecipesFragment.kt new file mode 100644 index 0000000..d978803 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/view/screens/RecipesFragment.kt @@ -0,0 +1,59 @@ +package com.moataz.phood.recipes.ui.view.screens + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.moataz.phood.R +import com.moataz.phood.databinding.FragmentRecipesBinding +import com.moataz.phood.recipes.ui.view.adapters.RecipesAdapter +import com.moataz.phood.recipes.ui.viewmodel.RecipesViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class RecipesFragment : Fragment() { + private val viewModel: RecipesViewModel by viewModels() + private lateinit var recipesAdapter: RecipesAdapter + private lateinit var binding: FragmentRecipesBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = DataBindingUtil.inflate( + inflater, + R.layout.fragment_recipes, + container, + false, + ) + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initRecyclerView() + observeEvents() + } + + private fun initRecyclerView() { + recipesAdapter = RecipesAdapter(emptyList()) + binding.recipesRecyclerView.adapter = recipesAdapter + } + + private fun observeEvents() { + lifecycleScope.launch { + viewModel.recipesUiState.collect { recipesUIState -> + recipesAdapter.setItems(recipesUIState.recipes) + binding.recipesRecyclerView.scrollToPosition(0) + } + } + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/RecipesViewModel.kt b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/RecipesViewModel.kt new file mode 100644 index 0000000..02332ad --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/RecipesViewModel.kt @@ -0,0 +1,67 @@ +package com.moataz.phood.recipes.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.moataz.phood.recipes.domain.usecases.GetRecipesByCategoriesUseCase +import com.moataz.phood.recipes.ui.viewmodel.enums.RecipesTypes +import com.moataz.phood.recipes.ui.viewmodel.mapper.toRecipesUIModel +import com.moataz.phood.recipes.ui.viewmodel.model.RecipesUIState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RecipesViewModel @Inject constructor( + private val getRecipesUseCase: GetRecipesByCategoriesUseCase, +) : ViewModel() { + + private val _recipesUiState = MutableStateFlow(RecipesUIState()) + val recipesUiState get() = _recipesUiState.asStateFlow() + + private val currentRecipeType = MutableStateFlow(RecipesTypes.ALL) + + init { + getRecipes() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getRecipes() { + viewModelScope.launch { + try { + currentRecipeType.flatMapLatest { recipeType -> + getRecipesUseCase(recipeType.pathName) + }.collectLatest { recipesModel -> + _recipesUiState.update { recipesUiState -> + recipesUiState.copy( + isLoading = false, + isSuccessful = true, + isError = false, + recipes = recipesModel.toRecipesUIModel(), + ) + } + } + } catch (e: Exception) { + _recipesUiState.update { recipesUiState -> + recipesUiState.copy( + isLoading = false, + isSuccessful = false, + isError = true, + recipes = emptyList(), + ) + } + } + } + } + + fun onChipTypeClicked(recipeType: RecipesTypes) { + if (recipeType != currentRecipeType.value) { + currentRecipeType.value = recipeType + } + } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/enums/RecipesTypes.kt b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/enums/RecipesTypes.kt new file mode 100644 index 0000000..022ae8e --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/enums/RecipesTypes.kt @@ -0,0 +1,8 @@ +package com.moataz.phood.recipes.ui.viewmodel.enums + +enum class RecipesTypes(val pathName: String) { + ALL("all"), + FAMILY_BOX("family-box"), + CLASSIC_BOX("classic-box"), + VEGGIE_BOX("veggie-box"), +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/mapper/RecipesUIMapper.kt b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/mapper/RecipesUIMapper.kt new file mode 100644 index 0000000..f59f28d --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/mapper/RecipesUIMapper.kt @@ -0,0 +1,25 @@ +package com.moataz.phood.recipes.ui.viewmodel.mapper + +import com.moataz.phood.recipes.domain.entities.Recipe +import com.moataz.phood.recipes.ui.viewmodel.model.RecipeUI + +internal fun Recipe.toRecipeUIModel(): RecipeUI { + return RecipeUI( + id = id, + name = name, + image = image, + difficulty = difficulty, + headline = headline, + fats = fats, + carbos = carbos, + calories = calories, + proteins = proteins, + description = description, + time = time, + ingredients = ingredients, + ) +} + +internal fun List.toRecipesUIModel(): List { + return map { recipeUI -> recipeUI.toRecipeUIModel() } +} diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/model/RecipeUI.kt b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/model/RecipeUI.kt new file mode 100644 index 0000000..b99d40b --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/model/RecipeUI.kt @@ -0,0 +1,16 @@ +package com.moataz.phood.recipes.ui.viewmodel.model + +data class RecipeUI( + val id: String, + val name: String, + val image: String, + val difficulty: Int, + val headline: String, + val fats: String, + val carbos: String, + val calories: String, + val proteins: String, + val description: String, + val time: String, + val ingredients: List, +) diff --git a/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/model/RecipesUIState.kt b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/model/RecipesUIState.kt new file mode 100644 index 0000000..76bfed2 --- /dev/null +++ b/app/src/main/java/com/moataz/phood/recipes/ui/viewmodel/model/RecipesUIState.kt @@ -0,0 +1,8 @@ +package com.moataz.phood.recipes.ui.viewmodel.model + +data class RecipesUIState( + val isLoading: Boolean = true, + val isSuccessful: Boolean = false, + val isError: Boolean? = false, + val recipes: List = emptyList(), +) diff --git a/app/src/main/res/color/chip_background_color.xml b/app/src/main/res/color/chip_background_color.xml new file mode 100644 index 0000000..464c0e7 --- /dev/null +++ b/app/src/main/res/color/chip_background_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/chip_text_color.xml b/app/src/main/res/color/chip_text_color.xml new file mode 100644 index 0000000..d0664a5 --- /dev/null +++ b/app/src/main/res/color/chip_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector.xml b/app/src/main/res/color/selector.xml new file mode 100644 index 0000000..43bc2ca --- /dev/null +++ b/app/src/main/res/color/selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_button_style.xml b/app/src/main/res/drawable/circle_button_style.xml new file mode 100644 index 0000000..96e5bfd --- /dev/null +++ b/app/src/main/res/drawable/circle_button_style.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/error.png b/app/src/main/res/drawable/error.png new file mode 100644 index 0000000..719dd1e Binary files /dev/null and b/app/src/main/res/drawable/error.png differ diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml new file mode 100644 index 0000000..4d470b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..65e6e15 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..f317243 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/img_login_video_background.jpg b/app/src/main/res/drawable/img_login_video_background.jpg new file mode 100644 index 0000000..cdd3263 Binary files /dev/null and b/app/src/main/res/drawable/img_login_video_background.jpg differ diff --git a/app/src/main/res/drawable/img_recipe_fake.webp b/app/src/main/res/drawable/img_recipe_fake.webp new file mode 100644 index 0000000..1f11141 Binary files /dev/null and b/app/src/main/res/drawable/img_recipe_fake.webp differ diff --git a/app/src/main/res/drawable/img_shadow_background.png b/app/src/main/res/drawable/img_shadow_background.png new file mode 100644 index 0000000..a818de0 Binary files /dev/null and b/app/src/main/res/drawable/img_shadow_background.png differ diff --git a/app/src/main/res/drawable/shp_login_button_background.xml b/app/src/main/res/drawable/shp_login_button_background.xml new file mode 100644 index 0000000..da12304 --- /dev/null +++ b/app/src/main/res/drawable/shp_login_button_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/shp_search_style.xml b/app/src/main/res/drawable/shp_search_style.xml new file mode 100644 index 0000000..faafa5f --- /dev/null +++ b/app/src/main/res/drawable/shp_search_style.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..851565e --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 17eab17..83ca552 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,18 +1,25 @@ - + - + - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recipes.xml b/app/src/main/res/layout/fragment_recipes.xml new file mode 100644 index 0000000..cfe38e2 --- /dev/null +++ b/app/src/main/res/layout/fragment_recipes.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_recipe.xml b/app/src/main/res/layout/item_recipe.xml new file mode 100644 index 0000000..ee23036 --- /dev/null +++ b/app/src/main/res/layout/item_recipe.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..036d09b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..036d09b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..02b7196 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..16a866c Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..c771c9e 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..7167fd1 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..af0d69f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..50d5e10 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..f44cfdd 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..c320325 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..6337ee8 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..f6ceaaf 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..23ead43 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..4e114ed 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..1a70c3f 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a1c7c71 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..c9d9c5a 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/main_nav.xml b/app/src/main/res/navigation/main_nav.xml new file mode 100644 index 0000000..4803ee7 --- /dev/null +++ b/app/src/main/res/navigation/main_nav.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/strings.xml b/app/src/main/res/values-night/strings.xml new file mode 100644 index 0000000..fecf007 --- /dev/null +++ b/app/src/main/res/values-night/strings.xml @@ -0,0 +1,23 @@ + + + Phood: Recipes App + Explore the world of recipes + Password + Email + Login + shadow background + Please enter a valid email address + Please enter a valid password + Phood + Search for your favorites recipes + Recipes + All + Family Box + Classic Box + Veggie Box + Favourites Recipes Button + Search Recipes Button + Food Image + Something went wrong, please try again + Error Icon + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index fb5f101..13e45bb 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c8524cd..be402d6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,6 @@ - #FF000000 + #E6000000 #FFFFFFFF + #E6000000 \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..beab31f --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #000000 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 839bf3b..c0b1f5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,24 @@ - Phood + Phood: Recipes App + Explore the world of recipes + Password + Email + Login + shadow background + Please enter a valid email address + Please enter a valid password + + Hello blank fragment + Phood + Search for your favorites recipes + Recipes + All + Family Box + Classic Box + Veggie Box + Favourites Recipes Button + Search Recipes Button + Food Image + Something went wrong, please try again + Error Icon \ No newline at end of file diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml new file mode 100644 index 0000000..0f48e29 --- /dev/null +++ b/app/src/main/res/values/style.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 92fe34b..d4e88d1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,8 +2,17 @@ \ No newline at end of file diff --git a/build.gradle b/build.gradle index e31c929..31bf66c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,30 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext { + hilt_version = '2.45' + media3_version = '1.1.0' + datastore_version = "1.1.0-alpha04" + sdp_version = '1.1.0' + ssp_version = '1.1.0' + mockito_version = '3.12.4' + retrofit_version = '2.9.0' + coil_version = "2.3.0" + room_version = "2.5.2" + } + + repositories { + google() + mavenCentral() + } +} + plugins { -id 'com.android.application' version '8.1.0' apply false + id 'com.android.application' version '8.1.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.0' apply false + id 'com.google.devtools.ksp' version '1.8.21-1.0.11' apply false + id 'com.google.dagger.hilt.android' version '2.45' apply false +} + +tasks.register('clean', Delete) { + delete rootProject.buildDir } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031e..11a9b24 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true