diff --git a/app/build.gradle b/app/build.gradle index e1a793f..65bfcec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,6 +84,11 @@ android { buildFeatures { dataBinding true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion '1.4.3' } packagingOptions { @@ -109,6 +114,7 @@ dependencies { implementation common implementation network implementation dagger + implementation compose kapt commonKapt debugImplementation commonDebug diff --git a/app/src/main/kotlin/com/adesso/movee/domain/IntroductionPage.kt b/app/src/main/kotlin/com/adesso/movee/domain/IntroductionPage.kt new file mode 100644 index 0000000..147ae8e --- /dev/null +++ b/app/src/main/kotlin/com/adesso/movee/domain/IntroductionPage.kt @@ -0,0 +1,3 @@ +package com.adesso.movee.domain + +data class IntroductionPage(val imageResId: Int, val text: String) diff --git a/app/src/main/kotlin/com/adesso/movee/scene/introduction/DoneButton.kt b/app/src/main/kotlin/com/adesso/movee/scene/introduction/DoneButton.kt new file mode 100644 index 0000000..cbcd4cb --- /dev/null +++ b/app/src/main/kotlin/com/adesso/movee/scene/introduction/DoneButton.kt @@ -0,0 +1,26 @@ +package com.adesso.movee.scene.introduction + +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.dimensionResource +import com.adesso.movee.R + +@Composable +fun DoneButton( + introductionSkipButtonCallback: () -> Unit, + text: String, + modifier: Modifier = Modifier +) { + Button( + onClick = introductionSkipButtonCallback, + modifier = modifier.width(dimensionResource(id = R.dimen.introduction_done_button_width)), + colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray) + ) { + Text(text = text) + } +} diff --git a/app/src/main/kotlin/com/adesso/movee/scene/introduction/IndicatorDot.kt b/app/src/main/kotlin/com/adesso/movee/scene/introduction/IndicatorDot.kt new file mode 100644 index 0000000..a5c578c --- /dev/null +++ b/app/src/main/kotlin/com/adesso/movee/scene/introduction/IndicatorDot.kt @@ -0,0 +1,29 @@ +package com.adesso.movee.scene.introduction + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import com.adesso.movee.R + +@Composable +fun IndicatorDot(isSelected: Boolean) { + val indicatorColor = + if (isSelected) { + colorResource(id = R.color.introduction_selected_indicator_color) + } else { + Color.White + } + Box( + modifier = Modifier + .size(dimensionResource(id = R.dimen.introduction_indicator_box_size)) + .padding(dimensionResource(id = R.dimen.introduction_indicator_box_padding)) + .background(color = indicatorColor, shape = CircleShape) + ) +} diff --git a/app/src/main/kotlin/com/adesso/movee/scene/introduction/IntroductionScreen.kt b/app/src/main/kotlin/com/adesso/movee/scene/introduction/IntroductionScreen.kt new file mode 100644 index 0000000..babc1c9 --- /dev/null +++ b/app/src/main/kotlin/com/adesso/movee/scene/introduction/IntroductionScreen.kt @@ -0,0 +1,98 @@ +package com.adesso.movee.scene.introduction + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.adesso.movee.R +import com.adesso.movee.domain.IntroductionPage + +private const val PAGE_COUNT = 3 +private const val LAST_PAGE_INDEX = 2 + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun IntroductionScreen(introductionSkipButtonCallback: () -> Unit) { + val introductionPageList = listOf( + IntroductionPage( + imageResId = R.drawable.introduction_first, + text = stringResource(id = R.string.introduction_title_first) + ), + IntroductionPage( + imageResId = R.drawable.introduction_second, + text = stringResource(id = R.string.introduction_title_second) + ), + IntroductionPage( + imageResId = R.drawable.introduction_third, + text = stringResource(id = R.string.introduction_title_third) + ) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(colorResource(id = R.color.introduction_background_color)) + ) { + Image( + painter = painterResource(id = R.drawable.movee_logo), + contentDescription = null, + modifier = Modifier + .height(dimensionResource(id = R.dimen.introduction_header_height)) + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + ) + + val pagerState = rememberPagerState() + HorizontalPager( + state = pagerState, + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding( + bottom = dimensionResource(id = R.dimen.introduction_slide_bottom_padding) + ), + pageCount = PAGE_COUNT + ) { page -> + IntroductionSlideView(introductionPageList[page]) + } + + DoneButton( + introductionSkipButtonCallback, + text = if (pagerState.currentPage == LAST_PAGE_INDEX) { + stringResource(id = R.string.introduction_get_started) + } else { + stringResource(id = R.string.introduction_skip) + }, + modifier = Modifier + .padding(top = dimensionResource(id = R.dimen.introduction_done_button_top_padding)) + .align(Alignment.CenterHorizontally) + ) + + Row( + modifier = Modifier + .padding(vertical = dimensionResource(id = R.dimen.introduction_indicator_padding)) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Bottom + ) { + for (pageIndex in introductionPageList.indices) { + IndicatorDot(isSelected = pageIndex == pagerState.currentPage) + } + } + } +} diff --git a/app/src/main/kotlin/com/adesso/movee/scene/introduction/IntroductionSlideView.kt b/app/src/main/kotlin/com/adesso/movee/scene/introduction/IntroductionSlideView.kt new file mode 100644 index 0000000..59d019c --- /dev/null +++ b/app/src/main/kotlin/com/adesso/movee/scene/introduction/IntroductionSlideView.kt @@ -0,0 +1,41 @@ +package com.adesso.movee.scene.introduction + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import com.adesso.movee.R +import com.adesso.movee.domain.IntroductionPage + +@Composable +fun IntroductionSlideView(page: IntroductionPage) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = page.imageResId), + contentDescription = null, + modifier = Modifier.weight(1f) + ) + Text( + text = page.text, style = MaterialTheme.typography.body1, + modifier = Modifier.padding( + top = dimensionResource( + id = R.dimen.introduction_slide_text_top_padding + ) + ), + color = Color.White + ) + } +} diff --git a/app/src/main/kotlin/com/adesso/movee/scene/main/MainActivity.kt b/app/src/main/kotlin/com/adesso/movee/scene/main/MainActivity.kt index 6ff1bad..0734b40 100644 --- a/app/src/main/kotlin/com/adesso/movee/scene/main/MainActivity.kt +++ b/app/src/main/kotlin/com/adesso/movee/scene/main/MainActivity.kt @@ -2,9 +2,11 @@ package com.adesso.movee.scene.main import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.core.net.toUri +import androidx.core.view.isVisible import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.setupWithNavController @@ -14,22 +16,37 @@ import com.adesso.movee.databinding.ActivityMainBinding import com.adesso.movee.internal.extension.observeNonNull import com.adesso.movee.internal.extension.showPopup import com.adesso.movee.navigation.NavigationCommand +import com.adesso.movee.scene.introduction.IntroductionScreen class MainActivity : BaseBindingActivity() { override val layoutId get() = R.layout.activity_main + private lateinit var sharedPrefs: SharedPreferences val navController: NavController by lazy { findNavController(R.id.main_host_fragment) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + sharedPrefs = getPreferences(Context.MODE_PRIVATE) binder.viewModel = viewModel binder.lifecycleOwner = this observeNavigation() setupBottomNavigationView() listenDestinationChanges() + if (isFirstLaunch()) { + showIntroductionPage() + } + } + + private fun showIntroductionPage() { + binder.introductionPage.setContent { + binder.introductionPage.isVisible = true + val skipIntroductionCallback = { + binder.introductionPage.isVisible = false + } + IntroductionScreen(skipIntroductionCallback) + } } override fun onSupportNavigateUp(): Boolean { @@ -71,7 +88,16 @@ class MainActivity : BaseBindingActivity() { return args?.getBoolean(getString(R.string.arg_hide_bottom_nav)) == true } + private fun isFirstLaunch(): Boolean { + val isFirstLaunch = sharedPrefs.getBoolean(KEY_FIRST_LAUNCH, true) + if (isFirstLaunch) { + sharedPrefs.edit().putBoolean(KEY_FIRST_LAUNCH, false).apply() + } + return true + } + companion object { + private const val KEY_FIRST_LAUNCH = "first_launch" fun getStartIntent(context: Context) = Intent(context, MainActivity::class.java) } } diff --git a/app/src/main/kotlin/com/adesso/movee/scene/profile/ProfileViewModel.kt b/app/src/main/kotlin/com/adesso/movee/scene/profile/ProfileViewModel.kt index 08355df..0813263 100644 --- a/app/src/main/kotlin/com/adesso/movee/scene/profile/ProfileViewModel.kt +++ b/app/src/main/kotlin/com/adesso/movee/scene/profile/ProfileViewModel.kt @@ -3,7 +3,6 @@ package com.adesso.movee.scene.profile import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations import com.adesso.movee.base.BaseAndroidViewModel import com.adesso.movee.domain.FetchUserDetailsUseCase import com.adesso.movee.domain.GetLoginStateUseCase @@ -24,9 +23,12 @@ class ProfileViewModel @Inject constructor( private val _userDetails = MutableLiveData() val userDetails: LiveData get() = _userDetails private val _loginState = MutableLiveData() - val shouldShowUserDetails: LiveData = Transformations.map(_userDetails) { it != null } - val shouldShowLoginView: LiveData = Transformations.map(_loginState) { loginState -> - loginState == LoginState.LOGGED_IN + val shouldShowUserDetails: LiveData = MutableLiveData().apply { + value = _userDetails.value != null + } + + val shouldShowLoginView: LiveData = MutableLiveData().apply { + value = _loginState.value == LoginState.LOGGED_IN } init { diff --git a/app/src/main/kotlin/com/adesso/movee/scene/tvshowdetail/TvShowDetailViewModel.kt b/app/src/main/kotlin/com/adesso/movee/scene/tvshowdetail/TvShowDetailViewModel.kt index 43a4ecc..b2fa679 100644 --- a/app/src/main/kotlin/com/adesso/movee/scene/tvshowdetail/TvShowDetailViewModel.kt +++ b/app/src/main/kotlin/com/adesso/movee/scene/tvshowdetail/TvShowDetailViewModel.kt @@ -3,7 +3,6 @@ package com.adesso.movee.scene.tvshowdetail import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations import com.adesso.movee.base.BaseAndroidViewModel import com.adesso.movee.domain.FetchTvShowCreditsUseCase import com.adesso.movee.domain.FetchTvShowDetailUseCase @@ -25,8 +24,8 @@ class TvShowDetailViewModel @Inject constructor( val tvShowDetails: LiveData get() = _tvShowDetails private val _tvShowCredits = MutableLiveData() val tvShowCasts: LiveData> = - Transformations.map(_tvShowCredits) { tvShowCredits -> - tvShowCredits.cast + MutableLiveData>().apply { + value = _tvShowCredits.value?.cast.orEmpty() } fun fetchTvShowDetail(id: Long) { @@ -36,9 +35,7 @@ class TvShowDetailViewModel @Inject constructor( fetchTvShowDetailUseCase.run(FetchTvShowDetailUseCase.Params(id)) onUIThread { - tvShowDetailResult - .onSuccess(::postTvShowDetail) - .onFailure(::handleFailure) + tvShowDetailResult.onSuccess(::postTvShowDetail).onFailure(::handleFailure) } } } @@ -55,9 +52,7 @@ class TvShowDetailViewModel @Inject constructor( fetchTvShowCreditsUseCase.run(FetchTvShowCreditsUseCase.Params(id)) onUIThread { - tvShowCreditResult - .onSuccess(::postTvShowCredits) - .onFailure(::handleFailure) + tvShowCreditResult.onSuccess(::postTvShowCredits).onFailure(::handleFailure) } } } diff --git a/app/src/main/res/drawable/introduction_first.jpg b/app/src/main/res/drawable/introduction_first.jpg new file mode 100644 index 0000000..424eab7 Binary files /dev/null and b/app/src/main/res/drawable/introduction_first.jpg differ diff --git a/app/src/main/res/drawable/introduction_second.jpg b/app/src/main/res/drawable/introduction_second.jpg new file mode 100644 index 0000000..8913275 Binary files /dev/null and b/app/src/main/res/drawable/introduction_second.jpg differ diff --git a/app/src/main/res/drawable/introduction_third.jpg b/app/src/main/res/drawable/introduction_third.jpg new file mode 100644 index 0000000..7b14cce Binary files /dev/null and b/app/src/main/res/drawable/introduction_third.jpg differ diff --git a/app/src/main/res/drawable/login_page.png b/app/src/main/res/drawable/login_page.png new file mode 100644 index 0000000..e64b4dc Binary files /dev/null and b/app/src/main/res/drawable/login_page.png differ diff --git a/app/src/main/res/drawable/movee_logo.png b/app/src/main/res/drawable/movee_logo.png new file mode 100644 index 0000000..72c040a Binary files /dev/null and b/app/src/main/res/drawable/movee_logo.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fa33b16..83d2462 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -17,6 +17,12 @@ android:layout_height="match_parent" android:orientation="vertical"> + + @color/almost_black @color/almost_black_60 #393a3b + #FF056380 + #FF54B9EC diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8dfd80c..ab3b6e9 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -55,4 +55,15 @@ 100dp + + 200dp + 16dp + 1f + 90dp + 16dp + 8dp + 18dp + 4dp + 200dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad04406..bac9844 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,6 +75,14 @@ Login + + If you looking for latest movies + Welcome to best movies + Only with us only for you + Skip + Get Started + + Username Password diff --git a/config/dependencies.gradle b/config/dependencies.gradle index 3f0e931..11116c9 100644 --- a/config/dependencies.gradle +++ b/config/dependencies.gradle @@ -27,38 +27,40 @@ ext { ] version = [ - dagger : '2.45', - retrofit : '2.9.0', - okhttp : '4.10.0', - moshi : '1.14.0', - moshiLazyAdapter: '2.2', - constraintLayout: '2.1.4', - navigation : '2.5.3', - lifecycle : '2.5.1', - androidKtx : '1.9.0', - appCompat : '1.6.1', - coroutines : '1.3.9', - espresso : '3.0.2', - chucker : '3.5.2', - gson : '2.8.2', - glide : '4.14.2', - ok2curl : '0.8.0', - room : '2.5.0', - jUnit : '4.13.2', - mockito : '2.13.+', - lottie : '5.2.0', - material : '1.0.0', - preference : '1.2.0', - viewPager2 : '1.0.0', - timber : '5.0.1', - spotless : '6.15.0', - detekt : '1.21.0', - gradleVersions : '0.33.0', - playServices : '18.1.0', - crypto : '1.1.0-alpha03', - robolectric : '4.9', - ktlint : '0.40.0', - kotlinResult : '1.1.16' + dagger : '2.45', + retrofit : '2.9.0', + okhttp : '4.10.0', + moshi : '1.14.0', + moshiLazyAdapter : '2.2', + constraintLayout : '2.1.4', + navigation : '2.5.3', + lifecycle : '2.5.1', + androidKtx : '1.9.0', + appCompat : '1.6.1', + coroutines : '1.3.9', + espresso : '3.0.2', + chucker : '3.5.2', + gson : '2.8.2', + glide : '4.14.2', + ok2curl : '0.8.0', + room : '2.5.0', + jUnit : '4.13.2', + mockito : '2.13.+', + lottie : '5.2.0', + material : '1.0.0', + preference : '1.2.0', + viewPager2 : '1.0.0', + timber : '5.0.1', + spotless : '6.15.0', + detekt : '1.21.0', + gradleVersions : '0.33.0', + playServices : '18.1.0', + crypto : '1.1.0-alpha03', + robolectric : '4.9', + ktlint : '0.40.0', + kotlinResult : '1.1.16', + compose : '1.4.3', + composeContraint : '1.0.0-alpha08' ] dependency = [ @@ -88,6 +90,14 @@ ext { "com.google.dagger:dagger-android-support:$version.dagger", ], + compose = [ + "androidx.compose.ui:ui:$version.compose", + "androidx.compose.material:material:$version.compose", + "androidx.constraintlayout:constraintlayout-compose:$version.composeContraint", + "androidx.compose.ui:ui-tooling-preview:$version.compose", + "androidx.compose.foundation:foundation:$version.compose" + ], + common = [ "com.github.bumptech.glide:glide:$version.glide", "com.airbnb.android:lottie:$version.lottie",