From 1734e888d6d84ec44c44d5ca3668c62295cc624c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 26 Dec 2023 20:24:14 +0200 Subject: [PATCH] Move new sources tip to catalog --- app/build.gradle | 2 +- .../kotatsu/explore/ui/ExploreFragment.kt | 2 +- .../kotatsu/explore/ui/ExploreViewModel.kt | 19 ++++------ .../kotatsu/list/ui/adapter/BadgeADUtil.kt | 35 +++++++++++++------ .../kotatsu/list/ui/adapter/ListHeaderAD.kt | 5 +++ .../kotatsu/list/ui/model/ListHeader.kt | 25 +++++++------ .../koitharu/kotatsu/main/ui/MainActivity.kt | 8 ++--- .../koitharu/kotatsu/main/ui/MainViewModel.kt | 24 ++----------- .../sources/catalog/SourcesCatalogActivity.kt | 33 +++++++++++++++++ .../catalog/SourcesCatalogViewModel.kt | 10 ++++++ 10 files changed, 100 insertions(+), 63 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9bc118911..b9a115076 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:904e0719eb') { + implementation('com.github.kotatsuapp:kotatsu-parsers:904e0719eb') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index b90f7f5e2..562cddba5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -111,7 +111,7 @@ class ExploreFragment : } override fun onListHeaderClick(item: ListHeader, view: View) { - startActivity(SettingsActivity.newManageSourcesIntent(view.context)) + startActivity(Intent(view.context, SourcesCatalogActivity::class.java)) } override fun onPrimaryButtonClick(tipView: TipView) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt index 00866ccc6..98973d443 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt @@ -29,7 +29,6 @@ import org.koitharu.kotatsu.list.ui.model.EmptyHint import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.list.ui.model.TipModel import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -126,24 +125,18 @@ class ExploreViewModel @Inject constructor( randomLoading: Boolean, newSources: Set, ): List { - val result = ArrayList(sources.size + 4) + val result = ArrayList(sources.size + 3) result += ExploreButtons(randomLoading) if (recommendation != null) { result += ListHeader(R.string.suggestions) result += RecommendationsItem(recommendation) } if (sources.isNotEmpty()) { - result += ListHeader(R.string.remote_sources, R.string.manage) - if (newSources.isNotEmpty()) { - result += TipModel( - key = TIP_NEW_SOURCES, - title = R.string.new_sources_text, - text = R.string.new_sources_text, - icon = R.drawable.ic_explore_normal, - primaryButtonText = R.string.manage, - secondaryButtonText = R.string.discard, - ) - } + result += ListHeader( + textRes = R.string.remote_sources, + buttonTextRes = R.string.catalog, + badge = if (newSources.isNotEmpty()) "" else null, + ) sources.mapTo(result) { MangaSourceItem(it, isGrid) } } else { result += EmptyHint( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt index 2cb70f71a..ab67bf9e5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt @@ -14,22 +14,37 @@ import com.google.android.material.R as materialR @CheckResult fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? { - return if (counter > 0) { - val badgeDrawable = badge ?: initBadge(this) - badgeDrawable.number = counter - badgeDrawable.isVisible = true - badgeDrawable.align(this) - badgeDrawable - } else { - badge?.isVisible = false - badge - } + return bindBadgeImpl(badge, null, counter) +} + +@CheckResult +fun View.bindBadge(badge: BadgeDrawable?, text: String?): BadgeDrawable? { + return bindBadgeImpl(badge, text, 0) } fun View.clearBadge(badge: BadgeDrawable?) { BadgeUtils.detachBadgeDrawable(badge, this) } +private fun View.bindBadgeImpl( + badge: BadgeDrawable?, + text: String?, + counter: Int, +): BadgeDrawable? = if (text != null || counter > 0) { + val badgeDrawable = badge ?: initBadge(this) + if (counter > 0) { + badgeDrawable.number = counter + } else { + badgeDrawable.text = text?.takeUnless { it.isEmpty() } + } + badgeDrawable.isVisible = true + badgeDrawable.align(this) + badgeDrawable +} else { + badge?.isVisible = false + badge +} + private fun initBadge(anchor: View): BadgeDrawable { val badge = BadgeDrawable.create(anchor.context) val resources = anchor.resources diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt index dc3920031..e29e1e49a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.core.view.isInvisible import androidx.core.view.isVisible +import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding import org.koitharu.kotatsu.list.ui.model.ListHeader @@ -12,6 +13,8 @@ fun listHeaderAD( ) = adapterDelegateViewBinding( { inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) }, ) { + var badge: BadgeDrawable? = null + if (listener != null) { binding.buttonMore.setOnClickListener { listener.onListHeaderClick(item, it) @@ -23,9 +26,11 @@ fun listHeaderAD( if (item.buttonTextRes == 0) { binding.buttonMore.isInvisible = true binding.buttonMore.text = null + binding.buttonMore.clearBadge(badge) } else { binding.buttonMore.setText(item.buttonTextRes) binding.buttonMore.isVisible = true + badge = itemView.bindBadge(badge, item.badge) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt index e225c1f28..3608e550d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -6,38 +6,41 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo @Suppress("DataClassPrivateConstructor") data class ListHeader private constructor( - private val text: CharSequence?, - @StringRes private val textRes: Int, - private val dateTimeAgo: DateTimeAgo?, + private val textRaw: Any, @StringRes val buttonTextRes: Int, val payload: Any?, + val badge: String?, ) : ListModel { constructor( text: CharSequence, @StringRes buttonTextRes: Int = 0, payload: Any? = null, - ) : this(text, 0, null, buttonTextRes, payload) + badge: String? = null, + ) : this(textRaw = text, buttonTextRes, payload, badge) constructor( @StringRes textRes: Int, @StringRes buttonTextRes: Int = 0, payload: Any? = null, - ) : this(null, textRes, null, buttonTextRes, payload) + badge: String? = null, + ) : this(textRaw = textRes, buttonTextRes, payload, badge) constructor( dateTimeAgo: DateTimeAgo, @StringRes buttonTextRes: Int = 0, payload: Any? = null, - ) : this(null, 0, dateTimeAgo, buttonTextRes, payload) + badge: String? = null, + ) : this(textRaw = dateTimeAgo, buttonTextRes, payload, badge) - fun getText(context: Context): CharSequence? = when { - text != null -> text - textRes != 0 -> context.getString(textRes) - else -> dateTimeAgo?.format(context.resources) + fun getText(context: Context): CharSequence? = when (textRaw) { + is CharSequence -> textRaw + is Int -> if (textRaw != 0) context.getString(textRaw) else null + is DateTimeAgo -> textRaw.format(context.resources) + else -> null } override fun areItemsTheSame(other: ListModel): Boolean { - return other is ListHeader && text == other.text && dateTimeAgo == other.dateTimeAgo && textRes == other.textRes + return other is ListHeader && textRaw == other.textRaw } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index b140fe2bd..e213ded93 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -125,7 +125,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null)) viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged) - viewModel.counters.observe(this, ::onCountersChanged) + viewModel.feedCounter.observe(this, ::onFeedCounterChanged) viewModel.appUpdate.observe(this, MenuInvalidator(this)) viewModel.onFirstStart.observeEvent(this) { WelcomeSheet.show(supportFragmentManager) @@ -278,10 +278,8 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav startActivity(IntentBuilder(this).manga(manga).build(), options) } - private fun onCountersChanged(counters: Map) { - counters.forEach { (navItem, counter) -> - navigationDelegate.setCounter(navItem, counter) - } + private fun onFeedCounterChanged(counter: Int) { + navigationDelegate.setCounter(NavItem.FEED, counter) } private fun onIncognitoModeChanged(isIncognito: Boolean) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt index 985992c44..c62274e39 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -4,15 +4,11 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.NavItem import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow @@ -22,7 +18,6 @@ import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.main.domain.ReadingResumeEnabledUseCase import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import java.util.EnumMap import javax.inject.Inject @HiltViewModel @@ -52,19 +47,8 @@ class MainViewModel @Inject constructor( val appUpdate = appUpdateRepository.observeAvailableUpdate() - val counters = combine( - trackingRepository.observeUpdatedMangaCount(), - observeNewSourcesCount(), - ) { tracks, newSources -> - val em = EnumMap(NavItem::class.java) - em[NavItem.EXPLORE] = newSources - em[NavItem.FEED] = tracks - em - }.stateIn( - scope = viewModelScope + Dispatchers.Default, - started = SharingStarted.WhileSubscribed(5000), - initialValue = emptyMap(), - ) + val feedCounter = trackingRepository.observeUpdatedMangaCount() + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, 0) init { launchJob { @@ -87,8 +71,4 @@ class MainViewModel @Inject constructor( fun setIncognitoMode(isEnabled: Boolean) { settings.isIncognitoModeEnabled = isEnabled } - - private fun observeNewSourcesCount() = sourcesRepository.observeNewSources() - .map { if (sourcesRepository.isSetupRequired()) 0 else it.size } - .distinctUntilChanged() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt index af70d59ae..a8b30a06a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogActivity.kt @@ -10,8 +10,10 @@ import androidx.core.view.isVisible import androidx.core.view.updatePadding import coil.ImageLoader import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver @@ -21,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.toLocale import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner +import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import javax.inject.Inject @AndroidEntryPoint @@ -31,6 +34,8 @@ class SourcesCatalogActivity : BaseActivity(), @Inject lateinit var coil: ImageLoader + private var newSourcesSnackbar: Snackbar? = null + override val appBar: AppBarLayout get() = viewBinding.appbar @@ -45,6 +50,7 @@ class SourcesCatalogActivity : BaseActivity(), val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter) tabMediator.attach() viewModel.content.observe(this, pagerAdapter) + viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged) viewModel.onActionDone.observeEvent( this, ReversibleActionObserver(viewBinding.pager), @@ -80,4 +86,31 @@ class SourcesCatalogActivity : BaseActivity(), viewModel.performSearch(null) return true } + + private fun onHasNewSourcesChanged(hasNewSources: Boolean) { + if (hasNewSources) { + if (newSourcesSnackbar?.isShownOrQueued == true) { + return + } + val snackbar = Snackbar.make(viewBinding.pager, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE) + snackbar.setAction(R.string.explore) { + NewSourcesDialogFragment.show(supportFragmentManager) + } + snackbar.addCallback( + object : Snackbar.Callback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (event == DISMISS_EVENT_SWIPE) { + viewModel.skipNewSources() + } + } + }, + ) + snackbar.show() + newSourcesSnackbar = snackbar + } else { + newSourcesSnackbar?.dismiss() + newSourcesSnackbar = null + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt index 135c8be4e..144f5e45d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogViewModel.kt @@ -41,6 +41,10 @@ class SourcesCatalogViewModel @Inject constructor( val locales = repository.allMangaSources.mapToSet { it.locale } val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales }) + val hasNewSources = repository.observeNewSources() + .map { it.isNotEmpty() } + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) + private val listProducers = locale.map { lc -> createListProducers(lc) }.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value)) @@ -71,6 +75,12 @@ class SourcesCatalogViewModel @Inject constructor( } } + fun skipNewSources() { + launchJob { + repository.assimilateNewSources() + } + } + @MainThread private fun createListProducers(lc: String?): Map { val types = EnumSet.allOf(ContentType::class.java)