From eacf52746ea15fb9f7eca0e60a622a22998c0c18 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Sun, 29 Nov 2020 18:57:08 +0100 Subject: [PATCH 01/30] Remove unused code for deprecated Suggestions feature --- .../at/shockbytes/dante/flagging/FeatureFlag.kt | 4 ++-- .../at/shockbytes/dante/ui/activity/MainActivity.kt | 3 +-- .../shockbytes/dante/ui/adapter/BookPagerAdapter.kt | 13 +++---------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt b/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt index 8a69cfc9..6429458b 100644 --- a/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt +++ b/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt @@ -2,12 +2,12 @@ package at.shockbytes.dante.flagging enum class FeatureFlag(val key: String, val displayName: String, val defaultValue: Boolean) { - BOOK_SUGGESTIONS("book_suggestions", "Suggestions", false); + INSPIRATIONS("inspirations", "Inspirations", true); companion object { fun activeFlags(): List { - return listOf(BOOK_SUGGESTIONS) + return listOf(INSPIRATIONS) } } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt b/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt index 78957d9c..942b515a 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt @@ -320,8 +320,7 @@ class MainActivity : BaseActivity(), ViewPager.OnPageChangeListener { private fun initializeNavigation() { // Setup the ViewPager - pagerAdapter = BookPagerAdapter(applicationContext, featureFlagging[FeatureFlag.BOOK_SUGGESTIONS], - supportFragmentManager) + pagerAdapter = BookPagerAdapter(applicationContext, supportFragmentManager) viewPager.adapter = pagerAdapter viewPager.removeOnPageChangeListener(this) // Remove first to avoid multiple listeners viewPager.addOnPageChangeListener(this) diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt index e8297093..fe3b1876 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt @@ -15,20 +15,15 @@ import at.shockbytes.dante.ui.fragment.SuggestionsFragment */ class BookPagerAdapter( private val context: Context, - private val enableSuggestions: Boolean = false, fm: FragmentManager ) : FragmentStatePagerAdapter(fm) { override fun getItem(position: Int): Fragment { - return if (position < COUNT_STANDARD) { - MainBookFragment.newInstance(BookState.values()[position]) - } else { - SuggestionsFragment.newInstance() - } + return MainBookFragment.newInstance(BookState.values()[position]) } override fun getCount(): Int { - return if (enableSuggestions) COUNT_SUGGESTIONS else COUNT_STANDARD + return COUNT_STANDARD } override fun getPageTitle(position: Int): CharSequence { @@ -36,13 +31,11 @@ class BookPagerAdapter( 0 -> context.getString(R.string.tab_upcoming) 1 -> context.getString(R.string.tab_current) 2 -> context.getString(R.string.tab_done) - 3 -> context.getString(R.string.tab_suggestions) - else -> "" // Never the case + else -> throw IllegalStateException("Invalid page position $position in BookPagerAdapter!") } } companion object { private const val COUNT_STANDARD = 3 - private const val COUNT_SUGGESTIONS = 4 } } From 89c5bcb2c0edfd16d004feca15e6d527a1834ffa Mon Sep 17 00:00:00 2001 From: shockbytes Date: Sun, 29 Nov 2020 19:23:39 +0100 Subject: [PATCH 02/30] Layout foundation for inspirations feature --- app/src/main/AndroidManifest.xml | 6 + .../shockbytes/dante/flagging/FeatureFlag.kt | 4 +- .../dante/injection/AppComponent.kt | 3 + .../dante/navigation/Destination.kt | 7 ++ .../dante/stats/BookStatsBuilder.kt | 8 +- .../dante/ui/activity/InspirationsActivity.kt | 22 ++++ .../dante/ui/activity/MainActivity.kt | 28 ++--- .../dante/ui/activity/core/BaseActivity.kt | 3 +- .../dante/ui/fragment/InspirationsFragment.kt | 31 +++++ .../dante/ui/fragment/MenuFragment.kt | 115 +++++++++--------- app/src/main/res/layout/bottom_sheet_menu.xml | 23 +++- .../main/res/layout/fragment_inspirations.xml | 45 +++++++ app/src/main/res/menu/menu_navigation.xml | 5 - ...ab_suggestions.xml => ic_inspirations.xml} | 0 core/src/main/res/values-de/strings.xml | 2 +- core/src/main/res/values/strings.xml | 2 +- 16 files changed, 217 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt create mode 100644 app/src/main/res/layout/fragment_inspirations.xml rename core/src/main/res/drawable/{ic_tab_suggestions.xml => ic_inspirations.xml} (100%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b9401f5b..d4a42466 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,13 +97,19 @@ android:name=".ui.activity.ManualAddActivity" android:label="" android:windowSoftInputMode="adjustResize" /> + + + + { - return listOf(INSPIRATIONS) + return listOf() } } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt b/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt index 06747d75..93a229dd 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt @@ -17,6 +17,7 @@ import at.shockbytes.dante.ui.fragment.BackupRestoreFragment import at.shockbytes.dante.ui.fragment.BookDetailFragment import at.shockbytes.dante.ui.fragment.FeatureFlagConfigFragment import at.shockbytes.dante.ui.fragment.ImportBooksStorageFragment +import at.shockbytes.dante.ui.fragment.InspirationsFragment import at.shockbytes.dante.ui.fragment.LabelCategoryBottomSheetFragment import at.shockbytes.dante.ui.fragment.LabelPickerBottomSheetFragment import at.shockbytes.dante.ui.fragment.LauncherIconPickerFragment @@ -98,6 +99,8 @@ interface AppComponent { fun inject(fragment: LoginFragment) + fun inject(fragment: InspirationsFragment) + fun inject(fragment: FeatureFlagConfigFragment) fun inject(fragment: AnnouncementFragment) diff --git a/app/src/main/java/at/shockbytes/dante/navigation/Destination.kt b/app/src/main/java/at/shockbytes/dante/navigation/Destination.kt index 84ab2950..c05f1fc3 100644 --- a/app/src/main/java/at/shockbytes/dante/navigation/Destination.kt +++ b/app/src/main/java/at/shockbytes/dante/navigation/Destination.kt @@ -9,6 +9,7 @@ import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.createSharingIntent import at.shockbytes.dante.ui.activity.BookStorageActivity import at.shockbytes.dante.ui.activity.DetailActivity +import at.shockbytes.dante.ui.activity.InspirationsActivity import at.shockbytes.dante.ui.activity.MainActivity import at.shockbytes.dante.ui.activity.ManualAddActivity import at.shockbytes.dante.ui.activity.NotesActivity @@ -110,4 +111,10 @@ sealed class Destination { return NotesActivity.newIntent(context, notesBundle) } } + + object Inspirations : Destination() { + override fun provideIntent(context: Context): Intent { + return InspirationsActivity.newIntent(context) + } + } } diff --git a/app/src/main/java/at/shockbytes/dante/stats/BookStatsBuilder.kt b/app/src/main/java/at/shockbytes/dante/stats/BookStatsBuilder.kt index e3fdfa68..84042b54 100644 --- a/app/src/main/java/at/shockbytes/dante/stats/BookStatsBuilder.kt +++ b/app/src/main/java/at/shockbytes/dante/stats/BookStatsBuilder.kt @@ -171,7 +171,7 @@ object BookStatsBuilder { .groupBy { book -> book.author } - .maxBy { it.value.size } + .maxByOrNull { it.value.size } ?.let { (author, books) -> FavoriteAuthor(author, books.map { it.bareBone() }) } @@ -183,7 +183,7 @@ object BookStatsBuilder { .filter { book -> book.rating == 5 && book.startDate > 0 } - .minBy { book -> + .minByOrNull { book -> book.startDate } ?.bareBone() @@ -243,7 +243,7 @@ object BookStatsBuilder { .filter { it.state == BookState.READ } .map { Pair(it.bareBone(), DateTime(it.endDate)) } .groupBy { it.second.monthOfYear * it.second.year } - .maxBy { it.value.size } + .maxByOrNull { it.value.size } ?.let { max -> val activeBooks = max.value.map { it.first } @@ -272,7 +272,7 @@ object BookStatsBuilder { val now = System.currentTimeMillis() val start = booksDone .map { it.startDate } - .min() ?: now + .minOrNull() ?: now val monthsReading = Months.monthsBetween(DateTime(start), DateTime(now)).months return if (monthsReading == 0) { diff --git a/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt b/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt new file mode 100644 index 00000000..5f02ada0 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt @@ -0,0 +1,22 @@ +package at.shockbytes.dante.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import at.shockbytes.dante.ui.activity.core.ContainerActivity +import at.shockbytes.dante.ui.fragment.InspirationsFragment + +class InspirationsActivity: ContainerActivity() { + + override val displayFragment: Fragment = InspirationsFragment.newInstance() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.hide() + } + + companion object { + fun newIntent(context: Context) = Intent(context, InspirationsActivity::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt b/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt index 942b515a..fdf6ed42 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt @@ -27,7 +27,6 @@ import at.shockbytes.dante.flagging.FeatureFlagging import at.shockbytes.dante.core.image.GlideImageLoader.loadBitmap import at.shockbytes.dante.ui.widget.DanteAppWidgetManager import at.shockbytes.dante.util.settings.DanteSettings -import at.shockbytes.dante.flagging.FeatureFlag import at.shockbytes.dante.navigation.Destination import at.shockbytes.dante.ui.fragment.AnnouncementFragment import at.shockbytes.dante.util.DanteUtils @@ -321,18 +320,21 @@ class MainActivity : BaseActivity(), ViewPager.OnPageChangeListener { // Setup the ViewPager pagerAdapter = BookPagerAdapter(applicationContext, supportFragmentManager) - viewPager.adapter = pagerAdapter - viewPager.removeOnPageChangeListener(this) // Remove first to avoid multiple listeners - viewPager.addOnPageChangeListener(this) - viewPager.offscreenPageLimit = 2 - - mainBottomNavigation.setOnNavigationItemSelectedListener { item -> - colorNavigationItems(item) - indexForNavigationItemId(item.itemId)?.let { viewPager.currentItem = it } - true + viewPager.apply { + adapter = pagerAdapter + removeOnPageChangeListener(this@MainActivity) // Remove first to avoid multiple listeners + addOnPageChangeListener(this@MainActivity) + offscreenPageLimit = 2 + } + + mainBottomNavigation.apply { + setOnNavigationItemSelectedListener { item -> + colorNavigationItems(item) + indexForNavigationItemId(item.itemId)?.let { viewPager.currentItem = it } + true + } + selectedItemId = tabId } - mainBottomNavigation.menu.getItem(3).isVisible = featureFlagging[FeatureFlag.BOOK_SUGGESTIONS] - mainBottomNavigation.selectedItemId = tabId } private fun colorNavigationItems(item: MenuItem) { @@ -341,7 +343,6 @@ class MainActivity : BaseActivity(), ViewPager.OnPageChangeListener { R.id.menu_navigation_upcoming -> R.drawable.navigation_item_upcoming R.id.menu_navigation_current -> R.drawable.navigation_item_current R.id.menu_navigation_done -> R.drawable.navigation_item_done - R.id.menu_navigation_suggestions -> R.drawable.navigation_item_suggestions else -> 0 } @@ -366,7 +367,6 @@ class MainActivity : BaseActivity(), ViewPager.OnPageChangeListener { R.id.menu_navigation_upcoming -> 0 R.id.menu_navigation_current -> 1 R.id.menu_navigation_done -> 2 - R.id.menu_navigation_suggestions -> 3 else -> null } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/activity/core/BaseActivity.kt b/app/src/main/java/at/shockbytes/dante/ui/activity/core/BaseActivity.kt index df28e285..612ed9de 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/activity/core/BaseActivity.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/activity/core/BaseActivity.kt @@ -5,6 +5,7 @@ import android.graphics.drawable.ColorDrawable import android.os.Build import android.os.Bundle import android.os.Handler +import android.os.Looper import com.google.android.material.snackbar.Snackbar import androidx.appcompat.app.AppCompatActivity import android.view.Window @@ -62,7 +63,7 @@ abstract class BaseActivity : AppCompatActivity() { } fun startActivityDelayed(intent: Intent, bundle: Bundle?, delay: Long) { - Handler().postDelayed({ + Handler(Looper.getMainLooper()).postDelayed({ startActivity(intent, bundle) }, delay) } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt new file mode 100644 index 00000000..ea7a4041 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt @@ -0,0 +1,31 @@ +package at.shockbytes.dante.ui.fragment + +import at.shockbytes.dante.R +import at.shockbytes.dante.injection.AppComponent +import kotlinx.android.synthetic.main.dante_toolbar.* + +class InspirationsFragment : BaseFragment() { + + override val layoutId: Int = R.layout.fragment_inspirations + + override fun setupViews() { + dante_toolbar_title.setText("Works!") + } + + override fun injectToGraph(appComponent: AppComponent) { + appComponent.inject(this) + } + + override fun bindViewModel() { + } + + override fun unbindViewModel() { + } + + companion object { + + fun newInstance(): InspirationsFragment { + return InspirationsFragment() + } + } +} diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MenuFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MenuFragment.kt index c11041ef..ccb136ac 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MenuFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MenuFragment.kt @@ -8,6 +8,7 @@ import androidx.core.app.ActivityOptionsCompat import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.lifecycle.Observer import at.shockbytes.dante.DanteApp import at.shockbytes.dante.R import at.shockbytes.dante.navigation.ActivityNavigator @@ -31,14 +32,18 @@ class MenuFragment : BottomSheetDialogFragment() { override fun getTheme() = R.style.BottomSheetDialogTheme - private val viewModel: MainViewModel by lazy { viewModelOf(vmFactory) } + private val viewModel: MainViewModel by lazy { viewModelOf(vmFactory) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activity?.application as DanteApp).appComponent.inject(this) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { return inflater.inflate(R.layout.bottom_sheet_menu, container, false) } @@ -53,75 +58,63 @@ class MenuFragment : BottomSheetDialogFragment() { } private fun bindViewModel() { + viewModel.getUserEvent().observe(this, Observer(::handleUserEvent)) + } - viewModel.getUserEvent().observe(this, { event -> + private fun handleUserEvent(event: MainViewModel.UserEvent) { - when (event) { + when (event) { - is MainViewModel.UserEvent.SuccessEvent -> { + is MainViewModel.UserEvent.SuccessEvent -> { - if (event.user != null) { - txtMenuUserName.text = event.user.displayName - txtMenuUserMail.text = event.user.email - btnMenuLogin.text = getString(R.string.logout) + if (event.user != null) { + txtMenuUserName.text = event.user.displayName + txtMenuUserMail.text = event.user.email + btnMenuLogin.text = getString(R.string.logout) - event.user.photoUrl?.loadRoundedBitmap(requireContext())?.subscribe({ image -> - imageViewMenuUser.setImageBitmap(image) - }, { throwable -> - throwable.printStackTrace() - }) - } else { - txtMenuUserName.text = getString(R.string.anonymous_user) - txtMenuUserMail.text = "" - btnMenuLogin.text = getString(R.string.login) - imageViewMenuUser.setImageResource(R.drawable.ic_user_template_dark) - } + event.user.photoUrl?.loadRoundedBitmap(requireContext())?.subscribe({ image -> + imageViewMenuUser.setImageBitmap(image) + }, { throwable -> + throwable.printStackTrace() + }) + } else { + txtMenuUserName.text = getString(R.string.anonymous_user) + txtMenuUserMail.text = "" + btnMenuLogin.text = getString(R.string.login) + imageViewMenuUser.setImageResource(R.drawable.ic_user_template_dark) } + } - is MainViewModel.UserEvent.LoginEvent -> { - GoogleSignInDialogFragment.newInstance() - .setSignInListener { - requireActivity().startActivityForResult(event.signInIntent, DanteUtils.rcSignIn) - } - .setMaybeLaterListener { viewModel.signInMaybeLater(true) } - .show(childFragmentManager, "sign-in-fragment") - } - is MainViewModel.UserEvent.ErrorEvent -> { - Toast.makeText(context, event.errorMsg, Toast.LENGTH_LONG).show() - } + is MainViewModel.UserEvent.LoginEvent -> { + GoogleSignInDialogFragment.newInstance() + .setSignInListener { + requireActivity().startActivityForResult(event.signInIntent, DanteUtils.rcSignIn) + } + .setMaybeLaterListener { viewModel.signInMaybeLater(true) } + .show(childFragmentManager, "sign-in-fragment") } - }) + is MainViewModel.UserEvent.ErrorEvent -> { + Toast.makeText(context, event.errorMsg, Toast.LENGTH_LONG).show() + } + } } private fun setupViews() { - val sceneTransition = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity()).toBundle() - btnMenuStatistics.setOnClickListener { - ActivityNavigator.navigateTo( - activity, - Destination.Statistics, - sceneTransition - ) - dismiss() + navigateToAndDismiss(Destination.Statistics) } btnMenuTimeline.setOnClickListener { - ActivityNavigator.navigateTo( - activity, - Destination.Timeline, - sceneTransition - ) - dismiss() + navigateToAndDismiss(Destination.Timeline) + } + + btnMenuInspirations.setOnClickListener { + navigateToAndDismiss(Destination.Inspirations) } btnMenuBookStorage.setOnClickListener { - ActivityNavigator.navigateTo( - activity, - Destination.BookStorage, - sceneTransition - ) - dismiss() + navigateToAndDismiss(Destination.BookStorage) } btnMenuLogin.setOnClickListener { @@ -129,15 +122,21 @@ class MenuFragment : BottomSheetDialogFragment() { } btnMenuSettings.setOnClickListener { - ActivityNavigator.navigateTo( - activity, - Destination.Settings, - sceneTransition - ) - dismiss() + navigateToAndDismiss(Destination.Settings) } } + private fun navigateToAndDismiss(destination: Destination) { + + val sceneTransition = ActivityOptionsCompat + .makeSceneTransitionAnimation(requireActivity()) + .toBundle() + + ActivityNavigator.navigateTo(activity, destination, sceneTransition) + + dismiss() + } + companion object { fun newInstance(): MenuFragment { diff --git a/app/src/main/res/layout/bottom_sheet_menu.xml b/app/src/main/res/layout/bottom_sheet_menu.xml index 65009f79..b2d453e0 100644 --- a/app/src/main/res/layout/bottom_sheet_menu.xml +++ b/app/src/main/res/layout/bottom_sheet_menu.xml @@ -110,6 +110,27 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/btnMenuStatistics" /> + + + app:layout_constraintTop_toBottomOf="@+id/btnMenuInspirations" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_navigation.xml b/app/src/main/res/menu/menu_navigation.xml index b191d2c9..16a741b1 100644 --- a/app/src/main/res/menu/menu_navigation.xml +++ b/app/src/main/res/menu/menu_navigation.xml @@ -16,9 +16,4 @@ android:icon="@drawable/ic_tab_done" android:title="@string/tab_done" /> - - \ No newline at end of file diff --git a/core/src/main/res/drawable/ic_tab_suggestions.xml b/core/src/main/res/drawable/ic_inspirations.xml similarity index 100% rename from core/src/main/res/drawable/ic_tab_suggestions.xml rename to core/src/main/res/drawable/ic_inspirations.xml diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 048a4386..fa9eb71f 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -191,7 +191,7 @@ Für später Gelesen Lesen - Vorschläge + Inspirationen Nein Ja diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index e6884579..ba145e26 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -349,7 +349,7 @@ For later Reading Done - Suggestions + Inspirations One of those books? Maybe one of them? From 2a9e1ca5dffef5c1126088ec7001da01a34055cc Mon Sep 17 00:00:00 2001 From: shockbytes Date: Sun, 29 Nov 2020 19:48:32 +0100 Subject: [PATCH 03/30] Setup Tab Layout for wishlist and suggestions --- .../shockbytes/dante/flagging/FeatureFlag.kt | 1 - .../dante/injection/AppComponent.kt | 4 +- .../dante/ui/activity/InspirationsActivity.kt | 2 +- .../dante/ui/adapter/BookPagerAdapter.kt | 1 - .../ui/adapter/InspirationsPagerAdapter.kt | 37 +++++++++++++++++++ .../dante/ui/fragment/InspirationsFragment.kt | 32 ++++++++++++++-- .../dante/ui/fragment/WishlistFragment.kt | 29 +++++++++++++++ .../main/res/layout/fragment_inspirations.xml | 6 +-- app/src/main/res/layout/fragment_wishlist.xml | 30 +++++++++++++++ core/src/main/res/values/strings.xml | 4 ++ 10 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt create mode 100644 app/src/main/res/layout/fragment_wishlist.xml diff --git a/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt b/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt index ceda47fc..d7fe3549 100644 --- a/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt +++ b/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt @@ -1,7 +1,6 @@ package at.shockbytes.dante.flagging enum class FeatureFlag(val key: String, val displayName: String, val defaultValue: Boolean) { - ; companion object { diff --git a/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt b/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt index 93a229dd..ecfaf19c 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt @@ -17,7 +17,6 @@ import at.shockbytes.dante.ui.fragment.BackupRestoreFragment import at.shockbytes.dante.ui.fragment.BookDetailFragment import at.shockbytes.dante.ui.fragment.FeatureFlagConfigFragment import at.shockbytes.dante.ui.fragment.ImportBooksStorageFragment -import at.shockbytes.dante.ui.fragment.InspirationsFragment import at.shockbytes.dante.ui.fragment.LabelCategoryBottomSheetFragment import at.shockbytes.dante.ui.fragment.LabelPickerBottomSheetFragment import at.shockbytes.dante.ui.fragment.LauncherIconPickerFragment @@ -34,6 +33,7 @@ import at.shockbytes.dante.ui.fragment.SettingsFragment import at.shockbytes.dante.ui.fragment.StatisticsFragment import at.shockbytes.dante.ui.fragment.SuggestionsFragment import at.shockbytes.dante.ui.fragment.TimeLineFragment +import at.shockbytes.dante.ui.fragment.WishlistFragment import at.shockbytes.dante.ui.fragment.dialog.GoogleSignInDialogFragment import at.shockbytes.dante.ui.fragment.dialog.GoogleWelcomeScreenDialogFragment import at.shockbytes.dante.ui.fragment.dialog.SortStrategyDialogFragment @@ -99,7 +99,7 @@ interface AppComponent { fun inject(fragment: LoginFragment) - fun inject(fragment: InspirationsFragment) + fun inject(fragment: WishlistFragment) fun inject(fragment: FeatureFlagConfigFragment) diff --git a/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt b/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt index 5f02ada0..92995bbf 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/activity/InspirationsActivity.kt @@ -7,7 +7,7 @@ import androidx.fragment.app.Fragment import at.shockbytes.dante.ui.activity.core.ContainerActivity import at.shockbytes.dante.ui.fragment.InspirationsFragment -class InspirationsActivity: ContainerActivity() { +class InspirationsActivity : ContainerActivity() { override val displayFragment: Fragment = InspirationsFragment.newInstance() diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt index fe3b1876..65ec13a5 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/BookPagerAdapter.kt @@ -7,7 +7,6 @@ import androidx.fragment.app.FragmentStatePagerAdapter import at.shockbytes.dante.R import at.shockbytes.dante.core.book.BookState import at.shockbytes.dante.ui.fragment.MainBookFragment -import at.shockbytes.dante.ui.fragment.SuggestionsFragment /** * Author: Martin Macheiner diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt new file mode 100644 index 00000000..a3f5fd5e --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt @@ -0,0 +1,37 @@ +package at.shockbytes.dante.ui.adapter + +import android.content.Context +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import at.shockbytes.dante.R +import at.shockbytes.dante.ui.fragment.SuggestionsFragment +import at.shockbytes.dante.ui.fragment.WishlistFragment + +class InspirationsPagerAdapter( + private val context: Context, + fragmentManager: FragmentManager +) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getCount(): Int = PAGE_COUNT + + override fun getPageTitle(position: Int): CharSequence { + return when (position) { + 0 -> context.getString(R.string.wishlist_title) + 1 -> context.getString(R.string.suggestions_title) + else -> throw IllegalStateException("Position $position out of bounds of InspirationsPagerAdapter!") + } + } + + override fun getItem(position: Int): Fragment { + return when (position) { + 0 -> WishlistFragment.newInstance() + 1 -> SuggestionsFragment.newInstance() + else -> throw IllegalStateException("Position $position out of bounds of InspirationsPagerAdapter!") + } + } + + companion object { + private const val PAGE_COUNT = 2 + } +} diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt index ea7a4041..7464f86d 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt @@ -2,20 +2,46 @@ package at.shockbytes.dante.ui.fragment import at.shockbytes.dante.R import at.shockbytes.dante.injection.AppComponent +import at.shockbytes.dante.ui.adapter.InspirationsPagerAdapter +import at.shockbytes.dante.util.setVisible import kotlinx.android.synthetic.main.dante_toolbar.* +import kotlinx.android.synthetic.main.fragment_inspirations.* class InspirationsFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_inspirations override fun setupViews() { - dante_toolbar_title.setText("Works!") + setupToolbar() + setupViewPager() + connectTabsAndViewPager() } - override fun injectToGraph(appComponent: AppComponent) { - appComponent.inject(this) + private fun setupToolbar() { + dante_toolbar_title.setText(R.string.inspirations) + dante_toolbar_back.apply { + setVisible(true) + setOnClickListener { + activity?.onBackPressed() + } + } + } + + private fun setupViewPager() { + val pagerAdapter = InspirationsPagerAdapter(requireContext(), childFragmentManager) + + vp_fragment_inspirations.apply { + adapter = pagerAdapter + offscreenPageLimit = 2 + } + } + + private fun connectTabsAndViewPager() { + tabs_fragment_inspirations.setupWithViewPager(vp_fragment_inspirations) } + override fun injectToGraph(appComponent: AppComponent) = Unit + override fun bindViewModel() { } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt new file mode 100644 index 00000000..7265a633 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt @@ -0,0 +1,29 @@ +package at.shockbytes.dante.ui.fragment + +import at.shockbytes.dante.R +import at.shockbytes.dante.injection.AppComponent + +class WishlistFragment : BaseFragment() { + + override val layoutId: Int = R.layout.fragment_wishlist + + override fun setupViews() { + } + + override fun injectToGraph(appComponent: AppComponent) { + appComponent.inject(this) + } + + override fun bindViewModel() { + } + + override fun unbindViewModel() { + } + + companion object { + + fun newInstance(): WishlistFragment { + return WishlistFragment() + } + } +} diff --git a/app/src/main/res/layout/fragment_inspirations.xml b/app/src/main/res/layout/fragment_inspirations.xml index 19dc1c8e..3d8a5271 100644 --- a/app/src/main/res/layout/fragment_inspirations.xml +++ b/app/src/main/res/layout/fragment_inspirations.xml @@ -20,7 +20,7 @@ + + + + + + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index ba145e26..0eb87bc5 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -351,6 +351,10 @@ Done Inspirations + Your wishlist is empty! Good! Seems like you have all your books already. + Your Wishlist + Suggestions + One of those books? Maybe one of them? Not my book From 0e0ccf29c378e39454b43cddd3cd543a2c7b2590 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Sun, 29 Nov 2020 20:59:27 +0100 Subject: [PATCH 04/30] Load suggestions from assets --- app/src/main/assets/suggestions-v1.json | 104 ++++++++++++++++++ .../shockbytes/dante/injection/AppModule.kt | 8 ++ .../dante/injection/ViewModelModule.kt | 6 + .../AssetsSuggestionsRepository.kt | 19 ++++ .../dante/suggestions/BookSuggestionEntity.kt | 17 +++ .../shockbytes/dante/suggestions/Suggester.kt | 6 + .../dante/suggestions/Suggestion.kt | 7 ++ .../dante/suggestions/Suggestions.kt | 3 + .../suggestions/SuggestionsRepository.kt | 8 ++ .../dante/ui/activity/MainActivity.kt | 4 - .../dante/ui/fragment/SuggestionsFragment.kt | 33 +++++- .../ui/viewmodel/SuggestionsViewModel.kt | 38 +++++++ .../java/at/shockbytes/dante/util/Assets.kt | 18 +++ .../main/res/layout/fragment_suggestions.xml | 2 +- .../core/data/local/RealmBookEntityDao.kt | 2 +- .../shockbytes/dante/util/DanteExtensions.kt | 4 + 16 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 app/src/main/assets/suggestions-v1.json create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/AssetsSuggestionsRepository.kt create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/Suggester.kt create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/Suggestions.kt create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt create mode 100644 app/src/main/java/at/shockbytes/dante/util/Assets.kt diff --git a/app/src/main/assets/suggestions-v1.json b/app/src/main/assets/suggestions-v1.json new file mode 100644 index 00000000..64dac565 --- /dev/null +++ b/app/src/main/assets/suggestions-v1.json @@ -0,0 +1,104 @@ +{ + "suggestions": [ + { + "suggestion": { + "title": "Stillness is the Key", + "subTitle": "An Ancient Strategy for Modern Life", + "author": "Ryan Holiday", + "pageCount": 262, + "publishedDate": "2019-10-01", + "isbn": "9781782835271", + "thumbnailAddress": "http://books.google.com/books/content?id=916TDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api", + "googleBooksLink": "http://books.google.at/books?id=916TDwAAQBAJ&dq=9781788162050&hl=&source=gbs_api", + "language": "en", + "summary": "'Whether you are an athlete, an investor, a writer or an entrepreneur, this little but soulful book will open the door to a healthier, less anxious and more productive life and career.' - Arianna Huffington 'Ryan's trilogy of The Obstacle is the Way, Ego is the Enemy and Stillness is the Key are for sure must-reads.' Manu Ginobili, 4x NBA champion and Olympic Gold Medalist Throughout history, there's been one indelible quality that great leaders, makers, artists and fighters have shared. The Zen Buddhists described it as inner peace, the Stoics called it ataraxia and Ryan Holiday calls it stillness: the ability to be steady, focussed and calm in a constantly busy world. This quality, valued by every major school of thought from Buddha to Seneca, John Stuart Mill to Nietzsche, is urgently necessary today. And, Holiday shows, it is entirely attainable. Just as Winston Churchill used bricklaying as a time to recharge and reflect, or Oprah Winfrey learned deep empathy from her quiet childhood, we can all benefit from stillness to feed into our greater ambitions - whether winning a battle, building a business, or simply finding happiness, peace and self-direction. Filled with wisdom and examples from historical and contemporary figures, this book shows how to cultivate this quality in your own life. Because stillness is not merely inactivity, but the doorway to the self-mastery, discipline and focus necessary to succeed in this competitive, noisy world.", + "state": "TODO" + }, + "suggester": { + "name": "Martin", + "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" + }, + "recommendation": "TODO" + }, + { + "suggestion": { + "title": "The Design of Everyday Things", + "subTitle": "", + "author": "Donald A. Norman", + "pageCount": 368, + "publishedDate": "2013", + "isbn": "9780262525671", + "thumbnailAddress": "http://books.google.com/books/content?id=heCtnQEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api", + "googleBooksLink": "http://books.google.at/books?id=heCtnQEACAAJ&dq=9780262525671&hl=&source=gbs_api", + "language": "en", + "summary": "Even the smartest among us can feel inept as we fail to figure out which light switch or oven burner to turn on, or whether to push, pull, or slide a door. The fault, argues this ingenious-even liberating-book, lies not in ourselves, but in product design that ignores the needs of users and the principles of cognitive psychology. The problems range from ambiguous and hidden controls to arbitrary relationships between controls and functions, coupled with a lack of feedback or other assistance and unreasonable demands on memorization. The Design of Everyday Things shows that good, usable design is possible. The rules are simple: make things visible, exploit natural relationships that couple function and control, and make intelligent use of constraints. The goal: guide the user effortlessly to the right action on the right control at the right time. In this entertaining and insightful analysis, cognitive scientist Don Norman hails excellence of design as the most important key to regaining the competitive edge in influencing consumer behavior. Now fully expanded and updated, with a new introduction by the author, The Design of Everyday Things is a powerful primer on how-and why-some products satisfy customers while others only frustrate them.", + "state": "TODO" + }, + "suggester": { + "name": "Martin", + "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" + }, + "recommendation": "TODO" + }, + { + "suggestion": { + "title": "Their Darkest Hour", + "subTitle": "People Tested to the Extreme in WWII", + "author": "Laurence Rees", + "pageCount": 336, + "publishedDate": "2011-10-31", + "isbn": "9781448116287", + "thumbnailAddress": "http://books.google.com/books/content?id=49Q2WWodc2QC&printsec=frontcover&img=1&zoom=1&source=gbs_api", + "googleBooksLink": "http://books.google.at/books?id=49Q2WWodc2QC&dq=9780091917593&hl=&source=gbs_api", + "language": "en", + "summary": "How could Nazi killers shoot Jewish women and children at close range? Why did Japanese soldiers rape and murder on such a horrendous scale? How was it possible to endure the torment of a Nazi concentration camp? Award-winning documentary maker and historian Laurence Rees has spent nearly 20 years wrestling with these questions in the course of filming hundreds of interviews with people tested to the extreme during World War II. He has come face-to-face with rapists, mass murderers, even cannibals, but he has also met courageous individuals who are an inspiration to us all. In Their Darkest Hour he presents 35 of his most electrifying encounters.", + "state": "TODO" + }, + "suggester": { + "name": "Martin", + "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" + }, + "recommendation": "TODO" + }, + { + "suggestion": { + "title": "All Quiet on the Western Front", + "subTitle": "A Novel", + "author": "Erich Maria Remarque", + "pageCount": 240, + "publishedDate": "2013-09-03", + "isbn": "9780812985535", + "thumbnailAddress": "http://books.google.com/books/content?id=pgvqsaYvgacC&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api", + "googleBooksLink": "http://books.google.at/books?id=pgvqsaYvgacC&dq=Nothing%2Bnew%2Bon%2Bthe%2Bwestern%2Bfront&hl=&source=gbs_api", + "language": "en", + "summary": "Considered by many the greatest war novel of all time, All Quiet on the Western Front is Erich Maria Remarque’s masterpiece of the German experience during World War I. I am young, I am twenty years old; yet I know nothing of life but despair, death, fear, and fatuous superficiality cast over an abyss of sorrow. . . . This is the testament of Paul Bäumer, who enlists with his classmates in the German army during World War I. They become soldiers with youthful enthusiasm. But the world of duty, culture, and progress they had been taught breaks in pieces under the first bombardment in the trenches. Through years of vivid horror, Paul holds fast to a single vow: to fight against the principle of hate that meaninglessly pits young men of the same generation but different uniforms against one another . . . if only he can come out of the war alive. “The world has a great writer in Erich Maria Remarque. He is a craftsman of unquestionably first rank, a man who can bend language to his will. Whether he writes of men or of inanimate nature, his touch is sensitive, firm, and sure.”—The New York Times Book Review", + "state": "TODO" + }, + "suggester": { + "name": "Martin", + "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" + }, + "recommendation": "TODO" + }, + { + "suggestion": { + "title": "Thinking, Fast and Slow", + "subTitle": "", + "author": "Daniel Kahneman", + "pageCount": 512, + "publishedDate": "2011-10-25", + "isbn": "9781429969352", + "thumbnailAddress": "http://books.google.com/books/content?id=ZuKTvERuPG8C&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api", + "googleBooksLink": "https://play.google.com/store/books/details?id=ZuKTvERuPG8C&source=gbs_api", + "language": "en", + "summary": "Major New York Times bestseller Winner of the National Academy of Sciences Best Book Award in 2012 Selected by the New York Times Book Review as one of the ten best books of 2011 A Globe and Mail Best Books of the Year 2011 Title One of The Economist's 2011 Books of the Year One of The Wall Street Journal's Best Nonfiction Books of the Year 2011 2013 Presidential Medal of Freedom Recipient Kahneman's work with Amos Tversky is the subject of Michael Lewis's The Undoing Project: A Friendship That Changed Our Minds In the international bestseller, Thinking, Fast and Slow, Daniel Kahneman, the renowned psychologist and winner of the Nobel Prize in Economics, takes us on a groundbreaking tour of the mind and explains the two systems that drive the way we think. System 1 is fast, intuitive, and emotional; System 2 is slower, more deliberative, and more logical. The impact of overconfidence on corporate strategies, the difficulties of predicting what will make us happy in the future, the profound effect of cognitive biases on everything from playing the stock market to planning our next vacation—each of these can be understood only by knowing how the two systems shape our judgments and decisions. Engaging the reader in a lively conversation about how we think, Kahneman reveals where we can and cannot trust our intuitions and how we can tap into the benefits of slow thinking. He offers practical and enlightening insights into how choices are made in both our business and our personal lives—and how we can use different techniques to guard against the mental glitches that often get us into trouble. Winner of the National Academy of Sciences Best Book Award and the Los Angeles Times Book Prize and selected by The New York Times Book Review as one of the ten best books of 2011, Thinking, Fast and Slow is destined to be a classic.", + "state": "TODO" + }, + "suggester": { + "name": "Martin", + "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" + }, + "recommendation": "TODO" + } + ] +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt b/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt index 8a86e332..f924933b 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt @@ -13,10 +13,13 @@ import at.shockbytes.dante.util.settings.DanteSettings import at.shockbytes.dante.flagging.FeatureFlagging import at.shockbytes.dante.flagging.FirebaseFeatureFlagging import at.shockbytes.dante.flagging.SharedPreferencesFeatureFlagging +import at.shockbytes.dante.suggestions.AssetsSuggestionsRepository +import at.shockbytes.dante.suggestions.SuggestionsRepository import at.shockbytes.dante.util.permission.AndroidPermissionManager import at.shockbytes.dante.util.permission.PermissionManager import at.shockbytes.dante.util.scheduler.SchedulerFacade import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.gson.Gson import dagger.Module import dagger.Provides @@ -68,4 +71,9 @@ class AppModule(private val app: Application) { val prefs = app.getSharedPreferences("announcements", Context.MODE_PRIVATE) return SharedPrefsAnnouncementProvider(prefs) } + + @Provides + fun provideSuggestionsRepository(): SuggestionsRepository { + return AssetsSuggestionsRepository(app.applicationContext, Gson()) + } } diff --git a/app/src/main/java/at/shockbytes/dante/injection/ViewModelModule.kt b/app/src/main/java/at/shockbytes/dante/injection/ViewModelModule.kt index 526d3ac8..2821adbe 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/ViewModelModule.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/ViewModelModule.kt @@ -18,6 +18,7 @@ import at.shockbytes.dante.ui.viewmodel.OnlineStorageViewModel import at.shockbytes.dante.ui.viewmodel.PageRecordsDetailViewModel import at.shockbytes.dante.ui.viewmodel.SearchViewModel import at.shockbytes.dante.ui.viewmodel.StatisticsViewModel +import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel import at.shockbytes.dante.ui.viewmodel.TimelineViewModel import dagger.Binds import dagger.MapKey @@ -133,4 +134,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(PageRecordsDetailViewModel::class) internal abstract fun pageRecordsDetailViewModel(viewModel: PageRecordsDetailViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(SuggestionsViewModel::class) + internal abstract fun suggestionsViewModel(viewModel: SuggestionsViewModel): ViewModel } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/AssetsSuggestionsRepository.kt b/app/src/main/java/at/shockbytes/dante/suggestions/AssetsSuggestionsRepository.kt new file mode 100644 index 00000000..13f922b1 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/AssetsSuggestionsRepository.kt @@ -0,0 +1,19 @@ +package at.shockbytes.dante.suggestions + +import android.content.Context +import at.shockbytes.dante.util.Assets +import com.google.gson.Gson +import io.reactivex.Single + +class AssetsSuggestionsRepository( + private val context: Context, + private val gson: Gson +) : SuggestionsRepository { + + override fun loadSuggestions(): Single { + return Assets.readFile(context, "suggestions-v1.json") + .map { jsonContent -> + gson.fromJson(jsonContent, Suggestions::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt b/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt new file mode 100644 index 00000000..59526fb6 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt @@ -0,0 +1,17 @@ +package at.shockbytes.dante.suggestions + +import at.shockbytes.dante.core.book.BookState + +data class BookSuggestionEntity( + val title: String = "", + val subTitle: String = "", + val author: String = "", + val state: BookState = BookState.READING, // TODO Change to WISHLIST + val pageCount: Int = 0, + val publishedDate: String = "", + val isbn: String = "", + val thumbnailAddress: String? = null, + val googleBooksLink: String? = null, + val language: String? = "NA", + val summary: String? = null +) diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/Suggester.kt b/app/src/main/java/at/shockbytes/dante/suggestions/Suggester.kt new file mode 100644 index 00000000..b4c4d8e5 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/Suggester.kt @@ -0,0 +1,6 @@ +package at.shockbytes.dante.suggestions + +data class Suggester( + val name: String, + val photoUrl: String? +) \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt new file mode 100644 index 00000000..d6bdd16b --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt @@ -0,0 +1,7 @@ +package at.shockbytes.dante.suggestions + +data class Suggestion( + val suggestion: BookSuggestionEntity, + val suggester: Suggester, + val recommendation: String +) diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestions.kt b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestions.kt new file mode 100644 index 00000000..a3d9391f --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestions.kt @@ -0,0 +1,3 @@ +package at.shockbytes.dante.suggestions + +data class Suggestions(val suggestions: List) diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt b/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt new file mode 100644 index 00000000..6833f8fb --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt @@ -0,0 +1,8 @@ +package at.shockbytes.dante.suggestions + +import io.reactivex.Single + +interface SuggestionsRepository { + + fun loadSuggestions(): Single +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt b/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt index fdf6ed42..81ef0827 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/activity/MainActivity.kt @@ -23,7 +23,6 @@ import at.shockbytes.dante.ui.fragment.dialog.GoogleSignInDialogFragment import at.shockbytes.dante.ui.fragment.dialog.GoogleWelcomeScreenDialogFragment import at.shockbytes.dante.ui.fragment.dialog.QueryDialogFragment import at.shockbytes.dante.ui.viewmodel.MainViewModel -import at.shockbytes.dante.flagging.FeatureFlagging import at.shockbytes.dante.core.image.GlideImageLoader.loadBitmap import at.shockbytes.dante.ui.widget.DanteAppWidgetManager import at.shockbytes.dante.util.settings.DanteSettings @@ -51,9 +50,6 @@ class MainActivity : BaseActivity(), ViewPager.OnPageChangeListener { @Inject lateinit var vmFactory: ViewModelProvider.Factory - @Inject - lateinit var featureFlagging: FeatureFlagging - @Inject lateinit var danteSettings: DanteSettings diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 8a9e319e..195be093 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -1,7 +1,13 @@ package at.shockbytes.dante.ui.fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import at.shockbytes.dante.R import at.shockbytes.dante.injection.AppComponent +import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel +import at.shockbytes.dante.util.viewModelOf +import javax.inject.Inject /** * Author: Martin Macheiner @@ -11,6 +17,11 @@ class SuggestionsFragment : BaseFragment() { override val layoutId = R.layout.fragment_suggestions + @Inject + lateinit var vmFactory: ViewModelProvider.Factory + + private val viewModel: SuggestionsViewModel by lazy { viewModelOf(vmFactory) } + override fun setupViews() { } @@ -19,13 +30,29 @@ class SuggestionsFragment : BaseFragment() { } override fun bindViewModel() { - // Not needed... + + viewModel.requestSuggestions() + viewModel.getSuggestionState().observe(this, Observer(::handleSuggestionState)) + } + + private fun handleSuggestionState(suggestionsState: SuggestionsViewModel.SuggestionsState) { + + when (suggestionsState) { + is SuggestionsViewModel.SuggestionsState.Present -> handleSuggestions(suggestionsState.suggestions) + SuggestionsViewModel.SuggestionsState.Empty -> handleEmptyState() + } } - override fun unbindViewModel() { - // Not needed... + private fun handleEmptyState() { + // TODO } + private fun handleSuggestions(suggestions: List) { + // TODO + } + + override fun unbindViewModel() = Unit + companion object { fun newInstance(): SuggestionsFragment { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt new file mode 100644 index 00000000..7b29a796 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -0,0 +1,38 @@ +package at.shockbytes.dante.ui.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.suggestions.SuggestionsRepository +import at.shockbytes.dante.util.ExceptionHandlers +import at.shockbytes.dante.util.addTo +import javax.inject.Inject + +class SuggestionsViewModel @Inject constructor( + private val suggestionsRepository: SuggestionsRepository +) : BaseViewModel() { + + sealed class SuggestionsState { + + data class Present(val suggestions: List) : SuggestionsState() + + object Empty : SuggestionsState() + } + + private val suggestionState = MutableLiveData() + fun getSuggestionState(): LiveData = suggestionState + + fun requestSuggestions() { + suggestionsRepository.loadSuggestions() + .map { suggestions -> + + if (suggestions.suggestions.isEmpty()) { + SuggestionsState.Empty + } else { + SuggestionsState.Present(suggestions.suggestions) + } + } + .subscribe(suggestionState::postValue, ExceptionHandlers::defaultExceptionHandler) + .addTo(compositeDisposable) + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/Assets.kt b/app/src/main/java/at/shockbytes/dante/util/Assets.kt new file mode 100644 index 00000000..9cc05d96 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/util/Assets.kt @@ -0,0 +1,18 @@ +package at.shockbytes.dante.util + +import android.content.Context +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.io.BufferedReader + +object Assets { + + fun readFile(context: Context, fileName: String): Single { + return singleOf(Schedulers.io()) { + context.assets.open(fileName).use { inputStream -> + val reader = BufferedReader(inputStream.reader()) + reader.readText() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_suggestions.xml b/app/src/main/res/layout/fragment_suggestions.xml index 5d8c61f2..38530033 100644 --- a/app/src/main/res/layout/fragment_suggestions.xml +++ b/app/src/main/res/layout/fragment_suggestions.xml @@ -16,7 +16,7 @@ android:layout_marginRight="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" - tools:text="Coming soon..." + android:text="Coming soon..." app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/core/src/main/java/at/shockbytes/dante/core/data/local/RealmBookEntityDao.kt b/core/src/main/java/at/shockbytes/dante/core/data/local/RealmBookEntityDao.kt index 20225453..69f698ab 100644 --- a/core/src/main/java/at/shockbytes/dante/core/data/local/RealmBookEntityDao.kt +++ b/core/src/main/java/at/shockbytes/dante/core/data/local/RealmBookEntityDao.kt @@ -122,7 +122,7 @@ class RealmBookEntityDao(private val realm: RealmInstanceProvider) : BookEntityD .contains("author", query, Case.INSENSITIVE) .or() .contains("subTitle", query, Case.INSENSITIVE) - .findAll() + .findAllAsync() .asFlowable() .map { mapper.mapTo(it) } .toObservable() diff --git a/util/src/main/java/at/shockbytes/dante/util/DanteExtensions.kt b/util/src/main/java/at/shockbytes/dante/util/DanteExtensions.kt index 0a8b97b7..66c10677 100644 --- a/util/src/main/java/at/shockbytes/dante/util/DanteExtensions.kt +++ b/util/src/main/java/at/shockbytes/dante/util/DanteExtensions.kt @@ -145,6 +145,10 @@ inline fun Fragment.viewModelOf(factory: ViewModelProvid return ViewModelProvider(this.viewModelStore, factory)[T::class.java] } +inline fun Fragment.lazyViewModelOf(factory: ViewModelProvider.Factory): Lazy { + return lazyOf(ViewModelProvider(this.viewModelStore, factory)[T::class.java]) +} + inline fun Fragment.viewModelOfActivity(activity: FragmentActivity, factory: ViewModelProvider.Factory): T { return ViewModelProvider(activity.viewModelStore, factory)[T::class.java] } From eb6c7cb493f8b5d32f21f41d68140ace96c3ec33 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Mon, 30 Nov 2020 09:26:20 +0100 Subject: [PATCH 05/30] Add wishlist state to BookState --- app/src/main/assets/suggestions-v1.json | 10 ++--- .../dante/suggestions/BookSuggestionEntity.kt | 2 +- .../fragment/BookActionBottomSheetFragment.kt | 1 + .../dante/ui/fragment/ManualAddFragment.kt | 5 +-- .../dante/ui/viewmodel/ManualAddViewModel.kt | 1 + .../shockbytes/dante/core/book/BookEntity.kt | 40 ++++++++++++------- .../shockbytes/dante/core/book/BookState.kt | 3 +- 7 files changed, 38 insertions(+), 24 deletions(-) diff --git a/app/src/main/assets/suggestions-v1.json b/app/src/main/assets/suggestions-v1.json index 64dac565..b3010810 100644 --- a/app/src/main/assets/suggestions-v1.json +++ b/app/src/main/assets/suggestions-v1.json @@ -12,7 +12,7 @@ "googleBooksLink": "http://books.google.at/books?id=916TDwAAQBAJ&dq=9781788162050&hl=&source=gbs_api", "language": "en", "summary": "'Whether you are an athlete, an investor, a writer or an entrepreneur, this little but soulful book will open the door to a healthier, less anxious and more productive life and career.' - Arianna Huffington 'Ryan's trilogy of The Obstacle is the Way, Ego is the Enemy and Stillness is the Key are for sure must-reads.' Manu Ginobili, 4x NBA champion and Olympic Gold Medalist Throughout history, there's been one indelible quality that great leaders, makers, artists and fighters have shared. The Zen Buddhists described it as inner peace, the Stoics called it ataraxia and Ryan Holiday calls it stillness: the ability to be steady, focussed and calm in a constantly busy world. This quality, valued by every major school of thought from Buddha to Seneca, John Stuart Mill to Nietzsche, is urgently necessary today. And, Holiday shows, it is entirely attainable. Just as Winston Churchill used bricklaying as a time to recharge and reflect, or Oprah Winfrey learned deep empathy from her quiet childhood, we can all benefit from stillness to feed into our greater ambitions - whether winning a battle, building a business, or simply finding happiness, peace and self-direction. Filled with wisdom and examples from historical and contemporary figures, this book shows how to cultivate this quality in your own life. Because stillness is not merely inactivity, but the doorway to the self-mastery, discipline and focus necessary to succeed in this competitive, noisy world.", - "state": "TODO" + "state": "WISHLIST" }, "suggester": { "name": "Martin", @@ -32,7 +32,7 @@ "googleBooksLink": "http://books.google.at/books?id=heCtnQEACAAJ&dq=9780262525671&hl=&source=gbs_api", "language": "en", "summary": "Even the smartest among us can feel inept as we fail to figure out which light switch or oven burner to turn on, or whether to push, pull, or slide a door. The fault, argues this ingenious-even liberating-book, lies not in ourselves, but in product design that ignores the needs of users and the principles of cognitive psychology. The problems range from ambiguous and hidden controls to arbitrary relationships between controls and functions, coupled with a lack of feedback or other assistance and unreasonable demands on memorization. The Design of Everyday Things shows that good, usable design is possible. The rules are simple: make things visible, exploit natural relationships that couple function and control, and make intelligent use of constraints. The goal: guide the user effortlessly to the right action on the right control at the right time. In this entertaining and insightful analysis, cognitive scientist Don Norman hails excellence of design as the most important key to regaining the competitive edge in influencing consumer behavior. Now fully expanded and updated, with a new introduction by the author, The Design of Everyday Things is a powerful primer on how-and why-some products satisfy customers while others only frustrate them.", - "state": "TODO" + "state": "WISHLIST" }, "suggester": { "name": "Martin", @@ -52,7 +52,7 @@ "googleBooksLink": "http://books.google.at/books?id=49Q2WWodc2QC&dq=9780091917593&hl=&source=gbs_api", "language": "en", "summary": "How could Nazi killers shoot Jewish women and children at close range? Why did Japanese soldiers rape and murder on such a horrendous scale? How was it possible to endure the torment of a Nazi concentration camp? Award-winning documentary maker and historian Laurence Rees has spent nearly 20 years wrestling with these questions in the course of filming hundreds of interviews with people tested to the extreme during World War II. He has come face-to-face with rapists, mass murderers, even cannibals, but he has also met courageous individuals who are an inspiration to us all. In Their Darkest Hour he presents 35 of his most electrifying encounters.", - "state": "TODO" + "state": "WISHLIST" }, "suggester": { "name": "Martin", @@ -72,7 +72,7 @@ "googleBooksLink": "http://books.google.at/books?id=pgvqsaYvgacC&dq=Nothing%2Bnew%2Bon%2Bthe%2Bwestern%2Bfront&hl=&source=gbs_api", "language": "en", "summary": "Considered by many the greatest war novel of all time, All Quiet on the Western Front is Erich Maria Remarque’s masterpiece of the German experience during World War I. I am young, I am twenty years old; yet I know nothing of life but despair, death, fear, and fatuous superficiality cast over an abyss of sorrow. . . . This is the testament of Paul Bäumer, who enlists with his classmates in the German army during World War I. They become soldiers with youthful enthusiasm. But the world of duty, culture, and progress they had been taught breaks in pieces under the first bombardment in the trenches. Through years of vivid horror, Paul holds fast to a single vow: to fight against the principle of hate that meaninglessly pits young men of the same generation but different uniforms against one another . . . if only he can come out of the war alive. “The world has a great writer in Erich Maria Remarque. He is a craftsman of unquestionably first rank, a man who can bend language to his will. Whether he writes of men or of inanimate nature, his touch is sensitive, firm, and sure.”—The New York Times Book Review", - "state": "TODO" + "state": "WISHLIST" }, "suggester": { "name": "Martin", @@ -92,7 +92,7 @@ "googleBooksLink": "https://play.google.com/store/books/details?id=ZuKTvERuPG8C&source=gbs_api", "language": "en", "summary": "Major New York Times bestseller Winner of the National Academy of Sciences Best Book Award in 2012 Selected by the New York Times Book Review as one of the ten best books of 2011 A Globe and Mail Best Books of the Year 2011 Title One of The Economist's 2011 Books of the Year One of The Wall Street Journal's Best Nonfiction Books of the Year 2011 2013 Presidential Medal of Freedom Recipient Kahneman's work with Amos Tversky is the subject of Michael Lewis's The Undoing Project: A Friendship That Changed Our Minds In the international bestseller, Thinking, Fast and Slow, Daniel Kahneman, the renowned psychologist and winner of the Nobel Prize in Economics, takes us on a groundbreaking tour of the mind and explains the two systems that drive the way we think. System 1 is fast, intuitive, and emotional; System 2 is slower, more deliberative, and more logical. The impact of overconfidence on corporate strategies, the difficulties of predicting what will make us happy in the future, the profound effect of cognitive biases on everything from playing the stock market to planning our next vacation—each of these can be understood only by knowing how the two systems shape our judgments and decisions. Engaging the reader in a lively conversation about how we think, Kahneman reveals where we can and cannot trust our intuitions and how we can tap into the benefits of slow thinking. He offers practical and enlightening insights into how choices are made in both our business and our personal lives—and how we can use different techniques to guard against the mental glitches that often get us into trouble. Winner of the National Academy of Sciences Best Book Award and the Los Angeles Times Book Prize and selected by The New York Times Book Review as one of the ten best books of 2011, Thinking, Fast and Slow is destined to be a classic.", - "state": "TODO" + "state": "WISHLIST" }, "suggester": { "name": "Martin", diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt b/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt index 59526fb6..bbef86d2 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/BookSuggestionEntity.kt @@ -6,7 +6,7 @@ data class BookSuggestionEntity( val title: String = "", val subTitle: String = "", val author: String = "", - val state: BookState = BookState.READING, // TODO Change to WISHLIST + val state: BookState = BookState.WISHLIST, val pageCount: Int = 0, val publishedDate: String = "", val isbn: String = "", diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt index 8bdf6bfd..ad306eb2 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt @@ -38,6 +38,7 @@ class BookActionBottomSheetFragment : BaseBottomSheetFragment() { BookState.READ_LATER -> btn_book_action_move_to_upcoming BookState.READING -> btn_book_action_move_to_current BookState.READ -> btn_book_action_move_to_done + BookState.WISHLIST -> throw IllegalStateException("Wishlist state not supported in BookActionBottomSheetFragment!") }.setVisible(false) } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt index 0d61bc20..a8c5c751 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt @@ -1,7 +1,6 @@ package at.shockbytes.dante.ui.fragment import android.content.Intent -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable @@ -148,7 +147,7 @@ class ManualAddFragment : BaseFragment(), ImageLoadingCallback { private fun setupObserver() { - viewModel.getImageState().observe(this, Observer { imageState -> + viewModel.getImageState().observe(this, { imageState -> when (imageState) { is ManualAddViewModel.ImageState.ThumbnailUri -> { @@ -172,7 +171,7 @@ class ManualAddFragment : BaseFragment(), ImageLoadingCallback { } }) - viewModel.getViewState().observe(this, Observer { viewState -> + viewModel.getViewState().observe(this, { viewState -> when (viewState) { ManualAddViewModel.ViewState.ManualAdd -> { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt index d0756b5e..9204d2e2 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt @@ -210,6 +210,7 @@ class ManualAddViewModel @Inject constructor( BookState.READ_LATER -> entity.wishlistDate = System.currentTimeMillis() BookState.READING -> entity.startDate = System.currentTimeMillis() BookState.READ -> entity.endDate = System.currentTimeMillis() + BookState.WISHLIST -> throw IllegalStateException("WISHLIST not supported for manual adding") } entity } diff --git a/core/src/main/java/at/shockbytes/dante/core/book/BookEntity.kt b/core/src/main/java/at/shockbytes/dante/core/book/BookEntity.kt index 066ab0c0..4b5e4066 100644 --- a/core/src/main/java/at/shockbytes/dante/core/book/BookEntity.kt +++ b/core/src/main/java/at/shockbytes/dante/core/book/BookEntity.kt @@ -7,29 +7,36 @@ import kotlinx.android.parcel.Parcelize /** * Author: Martin Macheiner * Date: 12.06.2018 + * + * NOTE: Would be great if we can get rid off all the vars and make them vals. */ @Parcelize data class BookEntity( var id: Long = -1, - var title: String = "", - var subTitle: String = "", - var author: String = "", + val title: String = "", + val subTitle: String = "", + val author: String = "", var state: BookState = BookState.READING, - var pageCount: Int = 0, - var publishedDate: String = "", + val pageCount: Int = 0, + val publishedDate: String = "", var position: Int = 0, - var isbn: String = "", - var thumbnailAddress: String? = null, - var googleBooksLink: String? = null, + val isbn: String = "", + val thumbnailAddress: String? = null, + val googleBooksLink: String? = null, var startDate: Long = 0, var endDate: Long = 0, + /** + * Actually `forLaterDate` and should not be confused with BookState.WISHLIST. This mishap + * is due to the initial naming and cannot be changed without breaking prior backups. So, just + * treat this as `forLaterDate` and everything is fine + */ var wishlistDate: Long = 0, - var language: String? = "NA", - var rating: Int = 0, - var currentPage: Int = 0, - var notes: String? = null, - var summary: String? = null, - var labels: List = listOf() + val language: String? = "NA", + val rating: Int = 0, + val currentPage: Int = 0, + val notes: String? = null, + val summary: String? = null, + val labels: List = listOf() ) : Parcelable { val reading: Boolean @@ -57,6 +64,11 @@ data class BookEntity( BookState.READ -> { endDate = System.currentTimeMillis() } + BookState.WISHLIST -> { + startDate = 0 + endDate = 0 + wishlistDate = 0 + } } } diff --git a/core/src/main/java/at/shockbytes/dante/core/book/BookState.kt b/core/src/main/java/at/shockbytes/dante/core/book/BookState.kt index 7881c5ee..ff185e15 100644 --- a/core/src/main/java/at/shockbytes/dante/core/book/BookState.kt +++ b/core/src/main/java/at/shockbytes/dante/core/book/BookState.kt @@ -3,7 +3,8 @@ package at.shockbytes.dante.core.book enum class BookState { READ_LATER, READING, - READ; + READ, + WISHLIST; companion object { fun fromString(name: String?, defaultValue: BookState = READ_LATER): BookState { From 8e6530dc5d84ba5f56832b1e5bf86254fc560202 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Mon, 30 Nov 2020 21:02:39 +0100 Subject: [PATCH 06/30] Suggestions UI --- app/src/main/assets/suggestions-v1.json | 15 +- .../dante/suggestions/Suggestion.kt | 1 + .../OnSuggestionActionClickedListener.kt | 10 ++ .../dante/ui/adapter/SuggestionsAdapter.kt | 95 +++++++++++ .../dante/ui/fragment/InspirationsFragment.kt | 12 ++ .../dante/ui/fragment/MainBookFragment.kt | 4 - .../dante/ui/fragment/SuggestionsFragment.kt | 32 +++- .../dialog/CreateLabelDialogFragment.kt | 2 +- .../ui/viewmodel/SuggestionsViewModel.kt | 2 +- .../drawable/ic_onboarding_suggestions.xml | 2 +- app/src/main/res/drawable/ic_quote.xml | 10 ++ .../res/drawable/ic_report_suggestion.xml | 10 ++ app/src/main/res/drawable/ic_suggestions.xml | 10 ++ .../drawable/navigation_item_suggestions.xml | 2 +- .../main/res/layout/fragment_inspirations.xml | 4 +- .../main/res/layout/fragment_suggestions.xml | 41 +++-- app/src/main/res/layout/item_suggestion.xml | 158 ++++++++++++++++++ core/src/main/res/drawable/ic_wishlist.xml | 22 +++ core/src/main/res/values-de/strings.xml | 4 + core/src/main/res/values/colors.xml | 2 +- core/src/main/res/values/strings.xml | 2 + 21 files changed, 404 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt create mode 100644 app/src/main/res/drawable/ic_quote.xml create mode 100644 app/src/main/res/drawable/ic_report_suggestion.xml create mode 100644 app/src/main/res/drawable/ic_suggestions.xml create mode 100644 app/src/main/res/layout/item_suggestion.xml create mode 100644 core/src/main/res/drawable/ic_wishlist.xml diff --git a/app/src/main/assets/suggestions-v1.json b/app/src/main/assets/suggestions-v1.json index b3010810..34c3c48c 100644 --- a/app/src/main/assets/suggestions-v1.json +++ b/app/src/main/assets/suggestions-v1.json @@ -1,6 +1,7 @@ { "suggestions": [ { + "suggestionId": "1", "suggestion": { "title": "Stillness is the Key", "subTitle": "An Ancient Strategy for Modern Life", @@ -18,9 +19,10 @@ "name": "Martin", "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" }, - "recommendation": "TODO" + "recommendation": "How can one be still in such a fast-paced world? How can one stay on the own path? Ryan Holiday is such an amazing author, this is one of his best books!" }, { + "suggestionId": "2", "suggestion": { "title": "The Design of Everyday Things", "subTitle": "", @@ -38,9 +40,10 @@ "name": "Martin", "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" }, - "recommendation": "TODO" + "recommendation": "Anyone who works in the digital space needs to read this book. Honestly, there is no way around Donald Norman's classic." }, { + "suggestionId": "3", "suggestion": { "title": "Their Darkest Hour", "subTitle": "People Tested to the Extreme in WWII", @@ -58,9 +61,10 @@ "name": "Martin", "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" }, - "recommendation": "TODO" + "recommendation": "This is a must-read for everyone. It's unbelievable what humans were capable just 80 years ago. Definitely an eye-opener." }, { + "suggestionId": "4", "suggestion": { "title": "All Quiet on the Western Front", "subTitle": "A Novel", @@ -78,9 +82,10 @@ "name": "Martin", "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" }, - "recommendation": "TODO" + "recommendation": "Another great read about the catastrophes of war. Various pages that will leave you speechless." }, { + "suggestionId": "5", "suggestion": { "title": "Thinking, Fast and Slow", "subTitle": "", @@ -98,7 +103,7 @@ "name": "Martin", "photoUrl": "https://lh3.googleusercontent.com/a-/AAuE7mAPzi9I0oWnggqmVR5fL9Be88VNOChcDom8QgQ1fA=s96-c" }, - "recommendation": "TODO" + "recommendation": "Ever wondered how your brain rationalizes your irrational behavior? Then you will love this one. 2-3 pages a day are enough to challenge your brain, I guarantee." } ] } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt index d6bdd16b..be9bca30 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt @@ -1,6 +1,7 @@ package at.shockbytes.dante.suggestions data class Suggestion( + val suggestionId: String, val suggestion: BookSuggestionEntity, val suggester: Suggester, val recommendation: String diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt new file mode 100644 index 00000000..bedd0313 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt @@ -0,0 +1,10 @@ +package at.shockbytes.dante.ui.adapter + +import at.shockbytes.dante.suggestions.BookSuggestionEntity + +interface OnSuggestionActionClickedListener { + + fun onAddSuggestionToWishlist(data: BookSuggestionEntity) + + fun onReportBookSuggestion(suggestionId: String) +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt new file mode 100644 index 00000000..d70dcd7a --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt @@ -0,0 +1,95 @@ +package at.shockbytes.dante.ui.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import at.shockbytes.dante.R +import at.shockbytes.dante.core.image.ImageLoader +import at.shockbytes.dante.suggestions.BookSuggestionEntity +import at.shockbytes.dante.suggestions.Suggester +import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.util.AppUtils +import at.shockbytes.util.adapter.BaseAdapter +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_suggestion.* + +class SuggestionsAdapter( + ctx: Context, + private val imageLoader: ImageLoader, + private val onSuggestionActionClickedListener: OnSuggestionActionClickedListener +) : BaseAdapter(ctx) { + + fun updateData(suggestions: List) { + data.clear() + data.addAll(suggestions) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return SuggestionViewHolder(inflater.inflate(R.layout.item_suggestion, parent, false)) + } + + private inner class SuggestionViewHolder( + override val containerView: View + ) : BaseAdapter.ViewHolder(containerView), LayoutContainer { + + override fun bindToView(content: Suggestion, position: Int) { + with(content) { + setupOverflowMenu(suggestionId) + setupBook(suggestion) + setupSuggester(suggester) + setupRecommendation(recommendation) + setupBookActionListener(suggestion) + } + } + + private fun setupOverflowMenu(suggestionId: String) { + iv_item_suggestion_report.setOnClickListener { + onSuggestionActionClickedListener.onReportBookSuggestion(suggestionId) + } + } + + private fun setupBook(suggestion: BookSuggestionEntity) { + tv_item_suggestion_author.text = suggestion.author + tv_item_suggestion_title.text = suggestion.title + setThumbnailToView( + suggestion.thumbnailAddress, + iv_item_suggestion_cover, + context.resources.getDimension(R.dimen.thumbnail_rounded_corner).toInt() + ) + } + + private fun setupSuggester(suggester: Suggester) { + tv_item_suggestion_suggester.text = context.getString(R.string.suggestion_suggester, suggester.name) + setThumbnailToView( + suggester.photoUrl, + iv_item_suggestion_suggester, + AppUtils.convertDpInPixel(24, context) + ) + } + + private fun setupRecommendation(recommendation: String) { + tv_item_suggestion_recommendation.text = recommendation + } + + private fun setupBookActionListener(suggestion: BookSuggestionEntity) { + btn_item_suggestion_add.setOnClickListener { + onSuggestionActionClickedListener.onAddSuggestionToWishlist(suggestion) + } + } + + private fun setThumbnailToView( + url: String?, + view: ImageView, + radius: Int + ) { + if (!url.isNullOrEmpty()) { + imageLoader.loadImageWithCornerRadius(context, url, view, cornerDimension = radius) + } else { + // Books with no image will recycle another cover if not cleared here + view.setImageResource(R.drawable.ic_placeholder) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt index 7464f86d..cefc1792 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt @@ -1,9 +1,11 @@ package at.shockbytes.dante.ui.fragment +import androidx.core.content.ContextCompat import at.shockbytes.dante.R import at.shockbytes.dante.injection.AppComponent import at.shockbytes.dante.ui.adapter.InspirationsPagerAdapter import at.shockbytes.dante.util.setVisible +import com.google.android.material.tabs.TabLayout import kotlinx.android.synthetic.main.dante_toolbar.* import kotlinx.android.synthetic.main.fragment_inspirations.* @@ -15,6 +17,7 @@ class InspirationsFragment : BaseFragment() { setupToolbar() setupViewPager() connectTabsAndViewPager() + setupTabIcons() } private fun setupToolbar() { @@ -40,6 +43,15 @@ class InspirationsFragment : BaseFragment() { tabs_fragment_inspirations.setupWithViewPager(vp_fragment_inspirations) } + private fun setupTabIcons() { + getTabAt(0)?.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_wishlist) + getTabAt(1)?.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_suggestions) + } + + private fun getTabAt(index: Int): TabLayout.Tab? { + return tabs_fragment_inspirations.getTabAt(index) + } + override fun injectToGraph(appComponent: AppComponent) = Unit override fun bindViewModel() { diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index ad72199c..0422b0cd 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -26,7 +26,6 @@ import at.shockbytes.dante.navigation.Destination import at.shockbytes.dante.navigation.Destination.BookDetail.BookDetailInfo import at.shockbytes.dante.ui.adapter.main.BookAdapter import at.shockbytes.dante.core.image.ImageLoader -import at.shockbytes.dante.flagging.FeatureFlagging import at.shockbytes.dante.ui.activity.ManualAddActivity.Companion.EXTRA_UPDATED_BOOK_STATE import at.shockbytes.dante.ui.adapter.OnBookActionClickedListener import at.shockbytes.dante.ui.adapter.main.BookAdapterEntity @@ -60,9 +59,6 @@ class MainBookFragment : BaseFragment(), @Inject lateinit var imageLoader: ImageLoader - @Inject - lateinit var featureFlagging: FeatureFlagging - private lateinit var bookState: BookState private lateinit var bookAdapter: BookAdapter private lateinit var viewModel: BookListViewModel diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 195be093..555f129f 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -3,10 +3,15 @@ package at.shockbytes.dante.ui.fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import at.shockbytes.dante.R +import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.injection.AppComponent +import at.shockbytes.dante.suggestions.BookSuggestionEntity import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener +import at.shockbytes.dante.ui.adapter.SuggestionsAdapter import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel import at.shockbytes.dante.util.viewModelOf +import kotlinx.android.synthetic.main.fragment_suggestions.* import javax.inject.Inject /** @@ -20,9 +25,30 @@ class SuggestionsFragment : BaseFragment() { @Inject lateinit var vmFactory: ViewModelProvider.Factory + @Inject + lateinit var imageLoader: ImageLoader + private val viewModel: SuggestionsViewModel by lazy { viewModelOf(vmFactory) } + private lateinit var adapter: SuggestionsAdapter + override fun setupViews() { + + adapter = SuggestionsAdapter( + requireContext(), + imageLoader, + onSuggestionActionClickedListener = object : OnSuggestionActionClickedListener { + override fun onAddSuggestionToWishlist(data: BookSuggestionEntity) { + // TODO Add to wishlist & track event + showToast("Add to wishlist") + } + + override fun onReportBookSuggestion(suggestionId: String) { + showToast("Report suggestion!") + } + } + ) + rv_suggestions.adapter = adapter } override fun injectToGraph(appComponent: AppComponent) { @@ -30,13 +56,11 @@ class SuggestionsFragment : BaseFragment() { } override fun bindViewModel() { - viewModel.requestSuggestions() viewModel.getSuggestionState().observe(this, Observer(::handleSuggestionState)) } private fun handleSuggestionState(suggestionsState: SuggestionsViewModel.SuggestionsState) { - when (suggestionsState) { is SuggestionsViewModel.SuggestionsState.Present -> handleSuggestions(suggestionsState.suggestions) SuggestionsViewModel.SuggestionsState.Empty -> handleEmptyState() @@ -44,11 +68,11 @@ class SuggestionsFragment : BaseFragment() { } private fun handleEmptyState() { - // TODO + // TODO Handle empty state! Important for online feature later } private fun handleSuggestions(suggestions: List) { - // TODO + adapter.data = suggestions.toMutableList() } override fun unbindViewModel() = Unit diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/dialog/CreateLabelDialogFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/dialog/CreateLabelDialogFragment.kt index 028c798d..795f65c4 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/dialog/CreateLabelDialogFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/dialog/CreateLabelDialogFragment.kt @@ -27,7 +27,7 @@ class CreateLabelDialogFragment : InteractiveViewDialogFragment() { R.color.color_error, R.color.nice_color, R.color.indigo, - R.color.tabcolor_suggestions, + R.color.inspirations, R.color.teal_500, R.color.pink_500 ) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 7b29a796..4d29e84e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -29,7 +29,7 @@ class SuggestionsViewModel @Inject constructor( if (suggestions.suggestions.isEmpty()) { SuggestionsState.Empty } else { - SuggestionsState.Present(suggestions.suggestions) + SuggestionsState.Present(suggestions.suggestions.sortedBy { it.suggestionId }) } } .subscribe(suggestionState::postValue, ExceptionHandlers::defaultExceptionHandler) diff --git a/app/src/main/res/drawable/ic_onboarding_suggestions.xml b/app/src/main/res/drawable/ic_onboarding_suggestions.xml index 96114834..86301cde 100644 --- a/app/src/main/res/drawable/ic_onboarding_suggestions.xml +++ b/app/src/main/res/drawable/ic_onboarding_suggestions.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_quote.xml b/app/src/main/res/drawable/ic_quote.xml new file mode 100644 index 00000000..fb86f1e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_quote.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_report_suggestion.xml b/app/src/main/res/drawable/ic_report_suggestion.xml new file mode 100644 index 00000000..dc41ef61 --- /dev/null +++ b/app/src/main/res/drawable/ic_report_suggestion.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_suggestions.xml b/app/src/main/res/drawable/ic_suggestions.xml new file mode 100644 index 00000000..210f8ea1 --- /dev/null +++ b/app/src/main/res/drawable/ic_suggestions.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/navigation_item_suggestions.xml b/app/src/main/res/drawable/navigation_item_suggestions.xml index 8c136257..467be0ce 100644 --- a/app/src/main/res/drawable/navigation_item_suggestions.xml +++ b/app/src/main/res/drawable/navigation_item_suggestions.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_inspirations.xml b/app/src/main/res/layout/fragment_inspirations.xml index 3d8a5271..71e82fc4 100644 --- a/app/src/main/res/layout/fragment_inspirations.xml +++ b/app/src/main/res/layout/fragment_inspirations.xml @@ -8,12 +8,13 @@ @@ -22,6 +23,7 @@ style="@style/Widget.MaterialComponents.TabLayout" android:id="@+id/tabs_fragment_inspirations" android:background="@color/mainBackground" + app:tabIconTint="@color/danteAccent" app:tabSelectedTextColor="@color/danteAccent" app:tabIndicatorColor="@color/danteAccent" android:layout_width="match_parent" diff --git a/app/src/main/res/layout/fragment_suggestions.xml b/app/src/main/res/layout/fragment_suggestions.xml index 38530033..01465447 100644 --- a/app/src/main/res/layout/fragment_suggestions.xml +++ b/app/src/main/res/layout/fragment_suggestions.xml @@ -1,24 +1,31 @@ - - + + - \ No newline at end of file + android:layout_gravity="center" + android:alpha="0" + android:drawableTop="@drawable/ic_empty_indicator" + android:drawablePadding="16dp" + android:gravity="center" + android:padding="8dp" + android:textColor="@color/colorSecondaryText" + android:textSize="25sp" + android:textStyle="bold" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_suggestion.xml b/app/src/main/res/layout/item_suggestion.xml new file mode 100644 index 00000000..eeb61a59 --- /dev/null +++ b/app/src/main/res/layout/item_suggestion.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/ic_wishlist.xml b/core/src/main/res/drawable/ic_wishlist.xml new file mode 100644 index 00000000..6cfd054c --- /dev/null +++ b/core/src/main/res/drawable/ic_wishlist.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index fa9eb71f..f0ac7458 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -324,5 +324,9 @@ Importiere deine Bibliothek von einem vorherigen exportierten Dante externen Backup. Du kannst hier nur solche externen Backups importieren. Öffnen Screen öffnen + Vorschläge + Deine Wunschliste + Zur Wunschliste hinzufügen + Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index 0eb37648..59083d81 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -53,7 +53,7 @@ #E91E63 #A8A8A8 - #9C27B0 + #9C27B0 #757575 #FFC107 #8BC34A diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 0eb87bc5..b83d8d08 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -353,6 +353,8 @@ Your wishlist is empty! Good! Seems like you have all your books already. Your Wishlist + Add to wishlist + - %s Suggestions One of those books? From a34693c6c3bc0bfcef89a6a6590bd5fe4e3296ac Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 19:41:34 +0100 Subject: [PATCH 07/30] Upload local uri to Firebase --- app/build.gradle | 1 + .../shockbytes/dante/flagging/FeatureFlag.kt | 2 +- .../dante/injection/FirebaseModule.kt | 7 ++ .../storage/FirebaseImageUploadStorage.kt | 61 ++++++++++++++ .../dante/storage/ImageUploadStorage.kt | 9 ++ .../dante/ui/fragment/ManualAddFragment.kt | 84 ++++++++++--------- .../dante/ui/viewmodel/ManualAddViewModel.kt | 27 +++++- 7 files changed, 147 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/storage/FirebaseImageUploadStorage.kt create mode 100644 app/src/main/java/at/shockbytes/dante/storage/ImageUploadStorage.kt diff --git a/app/build.gradle b/app/build.gradle index 94f0b1e0..0ccffd77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation "com.google.firebase:firebase-auth:$firebaseVersionAuth" implementation "com.google.firebase:firebase-ml-vision:$firebaseMLVision" + implementation 'com.google.firebase:firebase-storage-ktx:19.2.0' implementation ('com.google.http-client:google-http-client-gson:1.36.0') { exclude group: 'org.apache.httpcomponents' diff --git a/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt b/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt index d7fe3549..74977d48 100644 --- a/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt +++ b/app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt @@ -1,7 +1,7 @@ package at.shockbytes.dante.flagging enum class FeatureFlag(val key: String, val displayName: String, val defaultValue: Boolean) { - ; + Unused("", "", false); companion object { diff --git a/app/src/main/java/at/shockbytes/dante/injection/FirebaseModule.kt b/app/src/main/java/at/shockbytes/dante/injection/FirebaseModule.kt index 716da7cb..af67053d 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/FirebaseModule.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/FirebaseModule.kt @@ -3,6 +3,8 @@ package at.shockbytes.dante.injection import android.content.Context import at.shockbytes.dante.BuildConfig import at.shockbytes.dante.R +import at.shockbytes.dante.storage.FirebaseImageUploadStorage +import at.shockbytes.dante.storage.ImageUploadStorage import at.shockbytes.tracking.DebugTracker import at.shockbytes.tracking.FirebaseTracker import at.shockbytes.tracking.Tracker @@ -42,4 +44,9 @@ class FirebaseModule(private val context: Context) { FirebaseTracker(context) } } + + @Provides + fun provideImageUploadStorage(): ImageUploadStorage { + return FirebaseImageUploadStorage() + } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/storage/FirebaseImageUploadStorage.kt b/app/src/main/java/at/shockbytes/dante/storage/FirebaseImageUploadStorage.kt new file mode 100644 index 00000000..257ff3ff --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/storage/FirebaseImageUploadStorage.kt @@ -0,0 +1,61 @@ +package at.shockbytes.dante.storage + +import android.net.Uri +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.ktx.Firebase +import com.google.firebase.storage.UploadTask +import com.google.firebase.storage.ktx.storage +import io.reactivex.Single +import io.reactivex.SingleEmitter +import java.util.UUID + +class FirebaseImageUploadStorage : ImageUploadStorage { + + private val imageRef = Firebase.storage.getReference("/custom_images") + + private var _backupUid: String? = null + private val randomSessionUid: String + get() = if (_backupUid == null) { + "anonymous/${UUID.randomUUID()}".also { _backupUid = it } + } else _backupUid!! + + private val uid: String + get() = FirebaseAuth.getInstance().currentUser?.uid ?: randomSessionUid + + private val defaultException = IllegalStateException("Unable to upload image! No reason given!") + + override fun upload(image: Uri, progressListener: ((Int) -> Unit)?): Single { + return Single.create { emitter -> + + val ref = imageRef.child("/$uid/${image.lastPathSegment}") + + ref.putFile(image) + .addOnProgressListener { task -> + progressListener?.invoke(task.progress) + } + .continueWithTask { task -> + if (!task.isSuccessful) { + emitter.tryOnErrorOrDefault(task.exception, defaultException) + } + ref.downloadUrl + } + .addOnCompleteListener { task -> + if (task.isSuccessful) { + emitter.onSuccess(task.result) + } else { + emitter.tryOnErrorOrDefault(task.exception, defaultException) + } + } + } + } + + private val UploadTask.TaskSnapshot.progress: Int + get() = ((100.0 * bytesTransferred) / totalByteCount).toInt() + + private fun SingleEmitter.tryOnErrorOrDefault( + exception: Exception?, + defaultException: Exception + ) { + tryOnError(exception ?: defaultException) + } +} diff --git a/app/src/main/java/at/shockbytes/dante/storage/ImageUploadStorage.kt b/app/src/main/java/at/shockbytes/dante/storage/ImageUploadStorage.kt new file mode 100644 index 00000000..c5ba4410 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/storage/ImageUploadStorage.kt @@ -0,0 +1,9 @@ +package at.shockbytes.dante.storage + +import android.net.Uri +import io.reactivex.Single + +interface ImageUploadStorage { + + fun upload(image: Uri, progressListener: ((Int) -> Unit)? = null): Single +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt index a8c5c751..3d7bb5a4 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/ManualAddFragment.kt @@ -8,7 +8,9 @@ import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.HapticFeedbackConstants +import androidx.lifecycle.Observer import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.palette.graphics.Palette import at.shockbytes.dante.R import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.book.BookState @@ -60,9 +62,7 @@ class ManualAddFragment : BaseFragment(), ImageLoadingCallback { imgViewManualAdd.setOnClickListener { v -> v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) - activity?.let { a -> - viewModel.pickImage(a) - } + viewModel.pickImage(requireActivity()) } editTextManualAddTitle.addTextChangedListener(object : TextWatcher { @@ -122,54 +122,31 @@ class ManualAddFragment : BaseFragment(), ImageLoadingCallback { setupObserver() } - override fun unbindViewModel() { - // Not needed... - } + override fun unbindViewModel() = Unit - override fun onImageLoadingFailed(e: Exception?) { - Timber.e(e) - } + override fun onImageLoadingFailed(e: Exception?) = Timber.e(e) override fun onImageResourceReady(resource: Drawable?) { - (resource as? BitmapDrawable)?.bitmap?.let { bm -> - androidx.palette.graphics.Palette.from(bm).generate { palette -> + (resource as? BitmapDrawable)?.bitmap?.let(Palette::from)?.generate { palette -> - val actionBarColor = palette?.lightMutedSwatch?.rgb - val actionBarTextColor = palette?.lightMutedSwatch?.titleTextColor - val statusBarColor = palette?.darkMutedSwatch?.rgb + val actionBarColor = palette?.lightMutedSwatch?.rgb + val actionBarTextColor = palette?.lightMutedSwatch?.titleTextColor + val statusBarColor = palette?.darkMutedSwatch?.rgb - (activity as? TintableBackNavigableActivity)?.tintSystemBarsWithText(actionBarColor, - actionBarTextColor, statusBarColor) - } + (activity as? TintableBackNavigableActivity) + ?.tintSystemBarsWithText(actionBarColor, actionBarTextColor, statusBarColor) } } private fun setupObserver() { - viewModel.getImageState().observe(this, { imageState -> - - when (imageState) { - is ManualAddViewModel.ImageState.ThumbnailUri -> { - imageLoader.loadImageUri( - requireContext(), - imageState.uri, - imgViewManualAdd, - R.drawable.ic_placeholder, - circular = false, - callback = this, - callbackHandleValues = Pair(first = false, second = true) - ) - } - ManualAddViewModel.ImageState.NoImage -> { - imageLoader.loadImageResource( - requireContext(), - R.drawable.ic_placeholder, - imgViewManualAdd - ) - } - } - }) + viewModel.getImageState().observe(this, Observer(::handleImageState)) + + viewModel.getImageLoadingState() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(::handleImageLoadingState) + .addTo(compositeDisposable) viewModel.getViewState().observe(this, { viewState -> @@ -208,6 +185,33 @@ class ManualAddFragment : BaseFragment(), ImageLoadingCallback { .addTo(compositeDisposable) } + private fun handleImageState(imageState: ManualAddViewModel.ImageState) { + when (imageState) { + is ManualAddViewModel.ImageState.ThumbnailUri -> { + imageLoader.loadImageUri( + requireContext(), + imageState.uri, + imgViewManualAdd, + R.drawable.ic_placeholder, + circular = false, + callback = this, + callbackHandleValues = Pair(first = false, second = true) + ) + } + ManualAddViewModel.ImageState.NoImage -> { + imageLoader.loadImageResource( + requireContext(), + R.drawable.ic_placeholder, + imgViewManualAdd + ) + } + } + } + + private fun handleImageLoadingState(imageLoadingState: ManualAddViewModel.ImageLoadingState) { + // TODO Handle loading indicator + } + private fun sendBookCreatedBroadcast(createdBookState: BookState) { sendBroadcast( Intent(ACTION_BOOK_CREATED).putExtra(EXTRA_BOOK_CREATED_STATE, createdBookState) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt index 9204d2e2..905b78ec 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/ManualAddViewModel.kt @@ -9,6 +9,7 @@ import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.book.BookState import at.shockbytes.dante.core.data.BookRepository import at.shockbytes.dante.core.image.ImagePicker +import at.shockbytes.dante.storage.ImageUploadStorage import at.shockbytes.dante.ui.viewmodel.ManualAddViewModel.ImageState.ThumbnailUri import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.addTo @@ -23,7 +24,8 @@ import javax.inject.Inject */ class ManualAddViewModel @Inject constructor( private val bookRepository: BookRepository, - private val imagePicker: ImagePicker + private val imagePicker: ImagePicker, + private val imageUploadStorage: ImageUploadStorage ) : BaseViewModel() { data class BookUpdateData( @@ -53,11 +55,19 @@ class ManualAddViewModel @Inject constructor( } sealed class ImageState { - data class ThumbnailUri(val uri: Uri) : ImageState() object NoImage : ImageState() } + sealed class ImageLoadingState { + object Success : ImageLoadingState() + data class Loading(val progress: Int) : ImageLoadingState() + data class Error(val throwable: Throwable) : ImageLoadingState() + } + + private val imageLoadingState = PublishSubject.create() + fun getImageLoadingState(): Observable = imageLoadingState + private val imageState = MutableLiveData() fun getImageState(): LiveData = imageState @@ -84,14 +94,25 @@ class ManualAddViewModel @Inject constructor( fun pickImage(activity: FragmentActivity) { imagePicker .openGallery(activity) + .flatMapSingle { imageUri -> + imageUploadStorage.upload(imageUri, ::progressUpdate) + } .map(::ThumbnailUri) .doOnNext { thumbnailUri -> - Timber.d("Image thumbnail picked: ${thumbnailUri.uri}") + Timber.d("Image thumbnail uploaded and picked picked: ${thumbnailUri.uri}") + imageLoadingState.onNext(ImageLoadingState.Success) + } + .doOnError { throwable -> + imageLoadingState.onNext(ImageLoadingState.Error(throwable)) } .subscribe(imageState::postValue, ExceptionHandlers::defaultExceptionHandler) .addTo(compositeDisposable) } + private fun progressUpdate(progress: Int) { + imageLoadingState.onNext(ImageLoadingState.Loading(progress)) + } + fun storeBook( bookUpdateData: BookUpdateData, state: BookState From 61845ec1ccaf04750d42b8a1ce1518d8e607edd7 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 20:13:17 +0100 Subject: [PATCH 08/30] Add wishlist button to BarcodeScanResultBottomSheetDialogFragment --- ...codeScanResultBottomSheetDialogFragment.kt | 5 ++ .../fragment_barcode_scan_bottom_sheet.xml | 52 ++++++++++++------- core/src/main/res/layout/handle_view.xml | 4 +- core/src/main/res/values-de/strings.xml | 1 + core/src/main/res/values/strings.xml | 1 + 5 files changed, 43 insertions(+), 20 deletions(-) diff --git a/camera/src/main/java/at/shockbytes/dante/camera/BarcodeScanResultBottomSheetDialogFragment.kt b/camera/src/main/java/at/shockbytes/dante/camera/BarcodeScanResultBottomSheetDialogFragment.kt index 8964a911..4b5ca761 100644 --- a/camera/src/main/java/at/shockbytes/dante/camera/BarcodeScanResultBottomSheetDialogFragment.kt +++ b/camera/src/main/java/at/shockbytes/dante/camera/BarcodeScanResultBottomSheetDialogFragment.kt @@ -178,6 +178,11 @@ class BarcodeScanResultBottomSheetDialogFragment : BottomSheetDialogFragment() { onBookAddedListener?.invoke(title) } + btn_barcode_result_wishlist.setOnClickListener { + viewModel.storeBook(this, state = BookState.WISHLIST) + onBookAddedListener?.invoke(title) + } + btn_barcode_result_read.setOnClickListener { viewModel.storeBook(this, state = BookState.READ) onBookAddedListener?.invoke(title) diff --git a/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml b/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml index 6d39bd75..89f0e834 100644 --- a/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml +++ b/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml @@ -9,12 +9,12 @@ + app:layout_constraintStart_toStartOf="@+id/group_barcode_result" + app:layout_constraintTop_toTopOf="parent" /> @@ -42,9 +43,9 @@ @@ -105,12 +107,14 @@ + app:constraint_referenced_ids="layout_barcode_result_buttons,tv_barcode_result_author,tv_barcode_result_title,iv_barcode_scan_result_cover,btn_barcode_result_wishlist" /> + + \ No newline at end of file diff --git a/core/src/main/res/layout/handle_view.xml b/core/src/main/res/layout/handle_view.xml index d219a537..2a3491b0 100644 --- a/core/src/main/res/layout/handle_view.xml +++ b/core/src/main/res/layout/handle_view.xml @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index f0ac7458..be055816 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -328,5 +328,6 @@ Deine Wunschliste Zur Wunschliste hinzufügen Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! + Wunschliste diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index b83d8d08..f01ca7ac 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -353,6 +353,7 @@ Your wishlist is empty! Good! Seems like you have all your books already. Your Wishlist + Wishlist Add to wishlist - %s Suggestions From f38543bed4721c892307d0d7d95f8cb9df8ed8b6 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 20:13:48 +0100 Subject: [PATCH 09/30] Fix crash because RealmBook cannot resolve WISHLIST state --- .../main/java/at/shockbytes/dante/core/book/realm/RealmBook.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/at/shockbytes/dante/core/book/realm/RealmBook.kt b/core/src/main/java/at/shockbytes/dante/core/book/realm/RealmBook.kt index 84c46f1e..ca5f254d 100644 --- a/core/src/main/java/at/shockbytes/dante/core/book/realm/RealmBook.kt +++ b/core/src/main/java/at/shockbytes/dante/core/book/realm/RealmBook.kt @@ -33,7 +33,7 @@ open class RealmBook @JvmOverloads constructor( ) : RealmObject(), Gsonify { enum class State { - READ_LATER, READING, READ + READ_LATER, READING, READ, WISHLIST } private var ordinalState: Int = 0 From 589093dc44e07966fcc80d1193e38469ceace0b3 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 20:29:21 +0100 Subject: [PATCH 10/30] Move LayoutManager for books in dedicated class --- .../dante/ui/fragment/MainBookFragment.kt | 28 +++++-------------- .../dante/util/SharedViewComponents.kt | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index 0422b0cd..137dafbd 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -6,13 +6,9 @@ import android.content.Intent import android.content.IntentFilter import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import android.content.res.Configuration import android.os.Bundle import androidx.core.app.ActivityOptionsCompat import androidx.core.util.Pair -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import android.view.View import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -35,6 +31,7 @@ import at.shockbytes.dante.ui.viewmodel.BookListViewModel import at.shockbytes.dante.core.Constants.ACTION_BOOK_CREATED import at.shockbytes.dante.core.Constants.EXTRA_BOOK_CREATED_STATE import at.shockbytes.dante.util.DanteUtils +import at.shockbytes.dante.util.SharedViewComponents import at.shockbytes.dante.util.addTo import at.shockbytes.dante.util.runDelayed import at.shockbytes.dante.util.viewModelOf @@ -112,19 +109,6 @@ class MainBookFragment : BaseFragment(), } } - private val rvLayoutManager: RecyclerView.LayoutManager - get() = if (resources.getBoolean(R.bool.isTablet)) { - if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) - StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) - else - StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) - } else { - if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) - LinearLayoutManager(context, RecyclerView.VERTICAL, false) - else - StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = viewModelOf(vmFactory) @@ -233,14 +217,16 @@ class MainBookFragment : BaseFragment(), ) fragment_book_main_rv.apply { - layoutManager = rvLayoutManager + layoutManager = SharedViewComponents.layoutManagerForBooks(requireContext()) adapter = bookAdapter } val itemTouchHelper = ItemTouchHelper( - BaseItemTouchHelper(bookAdapter, - false, - BaseItemTouchHelper.DragAccess.VERTICAL) + BaseItemTouchHelper( + bookAdapter, + allowSwipeToDismiss = false, + BaseItemTouchHelper.DragAccess.VERTICAL + ) ) itemTouchHelper.attachToRecyclerView(fragment_book_main_rv) } diff --git a/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt b/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt new file mode 100644 index 00000000..4f57eceb --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt @@ -0,0 +1,28 @@ +package at.shockbytes.dante.util + +import android.content.Context +import android.content.res.Configuration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import at.shockbytes.dante.R + +object SharedViewComponents { + + fun layoutManagerForBooks(context: Context): RecyclerView.LayoutManager { + + val resources = context.resources + return if (resources.getBoolean(R.bool.isTablet)) { + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) + StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + else + StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + } else { + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) + LinearLayoutManager(context, RecyclerView.VERTICAL, false) + else + StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + } + } + +} \ No newline at end of file From 04e931aca60946031fea66a2e8200b4a0806e0ff Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 20:29:41 +0100 Subject: [PATCH 11/30] Implement ViewModel for Wishlist --- .../dante/ui/fragment/WishlistFragment.kt | 29 ++++++++++ .../dante/ui/viewmodel/WishlistViewModel.kt | 54 +++++++++++++++++++ .../main/res/layout/fragment_book_main.xml | 7 ++- app/src/main/res/layout/fragment_wishlist.xml | 8 +-- 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt index 7265a633..fe7c56da 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt @@ -1,12 +1,26 @@ package at.shockbytes.dante.ui.fragment +import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel import at.shockbytes.dante.R import at.shockbytes.dante.injection.AppComponent +import at.shockbytes.dante.injection.ViewModelFactory +import at.shockbytes.dante.ui.viewmodel.WishlistViewModel +import at.shockbytes.dante.util.setVisible +import at.shockbytes.dante.util.viewModelOf +import kotlinx.android.synthetic.main.fragment_wishlist.* +import javax.inject.Inject class WishlistFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_wishlist + @Inject + lateinit var vmFactory: ViewModelFactory + + private val viewModel: WishlistViewModel by lazy { viewModelOf(vmFactory) } + override fun setupViews() { } @@ -15,6 +29,21 @@ class WishlistFragment : BaseFragment() { } override fun bindViewModel() { + + viewModel.requestWishlist() + viewModel.getWishlist().observe(this, Observer(::handleWishlist)) + + } + + private fun handleWishlist(state: WishlistViewModel.WishlistState) { + when (state) { + WishlistViewModel.WishlistState.Empty -> { + tv_wishlist_empty.setVisible(true) + } + is WishlistViewModel.WishlistState.Present -> { + tv_wishlist_empty.setVisible(false) + } + } } override fun unbindViewModel() { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt new file mode 100644 index 00000000..ec5ddfb0 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt @@ -0,0 +1,54 @@ +package at.shockbytes.dante.ui.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import at.shockbytes.dante.core.book.BookEntity +import at.shockbytes.dante.core.book.BookState +import at.shockbytes.dante.core.data.BookRepository +import at.shockbytes.dante.util.ExceptionHandlers +import at.shockbytes.dante.util.addTo +import at.shockbytes.dante.util.settings.DanteSettings +import at.shockbytes.dante.util.sort.SortComparators +import javax.inject.Inject + +class WishlistViewModel @Inject constructor( + private val bookRepository: BookRepository, + private val settings: DanteSettings +) : BaseViewModel() { + + sealed class WishlistState { + + object Empty : WishlistState() + + data class Present(val books: List): WishlistState() + } + + private val wishlist = MutableLiveData() + fun getWishlist(): LiveData = wishlist + + private val sortComparator: Comparator + get() = SortComparators.of(settings.sortStrategy) + + fun requestWishlist() { + bookRepository.bookObservable + .map { fetchedBooks -> + fetchedBooks + .filter { it.state == BookState.WISHLIST } + .sortedWith(sortComparator) + } + .map(::mapBooksToWishlistState) + .doOnError { + wishlist.postValue(WishlistState.Empty) + } + .subscribe(wishlist::postValue, ExceptionHandlers::defaultExceptionHandler) + .addTo(compositeDisposable) + } + + private fun mapBooksToWishlistState(books: List): WishlistState { + return if (books.isEmpty()) { + WishlistState.Empty + } else { + WishlistState.Present(books) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_book_main.xml b/app/src/main/res/layout/fragment_book_main.xml index 17c9ae1f..dd585483 100644 --- a/app/src/main/res/layout/fragment_book_main.xml +++ b/app/src/main/res/layout/fragment_book_main.xml @@ -21,10 +21,9 @@ android:drawableTop="@drawable/ic_empty_indicator" android:drawablePadding="16dp" android:gravity="center" - android:padding="8dp" - android:textColor="@color/colorSecondaryText" - android:textSize="25sp" - android:textStyle="bold" /> + android:padding="16dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" + android:textColor="@color/colorSecondaryText" /> diff --git a/app/src/main/res/layout/fragment_wishlist.xml b/app/src/main/res/layout/fragment_wishlist.xml index e61f8d9b..55b1f2ce 100644 --- a/app/src/main/res/layout/fragment_wishlist.xml +++ b/app/src/main/res/layout/fragment_wishlist.xml @@ -16,15 +16,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:alpha="0" + android:visibility="gone" android:drawableTop="@drawable/ic_empty_indicator" android:drawablePadding="16dp" android:gravity="center" + android:padding="16dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" android:text="@string/wishlist_empty" android:textColor="@color/colorSecondaryText" - android:textSize="25sp" - android:textStyle="bold" - tools:alpha="1"/> + tools:visibility="visible"/> From a08104bb9a15ed99faa1c4b849eb9510a2ec3459 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 21:26:46 +0100 Subject: [PATCH 12/30] Use MainBookFragment for Wishlist feature --- .../dante/injection/AppComponent.kt | 3 - .../ui/adapter/InspirationsPagerAdapter.kt | 5 +- .../dante/ui/adapter/main/BookAdapter.kt | 4 +- .../dante/ui/adapter/main/BookViewHolder.kt | 12 ++-- .../ui/adapter/main/RandomPickViewHolder.kt | 8 +-- .../fragment/BookActionBottomSheetFragment.kt | 2 +- .../dante/ui/fragment/MainBookFragment.kt | 38 +++++++----- .../dante/ui/fragment/SuggestionsFragment.kt | 12 ++-- .../dante/ui/fragment/WishlistFragment.kt | 58 ------------------- .../dante/ui/viewmodel/BookListViewModel.kt | 7 +-- .../dante/ui/viewmodel/WishlistViewModel.kt | 54 ----------------- .../dante/util/AppModuleExtensions.kt | 8 +++ .../dante/util/SharedViewComponents.kt | 1 - .../main/res/layout/fragment_suggestions.xml | 3 - app/src/main/res/layout/fragment_wishlist.xml | 30 ---------- core/src/main/res/values-de/arrays.xml | 1 + core/src/main/res/values/arrays.xml | 1 + 17 files changed, 57 insertions(+), 190 deletions(-) delete mode 100644 app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt delete mode 100644 app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt delete mode 100644 app/src/main/res/layout/fragment_wishlist.xml diff --git a/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt b/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt index ecfaf19c..06747d75 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt @@ -33,7 +33,6 @@ import at.shockbytes.dante.ui.fragment.SettingsFragment import at.shockbytes.dante.ui.fragment.StatisticsFragment import at.shockbytes.dante.ui.fragment.SuggestionsFragment import at.shockbytes.dante.ui.fragment.TimeLineFragment -import at.shockbytes.dante.ui.fragment.WishlistFragment import at.shockbytes.dante.ui.fragment.dialog.GoogleSignInDialogFragment import at.shockbytes.dante.ui.fragment.dialog.GoogleWelcomeScreenDialogFragment import at.shockbytes.dante.ui.fragment.dialog.SortStrategyDialogFragment @@ -99,8 +98,6 @@ interface AppComponent { fun inject(fragment: LoginFragment) - fun inject(fragment: WishlistFragment) - fun inject(fragment: FeatureFlagConfigFragment) fun inject(fragment: AnnouncementFragment) diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt index a3f5fd5e..e7588587 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/InspirationsPagerAdapter.kt @@ -5,8 +5,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import at.shockbytes.dante.R +import at.shockbytes.dante.core.book.BookState +import at.shockbytes.dante.ui.fragment.MainBookFragment import at.shockbytes.dante.ui.fragment.SuggestionsFragment -import at.shockbytes.dante.ui.fragment.WishlistFragment class InspirationsPagerAdapter( private val context: Context, @@ -25,7 +26,7 @@ class InspirationsPagerAdapter( override fun getItem(position: Int): Fragment { return when (position) { - 0 -> WishlistFragment.newInstance() + 0 -> MainBookFragment.newInstance(BookState.WISHLIST, allowItemClick = false) 1 -> SuggestionsFragment.newInstance() else -> throw IllegalStateException("Position $position out of bounds of InspirationsPagerAdapter!") } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt index 695f4205..262c7b00 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt @@ -19,8 +19,8 @@ class BookAdapter( context: Context, private val imageLoader: ImageLoader, private val onOverflowActionClickedListener: (BookEntity) -> Unit, - private val onLabelClickedListener: (BookLabel) -> Unit, - private val randomPickCallback: RandomPickCallback, + private val onLabelClickedListener: ((BookLabel) -> Unit)? = null, + private val randomPickCallback: RandomPickCallback? = null, onItemClickListener: OnItemClickListener, onItemMoveListener: OnItemMoveListener ) : BaseAdapter( diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt index 2ea75f84..108fdefb 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt @@ -22,8 +22,8 @@ import kotlinx.android.synthetic.main.item_book.* class BookViewHolder( override val containerView: View, private val imageLoader: ImageLoader, - private val onOverflowActionClickedListener: (BookEntity) -> Unit, - private val onLabelClickedListener: (BookLabel) -> Unit + private val onOverflowActionClickedListener: ((BookEntity) -> Unit)?, + private val onLabelClickedListener: ((BookLabel) -> Unit)? ) : BaseAdapter.ViewHolder(containerView), LayoutContainer { private fun context(): Context = containerView.context @@ -63,14 +63,14 @@ class BookViewHolder( text = label.title setTextColor(Color.WHITE) setOnClickListener { - onLabelClickedListener(label) + onLabelClickedListener?.invoke(label) } } } private fun setOverflowClickListener(content: BookEntity) { item_book_img_overflow.setOnClickListener { - onOverflowActionClickedListener(content) + onOverflowActionClickedListener?.invoke(content) } } @@ -122,8 +122,8 @@ class BookViewHolder( fun forParent( parent: ViewGroup, imageLoader: ImageLoader, - onOverflowActionClickedListener: (BookEntity) -> Unit, - onLabelClickedListener: (BookLabel) -> Unit + onOverflowActionClickedListener: ((BookEntity) -> Unit)?, + onLabelClickedListener: ((BookLabel) -> Unit)? ): BookViewHolder { return BookViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_book, parent, false), diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt index 5d997aab..b3ae7281 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt @@ -10,17 +10,17 @@ import kotlinx.android.synthetic.main.item_random_pick.* class RandomPickViewHolder( override val containerView: View, - private val callback: RandomPickCallback + private val callback: RandomPickCallback? ) : BaseAdapter.ViewHolder(containerView), LayoutContainer { override fun bindToView(content: BookAdapterEntity, position: Int) { btn_item_random_pick.setOnClickListener { - callback.onRandomPickClicked() + callback?.onRandomPickClicked() } iv_item_random_pick_dismiss.setOnClickListener { - callback.onDismiss() + callback?.onDismiss() } } @@ -28,7 +28,7 @@ class RandomPickViewHolder( fun forParent( parent: ViewGroup, - callback: RandomPickCallback + callback: RandomPickCallback? ): RandomPickViewHolder { return RandomPickViewHolder( containerView = LayoutInflater.from(parent.context).inflate(R.layout.item_random_pick, parent, false), diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt index ad306eb2..f199c28a 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/BookActionBottomSheetFragment.kt @@ -38,7 +38,7 @@ class BookActionBottomSheetFragment : BaseBottomSheetFragment() { BookState.READ_LATER -> btn_book_action_move_to_upcoming BookState.READING -> btn_book_action_move_to_current BookState.READ -> btn_book_action_move_to_done - BookState.WISHLIST -> throw IllegalStateException("Wishlist state not supported in BookActionBottomSheetFragment!") + BookState.WISHLIST -> btn_book_action_edit // Do not allow editing wishlist books }.setVisible(false) } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index 137dafbd..c8b8965e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -33,7 +33,9 @@ import at.shockbytes.dante.core.Constants.EXTRA_BOOK_CREATED_STATE import at.shockbytes.dante.util.DanteUtils import at.shockbytes.dante.util.SharedViewComponents import at.shockbytes.dante.util.addTo +import at.shockbytes.dante.util.arguments.argument import at.shockbytes.dante.util.runDelayed +import at.shockbytes.dante.util.setVisible import at.shockbytes.dante.util.viewModelOf import at.shockbytes.util.AppUtils import at.shockbytes.util.adapter.BaseAdapter @@ -56,10 +58,12 @@ class MainBookFragment : BaseFragment(), @Inject lateinit var imageLoader: ImageLoader - private lateinit var bookState: BookState private lateinit var bookAdapter: BookAdapter private lateinit var viewModel: BookListViewModel + private var bookState: BookState by argument() + private var allowItemClick: Boolean by argument() + private val onLabelClickedListener: ((BookLabel) -> Unit) = { label -> LabelCategoryBottomSheetFragment.newInstance(label) .show(childFragmentManager, "show-label-bottom-sheet") @@ -113,7 +117,6 @@ class MainBookFragment : BaseFragment(), super.onCreate(savedInstanceState) viewModel = viewModelOf(vmFactory) - bookState = arguments?.getSerializable(ARG_STATE) as BookState viewModel.state = bookState registerBookUpdatedBroadcastReceiver() @@ -162,15 +165,18 @@ class MainBookFragment : BaseFragment(), when (state) { is BookListViewModel.BookLoadingState.Success -> { updateEmptyView(hide = true, animate = false) + fragment_book_main_rv.setVisible(true) bookAdapter.updateData(state.books) } is BookListViewModel.BookLoadingState.Empty -> { updateEmptyView(hide = false, animate = true) + fragment_book_main_rv.setVisible(false) } is BookListViewModel.BookLoadingState.Error -> { showSnackbar(getString(R.string.load_error), showLong = true) + fragment_book_main_rv.setVisible(false) } } } @@ -232,19 +238,22 @@ class MainBookFragment : BaseFragment(), } override fun onItemClick(content: BookAdapterEntity, position: Int, v: View) { - when (content) { - is BookAdapterEntity.Book -> { - ActivityNavigator.navigateTo( - context, - Destination.BookDetail(BookDetailInfo(content.id, content.title)), - getTransitionBundle(v) - ) - } + is BookAdapterEntity.Book -> handleBookClick(content, v) BookAdapterEntity.RandomPick -> Unit // Do nothing } } + private fun handleBookClick(content: BookAdapterEntity.Book, v: View) { + if (allowItemClick) { + ActivityNavigator.navigateTo( + context, + Destination.BookDetail(BookDetailInfo(content.id, content.title)), + getTransitionBundle(v) + ) + } + } + override fun onItemDismissed(t: BookAdapterEntity, position: Int) = Unit // Do nothing, only react to move actions in the on item move finished method @@ -332,13 +341,10 @@ class MainBookFragment : BaseFragment(), companion object { - private const val ARG_STATE = "arg_state" - - fun newInstance(state: BookState): MainBookFragment { + fun newInstance(state: BookState, allowItemClick: Boolean = true): MainBookFragment { return MainBookFragment().apply { - this.arguments = Bundle().apply { - putSerializable(ARG_STATE, state) - } + this.allowItemClick = allowItemClick + this.bookState = state } } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 555f129f..8f2f003e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -10,6 +10,7 @@ import at.shockbytes.dante.suggestions.Suggestion import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener import at.shockbytes.dante.ui.adapter.SuggestionsAdapter import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel +import at.shockbytes.dante.util.SharedViewComponents import at.shockbytes.dante.util.viewModelOf import kotlinx.android.synthetic.main.fragment_suggestions.* import javax.inject.Inject @@ -30,11 +31,11 @@ class SuggestionsFragment : BaseFragment() { private val viewModel: SuggestionsViewModel by lazy { viewModelOf(vmFactory) } - private lateinit var adapter: SuggestionsAdapter + private lateinit var suggestionAdapter: SuggestionsAdapter override fun setupViews() { - adapter = SuggestionsAdapter( + suggestionAdapter = SuggestionsAdapter( requireContext(), imageLoader, onSuggestionActionClickedListener = object : OnSuggestionActionClickedListener { @@ -48,7 +49,10 @@ class SuggestionsFragment : BaseFragment() { } } ) - rv_suggestions.adapter = adapter + rv_suggestions.apply { + layoutManager = SharedViewComponents.layoutManagerForBooks(requireContext()) + this.adapter = suggestionAdapter + } } override fun injectToGraph(appComponent: AppComponent) { @@ -72,7 +76,7 @@ class SuggestionsFragment : BaseFragment() { } private fun handleSuggestions(suggestions: List) { - adapter.data = suggestions.toMutableList() + suggestionAdapter.data = suggestions.toMutableList() } override fun unbindViewModel() = Unit diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt deleted file mode 100644 index fe7c56da..00000000 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/WishlistFragment.kt +++ /dev/null @@ -1,58 +0,0 @@ -package at.shockbytes.dante.ui.fragment - -import android.os.Bundle -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import at.shockbytes.dante.R -import at.shockbytes.dante.injection.AppComponent -import at.shockbytes.dante.injection.ViewModelFactory -import at.shockbytes.dante.ui.viewmodel.WishlistViewModel -import at.shockbytes.dante.util.setVisible -import at.shockbytes.dante.util.viewModelOf -import kotlinx.android.synthetic.main.fragment_wishlist.* -import javax.inject.Inject - -class WishlistFragment : BaseFragment() { - - override val layoutId: Int = R.layout.fragment_wishlist - - @Inject - lateinit var vmFactory: ViewModelFactory - - private val viewModel: WishlistViewModel by lazy { viewModelOf(vmFactory) } - - override fun setupViews() { - } - - override fun injectToGraph(appComponent: AppComponent) { - appComponent.inject(this) - } - - override fun bindViewModel() { - - viewModel.requestWishlist() - viewModel.getWishlist().observe(this, Observer(::handleWishlist)) - - } - - private fun handleWishlist(state: WishlistViewModel.WishlistState) { - when (state) { - WishlistViewModel.WishlistState.Empty -> { - tv_wishlist_empty.setVisible(true) - } - is WishlistViewModel.WishlistState.Present -> { - tv_wishlist_empty.setVisible(false) - } - } - } - - override fun unbindViewModel() { - } - - companion object { - - fun newInstance(): WishlistFragment { - return WishlistFragment() - } - } -} diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt index 38b796d0..6046407d 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt @@ -12,6 +12,7 @@ import at.shockbytes.dante.util.addTo import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.dante.util.sort.SortComparators import at.shockbytes.dante.util.sort.SortStrategy +import at.shockbytes.dante.util.toAdapterEntities import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent import io.reactivex.Observable @@ -129,12 +130,6 @@ class BookListViewModel @Inject constructor( return state == BookState.READ_LATER && size > 1 && danteSettings.showRandomPickInteraction } - private fun List.toAdapterEntities(): List { - return this.map { entity -> - BookAdapterEntity.Book(entity) - } - } - fun deleteBook(book: BookEntity) { bookRepository.delete(book.id) .doOnError(ExceptionHandlers::defaultExceptionHandler) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt deleted file mode 100644 index ec5ddfb0..00000000 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/WishlistViewModel.kt +++ /dev/null @@ -1,54 +0,0 @@ -package at.shockbytes.dante.ui.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import at.shockbytes.dante.core.book.BookEntity -import at.shockbytes.dante.core.book.BookState -import at.shockbytes.dante.core.data.BookRepository -import at.shockbytes.dante.util.ExceptionHandlers -import at.shockbytes.dante.util.addTo -import at.shockbytes.dante.util.settings.DanteSettings -import at.shockbytes.dante.util.sort.SortComparators -import javax.inject.Inject - -class WishlistViewModel @Inject constructor( - private val bookRepository: BookRepository, - private val settings: DanteSettings -) : BaseViewModel() { - - sealed class WishlistState { - - object Empty : WishlistState() - - data class Present(val books: List): WishlistState() - } - - private val wishlist = MutableLiveData() - fun getWishlist(): LiveData = wishlist - - private val sortComparator: Comparator - get() = SortComparators.of(settings.sortStrategy) - - fun requestWishlist() { - bookRepository.bookObservable - .map { fetchedBooks -> - fetchedBooks - .filter { it.state == BookState.WISHLIST } - .sortedWith(sortComparator) - } - .map(::mapBooksToWishlistState) - .doOnError { - wishlist.postValue(WishlistState.Empty) - } - .subscribe(wishlist::postValue, ExceptionHandlers::defaultExceptionHandler) - .addTo(compositeDisposable) - } - - private fun mapBooksToWishlistState(books: List): WishlistState { - return if (books.isEmpty()) { - WishlistState.Empty - } else { - WishlistState.Present(books) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt b/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt index 50762b1c..2c2f07c8 100644 --- a/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt +++ b/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt @@ -6,7 +6,9 @@ import android.net.Uri import android.os.Handler import android.os.Looper import at.shockbytes.dante.core.R +import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.signin.DanteUser +import at.shockbytes.dante.ui.adapter.main.BookAdapterEntity import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.tasks.Tasks import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -62,4 +64,10 @@ fun Context.openFile(fileToPath: File, mimeType: String): Intent { .setAction(Intent.ACTION_VIEW) .setDataAndType(uri, mimeType) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) +} + +fun List.toAdapterEntities(): List { + return this.map { entity -> + BookAdapterEntity.Book(entity) + } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt b/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt index 4f57eceb..57d76f7b 100644 --- a/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt +++ b/app/src/main/java/at/shockbytes/dante/util/SharedViewComponents.kt @@ -24,5 +24,4 @@ object SharedViewComponents { StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) } } - } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_suggestions.xml b/app/src/main/res/layout/fragment_suggestions.xml index 01465447..d000dfd6 100644 --- a/app/src/main/res/layout/fragment_suggestions.xml +++ b/app/src/main/res/layout/fragment_suggestions.xml @@ -1,17 +1,14 @@ - - - - - - - diff --git a/core/src/main/res/values-de/arrays.xml b/core/src/main/res/values-de/arrays.xml index d35fdbca..bcb6c0ed 100644 --- a/core/src/main/res/values-de/arrays.xml +++ b/core/src/main/res/values-de/arrays.xml @@ -20,6 +20,7 @@ Scanne Bücher um sie später zu lesen Warum liest du kein Buch? Du hast noch kein Buch gelesen + Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 7355f9fa..df852b7f 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -5,6 +5,7 @@ Scan some books for later Why are you not reading a book? No book finished right now + Your wishlist is empty! Good! Seems like you have all your books already. From ea8e25ac3dd9909b0e3ed17e37fa5fe655a5033e Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 21:38:35 +0100 Subject: [PATCH 13/30] Track suggestion added to wishlist event --- .../adapter/OnSuggestionActionClickedListener.kt | 4 ++-- .../dante/ui/adapter/SuggestionsAdapter.kt | 4 ++-- .../dante/ui/fragment/SuggestionsFragment.kt | 10 +++++++--- .../dante/ui/viewmodel/SuggestionsViewModel.kt | 9 ++++++++- .../dante/util/settings/DanteSettings.kt | 2 +- app/src/main/res/xml/settings.xml | 3 +-- .../tracking/event/DanteTrackingEvent.kt | 15 +++++++++++++-- 7 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt index bedd0313..d0b3b998 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt @@ -1,10 +1,10 @@ package at.shockbytes.dante.ui.adapter -import at.shockbytes.dante.suggestions.BookSuggestionEntity +import at.shockbytes.dante.suggestions.Suggestion interface OnSuggestionActionClickedListener { - fun onAddSuggestionToWishlist(data: BookSuggestionEntity) + fun onAddSuggestionToWishlist(suggestion: Suggestion) fun onReportBookSuggestion(suggestionId: String) } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt index d70dcd7a..89f09950 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt @@ -40,7 +40,7 @@ class SuggestionsAdapter( setupBook(suggestion) setupSuggester(suggester) setupRecommendation(recommendation) - setupBookActionListener(suggestion) + setupBookActionListener(this) } } @@ -73,7 +73,7 @@ class SuggestionsAdapter( tv_item_suggestion_recommendation.text = recommendation } - private fun setupBookActionListener(suggestion: BookSuggestionEntity) { + private fun setupBookActionListener(suggestion: Suggestion) { btn_item_suggestion_add.setOnClickListener { onSuggestionActionClickedListener.onAddSuggestionToWishlist(suggestion) } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 8f2f003e..69cff5c3 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModelProvider import at.shockbytes.dante.R import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.injection.AppComponent -import at.shockbytes.dante.suggestions.BookSuggestionEntity import at.shockbytes.dante.suggestions.Suggestion import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener import at.shockbytes.dante.ui.adapter.SuggestionsAdapter @@ -39,9 +38,14 @@ class SuggestionsFragment : BaseFragment() { requireContext(), imageLoader, onSuggestionActionClickedListener = object : OnSuggestionActionClickedListener { - override fun onAddSuggestionToWishlist(data: BookSuggestionEntity) { - // TODO Add to wishlist & track event + override fun onAddSuggestionToWishlist(suggestion: Suggestion) { + // TODO Add to wishlist showToast("Add to wishlist") + viewModel.trackAddSuggestionToWishlist( + suggestion.suggestionId, + suggestion.suggestion.title, + suggestion.suggester.name + ) } override fun onReportBookSuggestion(suggestionId: String) { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 4d29e84e..2b927474 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -6,10 +6,13 @@ import at.shockbytes.dante.suggestions.Suggestion import at.shockbytes.dante.suggestions.SuggestionsRepository import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.addTo +import at.shockbytes.tracking.Tracker +import at.shockbytes.tracking.event.DanteTrackingEvent import javax.inject.Inject class SuggestionsViewModel @Inject constructor( - private val suggestionsRepository: SuggestionsRepository + private val suggestionsRepository: SuggestionsRepository, + private val tracker: Tracker ) : BaseViewModel() { sealed class SuggestionsState { @@ -35,4 +38,8 @@ class SuggestionsViewModel @Inject constructor( .subscribe(suggestionState::postValue, ExceptionHandlers::defaultExceptionHandler) .addTo(compositeDisposable) } + + fun trackAddSuggestionToWishlist(suggestionId: String, bookTitle: String, suggester: String) { + tracker.track(DanteTrackingEvent.AddSuggestionToWishlist(suggestionId, bookTitle, suggester)) + } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/settings/DanteSettings.kt b/app/src/main/java/at/shockbytes/dante/util/settings/DanteSettings.kt index fb8dad26..0d9e7f78 100644 --- a/app/src/main/java/at/shockbytes/dante/util/settings/DanteSettings.kt +++ b/app/src/main/java/at/shockbytes/dante/util/settings/DanteSettings.kt @@ -45,7 +45,7 @@ class DanteSettings( var showSummary: Boolean by prefs.boolDelegate(context.getString(R.string.prefs_show_summary_key)) - var trackingEnabled: Boolean by prefs.boolDelegate(context.getString(R.string.prefs_tracking_key), defaultValue = false) + var trackingEnabled: Boolean by prefs.boolDelegate(context.getString(R.string.prefs_tracking_key), defaultValue = true) var sortStrategy: SortStrategy get() { diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 95d6f59c..8ad5c640 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -1,6 +1,5 @@ - + diff --git a/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt b/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt index b9d63aa4..759c326d 100644 --- a/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt +++ b/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt @@ -17,8 +17,6 @@ sealed class DanteTrackingEvent( listOf(TrackingProperty("importer_name", importer)) ) - object BurnDownLibrary : DanteTrackingEvent("burn_down_library") - data class TrackingStateChanged(val state: Boolean) : DanteTrackingEvent( "tracking_state_changed", listOf(TrackingProperty("state", state)) @@ -34,5 +32,18 @@ sealed class DanteTrackingEvent( listOf(TrackingProperty("backup_provider", providerAcronym)) ) + data class AddSuggestionToWishlist( + val suggestionId: String, + val bookTitle: String, + val suggester: String + ) : DanteTrackingEvent( + "add_suggestion_to_wishlist", + listOf( + TrackingProperty("suggestion_id", suggestionId), + TrackingProperty("suggestion_book", bookTitle), + TrackingProperty("suggestion_suggester", suggester) + ) + ) + object DisableRandomBookInteraction : DanteTrackingEvent("disable_random_book_interaction") } \ No newline at end of file From 0d1f31a031143252c8b36560dd86dd70d6afe11f Mon Sep 17 00:00:00 2001 From: shockbytes Date: Tue, 1 Dec 2020 21:50:14 +0100 Subject: [PATCH 14/30] Suggestions empty layout --- .../dante/ui/fragment/SuggestionsFragment.kt | 7 ++++++- .../dante/ui/viewmodel/SuggestionsViewModel.kt | 1 - app/src/main/res/layout/fragment_suggestions.xml | 10 ++++++---- core/src/main/res/values-de/strings.xml | 1 + core/src/main/res/values/strings.xml | 1 + 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 69cff5c3..c5c7fded 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -10,6 +10,7 @@ import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener import at.shockbytes.dante.ui.adapter.SuggestionsAdapter import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel import at.shockbytes.dante.util.SharedViewComponents +import at.shockbytes.dante.util.setVisible import at.shockbytes.dante.util.viewModelOf import kotlinx.android.synthetic.main.fragment_suggestions.* import javax.inject.Inject @@ -76,10 +77,14 @@ class SuggestionsFragment : BaseFragment() { } private fun handleEmptyState() { - // TODO Handle empty state! Important for online feature later + rv_suggestions.setVisible(false) + tv_suggestions_empty.setVisible(true) } private fun handleSuggestions(suggestions: List) { + rv_suggestions.setVisible(true) + tv_suggestions_empty.setVisible(false) + suggestionAdapter.data = suggestions.toMutableList() } diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 2b927474..554c187f 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -28,7 +28,6 @@ class SuggestionsViewModel @Inject constructor( fun requestSuggestions() { suggestionsRepository.loadSuggestions() .map { suggestions -> - if (suggestions.suggestions.isEmpty()) { SuggestionsState.Empty } else { diff --git a/app/src/main/res/layout/fragment_suggestions.xml b/app/src/main/res/layout/fragment_suggestions.xml index d000dfd6..cb0e611f 100644 --- a/app/src/main/res/layout/fragment_suggestions.xml +++ b/app/src/main/res/layout/fragment_suggestions.xml @@ -1,5 +1,6 @@ + android:visibility="gone" + tools:visibility="visible" /> \ No newline at end of file diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index be055816..59881513 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -329,5 +329,6 @@ Zur Wunschliste hinzufügen Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! Wunschliste + Es tut uns leid. Wir können dir gerade keine Vorschläge anzeigen. diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index f01ca7ac..3c762330 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -358,6 +358,7 @@ - %s Suggestions + We have to apologize. We were unable to load you some suggestions. One of those books? Maybe one of them? Not my book From f7f7093f6666b1699944bd87d61990bc3fc3ef64 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Wed, 2 Dec 2020 19:36:57 +0100 Subject: [PATCH 15/30] Add book to wishlist (from suggestions) --- .../dante/ui/fragment/InspirationsFragment.kt | 8 ++- .../dante/ui/fragment/SuggestionsFragment.kt | 18 ++++--- .../ui/viewmodel/SuggestionsViewModel.kt | 50 ++++++++++++++++++- core/src/main/res/values-de/strings.xml | 1 + core/src/main/res/values/strings.xml | 1 + 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt index cefc1792..81dce889 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/InspirationsFragment.kt @@ -13,6 +13,8 @@ class InspirationsFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_inspirations + private lateinit var pagerAdapter: InspirationsPagerAdapter + override fun setupViews() { setupToolbar() setupViewPager() @@ -20,6 +22,10 @@ class InspirationsFragment : BaseFragment() { setupTabIcons() } + fun moveToWishlistTab() { + getTabAt(0)?.select() + } + private fun setupToolbar() { dante_toolbar_title.setText(R.string.inspirations) dante_toolbar_back.apply { @@ -31,7 +37,7 @@ class InspirationsFragment : BaseFragment() { } private fun setupViewPager() { - val pagerAdapter = InspirationsPagerAdapter(requireContext(), childFragmentManager) + pagerAdapter = InspirationsPagerAdapter(requireContext(), childFragmentManager) vp_fragment_inspirations.apply { adapter = pagerAdapter diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index c5c7fded..0b3cd8eb 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -10,8 +10,10 @@ import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener import at.shockbytes.dante.ui.adapter.SuggestionsAdapter import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel import at.shockbytes.dante.util.SharedViewComponents +import at.shockbytes.dante.util.addTo import at.shockbytes.dante.util.setVisible import at.shockbytes.dante.util.viewModelOf +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_suggestions.* import javax.inject.Inject @@ -40,13 +42,7 @@ class SuggestionsFragment : BaseFragment() { imageLoader, onSuggestionActionClickedListener = object : OnSuggestionActionClickedListener { override fun onAddSuggestionToWishlist(suggestion: Suggestion) { - // TODO Add to wishlist - showToast("Add to wishlist") - viewModel.trackAddSuggestionToWishlist( - suggestion.suggestionId, - suggestion.suggestion.title, - suggestion.suggester.name - ) + viewModel.addSuggestionToWishlist(suggestion) } override fun onReportBookSuggestion(suggestionId: String) { @@ -67,6 +63,14 @@ class SuggestionsFragment : BaseFragment() { override fun bindViewModel() { viewModel.requestSuggestions() viewModel.getSuggestionState().observe(this, Observer(::handleSuggestionState)) + + viewModel.onMoveToWishlistEvent() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { bookTitle -> + (parentFragment as? InspirationsFragment)?.moveToWishlistTab() + showSnackbar(getString(R.string.book_added_to_wishlist, bookTitle)) + } + .addTo(compositeDisposable) } private fun handleSuggestionState(suggestionsState: SuggestionsViewModel.SuggestionsState) { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 554c187f..8f9e3877 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -2,16 +2,24 @@ package at.shockbytes.dante.ui.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import at.shockbytes.dante.core.book.BookEntity +import at.shockbytes.dante.core.book.BookState +import at.shockbytes.dante.core.data.BookRepository +import at.shockbytes.dante.suggestions.BookSuggestionEntity import at.shockbytes.dante.suggestions.Suggestion import at.shockbytes.dante.suggestions.SuggestionsRepository import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.addTo import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject +import timber.log.Timber import javax.inject.Inject class SuggestionsViewModel @Inject constructor( private val suggestionsRepository: SuggestionsRepository, + private val bookRepository: BookRepository, private val tracker: Tracker ) : BaseViewModel() { @@ -22,6 +30,9 @@ class SuggestionsViewModel @Inject constructor( object Empty : SuggestionsState() } + private val onMoveToWishlistEvent = PublishSubject.create() + fun onMoveToWishlistEvent(): Observable = onMoveToWishlistEvent + private val suggestionState = MutableLiveData() fun getSuggestionState(): LiveData = suggestionState @@ -38,7 +49,44 @@ class SuggestionsViewModel @Inject constructor( .addTo(compositeDisposable) } - fun trackAddSuggestionToWishlist(suggestionId: String, bookTitle: String, suggester: String) { + fun addSuggestionToWishlist(suggestion: Suggestion) { + bookRepository.create(suggestion.suggestion.toBookEntity()) + .doOnComplete { + trackAddSuggestionToWishlist( + suggestion.suggestionId, + suggestion.suggestion.title, + suggestion.suggester.name + ) + } + .subscribe({ + onMoveToWishlistEvent.onNext(suggestion.suggestion.title) + }, { throwable -> + Timber.e(throwable) + }) + .addTo(compositeDisposable) + } + + private fun BookSuggestionEntity.toBookEntity(): BookEntity { + return BookEntity( + title = title, + subTitle = subTitle, + author = author, + state = BookState.WISHLIST, + pageCount = pageCount, + publishedDate = publishedDate, + isbn = isbn, + thumbnailAddress = thumbnailAddress, + googleBooksLink = googleBooksLink, + language = language, + summary = summary + ) + } + + private fun trackAddSuggestionToWishlist( + suggestionId: String, + bookTitle: String, + suggester: String + ) { tracker.track(DanteTrackingEvent.AddSuggestionToWishlist(suggestionId, bookTitle, suggester)) } } \ No newline at end of file diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 59881513..69461fc8 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -330,5 +330,6 @@ Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! Wunschliste Es tut uns leid. Wir können dir gerade keine Vorschläge anzeigen. + %s wurde zur Wunschliste hinzugefügt diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 3c762330..5306e3bf 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -370,6 +370,7 @@ Do you want to scan another book? %s added to library + %s added to wishlist Nope The app may restart shortly… From 328e28781bb765213ca9b63bc1f2ffbf57b3a672 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Wed, 2 Dec 2020 19:47:41 +0100 Subject: [PATCH 16/30] Update build number, version and README --- README.md | 24 ++++++++++++++---------- app/build.gradle | 4 ++-- build.gradle | 6 +++--- core/src/debug/res/values/strings.xml | 2 +- core/src/main/res/values/strings.xml | 2 +- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2eeb6e5e..20f16d14 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,11 @@ The backlog is currently empty. ## Outlook & planned features ### Versions 5.x -- [ ] Use Firebase Data for book suggestions -- [ ] Let users suggest favorite books to others +- [ ] Suggesions + - [ ] Use Firebase Data for book suggestions + - [ ] Let users suggest favorite books to others - [ ] Improved search database (Google Books API) lookup query -- [ ] Shockbytes Backup +- [ ] Shockbytes Firestore Backup - [ ] Simplify book management - [ ] Remove local backup - [ ] Add online Shockbytes backup as only way to backup data @@ -49,9 +50,6 @@ The backlog is currently empty. - [ ] Add web client support - [ ] Paged request when user clicks on "not my book" in book download view -### Version 4.1 -- [ ] Wishlist for books that are not purchased yet - ### Version 4.0 - CAMPING WITH FIREBASE - [ ] Add online sync capability - [ ] Migrate from local to remote storage @@ -59,19 +57,25 @@ The backlog is currently empty. ## Current development -### Version 3.17 +### Version 3.18 - [ ] Move actions into Book item (https://github.com/florent37/ExpansionPanel) - [ ] Add Onboarding + optional Login - [ ] Backup file improvements - [ ] Show path to local backup files - [ ] Open file with FileProvider - [ ] Experimental remote storage Firestore implementation (for test account) -- [ ] Fix bug with local book covers - - [ ] In MultiBareBoneBookView (not showing up) - - [ ] In the label overview (too big) +- [ ] Smooth loading animator when uploading local images ## Changelog +### Version 3.17 - INNOVATIVE INSPIRATIONS +* Inspirations Feature + * Wishlist for books that are not purchased yetst + * Suggestions +* Fix bug with local book covers + * In MultiBareBoneBookView (not showing up) + * In the label overview (too big) + ### Version 3.16 - BRING BACK BACKUP * Google Drive REST Backup * Add DriveRestClient diff --git a/app/build.gradle b/app/build.gradle index 0ccffd77..b319bd9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "at.shockbytes.dante" minSdkVersion 21 targetSdkVersion 30 - versionCode 38 - versionName "3.16" + versionCode 39 + versionName "3.17" multiDexEnabled true vectorDrawables.useSupportLibrary = true diff --git a/build.gradle b/build.gradle index 341d927f..767803e8 100644 --- a/build.gradle +++ b/build.gradle @@ -4,18 +4,18 @@ def ktlintVersion = "0.31.0" buildscript { def realmVersion = "10.0.0" - ext.kotlin_version = '1.4.10' + ext.kotlin_version = '1.4.20' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.google.gms:google-services:4.3.4' classpath "io.realm:realm-gradle-plugin:$realmVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' } } diff --git a/core/src/debug/res/values/strings.xml b/core/src/debug/res/values/strings.xml index 5c35fd6f..4d91b6b5 100644 --- a/core/src/debug/res/values/strings.xml +++ b/core/src/debug/res/values/strings.xml @@ -2,6 +2,6 @@ Dante BETA - 3.16 (Beta Build) + 3.17 (Beta Build) \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5306e3bf..0bb26050 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Dante - 3.16 + 3.17 https://github.com/shockbytes/Dante From 01fe9f71ceb7e746bcc6ffda6c03085346488cb2 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Wed, 2 Dec 2020 19:55:54 +0100 Subject: [PATCH 17/30] Downgrade to Kotlin 1.4.10 --- README.md | 7 +++++-- build.gradle | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 20f16d14..ac336452 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,17 @@ The backlog is currently empty. ## Current development +### Version 3.19 +- [ ] Experimental remote storage Firestore implementation (for test account) +- [ ] Add Onboarding + optional Login + ### Version 3.18 - [ ] Move actions into Book item (https://github.com/florent37/ExpansionPanel) -- [ ] Add Onboarding + optional Login - [ ] Backup file improvements - [ ] Show path to local backup files - [ ] Open file with FileProvider -- [ ] Experimental remote storage Firestore implementation (for test account) - [ ] Smooth loading animator when uploading local images +- [ ] Upgrade to Kotlin 1.4.20 -> Use ViewBinding ## Changelog diff --git a/build.gradle b/build.gradle index 767803e8..43f0134f 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ def ktlintVersion = "0.31.0" buildscript { def realmVersion = "10.0.0" - ext.kotlin_version = '1.4.20' + ext.kotlin_version = '1.4.10' repositories { google() From 7b7ddc2cb69535b63a57ba75a5b0891276dcd607 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 09:22:21 +0100 Subject: [PATCH 18/30] Do not obfuscate Suggestion data model --- app/proguard-rules.pro | 8 ++++++++ .../at/shockbytes/dante/ui/fragment/MainBookFragment.kt | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 351743e9..14861689 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -46,6 +46,14 @@ ; } +# Suggestions feature +-keep class at.shockbytes.dante.suggestions.** {*;} +-keepclassmembers class at.shockbytes.dante.suggestion.* { + ; + (); + ; +} + -keepclassmembers class * extends java.lang.Enum { ; public static **[] values(); diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index c8b8965e..b3ecf40e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -333,7 +333,7 @@ class MainBookFragment : BaseFragment(), .setDuration(450) .start() } else { - fragment_book_main_empty_view.alpha = (alpha) + fragment_book_main_empty_view.alpha = alpha } } From 1db8d185861571d3dd60b9849ff62f80167fb55f Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 09:41:58 +0100 Subject: [PATCH 19/30] Fix bug when saving pages --- .../dante/ui/fragment/BookDetailFragment.kt | 4 +- .../dante/ui/viewmodel/BookDetailViewModel.kt | 38 +++++++++++-------- .../dante/core/data/PageRecordDao.kt | 2 +- .../core/data/local/RealmPageRecordDao.kt | 18 +++++---- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/BookDetailFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/BookDetailFragment.kt index c5565cd3..b5c16c1e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/BookDetailFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/BookDetailFragment.kt @@ -304,11 +304,13 @@ class BookDetailFragment : BaseFragment(), } override fun onDestroy() { - viewModel.onFragmentDestroyed() LocalBroadcastManager.getInstance(requireContext()).apply { unregisterReceiver(notesReceiver) unregisterReceiver(bookUpdatedReceiver) } + + viewModel.onFragmentDestroyed() + super.onDestroy() } diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookDetailViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookDetailViewModel.kt index a1c75d9b..466f467a 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookDetailViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookDetailViewModel.kt @@ -88,9 +88,31 @@ class BookDetailViewModel @Inject constructor( } fun onFragmentDestroyed() { + viewCompositeDisposable.clear() - onPageCountMayChanged() + val currentPage = getBookFromLiveData()?.currentPage ?: 0 + val startPage = pagesAtInit ?: 0 + + if (hasPageCountChanged(currentPage, startPage)) { + savePageChanges(currentPage, startPage) + } + } + + private fun hasPageCountChanged(currentPage: Int, startPage: Int): Boolean { + return currentPage != startPage + } + + private fun savePageChanges(currentPage: Int, startPage: Int) { + pageRecordDao + .insertPageRecordForBookId( + bookId = bookId, + fromPage = startPage, + toPage = currentPage, + nowInMillis = System.currentTimeMillis() + ) + .subscribe() + .addTo(compositeDisposable) } private fun fetchBook(bookId: Long) { @@ -308,20 +330,6 @@ class BookDetailViewModel @Inject constructor( .addTo(compositeDisposable) } - private fun onPageCountMayChanged() { - - val currentPage = getBookFromLiveData()?.currentPage ?: 0 - val startPage = pagesAtInit ?: 0 - if (currentPage != startPage) { - pageRecordDao.insertPageRecordForBookId( - bookId = bookId, - fromPage = startPage, - toPage = currentPage, - nowInMillis = System.currentTimeMillis() - ) - } - } - /** * 1. Delete all page records for this particular book * 2. Reset the current page to 0 diff --git a/core/src/main/java/at/shockbytes/dante/core/data/PageRecordDao.kt b/core/src/main/java/at/shockbytes/dante/core/data/PageRecordDao.kt index 947ff8df..3c34e42c 100644 --- a/core/src/main/java/at/shockbytes/dante/core/data/PageRecordDao.kt +++ b/core/src/main/java/at/shockbytes/dante/core/data/PageRecordDao.kt @@ -12,7 +12,7 @@ interface PageRecordDao { fromPage: Int, toPage: Int, nowInMillis: Long - ) + ): Completable fun updatePageRecord(pageRecord: PageRecord, fromPage: Int?, toPage: Int?): Completable diff --git a/core/src/main/java/at/shockbytes/dante/core/data/local/RealmPageRecordDao.kt b/core/src/main/java/at/shockbytes/dante/core/data/local/RealmPageRecordDao.kt index 1b0a03dd..e64886ea 100644 --- a/core/src/main/java/at/shockbytes/dante/core/data/local/RealmPageRecordDao.kt +++ b/core/src/main/java/at/shockbytes/dante/core/data/local/RealmPageRecordDao.kt @@ -22,15 +22,17 @@ class RealmPageRecordDao(private val realm: RealmInstanceProvider) : PageRecordD fromPage: Int, toPage: Int, nowInMillis: Long - ) { - insert( - PageRecord( - bookId = bookId, - fromPage = fromPage, - toPage = toPage, - timestamp = nowInMillis + ): Completable { + return completableOf { + insert( + PageRecord( + bookId = bookId, + fromPage = fromPage, + toPage = toPage, + timestamp = nowInMillis + ) ) - ) + } } override fun updatePageRecord( From 295c7b61aacaab58f14404d25ad92369b6fa232f Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 10:46:14 +0100 Subject: [PATCH 20/30] Change your/my wording --- core/src/main/res/values-de/strings.xml | 12 ++++++------ core/src/main/res/values/strings.xml | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 69461fc8..a1ddea0d 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -8,13 +8,13 @@ Hilfe Statistiken Einstellungen - Deine Statistiken - Deine Timeline + Meine Statistiken + Meine Timeline App Version Über Entwickler - Die App benötigt den Zugang zu deiner Kamera, um die Barcodes deiner Bücher zu scannen. + Wir benötigen den Zugriff zu deiner Kamera, um die ISBN codes deiner Bücher zu scannen. Die App benötigt die Kameraberechtigung um zu funktionieren. Das Scanfenster wird geschlossen. Teilen Buch scannen @@ -63,7 +63,7 @@ Aktuelle Seite Buchseiten Speichern - Deine Gedanken gehören hier her… + Meine Gedanken gehören hier her… Schreibe deine Gedanken, Gefühle oder einfach nur Bemerkungen über %s nieder. Ungültige Eingabe! Die Seitenanzahl kann nicht kleiner als die aktuelle Seite sein, oder die Eingabe ist leer! Meine Notizen @@ -199,7 +199,7 @@ Nope %s wurde hinzugefügt Willst du noch ein weiteres Buch scannen? - Installation von Dante + Die gemeinsame Reise beginnt Die Timeline ordnet deine gelesenen Bücher chronologisch an. Du kannst entscheiden, wie die Timeline sortiert werden soll. Meine Notizen Erstes 5-Sterne Buch @@ -325,7 +325,7 @@ Öffnen Screen öffnen Vorschläge - Deine Wunschliste + Meine Wunschliste Zur Wunschliste hinzufügen Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! Wunschliste diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 0bb26050..978d5767 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -9,8 +9,8 @@ Cancel Open Settings - Your statistics - Your timeline + My statistics + My timeline My notes N/A import @@ -27,7 +27,7 @@ Open backup file Currently reading - Add a book to your library + Add a book to my library Reading behavior %d pages • %s @@ -58,7 +58,7 @@ Not available Got it - The app needs access to your camera, in order to detect the barcodes of the books. + We need access to your camera, in order to detect the ISBN codes of the books. This application cannot run because it does not have the camera permission. The scan window will terminate. transition_name_thumb @@ -86,8 +86,8 @@ Thumbnail for book cover Overflow icon for list item - Share your book - Manage your books + Share my book + Manage books Restore (%d) Backup @@ -135,7 +135,7 @@ Write down your thoughts, feelings or some general remarks about %s. Save Discard - Your thoughts belong in here… + My thoughts belong in here… My notes Clear @@ -352,7 +352,7 @@ Inspirations Your wishlist is empty! Good! Seems like you have all your books already. - Your Wishlist + My Wishlist Wishlist Add to wishlist - %s @@ -377,7 +377,7 @@ Changing the icon may \n automatically restart the app %s %s - You installed Dante + The journey starts The timeline shows you exactly when you read each and every book in your library. You can choose how it should be sorted. First 5 star book Other From d3370a06b247d8fd7249b15165c82c15f786ea24 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 10:53:49 +0100 Subject: [PATCH 21/30] Fix display issue where empty view does not hide when new books are added --- .../dante/ui/fragment/MainBookFragment.kt | 36 +++++++------------ .../main/res/layout/fragment_book_main.xml | 9 ++--- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index b3ecf40e..34275fa3 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -107,7 +107,7 @@ class MainBookFragment : BaseFragment(), ?.let { createdBookState -> if (viewModel.state == createdBookState) { runDelayed(500) { - fragment_book_main_rv.smoothScrollToPosition(0) + rv_main_book_fragment.smoothScrollToPosition(0) } } } @@ -139,12 +139,12 @@ class MainBookFragment : BaseFragment(), override fun onResume() { super.onResume() - fragment_book_main_rv.suppressLayout(false) + rv_main_book_fragment.suppressLayout(false) } override fun onPause() { super.onPause() - fragment_book_main_rv.suppressLayout(true) + rv_main_book_fragment.suppressLayout(true) } override fun onDestroy() { @@ -164,19 +164,20 @@ class MainBookFragment : BaseFragment(), when (state) { is BookListViewModel.BookLoadingState.Success -> { - updateEmptyView(hide = true, animate = false) - fragment_book_main_rv.setVisible(true) + tv_main_book_fragment_empty.setVisible(false) + rv_main_book_fragment.setVisible(true) + bookAdapter.updateData(state.books) } is BookListViewModel.BookLoadingState.Empty -> { - updateEmptyView(hide = false, animate = true) - fragment_book_main_rv.setVisible(false) + tv_main_book_fragment_empty.setVisible(true) + rv_main_book_fragment.setVisible(false) } is BookListViewModel.BookLoadingState.Error -> { showSnackbar(getString(R.string.load_error), showLong = true) - fragment_book_main_rv.setVisible(false) + rv_main_book_fragment.setVisible(false) } } } @@ -210,7 +211,7 @@ class MainBookFragment : BaseFragment(), override fun setupViews() { - fragment_book_main_empty_view.text = resources.getStringArray(R.array.empty_indicators)[bookState.ordinal] + tv_main_book_fragment_empty.text = resources.getStringArray(R.array.empty_indicators)[bookState.ordinal] bookAdapter = BookAdapter( requireContext(), @@ -222,7 +223,7 @@ class MainBookFragment : BaseFragment(), randomPickCallback = randomPickCallback ) - fragment_book_main_rv.apply { + rv_main_book_fragment.apply { layoutManager = SharedViewComponents.layoutManagerForBooks(requireContext()) adapter = bookAdapter } @@ -234,7 +235,7 @@ class MainBookFragment : BaseFragment(), BaseItemTouchHelper.DragAccess.VERTICAL ) ) - itemTouchHelper.attachToRecyclerView(fragment_book_main_rv) + itemTouchHelper.attachToRecyclerView(rv_main_book_fragment) } override fun onItemClick(content: BookAdapterEntity, position: Int, v: View) { @@ -324,19 +325,6 @@ class MainBookFragment : BaseFragment(), .toBundle() } - private fun updateEmptyView(hide: Boolean, animate: Boolean) { - - val alpha = if (hide) 0f else 1f - if (animate) { - fragment_book_main_empty_view.animate() - .alpha(alpha) - .setDuration(450) - .start() - } else { - fragment_book_main_empty_view.alpha = alpha - } - } - private fun BookEntity.toAdapterEntity(): BookAdapterEntity = BookAdapterEntity.Book(this) companion object { diff --git a/app/src/main/res/layout/fragment_book_main.xml b/app/src/main/res/layout/fragment_book_main.xml index dd585483..de3ed341 100644 --- a/app/src/main/res/layout/fragment_book_main.xml +++ b/app/src/main/res/layout/fragment_book_main.xml @@ -6,24 +6,25 @@ tools:context=".ui.fragment.MainBookFragment"> + android:textColor="@color/colorSecondaryText" + android:visibility="gone" + tools:visibility="visible" /> From caaf903bb1b75f9fc4057a778f754a1bdd023278 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 11:59:59 +0100 Subject: [PATCH 22/30] Fix issue with barcode bottomsheet buttons --- app/src/main/res/layout/fragment_manual_add.xml | 1 - .../res/layout/fragment_barcode_scan_bottom_sheet.xml | 11 ++++++----- core/src/main/res/values/styles.xml | 4 ++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/fragment_manual_add.xml b/app/src/main/res/layout/fragment_manual_add.xml index 4b99cfa0..1399574c 100644 --- a/app/src/main/res/layout/fragment_manual_add.xml +++ b/app/src/main/res/layout/fragment_manual_add.xml @@ -411,7 +411,6 @@ android:textColor="@color/tabcolor_done" /> - diff --git a/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml b/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml index 89f0e834..e403a467 100644 --- a/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml +++ b/camera/src/main/res/layout/fragment_barcode_scan_bottom_sheet.xml @@ -42,7 +42,8 @@ @drawable/bg_menu_bottom_sheet + + \ No newline at end of file From 0610550024ec3beaa9117ad10dd0a07963ffccfd Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 12:25:07 +0100 Subject: [PATCH 23/30] Introduce SuggestionsAdapterItem and wrap suggestions in it --- .../dante/ui/adapter/SuggestionsAdapter.kt | 95 ------------------ .../dante/ui/adapter/main/BookAdapter.kt | 1 - .../adapter/main}/BookDiffUtilCallback.kt | 2 +- .../suggestions/SuggestionViewHolder.kt | 99 +++++++++++++++++++ .../adapter/suggestions/SuggestionsAdapter.kt | 49 +++++++++ .../suggestions/SuggestionsAdapterItem.kt | 30 ++++++ .../SuggestionsDiffUtilCallback.kt | 41 ++++++++ .../dante/ui/fragment/SuggestionsFragment.kt | 5 +- .../ui/viewmodel/SuggestionsViewModel.kt | 12 ++- .../layout/item_suggestion_explanation.xml | 7 ++ 10 files changed, 240 insertions(+), 101 deletions(-) delete mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt rename app/src/main/java/at/shockbytes/dante/{util/view => ui/adapter/main}/BookDiffUtilCallback.kt (96%) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsDiffUtilCallback.kt create mode 100644 app/src/main/res/layout/item_suggestion_explanation.xml diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt deleted file mode 100644 index 89f09950..00000000 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/SuggestionsAdapter.kt +++ /dev/null @@ -1,95 +0,0 @@ -package at.shockbytes.dante.ui.adapter - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import at.shockbytes.dante.R -import at.shockbytes.dante.core.image.ImageLoader -import at.shockbytes.dante.suggestions.BookSuggestionEntity -import at.shockbytes.dante.suggestions.Suggester -import at.shockbytes.dante.suggestions.Suggestion -import at.shockbytes.util.AppUtils -import at.shockbytes.util.adapter.BaseAdapter -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_suggestion.* - -class SuggestionsAdapter( - ctx: Context, - private val imageLoader: ImageLoader, - private val onSuggestionActionClickedListener: OnSuggestionActionClickedListener -) : BaseAdapter(ctx) { - - fun updateData(suggestions: List) { - data.clear() - data.addAll(suggestions) - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return SuggestionViewHolder(inflater.inflate(R.layout.item_suggestion, parent, false)) - } - - private inner class SuggestionViewHolder( - override val containerView: View - ) : BaseAdapter.ViewHolder(containerView), LayoutContainer { - - override fun bindToView(content: Suggestion, position: Int) { - with(content) { - setupOverflowMenu(suggestionId) - setupBook(suggestion) - setupSuggester(suggester) - setupRecommendation(recommendation) - setupBookActionListener(this) - } - } - - private fun setupOverflowMenu(suggestionId: String) { - iv_item_suggestion_report.setOnClickListener { - onSuggestionActionClickedListener.onReportBookSuggestion(suggestionId) - } - } - - private fun setupBook(suggestion: BookSuggestionEntity) { - tv_item_suggestion_author.text = suggestion.author - tv_item_suggestion_title.text = suggestion.title - setThumbnailToView( - suggestion.thumbnailAddress, - iv_item_suggestion_cover, - context.resources.getDimension(R.dimen.thumbnail_rounded_corner).toInt() - ) - } - - private fun setupSuggester(suggester: Suggester) { - tv_item_suggestion_suggester.text = context.getString(R.string.suggestion_suggester, suggester.name) - setThumbnailToView( - suggester.photoUrl, - iv_item_suggestion_suggester, - AppUtils.convertDpInPixel(24, context) - ) - } - - private fun setupRecommendation(recommendation: String) { - tv_item_suggestion_recommendation.text = recommendation - } - - private fun setupBookActionListener(suggestion: Suggestion) { - btn_item_suggestion_add.setOnClickListener { - onSuggestionActionClickedListener.onAddSuggestionToWishlist(suggestion) - } - } - - private fun setThumbnailToView( - url: String?, - view: ImageView, - radius: Int - ) { - if (!url.isNullOrEmpty()) { - imageLoader.loadImageWithCornerRadius(context, url, view, cornerDimension = radius) - } else { - // Books with no image will recycle another cover if not cleared here - view.setImageResource(R.drawable.ic_placeholder) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt index 262c7b00..409ddb04 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt @@ -6,7 +6,6 @@ import androidx.recyclerview.widget.DiffUtil import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.book.BookLabel import at.shockbytes.dante.core.image.ImageLoader -import at.shockbytes.dante.util.view.BookDiffUtilCallback import at.shockbytes.util.adapter.BaseAdapter import at.shockbytes.util.adapter.ItemTouchHelperAdapter import java.util.Collections diff --git a/app/src/main/java/at/shockbytes/dante/util/view/BookDiffUtilCallback.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt similarity index 96% rename from app/src/main/java/at/shockbytes/dante/util/view/BookDiffUtilCallback.kt rename to app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt index 2104c14e..e1ff3b58 100644 --- a/app/src/main/java/at/shockbytes/dante/util/view/BookDiffUtilCallback.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt @@ -1,4 +1,4 @@ -package at.shockbytes.dante.util.view +package at.shockbytes.dante.ui.adapter.main import androidx.recyclerview.widget.DiffUtil import at.shockbytes.dante.core.isContentSame diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt new file mode 100644 index 00000000..e4484773 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt @@ -0,0 +1,99 @@ +package at.shockbytes.dante.ui.adapter.suggestions + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import at.shockbytes.dante.R +import at.shockbytes.dante.core.image.ImageLoader +import at.shockbytes.dante.suggestions.BookSuggestionEntity +import at.shockbytes.dante.suggestions.Suggester +import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener +import at.shockbytes.util.AppUtils +import at.shockbytes.util.adapter.BaseAdapter +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_suggestion.* + +class SuggestionViewHolder( + override val containerView: View, + private val imageLoader: ImageLoader, + private val onSuggestionActionClickedListener: OnSuggestionActionClickedListener +) : BaseAdapter.ViewHolder(containerView), LayoutContainer { + + private fun context(): Context = containerView.context + + override fun bindToView(content: SuggestionsAdapterItem, position: Int) { + with((content as SuggestionsAdapterItem.SuggestedBook).suggestion) { + setupOverflowMenu(suggestionId) + setupBook(suggestion) + setupSuggester(suggester) + setupRecommendation(recommendation) + setupBookActionListener(this) + } + } + + private fun setupOverflowMenu(suggestionId: String) { + iv_item_suggestion_report.setOnClickListener { + onSuggestionActionClickedListener.onReportBookSuggestion(suggestionId) + } + } + + private fun setupBook(suggestion: BookSuggestionEntity) { + tv_item_suggestion_author.text = suggestion.author + tv_item_suggestion_title.text = suggestion.title + setThumbnailToView( + suggestion.thumbnailAddress, + iv_item_suggestion_cover, + context().resources.getDimension(R.dimen.thumbnail_rounded_corner).toInt() + ) + } + + private fun setupSuggester(suggester: Suggester) { + tv_item_suggestion_suggester.text = context().getString(R.string.suggestion_suggester, suggester.name) + setThumbnailToView( + suggester.photoUrl, + iv_item_suggestion_suggester, + AppUtils.convertDpInPixel(24, context()) + ) + } + + private fun setupRecommendation(recommendation: String) { + tv_item_suggestion_recommendation.text = recommendation + } + + private fun setupBookActionListener(suggestion: Suggestion) { + btn_item_suggestion_add.setOnClickListener { + onSuggestionActionClickedListener.onAddSuggestionToWishlist(suggestion) + } + } + + private fun setThumbnailToView( + url: String?, + view: ImageView, + radius: Int + ) { + if (!url.isNullOrEmpty()) { + imageLoader.loadImageWithCornerRadius(context(), url, view, cornerDimension = radius) + } else { + // Books with no image will recycle another cover if not cleared here + view.setImageResource(R.drawable.ic_placeholder) + } + } + + companion object { + + fun forParent( + parent: ViewGroup, + imageLoader: ImageLoader, + onSuggestionActionClickedListener: OnSuggestionActionClickedListener + ): SuggestionViewHolder { + return SuggestionViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_suggestion, parent, false), + imageLoader, + onSuggestionActionClickedListener + ) + } + } +} diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt new file mode 100644 index 00000000..eb7dfd66 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt @@ -0,0 +1,49 @@ +package at.shockbytes.dante.ui.adapter.suggestions + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.DiffUtil +import at.shockbytes.dante.R +import at.shockbytes.dante.core.image.ImageLoader +import at.shockbytes.dante.suggestions.BookSuggestionEntity +import at.shockbytes.dante.suggestions.Suggester +import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener +import at.shockbytes.util.AppUtils +import at.shockbytes.util.adapter.BaseAdapter +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_suggestion.* + +class SuggestionsAdapter( + ctx: Context, + private val imageLoader: ImageLoader, + private val onSuggestionActionClickedListener: OnSuggestionActionClickedListener +) : BaseAdapter(ctx) { + + fun updateData(suggestions: List) { + val diffResult = DiffUtil.calculateDiff(SuggestionsDiffUtilCallback(data, suggestions)) + + data.clear() + data.addAll(suggestions) + + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemViewType(position: Int): Int = data[position].viewType + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + + return when (viewType) { + + R.layout.item_suggestion -> SuggestionViewHolder.forParent( + parent, + imageLoader, + onSuggestionActionClickedListener + ) + + else -> throw IllegalStateException("Unknown ViewType $viewType in ${this.javaClass.simpleName}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt new file mode 100644 index 00000000..b8c54095 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt @@ -0,0 +1,30 @@ +package at.shockbytes.dante.ui.adapter.suggestions + +import at.shockbytes.dante.R +import at.shockbytes.dante.suggestions.Suggestion + +sealed class SuggestionsAdapterItem { + + abstract val id: String + abstract val viewType: Int + + data class SuggestedBook( + val suggestion: Suggestion, + override val viewType: Int = R.layout.item_suggestion + ) : SuggestionsAdapterItem() { + + override val id: String + get() = suggestion.suggestionId + } + + data class Explanation( + val wantsToSuggest: Boolean, + override val id: String = EXPLANATION_ID, + override val viewType: Int = R.layout.item_suggestion_explanation, + ) : SuggestionsAdapterItem() + + companion object { + + const val EXPLANATION_ID = "-1" + } +} diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsDiffUtilCallback.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsDiffUtilCallback.kt new file mode 100644 index 00000000..88571baa --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsDiffUtilCallback.kt @@ -0,0 +1,41 @@ +package at.shockbytes.dante.ui.adapter.suggestions + +import androidx.recyclerview.widget.DiffUtil + +/** + * Author: Martin Macheiner + * Date: 03.12.202 + */ +class SuggestionsDiffUtilCallback( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].id == newList[newItemPosition].id + } + + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + + return when { + + oldItem is SuggestionsAdapterItem.SuggestedBook && newItem is SuggestionsAdapterItem.SuggestedBook -> { + oldItem.suggestion == newItem.suggestion + } + oldItem is SuggestionsAdapterItem.Explanation && newItem is SuggestionsAdapterItem.Explanation -> { + oldItem == newItem + } + else -> { + // If adapter entities don't match, content can't be the same + false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 0b3cd8eb..6ad57b20 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -7,7 +7,8 @@ import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.injection.AppComponent import at.shockbytes.dante.suggestions.Suggestion import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener -import at.shockbytes.dante.ui.adapter.SuggestionsAdapter +import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapter +import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapterItem import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel import at.shockbytes.dante.util.SharedViewComponents import at.shockbytes.dante.util.addTo @@ -85,7 +86,7 @@ class SuggestionsFragment : BaseFragment() { tv_suggestions_empty.setVisible(true) } - private fun handleSuggestions(suggestions: List) { + private fun handleSuggestions(suggestions: List) { rv_suggestions.setVisible(true) tv_suggestions_empty.setVisible(false) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 8f9e3877..4b4426e6 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -7,7 +7,9 @@ import at.shockbytes.dante.core.book.BookState import at.shockbytes.dante.core.data.BookRepository import at.shockbytes.dante.suggestions.BookSuggestionEntity import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.suggestions.Suggestions import at.shockbytes.dante.suggestions.SuggestionsRepository +import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapterItem import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.addTo import at.shockbytes.tracking.Tracker @@ -25,7 +27,7 @@ class SuggestionsViewModel @Inject constructor( sealed class SuggestionsState { - data class Present(val suggestions: List) : SuggestionsState() + data class Present(val suggestions: List) : SuggestionsState() object Empty : SuggestionsState() } @@ -42,13 +44,19 @@ class SuggestionsViewModel @Inject constructor( if (suggestions.suggestions.isEmpty()) { SuggestionsState.Empty } else { - SuggestionsState.Present(suggestions.suggestions.sortedBy { it.suggestionId }) + SuggestionsState.Present(buildSuggestionsAdapterItems(suggestions)) } } .subscribe(suggestionState::postValue, ExceptionHandlers::defaultExceptionHandler) .addTo(compositeDisposable) } + private fun buildSuggestionsAdapterItems(suggestions: Suggestions): List { + return suggestions.suggestions + .sortedBy { it.suggestionId } + .map(SuggestionsAdapterItem::SuggestedBook) + } + fun addSuggestionToWishlist(suggestion: Suggestion) { bookRepository.create(suggestion.suggestion.toBookEntity()) .doOnComplete { diff --git a/app/src/main/res/layout/item_suggestion_explanation.xml b/app/src/main/res/layout/item_suggestion_explanation.xml new file mode 100644 index 00000000..eaf083f6 --- /dev/null +++ b/app/src/main/res/layout/item_suggestion_explanation.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file From 70ecd2b7587ff30e775712e7eab090b9752587a2 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 13:00:33 +0100 Subject: [PATCH 24/30] Add explanations structure --- .../shockbytes/dante/injection/AppModule.kt | 8 +++++ .../ui/viewmodel/SuggestionsViewModel.kt | 15 +++++++-- .../dante/util/explanations/Explanation.kt | 15 +++++++++ .../dante/util/explanations/Explanations.kt | 11 +++++++ .../explanations/SharedPrefsExplanations.kt | 32 +++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/util/explanations/Explanation.kt create mode 100644 app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt create mode 100644 app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt diff --git a/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt b/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt index f924933b..a31bc638 100644 --- a/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt +++ b/app/src/main/java/at/shockbytes/dante/injection/AppModule.kt @@ -15,6 +15,8 @@ import at.shockbytes.dante.flagging.FirebaseFeatureFlagging import at.shockbytes.dante.flagging.SharedPreferencesFeatureFlagging import at.shockbytes.dante.suggestions.AssetsSuggestionsRepository import at.shockbytes.dante.suggestions.SuggestionsRepository +import at.shockbytes.dante.util.explanations.Explanations +import at.shockbytes.dante.util.explanations.SharedPrefsExplanations import at.shockbytes.dante.util.permission.AndroidPermissionManager import at.shockbytes.dante.util.permission.PermissionManager import at.shockbytes.dante.util.scheduler.SchedulerFacade @@ -76,4 +78,10 @@ class AppModule(private val app: Application) { fun provideSuggestionsRepository(): SuggestionsRepository { return AssetsSuggestionsRepository(app.applicationContext, Gson()) } + + @Provides + fun provideExplanations(): Explanations { + val sharedPreferences = app.getSharedPreferences("preferences_explanations", Context.MODE_PRIVATE) + return SharedPrefsExplanations(sharedPreferences) + } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 4b4426e6..0b4f5028 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -11,6 +11,7 @@ import at.shockbytes.dante.suggestions.Suggestions import at.shockbytes.dante.suggestions.SuggestionsRepository import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapterItem import at.shockbytes.dante.util.ExceptionHandlers +import at.shockbytes.dante.util.explanations.Explanations import at.shockbytes.dante.util.addTo import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent @@ -22,7 +23,8 @@ import javax.inject.Inject class SuggestionsViewModel @Inject constructor( private val suggestionsRepository: SuggestionsRepository, private val bookRepository: BookRepository, - private val tracker: Tracker + private val tracker: Tracker, + private val explanations: Explanations ) : BaseViewModel() { sealed class SuggestionsState { @@ -52,9 +54,18 @@ class SuggestionsViewModel @Inject constructor( } private fun buildSuggestionsAdapterItems(suggestions: Suggestions): List { - return suggestions.suggestions + + val explanation = explanations.suggestion() + + val suggestedItems = suggestions.suggestions .sortedBy { it.suggestionId } .map(SuggestionsAdapterItem::SuggestedBook) + + return if (explanation.show) { + listOf(SuggestionsAdapterItem.Explanation(explanation.userWantsToSuggest)) + suggestedItems + } else { + suggestedItems + } } fun addSuggestionToWishlist(suggestion: Suggestion) { diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/Explanation.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanation.kt new file mode 100644 index 00000000..911dbd80 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanation.kt @@ -0,0 +1,15 @@ +package at.shockbytes.dante.util.explanations + +sealed class Explanation { + + abstract val show: Boolean + + data class Suggestion( + override val show: Boolean, + val userWantsToSuggest: Boolean + ) : Explanation() + + data class Wishlist( + override val show: Boolean + ) : Explanation() +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt new file mode 100644 index 00000000..cf5b6df7 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt @@ -0,0 +1,11 @@ +package at.shockbytes.dante.util.explanations + + +interface Explanations { + + fun suggestion(): Explanation.Suggestion + + fun wishlist(): Explanation.Wishlist + + fun markSeen(explanation: Explanation) +} diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt new file mode 100644 index 00000000..68e655cf --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt @@ -0,0 +1,32 @@ +package at.shockbytes.dante.util.explanations + +import android.content.SharedPreferences + +class SharedPrefsExplanations( + private val sharedPreferences: SharedPreferences +) : Explanations { + + override fun suggestion(): Explanation.Suggestion { + return Explanation.Suggestion( + show = getShowFor(), + userWantsToSuggest = getBooleanForKey("user_wants_to_suggest", false) + ) + } + + private fun getBooleanForKey(key: String, defaultValue: Boolean): Boolean { + return sharedPreferences.getBoolean(key, defaultValue) + } + + override fun wishlist(): Explanation.Wishlist { + return Explanation.Wishlist(show = getShowFor()) + } + + private inline fun getShowFor(): Boolean { + return sharedPreferences.getBoolean(T::class.java.simpleName, true) + } + + override fun markSeen(explanation: Explanation) { + sharedPreferences.edit().putBoolean(explanation::class.java.simpleName, true).apply() + } + +} \ No newline at end of file From 907cc6316d7b269434693b0fb9998968bf214863 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 13:07:13 +0100 Subject: [PATCH 25/30] Show suggestion explanation on top of suggestion list --- .../SuggestionExplanationViewHolder.kt | 26 +++++++++++++++++++ .../adapter/suggestions/SuggestionsAdapter.kt | 2 ++ .../dante/ui/fragment/SuggestionsFragment.kt | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt new file mode 100644 index 00000000..71ca1df3 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt @@ -0,0 +1,26 @@ +package at.shockbytes.dante.ui.adapter.suggestions + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import at.shockbytes.dante.R +import at.shockbytes.util.adapter.BaseAdapter +import kotlinx.android.extensions.LayoutContainer + +class SuggestionExplanationViewHolder( + override val containerView: View +) : BaseAdapter.ViewHolder(containerView), LayoutContainer { + + override fun bindToView(content: SuggestionsAdapterItem, position: Int) { + // TODO + } + + companion object { + + fun forParent(parent: ViewGroup): SuggestionExplanationViewHolder { + return SuggestionExplanationViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_suggestion_explanation, parent, false) + ) + } + } +} diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt index eb7dfd66..476e200b 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt @@ -43,6 +43,8 @@ class SuggestionsAdapter( onSuggestionActionClickedListener ) + R.layout.item_suggestion_explanation -> SuggestionExplanationViewHolder.forParent(parent) + else -> throw IllegalStateException("Unknown ViewType $viewType in ${this.javaClass.simpleName}") } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index 6ad57b20..dc7c9863 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -90,7 +90,7 @@ class SuggestionsFragment : BaseFragment() { rv_suggestions.setVisible(true) tv_suggestions_empty.setVisible(false) - suggestionAdapter.data = suggestions.toMutableList() + suggestionAdapter.updateData(suggestions) } override fun unbindViewModel() = Unit From 601d58e4303bc22d50664b5c74e238cb23c5870c Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 13:30:02 +0100 Subject: [PATCH 26/30] Show wishlist explanation in wishlist tab --- .../dante/ui/adapter/main/BookAdapter.kt | 17 ++++---- ...ookAdapterEntity.kt => BookAdapterItem.kt} | 14 +++++-- .../ui/adapter/main/BookDiffUtilCallback.kt | 13 +++--- .../dante/ui/adapter/main/BookViewHolder.kt | 6 +-- .../ui/adapter/main/RandomPickViewHolder.kt | 4 +- .../main/WishlistExplanationViewHolder.kt | 26 ++++++++++++ .../adapter/suggestions/SuggestionsAdapter.kt | 8 ---- .../suggestions/SuggestionsAdapterItem.kt | 2 +- .../dante/ui/fragment/MainBookFragment.kt | 22 +++++----- .../dante/ui/viewmodel/BookListViewModel.kt | 42 +++++++++++-------- .../dante/util/AppModuleExtensions.kt | 6 +-- .../dante/util/explanations/Explanations.kt | 1 - .../explanations/SharedPrefsExplanations.kt | 1 - .../res/layout/item_generic_explanation.xml | 7 ++++ 14 files changed, 106 insertions(+), 63 deletions(-) rename app/src/main/java/at/shockbytes/dante/ui/adapter/main/{BookAdapterEntity.kt => BookAdapterItem.kt} (62%) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt create mode 100644 app/src/main/res/layout/item_generic_explanation.xml diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt index 409ddb04..d81c9a38 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt @@ -20,9 +20,9 @@ class BookAdapter( private val onOverflowActionClickedListener: (BookEntity) -> Unit, private val onLabelClickedListener: ((BookLabel) -> Unit)? = null, private val randomPickCallback: RandomPickCallback? = null, - onItemClickListener: OnItemClickListener, - onItemMoveListener: OnItemMoveListener -) : BaseAdapter( + onItemClickListener: OnItemClickListener, + onItemMoveListener: OnItemMoveListener +) : BaseAdapter( context, onItemClickListener = onItemClickListener, onItemMoveListener = onItemMoveListener @@ -40,11 +40,11 @@ class BookAdapter( return data[position].viewType } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return when (viewType) { - BookAdapterEntity.VIEW_TYPE_BOOK -> { + BookAdapterItem.VIEW_TYPE_BOOK -> { BookViewHolder.forParent( parent, imageLoader, @@ -52,12 +52,15 @@ class BookAdapter( onLabelClickedListener ) } - BookAdapterEntity.VIEW_TYPE_RANDOM_PICK -> { + BookAdapterItem.VIEW_TYPE_RANDOM_PICK -> { RandomPickViewHolder.forParent( parent, randomPickCallback ) } + BookAdapterItem.VIEW_TYPE_EXPLANATION_WISHLIST -> { + WishlistExplanationViewHolder.forParent(parent) + } else -> throw IllegalStateException("Unknown view type $viewType") } } @@ -89,7 +92,7 @@ class BookAdapter( onItemMoveListener?.onItemMoveFinished() } - fun updateData(books: List) { + fun updateData(books: List) { val diffResult = DiffUtil.calculateDiff(BookDiffUtilCallback(data, books)) data.clear() diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapterEntity.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapterItem.kt similarity index 62% rename from app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapterEntity.kt rename to app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapterItem.kt index 0abbe3be..d22612c1 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapterEntity.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapterItem.kt @@ -2,7 +2,7 @@ package at.shockbytes.dante.ui.adapter.main import at.shockbytes.dante.core.book.BookEntity -sealed class BookAdapterEntity { +sealed class BookAdapterItem { abstract val id: Long abstract val viewType: Int @@ -10,7 +10,7 @@ sealed class BookAdapterEntity { data class Book( val bookEntity: BookEntity, override val viewType: Int = VIEW_TYPE_BOOK - ) : BookAdapterEntity() { + ) : BookAdapterItem() { override val id: Long get() = bookEntity.id @@ -18,16 +18,24 @@ sealed class BookAdapterEntity { val title: String = bookEntity.title } - object RandomPick : BookAdapterEntity() { + object RandomPick : BookAdapterItem() { override val id: Long = RANDOM_PICK_ID override val viewType: Int = VIEW_TYPE_RANDOM_PICK } + object WishlistExplanation : BookAdapterItem() { + + override val id: Long = EXPLANATION_WISHLIST_ID + override val viewType: Int = VIEW_TYPE_EXPLANATION_WISHLIST + } + companion object { const val RANDOM_PICK_ID = -1L + const val EXPLANATION_WISHLIST_ID = -2L const val VIEW_TYPE_BOOK = 1 const val VIEW_TYPE_RANDOM_PICK = 2 + const val VIEW_TYPE_EXPLANATION_WISHLIST = 3 } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt index e1ff3b58..c5435ce9 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookDiffUtilCallback.kt @@ -2,15 +2,14 @@ package at.shockbytes.dante.ui.adapter.main import androidx.recyclerview.widget.DiffUtil import at.shockbytes.dante.core.isContentSame -import at.shockbytes.dante.ui.adapter.main.BookAdapterEntity /** * Author: Martin Macheiner * Date: 12.06.2018 */ class BookDiffUtilCallback( - private val oldList: List, - private val newList: List + private val oldList: List, + private val newList: List ) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { @@ -28,10 +27,14 @@ class BookDiffUtilCallback( return when { - oldItem is BookAdapterEntity.Book && newItem is BookAdapterEntity.Book -> { + oldItem is BookAdapterItem.Book && newItem is BookAdapterItem.Book -> { oldItem.bookEntity.isContentSame(newItem.bookEntity) } - oldItem is BookAdapterEntity.RandomPick && newItem is BookAdapterEntity.RandomPick -> { + oldItem is BookAdapterItem.RandomPick && newItem is BookAdapterItem.RandomPick -> { + // Both are objects, it's always true + true + } + oldItem is BookAdapterItem.WishlistExplanation && newItem is BookAdapterItem.WishlistExplanation -> { // Both are objects, it's always true true } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt index 108fdefb..fd115ec6 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookViewHolder.kt @@ -24,12 +24,12 @@ class BookViewHolder( private val imageLoader: ImageLoader, private val onOverflowActionClickedListener: ((BookEntity) -> Unit)?, private val onLabelClickedListener: ((BookLabel) -> Unit)? -) : BaseAdapter.ViewHolder(containerView), LayoutContainer { +) : BaseAdapter.ViewHolder(containerView), LayoutContainer { private fun context(): Context = containerView.context - override fun bindToView(content: BookAdapterEntity, position: Int) { - with(content as BookAdapterEntity.Book) { + override fun bindToView(content: BookAdapterItem, position: Int) { + with(content as BookAdapterItem.Book) { updateTexts(bookEntity) updateImageThumbnail(bookEntity.thumbnailAddress) updateProgress(bookEntity) diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt index b3ae7281..1eb15d4e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/RandomPickViewHolder.kt @@ -11,9 +11,9 @@ import kotlinx.android.synthetic.main.item_random_pick.* class RandomPickViewHolder( override val containerView: View, private val callback: RandomPickCallback? -) : BaseAdapter.ViewHolder(containerView), LayoutContainer { +) : BaseAdapter.ViewHolder(containerView), LayoutContainer { - override fun bindToView(content: BookAdapterEntity, position: Int) { + override fun bindToView(content: BookAdapterItem, position: Int) { btn_item_random_pick.setOnClickListener { callback?.onRandomPickClicked() diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt new file mode 100644 index 00000000..143cedf1 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt @@ -0,0 +1,26 @@ +package at.shockbytes.dante.ui.adapter.main + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import at.shockbytes.dante.R +import at.shockbytes.util.adapter.BaseAdapter +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_random_pick.* + +class WishlistExplanationViewHolder( + override val containerView: View +) : BaseAdapter.ViewHolder(containerView), LayoutContainer { + + override fun bindToView(content: BookAdapterItem, position: Int) { + } + + companion object { + + fun forParent(parent: ViewGroup): WishlistExplanationViewHolder { + return WishlistExplanationViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_generic_explanation, parent, false) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt index 476e200b..f3815100 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt @@ -1,20 +1,12 @@ package at.shockbytes.dante.ui.adapter.suggestions import android.content.Context -import android.view.View import android.view.ViewGroup -import android.widget.ImageView import androidx.recyclerview.widget.DiffUtil import at.shockbytes.dante.R import at.shockbytes.dante.core.image.ImageLoader -import at.shockbytes.dante.suggestions.BookSuggestionEntity -import at.shockbytes.dante.suggestions.Suggester -import at.shockbytes.dante.suggestions.Suggestion import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener -import at.shockbytes.util.AppUtils import at.shockbytes.util.adapter.BaseAdapter -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_suggestion.* class SuggestionsAdapter( ctx: Context, diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt index b8c54095..b5d89932 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt @@ -20,7 +20,7 @@ sealed class SuggestionsAdapterItem { data class Explanation( val wantsToSuggest: Boolean, override val id: String = EXPLANATION_ID, - override val viewType: Int = R.layout.item_suggestion_explanation, + override val viewType: Int = R.layout.item_suggestion_explanation ) : SuggestionsAdapterItem() companion object { diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index 34275fa3..9a3c586c 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -24,7 +24,7 @@ import at.shockbytes.dante.ui.adapter.main.BookAdapter import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.ui.activity.ManualAddActivity.Companion.EXTRA_UPDATED_BOOK_STATE import at.shockbytes.dante.ui.adapter.OnBookActionClickedListener -import at.shockbytes.dante.ui.adapter.main.BookAdapterEntity +import at.shockbytes.dante.ui.adapter.main.BookAdapterItem import at.shockbytes.dante.ui.adapter.main.RandomPickCallback import at.shockbytes.dante.ui.fragment.BookDetailFragment.Companion.ACTION_BOOK_CHANGED import at.shockbytes.dante.ui.viewmodel.BookListViewModel @@ -46,8 +46,8 @@ import timber.log.Timber import javax.inject.Inject class MainBookFragment : BaseFragment(), - BaseAdapter.OnItemClickListener, - BaseAdapter.OnItemMoveListener, + BaseAdapter.OnItemClickListener, + BaseAdapter.OnItemMoveListener, OnBookActionClickedListener { override val layoutId = R.layout.fragment_book_main @@ -78,7 +78,7 @@ class MainBookFragment : BaseFragment(), override fun onDismiss() { showToast(R.string.random_pick_restore_instruction) viewModel.onDismissRandomBookPicker() - bookAdapter.deleteEntity(BookAdapterEntity.RandomPick) + bookAdapter.deleteEntity(BookAdapterItem.RandomPick) } override fun onRandomPickClicked() { @@ -238,14 +238,14 @@ class MainBookFragment : BaseFragment(), itemTouchHelper.attachToRecyclerView(rv_main_book_fragment) } - override fun onItemClick(content: BookAdapterEntity, position: Int, v: View) { + override fun onItemClick(content: BookAdapterItem, position: Int, v: View) { when (content) { - is BookAdapterEntity.Book -> handleBookClick(content, v) - BookAdapterEntity.RandomPick -> Unit // Do nothing + is BookAdapterItem.Book -> handleBookClick(content, v) + BookAdapterItem.RandomPick -> Unit // Do nothing } } - private fun handleBookClick(content: BookAdapterEntity.Book, v: View) { + private fun handleBookClick(content: BookAdapterItem.Book, v: View) { if (allowItemClick) { ActivityNavigator.navigateTo( context, @@ -255,10 +255,10 @@ class MainBookFragment : BaseFragment(), } } - override fun onItemDismissed(t: BookAdapterEntity, position: Int) = Unit + override fun onItemDismissed(t: BookAdapterItem, position: Int) = Unit // Do nothing, only react to move actions in the on item move finished method - override fun onItemMove(t: BookAdapterEntity, from: Int, to: Int) = Unit + override fun onItemMove(t: BookAdapterItem, from: Int, to: Int) = Unit override fun onItemMoveFinished() = viewModel.updateBookPositions(bookAdapter.data) @@ -325,7 +325,7 @@ class MainBookFragment : BaseFragment(), .toBundle() } - private fun BookEntity.toAdapterEntity(): BookAdapterEntity = BookAdapterEntity.Book(this) + private fun BookEntity.toAdapterEntity(): BookAdapterItem = BookAdapterItem.Book(this) companion object { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt index 6046407d..3fca1d16 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt @@ -5,14 +5,15 @@ import androidx.lifecycle.MutableLiveData import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.book.BookState import at.shockbytes.dante.core.data.BookRepository -import at.shockbytes.dante.ui.adapter.main.BookAdapterEntity +import at.shockbytes.dante.ui.adapter.main.BookAdapterItem import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.settings.DanteSettings import at.shockbytes.dante.util.addTo +import at.shockbytes.dante.util.explanations.Explanations import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.dante.util.sort.SortComparators import at.shockbytes.dante.util.sort.SortStrategy -import at.shockbytes.dante.util.toAdapterEntities +import at.shockbytes.dante.util.toAdapterItems import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent import io.reactivex.Observable @@ -30,7 +31,8 @@ class BookListViewModel @Inject constructor( private val settings: DanteSettings, private val schedulers: SchedulerFacade, private val danteSettings: DanteSettings, - private val tracker: Tracker + private val tracker: Tracker, + private val explanations: Explanations ) : BaseViewModel() { var state: BookState = BookState.READING @@ -41,7 +43,7 @@ class BookListViewModel @Inject constructor( sealed class BookLoadingState { - data class Success(val books: List) : BookLoadingState() + data class Success(val books: List) : BookLoadingState() data class Error(val throwable: Throwable) : BookLoadingState() @@ -105,21 +107,25 @@ class BookListViewModel @Inject constructor( private fun mapBooksToBookLoadingState(books: List): BookLoadingState { return if (books.isNotEmpty()) { - - val bookAdapterEntities = if (shouldShowRandomPickInteraction(books.size)) { - books.toAdapterEntities().toMutableList().apply { - add(0, BookAdapterEntity.RandomPick) - } - } else { - books.toAdapterEntities() - } - - BookLoadingState.Success(bookAdapterEntities) + val bookAdapterItems = lookupForHeaderItem(books) + books.toAdapterItems() + BookLoadingState.Success(bookAdapterItems) } else { BookLoadingState.Empty } } + private fun lookupForHeaderItem(books: List): List { + return when { + (state == BookState.READ_LATER && shouldShowRandomPickInteraction(books.size)) -> { + listOf(BookAdapterItem.RandomPick) + } + (state == BookState.WISHLIST && explanations.wishlist().show) -> { + listOf(BookAdapterItem.WishlistExplanation) + } + else -> listOf() + } + } + /** * Show random pick interaction if: * - User is in read_later tab @@ -137,9 +143,9 @@ class BookListViewModel @Inject constructor( .addTo(compositeDisposable) } - fun updateBookPositions(data: MutableList) { + fun updateBookPositions(data: MutableList) { data.forEachIndexed { index, entity -> - if (entity is BookAdapterEntity.Book) { + if (entity is BookAdapterItem.Book) { entity.bookEntity.position = index updateBook(entity.bookEntity) } @@ -187,12 +193,12 @@ class BookListViewModel @Inject constructor( (state as? BookLoadingState.Success)?.books } ?.also { adapterEntities -> - val books = adapterEntities.filterIsInstance().count() + val books = adapterEntities.filterIsInstance().count() tracker.track(DanteTrackingEvent.PickRandomBook(books)) } ?.let { books -> val randomPick = books - .filterIsInstance() + .filterIsInstance() .random() .bookEntity RandomPickEvent.RandomPick(randomPick) diff --git a/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt b/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt index 2c2f07c8..7dc24227 100644 --- a/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt +++ b/app/src/main/java/at/shockbytes/dante/util/AppModuleExtensions.kt @@ -8,7 +8,7 @@ import android.os.Looper import at.shockbytes.dante.core.R import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.signin.DanteUser -import at.shockbytes.dante.ui.adapter.main.BookAdapterEntity +import at.shockbytes.dante.ui.adapter.main.BookAdapterItem import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.tasks.Tasks import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -66,8 +66,8 @@ fun Context.openFile(fileToPath: File, mimeType: String): Intent { .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } -fun List.toAdapterEntities(): List { +fun List.toAdapterItems(): List { return this.map { entity -> - BookAdapterEntity.Book(entity) + BookAdapterItem.Book(entity) } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt index cf5b6df7..e647006f 100644 --- a/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt @@ -1,6 +1,5 @@ package at.shockbytes.dante.util.explanations - interface Explanations { fun suggestion(): Explanation.Suggestion diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt index 68e655cf..45768a8f 100644 --- a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt @@ -28,5 +28,4 @@ class SharedPrefsExplanations( override fun markSeen(explanation: Explanation) { sharedPreferences.edit().putBoolean(explanation::class.java.simpleName, true).apply() } - } \ No newline at end of file diff --git a/app/src/main/res/layout/item_generic_explanation.xml b/app/src/main/res/layout/item_generic_explanation.xml new file mode 100644 index 00000000..f6fa72a5 --- /dev/null +++ b/app/src/main/res/layout/item_generic_explanation.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file From 937117ea6bf4d9aa350c9dbe6cb61e9e51c9a652 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 20:29:25 +0100 Subject: [PATCH 27/30] Use generic layout for explanation views --- .../dante/ui/adapter/main/BookAdapter.kt | 3 +- .../main/WishlistExplanationViewHolder.kt | 24 ++++- .../SuggestionExplanationViewHolder.kt | 15 ++- .../adapter/suggestions/SuggestionsAdapter.kt | 2 +- .../suggestions/SuggestionsAdapterItem.kt | 4 +- .../res/layout/item_generic_explanation.xml | 93 ++++++++++++++++++- .../layout/item_suggestion_explanation.xml | 7 -- 7 files changed, 129 insertions(+), 19 deletions(-) delete mode 100644 app/src/main/res/layout/item_suggestion_explanation.xml diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt index d81c9a38..dddb37bf 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/BookAdapter.kt @@ -20,6 +20,7 @@ class BookAdapter( private val onOverflowActionClickedListener: (BookEntity) -> Unit, private val onLabelClickedListener: ((BookLabel) -> Unit)? = null, private val randomPickCallback: RandomPickCallback? = null, + private val wishlistExplanationDismissListener: (() -> Unit)? = null, onItemClickListener: OnItemClickListener, onItemMoveListener: OnItemMoveListener ) : BaseAdapter( @@ -59,7 +60,7 @@ class BookAdapter( ) } BookAdapterItem.VIEW_TYPE_EXPLANATION_WISHLIST -> { - WishlistExplanationViewHolder.forParent(parent) + WishlistExplanationViewHolder.forParent(parent, wishlistExplanationDismissListener) } else -> throw IllegalStateException("Unknown view type $viewType") } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt index 143cedf1..e0c281d5 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt @@ -4,22 +4,40 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import at.shockbytes.dante.R +import at.shockbytes.dante.util.setVisible import at.shockbytes.util.adapter.BaseAdapter import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_generic_explanation.* import kotlinx.android.synthetic.main.item_random_pick.* class WishlistExplanationViewHolder( - override val containerView: View + override val containerView: View, + private val dismissListener: (() -> Unit)? ) : BaseAdapter.ViewHolder(containerView), LayoutContainer { override fun bindToView(content: BookAdapterItem, position: Int) { + + iv_item_generic_explanation_dismiss.setOnClickListener { + dismissListener?.invoke() + } + + tv_item_generic_explanation.text = "Describe Wishlist" + + btn_item_generic_explanation.setVisible(false, invisibilityState = View.INVISIBLE) + + iv_item_generic_explanation_decoration_start.setImageResource(R.drawable.ic_wishlist) + iv_item_generic_explanation_decoration_end.setImageResource(R.drawable.ic_wishlist) } companion object { - fun forParent(parent: ViewGroup): WishlistExplanationViewHolder { + fun forParent( + parent: ViewGroup, + dismissListener: (() -> Unit)? + ): WishlistExplanationViewHolder { return WishlistExplanationViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.item_generic_explanation, parent, false) + LayoutInflater.from(parent.context).inflate(R.layout.item_generic_explanation, parent, false), + dismissListener ) } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt index 71ca1df3..5029d364 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt @@ -4,8 +4,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import at.shockbytes.dante.R +import at.shockbytes.dante.util.setVisible import at.shockbytes.util.adapter.BaseAdapter import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.item_generic_explanation.* class SuggestionExplanationViewHolder( override val containerView: View @@ -13,13 +15,24 @@ class SuggestionExplanationViewHolder( override fun bindToView(content: SuggestionsAdapterItem, position: Int) { // TODO + + iv_item_generic_explanation_dismiss.setOnClickListener { + // TODO Callback + } + + tv_item_generic_explanation.text = "Describe" + + btn_item_generic_explanation.setVisible(false, invisibilityState = View.INVISIBLE) + + iv_item_generic_explanation_decoration_start.setImageResource(R.drawable.ic_suggestions) + iv_item_generic_explanation_decoration_end.setImageResource(R.drawable.ic_suggestions) } companion object { fun forParent(parent: ViewGroup): SuggestionExplanationViewHolder { return SuggestionExplanationViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.item_suggestion_explanation, parent, false) + LayoutInflater.from(parent.context).inflate(R.layout.item_generic_explanation, parent, false) ) } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt index f3815100..1b7a5cc7 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt @@ -35,7 +35,7 @@ class SuggestionsAdapter( onSuggestionActionClickedListener ) - R.layout.item_suggestion_explanation -> SuggestionExplanationViewHolder.forParent(parent) + R.layout.item_generic_explanation -> SuggestionExplanationViewHolder.forParent(parent) else -> throw IllegalStateException("Unknown ViewType $viewType in ${this.javaClass.simpleName}") } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt index b5d89932..20b2e05d 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapterItem.kt @@ -20,11 +20,11 @@ sealed class SuggestionsAdapterItem { data class Explanation( val wantsToSuggest: Boolean, override val id: String = EXPLANATION_ID, - override val viewType: Int = R.layout.item_suggestion_explanation + override val viewType: Int = R.layout.item_generic_explanation ) : SuggestionsAdapterItem() companion object { - const val EXPLANATION_ID = "-1" + private const val EXPLANATION_ID = "-1" } } diff --git a/app/src/main/res/layout/item_generic_explanation.xml b/app/src/main/res/layout/item_generic_explanation.xml index f6fa72a5..0039ffca 100644 --- a/app/src/main/res/layout/item_generic_explanation.xml +++ b/app/src/main/res/layout/item_generic_explanation.xml @@ -1,7 +1,92 @@ - + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/item_book_horizontal_spacing" + android:layout_marginLeft="@dimen/item_book_horizontal_spacing" + android:layout_marginTop="6dp" + android:layout_marginEnd="@dimen/item_book_horizontal_spacing" + android:layout_marginRight="@dimen/item_book_horizontal_spacing" + android:layout_marginBottom="6dp" + android:elevation="0dp" + app:cardElevation="0dp" + android:minHeight="100dp" + android:padding="2dp" + app:strokeColor="@color/border" + app:strokeWidth="0.8dp" + android:transitionName="@string/transition_name_card"> - \ No newline at end of file + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_suggestion_explanation.xml b/app/src/main/res/layout/item_suggestion_explanation.xml deleted file mode 100644 index eaf083f6..00000000 --- a/app/src/main/res/layout/item_suggestion_explanation.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file From 2b23130ebc28a2887b664869845868ad0e7781ee Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 20:57:32 +0100 Subject: [PATCH 28/30] Wishlist explanation --- .../main/WishlistExplanationViewHolder.kt | 4 ++-- .../dante/ui/fragment/MainBookFragment.kt | 6 ++++++ .../dante/ui/viewmodel/BookListViewModel.kt | 6 ++++++ .../explanations/SharedPrefsExplanations.kt | 2 +- .../res/layout/item_generic_explanation.xml | 19 ++++++++++--------- core/src/main/res/values-de/strings.xml | 1 + core/src/main/res/values/strings.xml | 3 ++- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt index e0c281d5..f7439ae1 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/main/WishlistExplanationViewHolder.kt @@ -21,9 +21,9 @@ class WishlistExplanationViewHolder( dismissListener?.invoke() } - tv_item_generic_explanation.text = "Describe Wishlist" + tv_item_generic_explanation.setText(R.string.wishlist_explanation) - btn_item_generic_explanation.setVisible(false, invisibilityState = View.INVISIBLE) + btn_item_generic_explanation.setVisible(false) iv_item_generic_explanation_decoration_start.setImageResource(R.drawable.ic_wishlist) iv_item_generic_explanation_decoration_end.setImageResource(R.drawable.ic_wishlist) diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt index 9a3c586c..0501fc6e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/MainBookFragment.kt @@ -74,6 +74,11 @@ class MainBookFragment : BaseFragment(), .show(childFragmentManager, "book-action-bottom-sheet") } + private val dismissWishlistExplanation: () -> Unit = { + viewModel.dismissWishlistExplanation() + bookAdapter.deleteEntity(BookAdapterItem.WishlistExplanation) + } + private val randomPickCallback = object : RandomPickCallback { override fun onDismiss() { showToast(R.string.random_pick_restore_instruction) @@ -219,6 +224,7 @@ class MainBookFragment : BaseFragment(), onOverflowActionClickedListener = onBookOverflowClickedListener, onItemClickListener = this, onItemMoveListener = this, + wishlistExplanationDismissListener = dismissWishlistExplanation, onLabelClickedListener = onLabelClickedListener, randomPickCallback = randomPickCallback ) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt index 3fca1d16..0676fae2 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt @@ -9,6 +9,7 @@ import at.shockbytes.dante.ui.adapter.main.BookAdapterItem import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.settings.DanteSettings import at.shockbytes.dante.util.addTo +import at.shockbytes.dante.util.explanations.Explanation import at.shockbytes.dante.util.explanations.Explanations import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.dante.util.sort.SortComparators @@ -21,6 +22,7 @@ import io.reactivex.subjects.PublishSubject import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject +import kotlin.reflect.KClass /** * Author: Martin Macheiner @@ -213,4 +215,8 @@ class BookListViewModel @Inject constructor( tracker.track(DanteTrackingEvent.DisableRandomBookInteraction) } + + fun dismissWishlistExplanation() { + explanations.markSeen(explanations.wishlist()) + } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt index 45768a8f..dd64567e 100644 --- a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt @@ -26,6 +26,6 @@ class SharedPrefsExplanations( } override fun markSeen(explanation: Explanation) { - sharedPreferences.edit().putBoolean(explanation::class.java.simpleName, true).apply() + sharedPreferences.edit().putBoolean(explanation::class.java.simpleName, false).apply() } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_generic_explanation.xml b/app/src/main/res/layout/item_generic_explanation.xml index 0039ffca..b1e25b1b 100644 --- a/app/src/main/res/layout/item_generic_explanation.xml +++ b/app/src/main/res/layout/item_generic_explanation.xml @@ -32,7 +32,7 @@ android:layout_gravity="end" android:background="@drawable/bg_rounded_ripple" android:scaleType="center" - android:tint="@color/actionBarItemColor" + app:tint="@color/actionBarItemColor" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_close_grayscale" /> @@ -44,12 +44,15 @@ android:layout_marginStart="16dp" android:layout_marginTop="4dp" android:layout_marginEnd="16dp" + android:layout_marginBottom="16dp" android:gravity="center" - tools:text="This is a generic explanation of the feature! Mind it!" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + app:layout_constraintBottom_toTopOf="@+id/btn_item_generic_explanation" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/iv_item_generic_explanation_dismiss" /> + app:layout_constraintTop_toBottomOf="@id/iv_item_generic_explanation_dismiss" + app:layout_goneMarginBottom="16dp" + tools:text="This is a generic explanation of the feature! Mind it!" /> + tools:text="ACTION" + tools:visibility="visible" /> @@ -82,7 +83,7 @@ android:layout_width="72dp" android:layout_height="72dp" android:alpha="0.15" - android:tint="?attr/colorControlNormal" + app:tint="?attr/colorControlNormal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:srcCompat="@drawable/ic_wishlist" /> diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index a1ddea0d..6281497c 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -326,6 +326,7 @@ Screen öffnen Vorschläge Meine Wunschliste + Lege alle Bücher hier ab, die du noch nicht hast, aber in Zukunft lesen willst.\n\nKleiner Tipp: Teile diese Liste gelegentlich mit deinem Partner. Zur Wunschliste hinzufügen Deine Wunschliste ist leer. Gut! Sieht so aus als hättest du all deine Bücher bereits! Wunschliste diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 978d5767..4c1bbbe3 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -179,7 +179,7 @@ Sort by - Wishlist date must smaller than the others… + For later date must smaller than the others… Start date must lie within the range of the other dates… End date must be later than the other dates… @@ -353,6 +353,7 @@ Your wishlist is empty! Good! Seems like you have all your books already. My Wishlist + Put all the books here that you want to read in the future but haven\'t purchased yet.\n\nPro tip: Share this list occasionally with your partner. Wishlist Add to wishlist - %s From 47258e5418479231b516e1791547d707bab026b9 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 21:59:10 +0100 Subject: [PATCH 29/30] Wire up suggestion explanation --- .../OnSuggestionActionClickedListener.kt | 2 +- .../OnSuggestionExplanationClickedListener.kt | 8 +++++ .../SuggestionExplanationViewHolder.kt | 34 ++++++++++++++----- .../suggestions/SuggestionViewHolder.kt | 1 - .../adapter/suggestions/SuggestionsAdapter.kt | 9 +++-- .../dante/ui/fragment/SuggestionsFragment.kt | 7 +++- .../dante/ui/viewmodel/BookListViewModel.kt | 2 -- .../ui/viewmodel/SuggestionsViewModel.kt | 15 ++++++++ .../dante/util/explanations/Explanations.kt | 2 ++ .../explanations/SharedPrefsExplanations.kt | 21 ++++++++++-- core/src/main/res/values-de/strings.xml | 3 ++ core/src/main/res/values/strings.xml | 3 ++ .../tracking/event/DanteTrackingEvent.kt | 2 ++ 13 files changed, 91 insertions(+), 18 deletions(-) rename app/src/main/java/at/shockbytes/dante/ui/adapter/{ => suggestions}/OnSuggestionActionClickedListener.kt (80%) create mode 100644 app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionExplanationClickedListener.kt diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt similarity index 80% rename from app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt rename to app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt index d0b3b998..bed0b5b9 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/OnSuggestionActionClickedListener.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt @@ -1,4 +1,4 @@ -package at.shockbytes.dante.ui.adapter +package at.shockbytes.dante.ui.adapter.suggestions import at.shockbytes.dante.suggestions.Suggestion diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionExplanationClickedListener.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionExplanationClickedListener.kt new file mode 100644 index 00000000..ebaedc09 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionExplanationClickedListener.kt @@ -0,0 +1,8 @@ +package at.shockbytes.dante.ui.adapter.suggestions + +interface OnSuggestionExplanationClickedListener { + + fun onDismissClicked() + + fun onWantToSuggestClicked() +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt index 5029d364..bcb4ad59 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionExplanationViewHolder.kt @@ -10,19 +10,33 @@ import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.item_generic_explanation.* class SuggestionExplanationViewHolder( - override val containerView: View + override val containerView: View, + private val onSuggestionExplanationClickedListener: OnSuggestionExplanationClickedListener ) : BaseAdapter.ViewHolder(containerView), LayoutContainer { override fun bindToView(content: SuggestionsAdapterItem, position: Int) { - // TODO + + val userWantsToSuggest = (content as SuggestionsAdapterItem.Explanation).wantsToSuggest + val btnTextRes = if (userWantsToSuggest) { + R.string.suggestions_explanation_want_to_suggest_clicked + } else R.string.suggestions_explanation_want_to_suggest iv_item_generic_explanation_dismiss.setOnClickListener { - // TODO Callback + onSuggestionExplanationClickedListener.onDismissClicked() } - tv_item_generic_explanation.text = "Describe" - - btn_item_generic_explanation.setVisible(false, invisibilityState = View.INVISIBLE) + tv_item_generic_explanation.setText(R.string.suggestions_explanation) + + btn_item_generic_explanation.apply { + setText(btnTextRes) + setVisible(true) + isEnabled = !userWantsToSuggest + setOnClickListener { + if (!userWantsToSuggest) { + onSuggestionExplanationClickedListener.onWantToSuggestClicked() + } + } + } iv_item_generic_explanation_decoration_start.setImageResource(R.drawable.ic_suggestions) iv_item_generic_explanation_decoration_end.setImageResource(R.drawable.ic_suggestions) @@ -30,9 +44,13 @@ class SuggestionExplanationViewHolder( companion object { - fun forParent(parent: ViewGroup): SuggestionExplanationViewHolder { + fun forParent( + parent: ViewGroup, + onSuggestionExplanationClickedListener: OnSuggestionExplanationClickedListener + ): SuggestionExplanationViewHolder { return SuggestionExplanationViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.item_generic_explanation, parent, false) + LayoutInflater.from(parent.context).inflate(R.layout.item_generic_explanation, parent, false), + onSuggestionExplanationClickedListener ) } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt index e4484773..b215e185 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt @@ -10,7 +10,6 @@ import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.suggestions.BookSuggestionEntity import at.shockbytes.dante.suggestions.Suggester import at.shockbytes.dante.suggestions.Suggestion -import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener import at.shockbytes.util.AppUtils import at.shockbytes.util.adapter.BaseAdapter import kotlinx.android.extensions.LayoutContainer diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt index 1b7a5cc7..7113e80e 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionsAdapter.kt @@ -5,13 +5,13 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import at.shockbytes.dante.R import at.shockbytes.dante.core.image.ImageLoader -import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener import at.shockbytes.util.adapter.BaseAdapter class SuggestionsAdapter( ctx: Context, private val imageLoader: ImageLoader, - private val onSuggestionActionClickedListener: OnSuggestionActionClickedListener + private val onSuggestionActionClickedListener: OnSuggestionActionClickedListener, + private val onSuggestionExplanationClickedListener: OnSuggestionExplanationClickedListener ) : BaseAdapter(ctx) { fun updateData(suggestions: List) { @@ -35,7 +35,10 @@ class SuggestionsAdapter( onSuggestionActionClickedListener ) - R.layout.item_generic_explanation -> SuggestionExplanationViewHolder.forParent(parent) + R.layout.item_generic_explanation -> SuggestionExplanationViewHolder.forParent( + parent, + onSuggestionExplanationClickedListener + ) else -> throw IllegalStateException("Unknown ViewType $viewType in ${this.javaClass.simpleName}") } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index dc7c9863..927d5a5c 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -6,7 +6,8 @@ import at.shockbytes.dante.R import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.injection.AppComponent import at.shockbytes.dante.suggestions.Suggestion -import at.shockbytes.dante.ui.adapter.OnSuggestionActionClickedListener +import at.shockbytes.dante.ui.adapter.suggestions.OnSuggestionActionClickedListener +import at.shockbytes.dante.ui.adapter.suggestions.OnSuggestionExplanationClickedListener import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapter import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapterItem import at.shockbytes.dante.ui.viewmodel.SuggestionsViewModel @@ -49,6 +50,10 @@ class SuggestionsFragment : BaseFragment() { override fun onReportBookSuggestion(suggestionId: String) { showToast("Report suggestion!") } + }, + onSuggestionExplanationClickedListener = object : OnSuggestionExplanationClickedListener { + override fun onDismissClicked() = viewModel.dismissExplanation() + override fun onWantToSuggestClicked() = viewModel.wantToSuggestBooks() } ) rv_suggestions.apply { diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt index 0676fae2..85225325 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/BookListViewModel.kt @@ -9,7 +9,6 @@ import at.shockbytes.dante.ui.adapter.main.BookAdapterItem import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.settings.DanteSettings import at.shockbytes.dante.util.addTo -import at.shockbytes.dante.util.explanations.Explanation import at.shockbytes.dante.util.explanations.Explanations import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.dante.util.sort.SortComparators @@ -22,7 +21,6 @@ import io.reactivex.subjects.PublishSubject import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject -import kotlin.reflect.KClass /** * Author: Martin Macheiner diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index 0b4f5028..86b06e92 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -108,4 +108,19 @@ class SuggestionsViewModel @Inject constructor( ) { tracker.track(DanteTrackingEvent.AddSuggestionToWishlist(suggestionId, bookTitle, suggester)) } + + fun dismissExplanation() { + explanations.markSeen(explanations.suggestion()) + // Reload after mark explanation as seen + requestSuggestions() + } + + fun wantToSuggestBooks() { + + tracker.track(DanteTrackingEvent.InterestedInSuggestingBooks) + + explanations.update(explanations.suggestion().copy(userWantsToSuggest = true)) + // Reload after changing the explanation state + requestSuggestions() + } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt index e647006f..0e5ef876 100644 --- a/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/Explanations.kt @@ -7,4 +7,6 @@ interface Explanations { fun wishlist(): Explanation.Wishlist fun markSeen(explanation: Explanation) + + fun update(explanation: Explanation) } diff --git a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt index dd64567e..d16fe5d4 100644 --- a/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt +++ b/app/src/main/java/at/shockbytes/dante/util/explanations/SharedPrefsExplanations.kt @@ -9,7 +9,7 @@ class SharedPrefsExplanations( override fun suggestion(): Explanation.Suggestion { return Explanation.Suggestion( show = getShowFor(), - userWantsToSuggest = getBooleanForKey("user_wants_to_suggest", false) + userWantsToSuggest = getBooleanForKey(SUGGESTION_USER_WANTS_TO_SUGGEST, false) ) } @@ -26,6 +26,23 @@ class SharedPrefsExplanations( } override fun markSeen(explanation: Explanation) { - sharedPreferences.edit().putBoolean(explanation::class.java.simpleName, false).apply() + putBoolean(explanation::class.java.simpleName, false) + } + + override fun update(explanation: Explanation) { + when (explanation) { + is Explanation.Suggestion -> { + putBoolean(SUGGESTION_USER_WANTS_TO_SUGGEST, explanation.userWantsToSuggest) + } + else -> Unit + } + } + + private fun putBoolean(key: String, value: Boolean) { + sharedPreferences.edit().putBoolean(key, value).apply() + } + + companion object { + private const val SUGGESTION_USER_WANTS_TO_SUGGEST = "user_wants_to_suggest" } } \ No newline at end of file diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 6281497c..2d286e97 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -325,6 +325,9 @@ Öffnen Screen öffnen Vorschläge + Ich will Bücher vorschlagen + Vorschläge sind bald verfügbar… + Erhalte einige Vorschläge von Buchliebhabern von der ganzen Welt. Meine Wunschliste Lege alle Bücher hier ab, die du noch nicht hast, aber in Zukunft lesen willst.\n\nKleiner Tipp: Teile diese Liste gelegentlich mit deinem Partner. Zur Wunschliste hinzufügen diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 4c1bbbe3..26437ac7 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -358,6 +358,9 @@ Add to wishlist - %s Suggestions + I\'d like to suggest books + Suggestions are available soon + Receive some suggestions from book lovers all over the world. We have to apologize. We were unable to load you some suggestions. One of those books? diff --git a/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt b/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt index 759c326d..2a12ccf7 100644 --- a/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt +++ b/tracking/src/main/java/at/shockbytes/tracking/event/DanteTrackingEvent.kt @@ -12,6 +12,8 @@ sealed class DanteTrackingEvent( object InterestedInOnlineStorageEvent : DanteTrackingEvent("interested_in_online_storage") + object InterestedInSuggestingBooks : DanteTrackingEvent("suggestions_interested") + data class StartImport(val importer: String) : DanteTrackingEvent( "start_import", listOf(TrackingProperty("importer_name", importer)) From 40ea2f36566986c59e782885835c2d83e7941ce7 Mon Sep 17 00:00:00 2001 From: shockbytes Date: Thu, 3 Dec 2020 22:06:09 +0100 Subject: [PATCH 30/30] Fix typo in suggestion explanation --- core/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 2d286e97..8fd6b1d1 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -326,7 +326,7 @@ Screen öffnen Vorschläge Ich will Bücher vorschlagen - Vorschläge sind bald verfügbar… + Vorschläge sind bald verfügbar Erhalte einige Vorschläge von Buchliebhabern von der ganzen Welt. Meine Wunschliste Lege alle Bücher hier ab, die du noch nicht hast, aber in Zukunft lesen willst.\n\nKleiner Tipp: Teile diese Liste gelegentlich mit deinem Partner.