From 5cd2c8df0674f596e39abfa06e48ff3c90c5bc11 Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Tue, 12 Dec 2023 14:59:58 +0900 Subject: [PATCH] v0.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸš€ Feature Updates - System Navigation Bar 색상도 ν…Œλ§ˆμ— λ°”λ€Œκ²Œλ” μˆ˜μ • - μƒν’ˆ 상세 ν™”λ©΄μ—μ„œ Graph μ˜μ—­μ΄ μ„Έλ‘œλ‘œ μŠ¬λΌμ΄λ“œν•  경우 슀크둀 μ˜μ—­μ— μ μš©λ˜λ„λ‘ λ³€κ²½ (μ΄μ œλΆ€ν„°λŠ” 길게 λˆ„λ₯Ό μ‹œ κ·Έλž˜ν”„ 라벨이 ν™œμ„±ν™”λ©λ‹ˆλ‹€) - 일뢀 νŽ˜μ΄μ§€μ—μ„œ μ ‘κ·Όμ„± κ°œμ„  πŸ”§ Bug Fixes - μƒν’ˆ λͺ©λ‘ ν™”λ©΄μ—μ„œ μ•Œλ¦Ό 토글을 껐닀가 μΌ°μ„λ•Œ ν™œμ„±ν™” μƒνƒœμ—μ„œ 꺼짐 μƒνƒœμ— μ•„μ΄μ½˜μ΄ λœ¨λŠ” 였λ₯˜ μˆ˜μ • - μƒν’ˆ λͺ©λ‘ ν™”λ©΄μ—μ„œ ν™”λ©΄μ˜ 토글값을 λ³€κ²½ν•œ ν›„, μƒˆλ‘œκ³ μΉ¨μ„ ν–ˆμ„λ•Œ μ›λž˜λŒ€λ‘œ λŒμ•„μ˜€λŠ” 문제 μˆ˜μ • - λ‘œλ”©μ€‘μΌλ•Œ μƒν’ˆ 상세 ν™”λ©΄μ—μ„œ λ²„νŠΌλ“€ λΉ„ν™œμ„±ν™” - Dynamic Theme을 μ§€μ›ν•˜μ§€ μ•ŠλŠ” OS의 경우 ν•΄λ‹Ή ν…Œλ§ˆ μ„€μ • View λΉ„ν™œμ„±ν™” - μƒν’ˆ, μƒν’ˆ μΆ”μ²œ λͺ©λ‘ μ „ν™˜μ‹œ 슀크둀 / λ‹Ήκ²¨μ„œ μƒˆλ‘œκ³ μΉ¨ 였λ₯˜ μˆ˜μ • - ν™”λ©΄ νšŒμ „ μ‹œ λ‹€μ΄μ–Όλ‘œκ·Έκ°€ μ‚¬λΌμ§€λŠ” ν˜„μƒ μˆ˜μ • - μƒν’ˆ 상세 νŽ˜μ΄μ§€μ—μ„œ ν™”λ©΄ νšŒμ „ μ‹œ Scroll Stateκ°€ μ΄ˆκΈ°ν™”λ˜λŠ” ν˜„μƒ μˆ˜μ • - λͺ¨λ“  νŽ˜μ΄μ§€μ—μ„œ λ²„νŠΌ 연속 μž…λ ₯ 방지 처리 - λͺ¨λ“  νŽ˜μ΄μ§€μ—μ„œ λ©”λͺ¨λ¦¬ λˆ„μˆ˜κ°€ μΌμ–΄λ‚˜λŠ” 일뢀 ν•­λͺ©λ“€ μˆ˜μ • - μ‹œμŠ€ν…œ 글꼴을 ν‚€μ› μ„λ•Œ UIκ°€ κΉ¨μ§€λŠ” 문제 μˆ˜μ • - ν…Œλ§ˆ λ³€κ²½ ν›„ μ•± μž¬μ‹œμž‘ μ‹œ ν…Œλ§ˆ 변경이 μ•ˆλ˜λŠ” 였λ₯˜ μˆ˜μ • - λͺ©ν‘œ 가격 μˆ˜μ • μ‹œ 가격 μˆ˜μ • ν™”λ©΄ 초기 κ°’ κΈ°μ‘΄ λͺ©ν‘œ κ°€κ²©μœΌλ‘œ μˆ˜μ • - λ‚΄ 정보 νŽ˜μ΄μ§€μ—μ„œ 이름에 문자만 λ‚˜μ˜¬ 수 μžˆλ„λ‘ μˆ˜μ • Material Chart Changelog: https://github.com/Taewan-P/material-android-chart/releases/tag/v0.2.0 Co-Authored-By: EunhoKang Co-Authored-By: ootr47 <83055885+ootr47@users.noreply.github.com> Co-Authored-By: 손문기 <39684860+Muungi@users.noreply.github.com> Co-Authored-By: ByeongIk Choi --- android/app/build.gradle.kts | 6 +- .../product/ProductRepositoryImpl.kt | 2 +- .../app/priceguard/ui/ErrorDialogFragment.kt | 44 ++++++ .../java/app/priceguard/ui/PriceGuardApp.kt | 22 +-- .../priceguard/ui/additem/AddItemActivity.kt | 14 +- .../confirm/ConfirmItemLinkFragment.kt | 3 +- .../additem/link/LinkHelperWebViewActivity.kt | 17 ++- .../additem/link/RegisterItemLinkFragment.kt | 11 +- .../setprice/SetTargetPriceFragment.kt | 86 +++++------ .../setprice/SetTargetPriceViewModel.kt | 5 +- .../priceguard/ui/data/DialogConfirmAction.kt | 7 + .../priceguard/ui/detail/DetailActivity.kt | 100 +++++++------ .../app/priceguard/ui/home/HomeActivity.kt | 5 +- .../ui/home/ProductSummaryAdapter.kt | 75 +++++++--- .../ui/home/list/ProductListFragment.kt | 27 ++-- .../ui/home/list/ProductListViewModel.kt | 32 +++-- .../ui/home/mypage/MyPageFragment.kt | 43 ++++-- .../ui/home/mypage/MyPageViewModel.kt | 11 +- .../recommend/RecommendedProductFragment.kt | 21 ++- .../ui/home/theme/ThemeDialogFragment.kt | 100 +++++++------ .../app/priceguard/ui/intro/IntroActivity.kt | 3 + .../app/priceguard/ui/login/LoginActivity.kt | 44 +++--- .../priceguard/ui/signup/SignupActivity.kt | 33 ++--- .../priceguard/ui/signup/SignupViewModel.kt | 3 +- .../ui/splash/SplashScreenActivity.kt | 3 + .../ui/util/ConfirmDialogFragment.kt | 81 +++++++++++ .../java/app/priceguard/ui/util/Dialog.kt | 57 ++++++++ .../ui/util/{ui => }/ImageViewAdapter.kt | 2 +- .../ui/util/NavControllerExtensions.kt | 12 ++ .../app/priceguard/ui/util/NavigationBar.kt | 17 +++ .../ui/util/{ui => }/NotificationSetting.kt | 2 +- .../{ui => }/drawable/ProgressIndicator.kt | 2 +- .../ui/util/ui/DisableAppBarScroll.kt | 32 ----- .../priceguard/ui/util/ui/NetworkDialog.kt | 42 ------ .../src/main/res/layout/activity_detail.xml | 89 +++++++----- .../layout/activity_link_helper_web_view.xml | 1 + .../src/main/res/layout/fragment_my_page.xml | 6 +- .../main/res/layout/fragment_product_list.xml | 1 + .../layout/fragment_recommended_product.xml | 1 + .../res/layout/fragment_set_target_price.xml | 4 +- .../app/src/main/res/navigation/nav_graph.xml | 3 + .../app/src/main/res/values-night/themes.xml | 2 + android/app/src/main/res/values/colors.xml | 2 + android/app/src/main/res/values/themes.xml | 4 +- android/release_notes.txt | 32 +++-- backend/src/auth/auth.service.ts | 6 +- backend/src/auth/jwt/jwt.repository.ts | 20 --- backend/src/auth/jwt/jwt.service.ts | 3 +- backend/src/cache/cache.node.ts | 11 ++ backend/src/cache/cache.service.ts | 114 +++++++++++++++ .../{utils/cache.ts => cache/rank.cache.ts} | 45 +++--- backend/src/cache/tracking.cache.ts | 120 ++++++++++++++++ backend/src/constants.ts | 6 +- backend/src/entities/token.entity.ts | 10 -- backend/src/firebase/firebase.config.ts | 2 +- backend/src/product/product.module.ts | 10 +- backend/src/product/product.service.ts | 135 ++++++++++-------- 57 files changed, 1056 insertions(+), 535 deletions(-) create mode 100644 android/app/src/main/java/app/priceguard/ui/ErrorDialogFragment.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/data/DialogConfirmAction.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/util/ConfirmDialogFragment.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/util/Dialog.kt rename android/app/src/main/java/app/priceguard/ui/util/{ui => }/ImageViewAdapter.kt (93%) create mode 100644 android/app/src/main/java/app/priceguard/ui/util/NavControllerExtensions.kt create mode 100644 android/app/src/main/java/app/priceguard/ui/util/NavigationBar.kt rename android/app/src/main/java/app/priceguard/ui/util/{ui => }/NotificationSetting.kt (89%) rename android/app/src/main/java/app/priceguard/ui/util/{ui => }/drawable/ProgressIndicator.kt (92%) delete mode 100644 android/app/src/main/java/app/priceguard/ui/util/ui/DisableAppBarScroll.kt delete mode 100644 android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt delete mode 100644 backend/src/auth/jwt/jwt.repository.ts create mode 100644 backend/src/cache/cache.node.ts create mode 100644 backend/src/cache/cache.service.ts rename backend/src/{utils/cache.ts => cache/rank.cache.ts} (74%) create mode 100644 backend/src/cache/tracking.cache.ts delete mode 100644 backend/src/entities/token.entity.ts diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 31914149..251fd6a3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "app.priceguard" minSdk = 29 targetSdk = 34 - versionCode = 4 - versionName = "0.3.0" + versionCode = 5 + versionName = "0.3.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -111,7 +111,7 @@ dependencies { kapt("androidx.hilt:hilt-compiler:1.1.0") // Material chart - implementation("app.priceguard:materialchart:0.1.2") + implementation("app.priceguard:materialchart:0.2.0") } kapt { diff --git a/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt index acc34735..88e32e05 100644 --- a/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt +++ b/android/app/src/main/java/app/priceguard/data/repository/product/ProductRepositoryImpl.kt @@ -175,7 +175,7 @@ class ProductRepositoryImpl @Inject constructor( dto.imageUrl ?: "", dto.price ?: 0, dto.rank ?: 0, - GraphDataConverter().toDataset(dto.priceData) + graphDataConverter.toDataset(dto.priceData) ) } ?: listOf() ) diff --git a/android/app/src/main/java/app/priceguard/ui/ErrorDialogFragment.kt b/android/app/src/main/java/app/priceguard/ui/ErrorDialogFragment.kt new file mode 100644 index 00000000..0ab68b0a --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/ErrorDialogFragment.kt @@ -0,0 +1,44 @@ +package app.priceguard.ui + +import android.app.Dialog +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import app.priceguard.R +import app.priceguard.data.repository.token.TokenRepository +import app.priceguard.ui.login.LoginActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ErrorDialogFragment : DialogFragment() { + + @Inject + lateinit var tokenRepository: TokenRepository + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder( + requireActivity(), + R.style.ThemeOverlay_App_MaterialAlertDialog + ).apply { + setTitle(getString(R.string.permission_denied_title)) + setMessage(getString(R.string.permission_denied_message)) + setPositiveButton(getString(R.string.confirm)) { _, _ -> goBackToLoginActivity(tokenRepository) } + }.create() + } + + override fun onStart() { + super.onStart() + dialog?.setCancelable(false) + } + + private fun goBackToLoginActivity(tokenRepository: TokenRepository) { + CoroutineScope(Dispatchers.IO).launch { tokenRepository.clearTokens() } + val intent = Intent(requireActivity(), LoginActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + requireActivity().finish() + } +} diff --git a/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt b/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt index 829419be..dc0e1908 100644 --- a/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt +++ b/android/app/src/main/java/app/priceguard/ui/PriceGuardApp.kt @@ -1,8 +1,6 @@ package app.priceguard.ui import android.app.Application -import android.app.UiModeManager -import android.content.Context import androidx.appcompat.app.AppCompatDelegate import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration @@ -47,23 +45,15 @@ class PriceGuardApp : Application(), Configuration.Provider { when (darkMode) { MODE_LIGHT -> { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - val uiModeManager = - getSystemService(Context.UI_MODE_SERVICE) as UiModeManager - uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_NO) - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) } MODE_DARK -> { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - val uiModeManager = - getSystemService(Context.UI_MODE_SERVICE) as UiModeManager - uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES) - } else { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - } + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + + else -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } } } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt b/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt index 132accb5..f33794bf 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/AddItemActivity.kt @@ -3,12 +3,13 @@ package app.priceguard.ui.additem import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.NavController -import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import app.priceguard.R import app.priceguard.databinding.ActivityAddItemBinding import app.priceguard.ui.additem.link.RegisterItemLinkFragmentDirections +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor +import app.priceguard.ui.util.safeNavigate import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -18,7 +19,7 @@ class AddItemActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + this.applySystemNavigationBarColor(SystemNavigationColorState.SURFACE) binding = ActivityAddItemBinding.inflate(layoutInflater) setStartDestination() setContentView(binding.root) @@ -43,13 +44,10 @@ class AddItemActivity : AppCompatActivity() { intent.getStringExtra("productCode") ?: "", intent.getStringExtra("productTitle") ?: "", intent.getIntExtra("productPrice", 0), - intent.getBooleanExtra("isAdding", true) + intent.getBooleanExtra("isAdding", true), + intent.getIntExtra("productTargetPrice", (intent.getIntExtra("productPrice", 0) * 0.8).toInt()) ) navController.safeNavigate(action) } } - - private fun NavController.safeNavigate(direction: NavDirections) { - currentDestination?.getAction(direction.actionId)?.run { navigate(direction) } - } } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt index f8f434a3..8f285a41 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/confirm/ConfirmItemLinkFragment.kt @@ -57,7 +57,8 @@ class ConfirmItemLinkFragment : Fragment() { arguments.getString("productCode") ?: "", arguments.getString("productName") ?: "", arguments.getInt("productPrice"), - true + true, + (arguments.getInt("productPrice") * 0.8).toInt() ) findNavController().navigate(action) } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt index 23727c6e..cb7996fe 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/link/LinkHelperWebViewActivity.kt @@ -1,12 +1,13 @@ package app.priceguard.ui.additem.link import android.os.Bundle +import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity import app.priceguard.databinding.ActivityLinkHelperWebViewBinding class LinkHelperWebViewActivity : AppCompatActivity() { - private lateinit var binding: ActivityLinkHelperWebViewBinding + private lateinit var webView: WebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -14,6 +15,18 @@ class LinkHelperWebViewActivity : AppCompatActivity() { binding = ActivityLinkHelperWebViewBinding.inflate(layoutInflater) setContentView(binding.root) - binding.wbLinkHelper.loadUrl("https://info-kr.priceguard.app/") + webView = WebView(this) + webView.loadUrl("https://info-kr.priceguard.app/") + binding.wbLinkHelper.addView(webView) + } + + override fun onDestroy() { + super.onDestroy() + binding.wbLinkHelper.removeAllViews() + webView.clearHistory() + webView.clearCache(true) + webView.loadUrl("about:blank") + webView.pauseTimers() + webView.destroy() } } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt index e5d3c2f3..25b033a9 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/link/RegisterItemLinkFragment.kt @@ -8,8 +8,6 @@ import android.view.ViewGroup import androidx.activity.addCallback import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.navigation.NavController -import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import app.priceguard.R import app.priceguard.data.repository.product.ProductErrorState @@ -17,7 +15,8 @@ import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentRegisterItemLinkBinding import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.showPermissionDeniedDialog +import app.priceguard.ui.util.safeNavigate +import app.priceguard.ui.util.showDialogWithLogout import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -111,7 +110,7 @@ class RegisterItemLinkFragment : Fragment() { is RegisterItemLinkViewModel.RegisterLinkEvent.FailureVerification -> { when (event.errorType) { ProductErrorState.PERMISSION_DENIED -> { - requireActivity().showPermissionDeniedDialog(tokenRepository) + showDialogWithLogout() } ProductErrorState.INVALID_REQUEST -> { @@ -132,10 +131,6 @@ class RegisterItemLinkFragment : Fragment() { binding.tvRegisterItemError.text = message } - private fun NavController.safeNavigate(direction: NavDirections) { - currentDestination?.getAction(direction.actionId)?.run { navigate(direction) } - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt index 209d6fd7..aa7e39be 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceFragment.kt @@ -1,6 +1,5 @@ package app.priceguard.ui.additem.setprice -import android.content.Intent import android.os.Bundle import android.text.Editable import android.view.LayoutInflater @@ -16,10 +15,10 @@ import app.priceguard.data.repository.product.ProductErrorState import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentSetTargetPriceBinding import app.priceguard.ui.additem.setprice.SetTargetPriceViewModel.SetTargetPriceEvent -import app.priceguard.ui.home.HomeActivity +import app.priceguard.ui.data.DialogConfirmAction import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.showPermissionDeniedDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import app.priceguard.ui.util.showDialogWithAction +import app.priceguard.ui.util.showDialogWithLogout import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider.OnSliderTouchListener import dagger.hilt.android.AndroidEntryPoint @@ -73,8 +72,9 @@ class SetTargetPriceFragment : Fragment() { val productCode = arguments.getString("productCode") ?: "" val title = arguments.getString("productTitle") ?: "" val price = arguments.getInt("productPrice") + var targetPrice = arguments.getInt("productTargetPrice") - setTargetPriceViewModel.updateTargetPrice((price * 0.8).toInt()) + setTargetPriceViewModel.updateTargetPrice(targetPrice) tvSetPriceCurrentPrice.text = String.format( @@ -85,7 +85,9 @@ class SetTargetPriceFragment : Fragment() { getString(R.string.current_price_info, tvSetPriceCurrentPrice.text) setTargetPriceViewModel.setProductInfo(productCode, title, price) - etTargetPrice.setText((price * 0.8).toInt().toString()) + etTargetPrice.setText(targetPrice.toString()) + + updateSlideValueWithPrice(targetPrice.toFloat()) } private fun FragmentSetTargetPriceBinding.initListener() { @@ -128,20 +130,7 @@ class SetTargetPriceFragment : Fragment() { } setTargetPriceViewModel.updateTargetPrice(targetPrice.toInt()) - - val percent = - ((targetPrice / setTargetPriceViewModel.state.value.productPrice) * MAX_PERCENT).toInt() - - binding.tvTargetPricePercent.text = - String.format(getString(R.string.current_price_percent), percent) - - binding.tvTargetPricePercent.contentDescription = getString( - R.string.target_price_percent_and_price, - binding.tvTargetPricePercent.text, - binding.tvSetPriceCurrentPrice.text - ) - - binding.updateSlideValueWithPrice(targetPrice, percent.roundAtFirstDigit()) + binding.updateSlideValueWithPrice(targetPrice) } } @@ -164,34 +153,37 @@ class SetTargetPriceFragment : Fragment() { setTargetPriceViewModel.event.collect { event -> when (event) { is SetTargetPriceEvent.SuccessProductAdd -> { - showActivityFinishDialog( + showDialogWithAction( getString(R.string.success_add), - getString(R.string.success_add_message) + getString(R.string.success_add_message), + DialogConfirmAction.FINISH ) } is SetTargetPriceEvent.SuccessPriceUpdate -> { - showActivityFinishDialog( + showDialogWithAction( getString(R.string.success_update), - getString(R.string.success_update_message) + getString(R.string.success_update_message), + DialogConfirmAction.FINISH ) } is SetTargetPriceEvent.FailurePriceAdd -> { when (event.errorType) { ProductErrorState.EXIST -> { - showActivityFinishDialog( + showDialogWithAction( getString(R.string.error_add_product), - getString(R.string.exist_product) + getString(R.string.exist_product), + DialogConfirmAction.FINISH ) } ProductErrorState.PERMISSION_DENIED -> { - requireActivity().showPermissionDeniedDialog(tokenRepository) + showDialogWithLogout() } else -> { - showActivityFinishDialog( + showDialogWithAction( getString(R.string.error), getString(R.string.retry) ) @@ -202,11 +194,11 @@ class SetTargetPriceFragment : Fragment() { is SetTargetPriceEvent.FailurePriceUpdate -> { when (event.errorType) { ProductErrorState.PERMISSION_DENIED -> { - requireActivity().showPermissionDeniedDialog(tokenRepository) + showDialogWithLogout() } else -> { - showActivityFinishDialog( + showDialogWithAction( getString(R.string.error_patch_price), getString(R.string.retry) ) @@ -218,34 +210,22 @@ class SetTargetPriceFragment : Fragment() { } } - private fun showActivityFinishDialog(title: String, message: String) { - MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.confirm) { _, _ -> goToHomeActivity() } - .setOnDismissListener { goToHomeActivity() } - .create() - .show() - } + private fun FragmentSetTargetPriceBinding.updateSlideValueWithPrice(targetPrice: Float) { + val percent = + ((targetPrice / setTargetPriceViewModel.state.value.productPrice) * MAX_PERCENT).toInt().roundAtFirstDigit() - private fun goToHomeActivity() { - val activityIntent = requireActivity().intent - if (activityIntent?.action == Intent.ACTION_SEND) { - val intent = Intent(requireActivity(), HomeActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - } - requireActivity().finish() - } - - private fun FragmentSetTargetPriceBinding.updateSlideValueWithPrice( - targetPrice: Float, - percent: Int - ) { val pricePercent = percent.coerceIn(MIN_PERCENT, MAX_PERCENT) if (targetPrice > setTargetPriceViewModel.state.value.productPrice) { tvTargetPricePercent.text = getString(R.string.over_current_price) + } else { + tvTargetPricePercent.text = + String.format(getString(R.string.current_price_percent), percent) } + binding.tvTargetPricePercent.contentDescription = getString( + R.string.target_price_percent_and_price, + binding.tvTargetPricePercent.text, + binding.tvSetPriceCurrentPrice.text + ) slTargetPrice.value = pricePercent.toFloat() } diff --git a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt index 51205227..c40b3d02 100644 --- a/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/additem/setprice/SetTargetPriceViewModel.kt @@ -21,7 +21,8 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository: val productCode: String = "", val targetPrice: Int = 0, val productName: String = "", - val productPrice: Int = 0 + val productPrice: Int = 0, + val isReady: Boolean = true ) sealed class SetTargetPriceEvent { @@ -39,6 +40,7 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository: fun addProduct() { viewModelScope.launch { + _state.value = state.value.copy(isReady = false) val response = productRepository.addProduct( _state.value.productCode, _state.value.targetPrice @@ -52,6 +54,7 @@ class SetTargetPriceViewModel @Inject constructor(private val productRepository: _event.emit(SetTargetPriceEvent.FailurePriceAdd(response.errorState)) } } + _state.value = state.value.copy(isReady = true) } } diff --git a/android/app/src/main/java/app/priceguard/ui/data/DialogConfirmAction.kt b/android/app/src/main/java/app/priceguard/ui/data/DialogConfirmAction.kt new file mode 100644 index 00000000..18de82ea --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/data/DialogConfirmAction.kt @@ -0,0 +1,7 @@ +package app.priceguard.ui.data + +enum class DialogConfirmAction { + NOTHING, + FINISH, + CUSTOM +} diff --git a/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt b/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt index e34804e6..7fe0a3e9 100644 --- a/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/detail/DetailActivity.kt @@ -1,11 +1,11 @@ package app.priceguard.ui.detail -import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle import android.widget.Toast import androidx.activity.addCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import app.priceguard.R @@ -16,24 +16,33 @@ import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.ActivityDetailBinding import app.priceguard.materialchart.data.GraphMode import app.priceguard.ui.additem.AddItemActivity +import app.priceguard.ui.data.DialogConfirmAction import app.priceguard.ui.home.HomeActivity +import app.priceguard.ui.util.ConfirmDialogFragment +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.showConfirmationDialog -import app.priceguard.ui.util.ui.showPermissionDeniedDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import app.priceguard.ui.util.showConfirmDialog +import app.priceguard.ui.util.showDialogWithLogout import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class DetailActivity : AppCompatActivity() { +class DetailActivity : AppCompatActivity(), ConfirmDialogFragment.OnDialogResultListener { @Inject lateinit var tokenRepository: TokenRepository private lateinit var binding: ActivityDetailBinding private val productDetailViewModel: ProductDetailViewModel by viewModels() + private val activityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + binding.btnDetailShare.isEnabled = true + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.applySystemNavigationBarColor(SystemNavigationColorState.SURFACE) binding = ActivityDetailBinding.inflate(layoutInflater) binding.lifecycleOwner = this binding.viewModel = productDetailViewModel @@ -74,6 +83,7 @@ class DetailActivity : AppCompatActivity() { intent.putExtra("productCode", productDetailViewModel.productCode) intent.putExtra("productTitle", productDetailViewModel.state.value.productName) intent.putExtra("productPrice", productDetailViewModel.state.value.price) + intent.putExtra("productTargetPrice", productDetailViewModel.state.value.targetPrice) intent.putExtra("isAdding", false) this@DetailActivity.startActivity(intent) } @@ -101,6 +111,7 @@ class DetailActivity : AppCompatActivity() { } binding.btnDetailShare.setOnClickListener { + binding.btnDetailShare.isEnabled = false val shareLink = getString(R.string.share_link_template, productDetailViewModel.productCode) @@ -112,7 +123,7 @@ class DetailActivity : AppCompatActivity() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + activityResultLauncher.launch(shareIntent) } } @@ -129,7 +140,11 @@ class DetailActivity : AppCompatActivity() { val productCodeFromDeepLink = deepLink?.getQueryParameter("code") if (productCode == null && productCodeFromDeepLink == null) { - showDialogAndExit(getString(R.string.error), getString(R.string.invalid_access)) + showConfirmDialog( + getString(R.string.error), + getString(R.string.invalid_access), + DialogConfirmAction.FINISH + ) return } @@ -165,20 +180,22 @@ class DetailActivity : AppCompatActivity() { productDetailViewModel.event.collect { event -> when (event) { ProductDetailViewModel.ProductDetailEvent.Logout -> { - showDialogAndExit(getString(R.string.error), getString(R.string.logged_out)) + showDialogWithLogout() } ProductDetailViewModel.ProductDetailEvent.NotFound -> { - showDialogAndExit( + showConfirmDialog( getString(R.string.error), - getString(R.string.product_not_found) + getString(R.string.product_not_found), + DialogConfirmAction.FINISH ) } ProductDetailViewModel.ProductDetailEvent.UnknownError -> { - showDialogAndExit( + showConfirmDialog( getString(R.string.error), - getString(R.string.undefined_error) + getString(R.string.undefined_error), + DialogConfirmAction.FINISH ) } @@ -190,34 +207,31 @@ class DetailActivity : AppCompatActivity() { } ProductDetailViewModel.ProductDetailEvent.DeleteTracking -> { - showConfirmationDialog( - getString(R.string.stop_tracking_confirm), - getString(R.string.stop_tracking_detail) - ) { _, _ -> productDetailViewModel.deleteProductTracking() } + showConfirmationDialogForResult() } is ProductDetailViewModel.ProductDetailEvent.DeleteFailed -> { when (event.errorType) { ProductErrorState.NOT_FOUND -> { - showConfirmationDialog( + showConfirmDialog( getString(R.string.delete_product_failed), getString(R.string.product_not_found) ) } ProductErrorState.INVALID_REQUEST -> { - showConfirmationDialog( + showConfirmDialog( getString(R.string.delete_product_failed), getString(R.string.invalid_request) ) } ProductErrorState.PERMISSION_DENIED -> { - showPermissionDeniedDialog(tokenRepository) + showDialogWithLogout() } else -> { - showConfirmationDialog( + showConfirmDialog( getString(R.string.delete_product_failed), getString(R.string.undefined_error) ) @@ -254,7 +268,11 @@ class DetailActivity : AppCompatActivity() { } private fun goToHomeActivityIfDeepLinked() { - if (intent.data?.getQueryParameter("code") != null || intent.getBooleanExtra("directed", false)) { + if (intent.data?.getQueryParameter("code") != null || intent.getBooleanExtra( + "directed", + false + ) + ) { val intent = Intent(this@DetailActivity, HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) @@ -262,31 +280,27 @@ class DetailActivity : AppCompatActivity() { finish() } - private fun showConfirmationDialog( - title: String, - message: String, - onConfirm: DialogInterface.OnClickListener - ) { - MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(title) - .setMessage(message) - .setPositiveButton(getString(R.string.confirm), onConfirm) - .setNegativeButton(getString(R.string.cancel)) { _, _ -> } - .create() - .show() - } - - private fun showDialogAndExit(title: String, message: String) { - MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(title) - .setMessage(message) - .setPositiveButton(getString(R.string.confirm)) { _, _ -> finish() } - .setCancelable(false) - .create() - .show() + private fun showConfirmationDialogForResult() { + val tag = "confirm_dialog_fragment_from_activity" + if (supportFragmentManager.findFragmentByTag(tag) != null) return + + val dialogFragment = ConfirmDialogFragment() + val bundle = Bundle() + bundle.putString("title", getString(R.string.stop_tracking_confirm)) + bundle.putString("message", getString(R.string.stop_tracking_detail)) + bundle.putString("actionString", DialogConfirmAction.CUSTOM.name) + dialogFragment.arguments = bundle + dialogFragment.setOnDialogResultListener(this) + dialogFragment.show(supportFragmentManager, tag) } private fun showToast(message: String) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } + + override fun onDialogResult(result: Boolean) { + if (result) { + productDetailViewModel.deleteProductTracking() + } + } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt b/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt index e3c789ff..14a48b7c 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/HomeActivity.kt @@ -16,7 +16,9 @@ import androidx.work.WorkManager import app.priceguard.R import app.priceguard.databinding.ActivityHomeBinding import app.priceguard.service.UpdateTokenWorker -import app.priceguard.ui.util.ui.openNotificationSettings +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor +import app.priceguard.ui.util.openNotificationSettings import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.material.snackbar.Snackbar @@ -31,6 +33,7 @@ class HomeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.applySystemNavigationBarColor(SystemNavigationColorState.BOTTOM_NAVIGATION) binding = ActivityHomeBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt index 3590c0a7..cf282f0d 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/ProductSummaryAdapter.kt @@ -13,8 +13,11 @@ import app.priceguard.data.graph.ProductChartDataset import app.priceguard.databinding.ItemProductSummaryBinding import app.priceguard.materialchart.data.GraphMode -class ProductSummaryAdapter(private val productSummaryClickListener: ProductSummaryClickListener) : - ListAdapter(diffUtil) { +class ProductSummaryAdapter( + private val productSummaryClickListener: ProductSummaryClickListener, + diffUtil: DiffUtil.ItemCallback +) : + ListAdapter(diffUtil) { init { setHasStableIds(true) @@ -43,6 +46,7 @@ class ProductSummaryAdapter(private val productSummaryClickListener: ProductSumm fun bind(item: ProductSummary) { with(binding) { + resetListener() summary = item setViewType(item) setClickListener(item.productCode) @@ -50,6 +54,10 @@ class ProductSummaryAdapter(private val productSummaryClickListener: ProductSumm } } + private fun ItemProductSummaryBinding.resetListener() { + msProduct.setOnCheckedChangeListener(null) + } + private fun ItemProductSummaryBinding.setViewType(item: ProductSummary) { when (item) { is ProductSummary.RecommendedProductSummary -> { @@ -71,18 +79,22 @@ class ProductSummaryAdapter(private val productSummaryClickListener: ProductSumm } private fun ItemProductSummaryBinding.setSwitchListener(item: ProductSummary) { - if (msProduct.isChecked.not()) { - msProduct.setThumbIconResource(R.drawable.ic_notifications_off) - } + updateThumbIcon(msProduct.isChecked) + msProduct.setOnCheckedChangeListener { _, isChecked -> - productSummaryClickListener.onToggle(item.productCode, msProduct.isChecked) - if (isChecked) { - msProduct.setThumbIconResource(R.drawable.ic_notifications_active) - } else { - msProduct.setThumbIconResource(R.drawable.ic_notifications_off) - } + productSummaryClickListener.onToggle(item.productCode, isChecked) + updateThumbIcon(isChecked) + } + msProduct.contentDescription = + msProduct.context.getString(R.string.single_product_notification_toggle, item.title) + } + + private fun ItemProductSummaryBinding.updateThumbIcon(checked: Boolean) { + if (checked) { + msProduct.setThumbIconResource(R.drawable.ic_notifications_active) + } else { + msProduct.setThumbIconResource(R.drawable.ic_notifications_off) } - msProduct.contentDescription = msProduct.context.getString(R.string.single_product_notification_toggle, item.title) } private fun ItemProductSummaryBinding.setDiscount(discount: Float) { @@ -105,14 +117,21 @@ class ProductSummaryAdapter(private val productSummaryClickListener: ProductSumm true ) tvProductDiscountPercent.setTextColor(value.data) - tvProductDiscountPercent.contentDescription = tvProductDiscountPercent.context.getString(R.string.target_price_delta, tvProductDiscountPercent.text) + tvProductDiscountPercent.contentDescription = + tvProductDiscountPercent.context.getString( + R.string.target_price_delta, + tvProductDiscountPercent.text + ) } private fun ItemProductSummaryBinding.setRecommendRank(item: ProductSummary.RecommendedProductSummary) { tvProductRecommendRank.text = tvProductRecommendRank.context.getString( R.string.recommand_rank, item.recommendRank ) - tvProductRecommendRank.contentDescription = tvProductRecommendRank.context.getString(R.string.current_rank_info, item.recommendRank) + tvProductRecommendRank.contentDescription = tvProductRecommendRank.context.getString( + R.string.current_rank_info, + item.recommendRank + ) } private fun ItemProductSummaryBinding.setClickListener(code: String) { @@ -136,12 +155,28 @@ class ProductSummaryAdapter(private val productSummaryClickListener: ProductSumm } companion object { - val diffUtil = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: ProductSummary, newItem: ProductSummary) = - oldItem == newItem - - override fun areItemsTheSame(oldItem: ProductSummary, newItem: ProductSummary) = - oldItem.productCode == newItem.productCode + val userDiffUtil = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: ProductSummary.UserProductSummary, + newItem: ProductSummary.UserProductSummary + ) = oldItem.productCode == newItem.productCode && + oldItem.price == newItem.price && + oldItem.discountPercent == newItem.discountPercent && + oldItem.title == newItem.title + + override fun areItemsTheSame( + oldItem: ProductSummary.UserProductSummary, + newItem: ProductSummary.UserProductSummary + ) = oldItem.productCode == newItem.productCode } + + val diffUtil = + object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: ProductSummary, newItem: ProductSummary) = + oldItem == newItem + + override fun areItemsTheSame(oldItem: ProductSummary, newItem: ProductSummary) = + oldItem.productCode == newItem.productCode + } } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt index d10b195d..bb08a5ea 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListFragment.kt @@ -5,10 +5,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.SimpleItemAnimator import androidx.work.WorkManager import app.priceguard.R import app.priceguard.data.repository.product.ProductErrorState @@ -20,9 +18,8 @@ import app.priceguard.ui.detail.DetailActivity import app.priceguard.ui.home.ProductSummaryAdapter import app.priceguard.ui.home.ProductSummaryClickListener import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.disableAppBarRecyclerView -import app.priceguard.ui.util.ui.showConfirmationDialog -import app.priceguard.ui.util.ui.showPermissionDeniedDialog +import app.priceguard.ui.util.showDialogWithAction +import app.priceguard.ui.util.showDialogWithLogout import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -54,10 +51,6 @@ class ProductListFragment : Fragment() { binding.initSettingAdapter() binding.initListener() collectEvent() - disableAppBarRecyclerView( - binding.ablProductList.layoutParams as CoordinatorLayout.LayoutParams, - binding.rvProductList - ) } override fun onStart() { @@ -66,11 +59,6 @@ class ProductListFragment : Fragment() { } private fun FragmentProductListBinding.initSettingAdapter() { - val animator = rvProductList.itemAnimator - if (animator is SimpleItemAnimator) { - animator.supportsChangeAnimations = false - } - val listener = object : ProductSummaryClickListener { override fun onClick(productCode: String) { val intent = Intent(context, DetailActivity::class.java) @@ -88,7 +76,7 @@ class ProductListFragment : Fragment() { } } - val adapter = ProductSummaryAdapter(listener) + val adapter = ProductSummaryAdapter(listener, ProductSummaryAdapter.userDiffUtil) rvProductList.adapter = adapter this@ProductListFragment.repeatOnStarted { productListViewModel.productList.collect { list -> @@ -117,25 +105,25 @@ class ProductListFragment : Fragment() { productListViewModel.events.collect { event -> when (event) { ProductErrorState.PERMISSION_DENIED -> { - requireActivity().showPermissionDeniedDialog(tokenRepository) + showDialogWithLogout() } ProductErrorState.INVALID_REQUEST -> { - requireActivity().showConfirmationDialog( + showDialogWithAction( getString(R.string.product_list_failed), getString(R.string.invalid_request) ) } ProductErrorState.NOT_FOUND -> { - requireActivity().showConfirmationDialog( + showDialogWithAction( getString(R.string.product_list_failed), getString(R.string.not_found) ) } else -> { - requireActivity().showConfirmationDialog( + showDialogWithAction( getString(R.string.product_list_failed), getString(R.string.undefined_error) ) @@ -156,6 +144,7 @@ class ProductListFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() + binding.rvProductList.adapter = null _binding = null } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt index 5da268c8..d1cdafc5 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/list/ProductListViewModel.kt @@ -7,6 +7,7 @@ import app.priceguard.data.repository.RepositoryResult import app.priceguard.data.repository.product.ProductErrorState import app.priceguard.data.repository.product.ProductRepository import app.priceguard.materialchart.data.GraphMode +import app.priceguard.ui.data.ProductData import app.priceguard.ui.home.ProductSummary.UserProductSummary import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -46,17 +47,7 @@ class ProductListViewModel @Inject constructor( when (result) { is RepositoryResult.Success -> { - _productList.value = result.data.map { data -> - UserProductSummary( - data.shop, - data.productName, - data.price, - data.productCode, - graphDataConverter.packWithEdgeData(data.priceData, GraphMode.WEEK), - calculateDiscountRate(data.targetPrice, data.price), - data.isAlert - ) - } + updateProductList(isRefresh, result.data) } is RepositoryResult.Error -> { @@ -66,6 +57,25 @@ class ProductListViewModel @Inject constructor( } } + private fun updateProductList(refresh: Boolean, fetched: List) { + val productMap = mutableMapOf() + _productList.value.forEach { product -> + productMap[product.productCode] = product.isAlarmOn + } + + _productList.value = fetched.map { data -> + UserProductSummary( + data.shop, + data.productName, + data.price, + data.productCode, + graphDataConverter.packWithEdgeData(data.priceData, GraphMode.WEEK), + calculateDiscountRate(data.targetPrice, data.price), + if (refresh) productMap[data.productCode] ?: data.isAlert else data.isAlert + ) + } + } + fun updateProductAlarmToggle(productCode: String, checked: Boolean) { _productList.value = productList.value.mapIndexed { _, product -> if (product.productCode == productCode) { diff --git a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt index 7ee3e6d4..77c0c7b3 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageFragment.kt @@ -12,23 +12,25 @@ import androidx.navigation.fragment.findNavController import app.priceguard.R import app.priceguard.data.repository.token.TokenRepository import app.priceguard.databinding.FragmentMyPageBinding +import app.priceguard.ui.data.DialogConfirmAction import app.priceguard.ui.home.mypage.MyPageViewModel.MyPageEvent import app.priceguard.ui.intro.IntroActivity +import app.priceguard.ui.util.ConfirmDialogFragment import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.openNotificationSettings +import app.priceguard.ui.util.openNotificationSettings +import app.priceguard.ui.util.safeNavigate import com.google.android.gms.oss.licenses.OssLicensesMenuActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class MyPageFragment : Fragment() { +class MyPageFragment : Fragment(), ConfirmDialogFragment.OnDialogResultListener { @Inject lateinit var tokenRepository: TokenRepository private var _binding: FragmentMyPageBinding? = null private val binding get() = _binding!! - private val viewModel: MyPageViewModel by viewModels() + private val myPageViewModel: MyPageViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -36,7 +38,7 @@ class MyPageFragment : Fragment() { savedInstanceState: Bundle? ): View { _binding = FragmentMyPageBinding.inflate(layoutInflater, container, false) - binding.viewModel = viewModel + binding.viewModel = myPageViewModel binding.lifecycleOwner = viewLifecycleOwner return binding.root } @@ -47,7 +49,7 @@ class MyPageFragment : Fragment() { initSettingAdapter() repeatOnStarted { - viewModel.event.collect { event -> + myPageViewModel.event.collect { event -> when (event) { is MyPageEvent.StartIntroAndExitHome -> startIntroAndExitHome() } @@ -68,7 +70,7 @@ class MyPageFragment : Fragment() { } Setting.THEME -> { - findNavController().navigate(R.id.action_myPageFragment_to_themeDialogFragment) + findNavController().safeNavigate(R.id.action_myPageFragment_to_themeDialogFragment) } Setting.LICENSE -> { @@ -76,7 +78,7 @@ class MyPageFragment : Fragment() { } Setting.LOGOUT -> { - showLogoutConfirmDialog() + showConfirmationDialogForResult() } } } @@ -109,13 +111,18 @@ class MyPageFragment : Fragment() { ) } - private fun showLogoutConfirmDialog() { - MaterialAlertDialogBuilder(requireActivity(), R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(getString(R.string.logout_confirm_title)) - .setMessage(getString(R.string.logout_confirm_message)) - .setPositiveButton(getString(R.string.yes)) { _, _ -> viewModel.logout() } - .setNegativeButton(R.string.no) { dialog, _ -> dialog.dismiss() } - .show() + private fun showConfirmationDialogForResult() { + val tag = "confirm_dialog_fragment_from_activity" + if (requireActivity().supportFragmentManager.findFragmentByTag(tag) != null) return + + val dialogFragment = ConfirmDialogFragment() + val bundle = Bundle() + bundle.putString("title", getString(R.string.logout_confirm_title)) + bundle.putString("message", getString(R.string.logout_confirm_message)) + bundle.putString("actionString", DialogConfirmAction.CUSTOM.name) + dialogFragment.arguments = bundle + dialogFragment.setOnDialogResultListener(this) + dialogFragment.show(requireActivity().supportFragmentManager, tag) } private fun startIntroAndExitHome() { @@ -124,6 +131,12 @@ class MyPageFragment : Fragment() { requireActivity().finish() } + override fun onDialogResult(result: Boolean) { + if (result) { + myPageViewModel.logout() + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt index 3ec33420..12e49dcf 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/mypage/MyPageViewModel.kt @@ -43,7 +43,7 @@ class MyPageViewModel @Inject constructor( MyPageInfo( userData.name, userData.email, - if (userData.name.isNotEmpty()) userData.name.first().toString() else "" + getFirstName(userData.name) ) } } @@ -54,4 +54,13 @@ class MyPageViewModel @Inject constructor( _event.emit(MyPageEvent.StartIntroAndExitHome) } } + + private fun getFirstName(name: String): String { + name.forEach { + if (it.isSurrogate().not() && it.toString().isNotBlank()) { + return it.uppercase() + } + } + return "" + } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt index bee82f27..f6f54181 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/recommend/RecommendedProductFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import app.priceguard.R @@ -16,9 +15,8 @@ import app.priceguard.ui.detail.DetailActivity import app.priceguard.ui.home.ProductSummaryAdapter import app.priceguard.ui.home.ProductSummaryClickListener import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.disableAppBarRecyclerView -import app.priceguard.ui.util.ui.showConfirmationDialog -import app.priceguard.ui.util.ui.showPermissionDeniedDialog +import app.priceguard.ui.util.showDialogWithAction +import app.priceguard.ui.util.showDialogWithLogout import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -48,10 +46,6 @@ class RecommendedProductFragment : Fragment() { binding.initSettingAdapter() binding.initListener() collectEvent() - disableAppBarRecyclerView( - binding.ablRecommendedProduct.layoutParams as CoordinatorLayout.LayoutParams, - binding.rvRecommendedProduct - ) } override fun onStart() { @@ -72,7 +66,7 @@ class RecommendedProductFragment : Fragment() { } } - val adapter = ProductSummaryAdapter(listener) + val adapter = ProductSummaryAdapter(listener, ProductSummaryAdapter.diffUtil) rvRecommendedProduct.adapter = adapter this@RecommendedProductFragment.repeatOnStarted { recommendedProductViewModel.recommendedProductList.collect { list -> @@ -92,25 +86,25 @@ class RecommendedProductFragment : Fragment() { recommendedProductViewModel.events.collect { event -> when (event) { ProductErrorState.PERMISSION_DENIED -> { - requireActivity().showPermissionDeniedDialog(tokenRepository) + showDialogWithLogout() } ProductErrorState.INVALID_REQUEST -> { - requireActivity().showConfirmationDialog( + showDialogWithAction( getString(R.string.recommended_product_failed), getString(R.string.invalid_request) ) } ProductErrorState.NOT_FOUND -> { - requireActivity().showConfirmationDialog( + showDialogWithAction( getString(R.string.recommended_product_failed), getString(R.string.not_found) ) } else -> { - requireActivity().showConfirmationDialog( + showDialogWithAction( getString(R.string.recommended_product_failed), getString(R.string.undefined_error) ) @@ -122,6 +116,7 @@ class RecommendedProductFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() + binding.rvRecommendedProduct.adapter = null _binding = null } } diff --git a/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt b/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt index 58e8160b..ede9df7d 100644 --- a/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt +++ b/android/app/src/main/java/app/priceguard/ui/home/theme/ThemeDialogFragment.kt @@ -1,6 +1,7 @@ package app.priceguard.ui.home.theme import android.app.Dialog +import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatDelegate import androidx.fragment.app.DialogFragment @@ -22,13 +23,15 @@ class ThemeDialogFragment : DialogFragment() { @Inject lateinit var configDataSource: ConfigDataSource + private var _binding: FragmentThemeDialogBinding? = null + private val binding get() = _binding!! override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val binding: FragmentThemeDialogBinding = - FragmentThemeDialogBinding.inflate(requireActivity().layoutInflater) + _binding = FragmentThemeDialogBinding.inflate(requireActivity().layoutInflater) val view = binding.root - setCheckedButton(binding) + setCheckedButton() + checkDynamicThemeSupport() return MaterialAlertDialogBuilder( requireActivity(), @@ -36,48 +39,65 @@ class ThemeDialogFragment : DialogFragment() { ).apply { setView(view) setPositiveButton(R.string.confirm) { _, _ -> - val dynamicMode = when (binding.rgDynamicColor.checkedRadioButtonId) { - R.id.rb_yes -> { - DynamicColors.applyToActivitiesIfAvailable(requireActivity().application) - requireActivity().recreate() - PriceGuardApp.MODE_DYNAMIC - } + val dynamicMode = applyDynamicMode() + val darkMode = applyDarkMode() + requireActivity().recreate() - else -> { - DynamicColors.applyToActivitiesIfAvailable( - requireActivity().application, - DynamicColorsOptions.Builder() - .setThemeOverlay(R.style.Theme_PriceGuard).build() - ) - requireActivity().recreate() - PriceGuardApp.MODE_DYNAMIC_NO - } - } + saveTheme(dynamicMode, darkMode) + dismiss() + } + }.create() + } - val darkMode = when (binding.rgDarkMode.checkedRadioButtonId) { - R.id.rb_system -> { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - PriceGuardApp.MODE_SYSTEM - } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } - R.id.rb_light -> { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - PriceGuardApp.MODE_LIGHT - } + private fun applyDynamicMode() = when (binding.rgDynamicColor.checkedRadioButtonId) { + R.id.rb_yes -> { + DynamicColors.applyToActivitiesIfAvailable(requireActivity().application) + PriceGuardApp.MODE_DYNAMIC + } - R.id.rb_dark -> { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - PriceGuardApp.MODE_DARK - } + else -> { + DynamicColors.applyToActivitiesIfAvailable( + requireActivity().application, + DynamicColorsOptions.Builder() + .setThemeOverlay(R.style.Theme_PriceGuard).build() + ) + PriceGuardApp.MODE_DYNAMIC_NO + } + } - else -> { - PriceGuardApp.MODE_SYSTEM - } - } - saveTheme(dynamicMode, darkMode) - dismiss() + private fun applyDarkMode() = when (binding.rgDarkMode.checkedRadioButtonId) { + R.id.rb_system -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + PriceGuardApp.MODE_SYSTEM + } + + R.id.rb_light -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + PriceGuardApp.MODE_LIGHT + } + + R.id.rb_dark -> { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + PriceGuardApp.MODE_DARK + } + + else -> { + PriceGuardApp.MODE_SYSTEM + } + } + + private fun checkDynamicThemeSupport() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // Disable Dynamic Theme Radio Group + (0 until binding.rgDynamicColor.childCount).forEach { idx -> + binding.rgDynamicColor.getChildAt(idx).isEnabled = false } - }.create() + } } private fun saveTheme(dynamicMode: Int, darkMode: Int) { @@ -87,7 +107,7 @@ class ThemeDialogFragment : DialogFragment() { } } - private fun setCheckedButton(binding: FragmentThemeDialogBinding) { + private fun setCheckedButton() { lifecycleScope.launch { val dynamicColorMode = configDataSource.getDynamicMode() val darkMode = configDataSource.getDarkMode() diff --git a/android/app/src/main/java/app/priceguard/ui/intro/IntroActivity.kt b/android/app/src/main/java/app/priceguard/ui/intro/IntroActivity.kt index f2ea2e8d..9b745ef0 100644 --- a/android/app/src/main/java/app/priceguard/ui/intro/IntroActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/intro/IntroActivity.kt @@ -6,6 +6,8 @@ import androidx.appcompat.app.AppCompatActivity import app.priceguard.databinding.ActivityIntroBinding import app.priceguard.ui.login.LoginActivity import app.priceguard.ui.signup.SignupActivity +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -14,6 +16,7 @@ class IntroActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.applySystemNavigationBarColor(SystemNavigationColorState.SURFACE) binding = ActivityIntroBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt b/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt index ed65567f..c2dca38c 100644 --- a/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/login/LoginActivity.kt @@ -10,34 +10,42 @@ import app.priceguard.databinding.ActivityLoginBinding import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.login.LoginViewModel.LoginEvent import app.priceguard.ui.signup.SignupActivity +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor +import app.priceguard.ui.util.drawable.getCircularProgressIndicatorDrawable import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.drawable.getCircularProgressIndicatorDrawable +import app.priceguard.ui.util.showConfirmDialog import com.google.android.material.button.MaterialButton -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.progressindicator.CircularProgressIndicatorSpec +import com.google.android.material.progressindicator.IndeterminateDrawable import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class LoginActivity : AppCompatActivity() { private val loginViewModel: LoginViewModel by viewModels() private lateinit var binding: ActivityLoginBinding + private lateinit var circularProgressIndicator: IndeterminateDrawable override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.applySystemNavigationBarColor(SystemNavigationColorState.SURFACE) binding = ActivityLoginBinding.inflate(layoutInflater) with(binding) { viewModel = loginViewModel } + circularProgressIndicator = getCircularProgressIndicatorDrawable(this@LoginActivity) initListener() collectEvent() setContentView(binding.root) } + override fun onDestroy() { + super.onDestroy() + circularProgressIndicator.stop() + } + private fun initListener() { with(binding) { - btnLoginLogin.setOnClickListener { - (binding.btnLoginLogin as MaterialButton).icon = - getCircularProgressIndicatorDrawable(this@LoginActivity) - } btnLoginSignup.setOnClickListener { gotoSignUp() } @@ -49,8 +57,7 @@ class LoginActivity : AppCompatActivity() { loginViewModel.event.collect { event -> when (event) { LoginEvent.LoginStart -> { - (binding.btnLoginLogin as MaterialButton).icon = - getCircularProgressIndicatorDrawable(this@LoginActivity) + (binding.btnLoginLogin as MaterialButton).icon = circularProgressIndicator } LoginEvent.TokenUpdateError, LoginEvent.FirebaseError -> { @@ -73,18 +80,24 @@ class LoginActivity : AppCompatActivity() { private fun setDialogMessageAndShow(eventType: LoginEvent) { when (eventType) { LoginEvent.Invalid -> { - showDialog( + showConfirmDialog( getString(R.string.login_invalid), getString(R.string.login_invalid_message) ) } is LoginEvent.LoginFailure -> { - showDialog(getString(R.string.login_fail), getString(R.string.login_fail_message)) + showConfirmDialog( + getString(R.string.login_fail), + getString(R.string.login_fail_message) + ) } is LoginEvent.UndefinedError -> { - showDialog(getString(R.string.login_fail), getString(R.string.undefined_error)) + showConfirmDialog( + getString(R.string.login_fail), + getString(R.string.undefined_error) + ) } is LoginEvent.LoginInfoSaved -> { @@ -95,15 +108,6 @@ class LoginActivity : AppCompatActivity() { } } - private fun showDialog(title: String, message: String) { - MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(title) - .setMessage(message) - .setPositiveButton(getString(R.string.confirm)) { _, _ -> } - .create() - .show() - } - private fun gotoSignUp() { startActivity(Intent(this, SignupActivity::class.java)) } diff --git a/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt b/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt index 2591df62..1fe52127 100644 --- a/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt @@ -13,12 +13,16 @@ import app.priceguard.databinding.ActivitySignupBinding import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.signup.SignupViewModel.SignupEvent import app.priceguard.ui.signup.SignupViewModel.SignupUIState +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor +import app.priceguard.ui.util.drawable.getCircularProgressIndicatorDrawable import app.priceguard.ui.util.lifecycle.repeatOnStarted -import app.priceguard.ui.util.ui.drawable.getCircularProgressIndicatorDrawable +import app.priceguard.ui.util.showConfirmDialog import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback import com.google.android.material.button.MaterialButton -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.progressindicator.CircularProgressIndicatorSpec +import com.google.android.material.progressindicator.IndeterminateDrawable import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -26,17 +30,25 @@ class SignupActivity : AppCompatActivity() { private lateinit var binding: ActivitySignupBinding private val signupViewModel: SignupViewModel by viewModels() + private lateinit var circularProgressIndicator: IndeterminateDrawable override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.applySystemNavigationBarColor(SystemNavigationColorState.SURFACE) binding = DataBindingUtil.setContentView(this, R.layout.activity_signup) binding.vm = signupViewModel binding.lifecycleOwner = this + circularProgressIndicator = getCircularProgressIndicatorDrawable(this@SignupActivity) setNavigationButton() disableAppBarScroll() observeState() } + override fun onDestroy() { + super.onDestroy() + circularProgressIndicator.stop() + } + private fun disableAppBarScroll() { val clLayoutParams = binding.ablSignupTopbar.layoutParams as CoordinatorLayout.LayoutParams val scrollView: NestedScrollView = binding.nsvSignupContent @@ -64,8 +76,6 @@ class SignupActivity : AppCompatActivity() { } private fun handleSignupEvent(event: SignupEvent) { - val circularProgressIndicator = getCircularProgressIndicatorDrawable(this) - when (event) { is SignupEvent.SignupStart -> { (binding.btnSignupSignup as MaterialButton).icon = circularProgressIndicator @@ -79,15 +89,15 @@ class SignupActivity : AppCompatActivity() { } SignupEvent.DuplicatedEmail -> { - showDialog(getString(R.string.error), getString(R.string.duplicate_email)) + showConfirmDialog(getString(R.string.error), getString(R.string.duplicate_email)) } SignupEvent.InvalidRequest -> { - showDialog(getString(R.string.error), getString(R.string.invalid_parameter)) + showConfirmDialog(getString(R.string.error), getString(R.string.invalid_parameter)) } SignupEvent.UndefinedError -> { - showDialog(getString(R.string.error), getString(R.string.undefined_error)) + showConfirmDialog(getString(R.string.error), getString(R.string.undefined_error)) } SignupEvent.TokenUpdateError, SignupEvent.FirebaseError -> { @@ -117,15 +127,6 @@ class SignupActivity : AppCompatActivity() { } } - private fun showDialog(title: String, message: String) { - MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(title) - .setMessage(message) - .setPositiveButton(getString(R.string.confirm)) { _, _ -> } - .create() - .show() - } - private fun observeState() { repeatOnStarted { signupViewModel.state.collect { state -> diff --git a/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt b/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt index b5e5d10f..1ed9b68c 100644 --- a/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt +++ b/android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt @@ -65,9 +65,10 @@ class SignupViewModel @Inject constructor( } viewModelScope.launch { + updateSignupStarted(true) sendSignupEvent(SignupEvent.SignupStart) Log.d("ViewModel", "Event Start Sent") - updateSignupStarted(true) + val result = authRepository.signUp(_state.value.email, _state.value.name, _state.value.password) diff --git a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt index e5af9705..15d756a5 100644 --- a/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt +++ b/android/app/src/main/java/app/priceguard/ui/splash/SplashScreenActivity.kt @@ -13,6 +13,8 @@ import app.priceguard.databinding.ActivitySplashScreenBinding import app.priceguard.ui.detail.DetailActivity import app.priceguard.ui.home.HomeActivity import app.priceguard.ui.intro.IntroActivity +import app.priceguard.ui.util.SystemNavigationColorState +import app.priceguard.ui.util.applySystemNavigationBarColor import app.priceguard.ui.util.lifecycle.repeatOnCreated import dagger.hilt.android.AndroidEntryPoint @@ -25,6 +27,7 @@ class SplashScreenActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.applySystemNavigationBarColor(SystemNavigationColorState.SURFACE) binding = ActivitySplashScreenBinding.inflate(layoutInflater) setContentView(binding.root) observeState() diff --git a/android/app/src/main/java/app/priceguard/ui/util/ConfirmDialogFragment.kt b/android/app/src/main/java/app/priceguard/ui/util/ConfirmDialogFragment.kt new file mode 100644 index 00000000..fdd309ed --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/util/ConfirmDialogFragment.kt @@ -0,0 +1,81 @@ +package app.priceguard.ui.util + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import app.priceguard.R +import app.priceguard.ui.data.DialogConfirmAction +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +class ConfirmDialogFragment : DialogFragment() { + + private lateinit var title: String + private lateinit var message: String + private lateinit var action: DialogConfirmAction + private lateinit var negativeButtonText: String + + private var resultListener: OnDialogResultListener? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + initDialogInfo() + + return MaterialAlertDialogBuilder( + requireActivity(), + R.style.ThemeOverlay_App_MaterialAlertDialog + ).apply { + setTitle(title) + setMessage(message) + setNegativeButton(negativeButtonText) { _, _ -> } + setPositiveButton(getString(R.string.confirm)) { _, _ -> + when (action) { + DialogConfirmAction.FINISH -> { + requireActivity().finish() + } + + DialogConfirmAction.CUSTOM -> { + resultListener?.onDialogResult(true) + } + + DialogConfirmAction.NOTHING -> {} + } + dismiss() + } + }.create() + } + + private fun initDialogInfo() { + title = arguments?.getString("title") ?: "" + message = arguments?.getString("message") ?: "" + + val actionString = arguments?.getString("actionString") + actionString?.let { actionString -> + action = DialogConfirmAction.valueOf(actionString) + }?.run { + DialogConfirmAction.NOTHING + } + + negativeButtonText = if (action == DialogConfirmAction.CUSTOM) { + getString(R.string.cancel) + } else { + "" + } + } + + override fun onStart() { + super.onStart() + + if (action == DialogConfirmAction.NOTHING || action == DialogConfirmAction.CUSTOM) { + dialog?.setCancelable(true) + } else { + dialog?.setCancelable(false) + } + } + + fun setOnDialogResultListener(listener: OnDialogResultListener) { + resultListener = listener + } + + interface OnDialogResultListener { + fun onDialogResult(result: Boolean) + } +} diff --git a/android/app/src/main/java/app/priceguard/ui/util/Dialog.kt b/android/app/src/main/java/app/priceguard/ui/util/Dialog.kt new file mode 100644 index 00000000..f6350913 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/util/Dialog.kt @@ -0,0 +1,57 @@ +package app.priceguard.ui.util + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import app.priceguard.ui.ErrorDialogFragment +import app.priceguard.ui.data.DialogConfirmAction + +fun AppCompatActivity.showConfirmDialog( + title: String, + message: String, + action: DialogConfirmAction = DialogConfirmAction.NOTHING +) { + val tag = "confirm_dialog_fragment_from_activity" + if (supportFragmentManager.findFragmentByTag(tag) != null) return + + val dialogFragment = ConfirmDialogFragment() + val bundle = Bundle() + bundle.putString("title", title) + bundle.putString("message", message) + bundle.putString("actionString", action.name) + dialogFragment.arguments = bundle + dialogFragment.show(supportFragmentManager, tag) +} + +fun Fragment.showDialogWithAction( + title: String, + message: String, + action: DialogConfirmAction = DialogConfirmAction.NOTHING +) { + val tag = "confirm_dialog_fragment_from_activity" + if (requireActivity().supportFragmentManager.findFragmentByTag(tag) != null) return + + val dialogFragment = ConfirmDialogFragment() + val bundle = Bundle() + bundle.putString("title", title) + bundle.putString("message", message) + bundle.putString("actionString", action.name) + dialogFragment.arguments = bundle + dialogFragment.show(requireActivity().supportFragmentManager, tag) +} + +fun AppCompatActivity.showDialogWithLogout() { + val tag = "error_dialog_fragment_from_fragment" + if (supportFragmentManager.findFragmentByTag(tag) != null) return + + val dialogFragment = ErrorDialogFragment() + dialogFragment.show(supportFragmentManager, "error_dialog_fragment_from_activity") +} + +fun Fragment.showDialogWithLogout() { + val tag = "error_dialog_fragment_from_fragment" + if (requireActivity().supportFragmentManager.findFragmentByTag(tag) != null) return + + val dialogFragment = ErrorDialogFragment() + dialogFragment.show(requireActivity().supportFragmentManager, tag) +} diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/ImageViewAdapter.kt b/android/app/src/main/java/app/priceguard/ui/util/ImageViewAdapter.kt similarity index 93% rename from android/app/src/main/java/app/priceguard/ui/util/ui/ImageViewAdapter.kt rename to android/app/src/main/java/app/priceguard/ui/util/ImageViewAdapter.kt index 7a49eeee..d0779059 100644 --- a/android/app/src/main/java/app/priceguard/ui/util/ui/ImageViewAdapter.kt +++ b/android/app/src/main/java/app/priceguard/ui/util/ImageViewAdapter.kt @@ -1,4 +1,4 @@ -package app.priceguard.ui.util.ui +package app.priceguard.ui.util import android.widget.ImageView import androidx.databinding.BindingAdapter diff --git a/android/app/src/main/java/app/priceguard/ui/util/NavControllerExtensions.kt b/android/app/src/main/java/app/priceguard/ui/util/NavControllerExtensions.kt new file mode 100644 index 00000000..7b45e79c --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/util/NavControllerExtensions.kt @@ -0,0 +1,12 @@ +package app.priceguard.ui.util + +import androidx.navigation.NavController +import androidx.navigation.NavDirections + +fun NavController.safeNavigate(direction: NavDirections) { + currentDestination?.getAction(direction.actionId)?.run { navigate(direction) } +} + +fun NavController.safeNavigate(actionId: Int) { + currentDestination?.getAction(actionId)?.run { navigate(actionId) } +} diff --git a/android/app/src/main/java/app/priceguard/ui/util/NavigationBar.kt b/android/app/src/main/java/app/priceguard/ui/util/NavigationBar.kt new file mode 100644 index 00000000..aaf91a95 --- /dev/null +++ b/android/app/src/main/java/app/priceguard/ui/util/NavigationBar.kt @@ -0,0 +1,17 @@ +package app.priceguard.ui.util + +import android.app.Activity +import com.google.android.material.color.MaterialColors + +fun Activity.applySystemNavigationBarColor(colorType: SystemNavigationColorState) { + window.navigationBarColor = MaterialColors.getColor( + this, + colorType.colorType, + getColor(android.R.color.transparent) + ) +} + +enum class SystemNavigationColorState(val colorType: Int) { + SURFACE(com.google.android.material.R.attr.colorSurface), + BOTTOM_NAVIGATION(com.google.android.material.R.attr.colorSurfaceContainerHigh) +} diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt b/android/app/src/main/java/app/priceguard/ui/util/NotificationSetting.kt similarity index 89% rename from android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt rename to android/app/src/main/java/app/priceguard/ui/util/NotificationSetting.kt index 428a4fc8..98776622 100644 --- a/android/app/src/main/java/app/priceguard/ui/util/ui/NotificationSetting.kt +++ b/android/app/src/main/java/app/priceguard/ui/util/NotificationSetting.kt @@ -1,4 +1,4 @@ -package app.priceguard.ui.util.ui +package app.priceguard.ui.util import android.content.Context import android.content.Intent diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/drawable/ProgressIndicator.kt b/android/app/src/main/java/app/priceguard/ui/util/drawable/ProgressIndicator.kt similarity index 92% rename from android/app/src/main/java/app/priceguard/ui/util/ui/drawable/ProgressIndicator.kt rename to android/app/src/main/java/app/priceguard/ui/util/drawable/ProgressIndicator.kt index d7b9c31e..af168d84 100644 --- a/android/app/src/main/java/app/priceguard/ui/util/ui/drawable/ProgressIndicator.kt +++ b/android/app/src/main/java/app/priceguard/ui/util/drawable/ProgressIndicator.kt @@ -1,4 +1,4 @@ -package app.priceguard.ui.util.ui.drawable +package app.priceguard.ui.util.drawable import android.content.Context import app.priceguard.R diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/DisableAppBarScroll.kt b/android/app/src/main/java/app/priceguard/ui/util/ui/DisableAppBarScroll.kt deleted file mode 100644 index e2d69612..00000000 --- a/android/app/src/main/java/app/priceguard/ui/util/ui/DisableAppBarScroll.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.priceguard.ui.util.ui - -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.appbar.AppBarLayout - -fun disableAppBarRecyclerView( - layoutParams: CoordinatorLayout.LayoutParams, - recyclerView: RecyclerView -) { - val viewTreeObserver = recyclerView.viewTreeObserver - val disabledAblBehavior = getAblBehavior(false) - val enabledAblBehavior = getAblBehavior(true) - - viewTreeObserver.addOnGlobalLayoutListener { - if (recyclerView.childCount == 0 || recyclerView.measuredHeight - recyclerView.getChildAt(0).height >= 0) { - layoutParams.behavior = disabledAblBehavior - } else { - layoutParams.behavior = enabledAblBehavior - } - } -} - -fun getAblBehavior(canDrag: Boolean): AppBarLayout.Behavior { - val ablBehavior = AppBarLayout.Behavior() - ablBehavior.setDragCallback(object : AppBarLayout.Behavior.DragCallback() { - override fun canDrag(appBarLayout: AppBarLayout): Boolean { - return canDrag - } - }) - return ablBehavior -} diff --git a/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt b/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt deleted file mode 100644 index d76f19c4..00000000 --- a/android/app/src/main/java/app/priceguard/ui/util/ui/NetworkDialog.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.priceguard.ui.util.ui - -import android.app.Activity -import android.content.Intent -import app.priceguard.R -import app.priceguard.data.repository.token.TokenRepository -import app.priceguard.ui.login.LoginActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -fun Activity.showPermissionDeniedDialog(tokenRepository: TokenRepository) { - MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(getString(R.string.permission_denied_title)) - .setMessage(getString(R.string.permission_denied_message)) - .setPositiveButton(getString(R.string.confirm)) { _, _ -> goBackToLoginActivity(tokenRepository) } - .setOnDismissListener { goBackToLoginActivity(tokenRepository) } - .create() - .show() -} - -fun Activity.goBackToLoginActivity(tokenRepository: TokenRepository) { - CoroutineScope(Dispatchers.IO).launch { tokenRepository.clearTokens() } - val intent = Intent(this, LoginActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - finish() -} - -fun Activity.showConfirmationDialog( - title: String, - message: String -) { - MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog) - .setTitle(title) - .setMessage(message) - .setPositiveButton(getString(R.string.confirm)) { _, _ -> } - .setNegativeButton(getString(R.string.cancel)) { _, _ -> } - .create() - .show() -} diff --git a/android/app/src/main/res/layout/activity_detail.xml b/android/app/src/main/res/layout/activity_detail.xml index ea751a07..327baf1d 100644 --- a/android/app/src/main/res/layout/activity_detail.xml +++ b/android/app/src/main/res/layout/activity_detail.xml @@ -48,6 +48,7 @@ android:layout_height="match_parent"> @@ -96,6 +97,7 @@ android:layout_marginTop="12dp" android:layout_marginEnd="12dp" android:contentDescription="@string/share_product" + android:enabled="@{viewModel.state.isReady}" app:icon="@drawable/ic_share" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/iv_detail_product" /> @@ -155,8 +157,9 @@ android:id="@+id/btn_detail_track" style="?attr/materialButtonOutlinedStyle" android:layout_width="0dp" - android:layout_height="48dp" + android:layout_height="wrap_content" android:layout_marginTop="8dp" + android:enabled="@{viewModel.state.isReady}" android:text="@string/add_to_tracking_list" android:visibility="@{viewModel.state.isTracking ? View.GONE : View.VISIBLE}" app:icon="@drawable/ic_track" @@ -177,36 +180,53 @@ app:layout_constraintStart_toEndOf="@id/gl_vertical_start_nested" app:layout_constraintTop_toBottomOf="@+id/btn_detail_track" /> -