Move new sources tip to catalog

pull/608/head
Koitharu 2 years ago
parent 9108646cea
commit 1734e888d6
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -82,7 +82,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:904e0719eb') { implementation('com.github.kotatsuapp:kotatsu-parsers:904e0719eb') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

@ -111,7 +111,7 @@ class ExploreFragment :
} }
override fun onListHeaderClick(item: ListHeader, view: View) { override fun onListHeaderClick(item: ListHeader, view: View) {
startActivity(SettingsActivity.newManageSourcesIntent(view.context)) startActivity(Intent(view.context, SourcesCatalogActivity::class.java))
} }
override fun onPrimaryButtonClick(tipView: TipView) { override fun onPrimaryButtonClick(tipView: TipView) {

@ -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.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@ -126,24 +125,18 @@ class ExploreViewModel @Inject constructor(
randomLoading: Boolean, randomLoading: Boolean,
newSources: Set<MangaSource>, newSources: Set<MangaSource>,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 4) val result = ArrayList<ListModel>(sources.size + 3)
result += ExploreButtons(randomLoading) result += ExploreButtons(randomLoading)
if (recommendation != null) { if (recommendation != null) {
result += ListHeader(R.string.suggestions) result += ListHeader(R.string.suggestions)
result += RecommendationsItem(recommendation) result += RecommendationsItem(recommendation)
} }
if (sources.isNotEmpty()) { if (sources.isNotEmpty()) {
result += ListHeader(R.string.remote_sources, R.string.manage) result += ListHeader(
if (newSources.isNotEmpty()) { textRes = R.string.remote_sources,
result += TipModel( buttonTextRes = R.string.catalog,
key = TIP_NEW_SOURCES, badge = if (newSources.isNotEmpty()) "" else null,
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,
)
}
sources.mapTo(result) { MangaSourceItem(it, isGrid) } sources.mapTo(result) { MangaSourceItem(it, isGrid) }
} else { } else {
result += EmptyHint( result += EmptyHint(

@ -14,22 +14,37 @@ import com.google.android.material.R as materialR
@CheckResult @CheckResult
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? { fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
return if (counter > 0) { return bindBadgeImpl(badge, null, counter)
val badgeDrawable = badge ?: initBadge(this) }
badgeDrawable.number = counter
badgeDrawable.isVisible = true @CheckResult
badgeDrawable.align(this) fun View.bindBadge(badge: BadgeDrawable?, text: String?): BadgeDrawable? {
badgeDrawable return bindBadgeImpl(badge, text, 0)
} else {
badge?.isVisible = false
badge
}
} }
fun View.clearBadge(badge: BadgeDrawable?) { fun View.clearBadge(badge: BadgeDrawable?) {
BadgeUtils.detachBadgeDrawable(badge, this) 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 { private fun initBadge(anchor: View): BadgeDrawable {
val badge = BadgeDrawable.create(anchor.context) val badge = BadgeDrawable.create(anchor.context)
val resources = anchor.resources val resources = anchor.resources

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
@ -12,6 +13,8 @@ fun listHeaderAD(
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>( ) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
) { ) {
var badge: BadgeDrawable? = null
if (listener != null) { if (listener != null) {
binding.buttonMore.setOnClickListener { binding.buttonMore.setOnClickListener {
listener.onListHeaderClick(item, it) listener.onListHeaderClick(item, it)
@ -23,9 +26,11 @@ fun listHeaderAD(
if (item.buttonTextRes == 0) { if (item.buttonTextRes == 0) {
binding.buttonMore.isInvisible = true binding.buttonMore.isInvisible = true
binding.buttonMore.text = null binding.buttonMore.text = null
binding.buttonMore.clearBadge(badge)
} else { } else {
binding.buttonMore.setText(item.buttonTextRes) binding.buttonMore.setText(item.buttonTextRes)
binding.buttonMore.isVisible = true binding.buttonMore.isVisible = true
badge = itemView.bindBadge(badge, item.badge)
} }
} }
} }

@ -6,38 +6,41 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
@Suppress("DataClassPrivateConstructor") @Suppress("DataClassPrivateConstructor")
data class ListHeader private constructor( data class ListHeader private constructor(
private val text: CharSequence?, private val textRaw: Any,
@StringRes private val textRes: Int,
private val dateTimeAgo: DateTimeAgo?,
@StringRes val buttonTextRes: Int, @StringRes val buttonTextRes: Int,
val payload: Any?, val payload: Any?,
val badge: String?,
) : ListModel { ) : ListModel {
constructor( constructor(
text: CharSequence, text: CharSequence,
@StringRes buttonTextRes: Int = 0, @StringRes buttonTextRes: Int = 0,
payload: Any? = null, payload: Any? = null,
) : this(text, 0, null, buttonTextRes, payload) badge: String? = null,
) : this(textRaw = text, buttonTextRes, payload, badge)
constructor( constructor(
@StringRes textRes: Int, @StringRes textRes: Int,
@StringRes buttonTextRes: Int = 0, @StringRes buttonTextRes: Int = 0,
payload: Any? = null, payload: Any? = null,
) : this(null, textRes, null, buttonTextRes, payload) badge: String? = null,
) : this(textRaw = textRes, buttonTextRes, payload, badge)
constructor( constructor(
dateTimeAgo: DateTimeAgo, dateTimeAgo: DateTimeAgo,
@StringRes buttonTextRes: Int = 0, @StringRes buttonTextRes: Int = 0,
payload: Any? = null, payload: Any? = null,
) : this(null, 0, dateTimeAgo, buttonTextRes, payload) badge: String? = null,
) : this(textRaw = dateTimeAgo, buttonTextRes, payload, badge)
fun getText(context: Context): CharSequence? = when { fun getText(context: Context): CharSequence? = when (textRaw) {
text != null -> text is CharSequence -> textRaw
textRes != 0 -> context.getString(textRes) is Int -> if (textRaw != 0) context.getString(textRaw) else null
else -> dateTimeAgo?.format(context.resources) is DateTimeAgo -> textRaw.format(context.resources)
else -> null
} }
override fun areItemsTheSame(other: ListModel): Boolean { 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
} }
} }

@ -125,7 +125,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null)) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null))
viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged) viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged) viewModel.feedCounter.observe(this, ::onFeedCounterChanged)
viewModel.appUpdate.observe(this, MenuInvalidator(this)) viewModel.appUpdate.observe(this, MenuInvalidator(this))
viewModel.onFirstStart.observeEvent(this) { viewModel.onFirstStart.observeEvent(this) {
WelcomeSheet.show(supportFragmentManager) WelcomeSheet.show(supportFragmentManager)
@ -278,10 +278,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
startActivity(IntentBuilder(this).manga(manga).build(), options) startActivity(IntentBuilder(this).manga(manga).build(), options)
} }
private fun onCountersChanged(counters: Map<NavItem, Int>) { private fun onFeedCounterChanged(counter: Int) {
counters.forEach { (navItem, counter) -> navigationDelegate.setCounter(NavItem.FEED, counter)
navigationDelegate.setCounter(navItem, counter)
}
} }
private fun onIncognitoModeChanged(isIncognito: Boolean) { private fun onIncognitoModeChanged(isIncognito: Boolean) {

@ -4,15 +4,11 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted 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.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings 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.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow 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.main.domain.ReadingResumeEnabledUseCase
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import java.util.EnumMap
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -52,19 +47,8 @@ class MainViewModel @Inject constructor(
val appUpdate = appUpdateRepository.observeAvailableUpdate() val appUpdate = appUpdateRepository.observeAvailableUpdate()
val counters = combine( val feedCounter = trackingRepository.observeUpdatedMangaCount()
trackingRepository.observeUpdatedMangaCount(), .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, 0)
observeNewSourcesCount(),
) { tracks, newSources ->
val em = EnumMap<NavItem, Int>(NavItem::class.java)
em[NavItem.EXPLORE] = newSources
em[NavItem.FEED] = tracks
em
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyMap<NavItem, Int>(),
)
init { init {
launchJob { launchJob {
@ -87,8 +71,4 @@ class MainViewModel @Inject constructor(
fun setIncognitoMode(isEnabled: Boolean) { fun setIncognitoMode(isEnabled: Boolean) {
settings.isIncognitoModeEnabled = isEnabled settings.isIncognitoModeEnabled = isEnabled
} }
private fun observeNewSourcesCount() = sourcesRepository.observeNewSources()
.map { if (sourcesRepository.isSetupRequired()) 0 else it.size }
.distinctUntilChanged()
} }

@ -10,8 +10,10 @@ import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver 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.core.util.ext.toLocale
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -31,6 +34,8 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
private var newSourcesSnackbar: Snackbar? = null
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = viewBinding.appbar get() = viewBinding.appbar
@ -45,6 +50,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter) val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter)
tabMediator.attach() tabMediator.attach()
viewModel.content.observe(this, pagerAdapter) viewModel.content.observe(this, pagerAdapter)
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged)
viewModel.onActionDone.observeEvent( viewModel.onActionDone.observeEvent(
this, this,
ReversibleActionObserver(viewBinding.pager), ReversibleActionObserver(viewBinding.pager),
@ -80,4 +86,31 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
viewModel.performSearch(null) viewModel.performSearch(null)
return true 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
}
}
} }

@ -41,6 +41,10 @@ class SourcesCatalogViewModel @Inject constructor(
val locales = repository.allMangaSources.mapToSet { it.locale } val locales = repository.allMangaSources.mapToSet { it.locale }
val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales }) 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 -> private val listProducers = locale.map { lc ->
createListProducers(lc) createListProducers(lc)
}.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value)) }.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value))
@ -71,6 +75,12 @@ class SourcesCatalogViewModel @Inject constructor(
} }
} }
fun skipNewSources() {
launchJob {
repository.assimilateNewSources()
}
}
@MainThread @MainThread
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> { private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> {
val types = EnumSet.allOf(ContentType::class.java) val types = EnumSet.allOf(ContentType::class.java)

Loading…
Cancel
Save