From 18169c2355ab252a9dfe35952078265ccbb286b9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 1 Jun 2024 17:13:27 +0300 Subject: [PATCH] Update sources catalog and repository --- app/build.gradle | 6 +- .../kotatsu/core/db/dao/MangaSourcesDao.kt | 3 + .../kotatsu/core/prefs/AppSettings.kt | 7 +- .../kotatsu/core/ui/widgets/ChipsView.kt | 25 +++- .../kotatsu/details/ui/DetailsActivity.kt | 3 - .../explore/data/MangaSourcesRepository.kt | 125 +++++++++++------- .../kotatsu/explore/ui/ExploreFragment.kt | 19 +-- .../kotatsu/explore/ui/ExploreViewModel.kt | 13 +- .../explore/ui/adapter/ExploreAdapter.kt | 4 - .../kotatsu/filter/ui/FilterCoordinator.kt | 8 +- .../kotatsu/filter/ui/FilterHeaderFragment.kt | 3 - .../filter/ui/sheet/FilterSheetFragment.kt | 18 --- .../list/ui/model/ListModelConversionExt.kt | 3 - .../list/ui/preview/PreviewViewModel.kt | 3 - .../kotatsu/main/ui/welcome/WelcomeSheet.kt | 4 - .../main/ui/welcome/WelcomeViewModel.kt | 2 +- .../suggestion/SearchSuggestionViewModel.kt | 4 - .../newsources/NewSourcesDialogFragment.kt | 76 ----------- .../newsources/NewSourcesViewModel.kt | 52 -------- .../newsources/SourcesSelectAdapter.kt | 19 --- .../sources/catalog/SourceCatalogItem.kt | 1 - .../sources/catalog/SourceCatalogItemAD.kt | 10 +- .../sources/catalog/SourcesCatalogActivity.kt | 90 ++++++++----- .../sources/catalog/SourcesCatalogFilter.kt | 2 +- .../catalog/SourcesCatalogListProducer.kt | 106 --------------- .../catalog/SourcesCatalogMenuProvider.kt | 34 +---- .../catalog/SourcesCatalogViewModel.kt | 61 +++------ .../main/res/layout/item_source_catalog.xml | 9 +- app/src/main/res/menu/opt_sources_catalog.xml | 6 - app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/pref_sources.xml | 6 - 31 files changed, 213 insertions(+), 511 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt diff --git a/app/build.gradle b/app/build.gradle index 6b55b57cc..6e36dd8df 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 34 - versionCode = 645 - versionName = '7.1.2' + versionCode = 646 + versionName = '7.1.3' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:26be293f24') { + implementation('com.github.KotatsuApp:kotatsu-parsers:77a733a062') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt index 5e0255111..de66fd655 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt @@ -24,6 +24,9 @@ abstract class MangaSourcesDao { @Query("SELECT source FROM sources WHERE enabled = 1") abstract suspend fun findAllEnabledNames(): List + @Query("SELECT * FROM sources WHERE added_in >= :version") + abstract suspend fun findAllFromVersion(version: Int): List + @Query("SELECT * FROM sources ORDER BY sort_key") abstract fun observeAll(): Flow> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index ed9129ada..2fa4c6890 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -290,8 +290,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getBoolean(KEY_SOURCES_GRID, true) set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) } - val isNewSourcesTipEnabled: Boolean - get() = prefs.getBoolean(KEY_SOURCES_NEW, true) + var sourcesVersion: Int + get() = prefs.getInt(KEY_SOURCES_VERSION, 0) + set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) } val isPagesNumbersEnabled: Boolean get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false) @@ -653,7 +654,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_APP_LOCALE = "app_locale" const val KEY_LOGGING_ENABLED = "logging" const val KEY_SOURCES_GRID = "sources_grid" - const val KEY_SOURCES_NEW = "sources_new" const val KEY_UPDATES_UNSTABLE = "updates_unstable" const val KEY_TIPS_CLOSED = "tips_closed" const val KEY_SSL_BYPASS = "ssl_bypass" @@ -689,6 +689,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_STATS_ENABLED = "stats_on" const val KEY_FEED_HEADER = "feed_header" const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types" + const val KEY_SOURCES_VERSION = "sources_version" // keys for non-persistent preferences const val KEY_APP_VERSION = "app_version" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt index 812e56285..0a3ad0e13 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt @@ -12,6 +12,8 @@ import com.google.android.material.chip.ChipGroup import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.castOrNull +import com.google.android.material.R as materialR + class ChipsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -48,7 +50,7 @@ class ChipsView @JvmOverloads constructor( if (isInEditMode) { setChips( List(5) { - ChipModel(0, "Chip $it", 0, isCheckable = false, isChecked = false) + ChipModel(title = "Chip $it") }, ) } @@ -99,6 +101,15 @@ class ChipsView @JvmOverloads constructor( chip.isChipIconVisible = true } chip.isChecked = model.isChecked + chip.isCheckedIconVisible = chip.isCheckable && model.icon == 0 + chip.isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) { + chip.setCloseIconResource( + if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close, + ) + true + } else { + false + } chip.tag = model.data } @@ -106,12 +117,11 @@ class ChipsView @JvmOverloads constructor( val chip = Chip(context) val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle) chip.setChipDrawable(drawable) - chip.isCheckedIconVisible = true chip.isChipIconVisible = false - chip.isCloseIconVisible = onChipCloseClickListener != null chip.setOnCloseIconClickListener(chipOnCloseListener) chip.setEnsureMinTouchTargetSize(false) chip.setOnClickListener(chipOnClickListener) + chip.isElegantTextHeight = false addView(chip) return chip } @@ -127,11 +137,12 @@ class ChipsView @JvmOverloads constructor( } data class ChipModel( - @ColorRes val tint: Int, val title: CharSequence, - @DrawableRes val icon: Int, - val isCheckable: Boolean, - val isChecked: Boolean, + @DrawableRes val icon: Int = 0, + val isCheckable: Boolean = false, + @ColorRes val tint: Int = 0, + val isChecked: Boolean = false, + val isDropdown: Boolean = false, val data: Any? = null, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 777c4ae4b..cad4bccc4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -612,10 +612,7 @@ class DetailsActivity : ChipsView.ChipModel( title = tag.title, tint = tagHighlighter.getTagTint(tag), - icon = 0, data = tag, - isCheckable = false, - isChecked = false, ) }, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index 8936dd8a0..17b0462f2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -6,21 +6,22 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity -import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet import java.util.Collections import java.util.EnumSet +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @Reusable @@ -29,6 +30,7 @@ class MangaSourcesRepository @Inject constructor( private val settings: AppSettings, ) { + private val isNewSourcesAssimilated = AtomicBoolean(false) private val dao: MangaSourcesDao get() = db.getSourcesDao() @@ -43,25 +45,58 @@ class MangaSourcesRepository @Inject constructor( get() = Collections.unmodifiableSet(remoteSources) suspend fun getEnabledSources(): List { + assimilateNewSources() val order = settings.sourcesSortOrder return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order) } suspend fun getDisabledSources(): Set { + assimilateNewSources() val result = EnumSet.copyOf(remoteSources) val enabled = dao.findAllEnabledNames() for (name in enabled) { - val source = MangaSource(name) + val source = name.toMangaSourceOrNull() ?: continue result.remove(source) } - if (settings.isNsfwContentDisabled) { - result.removeAll { it.isNsfw() } - } return result } + suspend fun getAvailableSources( + isDisabledOnly: Boolean, + isNewOnly: Boolean, + excludeBroken: Boolean, + types: Set, + query: String?, + sortOrder: SourcesSortOrder?, + ): List { + assimilateNewSources() + val entities = dao.findAll().toMutableList() + if (isDisabledOnly) { + entities.removeAll { it.isEnabled } + } + if (isNewOnly) { + entities.retainAll { it.addedIn == BuildConfig.VERSION_CODE } + } + val sources = entities.toSources( + skipNsfwSources = settings.isNsfwContentDisabled, + sortOrder = sortOrder, + ) + if (excludeBroken) { + sources.removeAll { it.isBroken } + } + if (types.isNotEmpty()) { + sources.retainAll { it.contentType in types } + } + if (!query.isNullOrEmpty()) { + sources.retainAll { + it.title.contains(query, ignoreCase = true) || it.name.contains(query, ignoreCase = true) + } + } + return sources + } + fun observeIsEnabled(source: MangaSource): Flow { - return dao.observeIsEnabled(source.name) + return dao.observeIsEnabled(source.name).onStart { assimilateNewSources() } } fun observeEnabledSourcesCount(): Flow { @@ -69,8 +104,10 @@ class MangaSourcesRepository @Inject constructor( observeIsNsfwDisabled(), dao.observeEnabled(SourcesSortOrder.MANUAL), ) { skipNsfw, sources -> - sources.count { !skipNsfw || !MangaSource(it.source).isNsfw() } - }.distinctUntilChanged() + sources.count { + it.source.toMangaSourceOrNull()?.let { s -> !skipNsfw || !s.isNsfw() } == true + } + }.distinctUntilChanged().onStart { assimilateNewSources() } } fun observeAvailableSourcesCount(): Flow { @@ -82,7 +119,7 @@ class MangaSourcesRepository @Inject constructor( allMangaSources.count { x -> x.name !in enabled && (!skipNsfw || !x.isNsfw()) } - }.distinctUntilChanged() + }.distinctUntilChanged().onStart { assimilateNewSources() } } fun observeEnabledSources(): Flow> = combine( @@ -92,18 +129,18 @@ class MangaSourcesRepository @Inject constructor( dao.observeEnabled(order).map { it.toSources(skipNsfw, order) } - }.flatMapLatest { it } + }.flatMapLatest { it }.onStart { assimilateNewSources() } fun observeAll(): Flow>> = dao.observeAll().map { entities -> val result = ArrayList>(entities.size) for (entity in entities) { - val source = MangaSource(entity.source) + val source = entity.source.toMangaSourceOrNull() ?: continue if (source in remoteSources) { result.add(source to entity.isEnabled) } } result - } + }.onStart { assimilateNewSources() } suspend fun setSourcesEnabled(sources: Collection, isEnabled: Boolean): ReversibleHandle { setSourcesEnabledImpl(sources, isEnabled) @@ -114,6 +151,7 @@ class MangaSourcesRepository @Inject constructor( suspend fun setSourcesEnabledExclusive(sources: Set) { db.withTransaction { + assimilateNewSources() for (s in remoteSources) { dao.setEnabled(s.name, s in sources) } @@ -135,32 +173,34 @@ class MangaSourcesRepository @Inject constructor( } } - fun observeNewSources(): Flow> = observeIsNewSourcesEnabled().flatMapLatest { - if (it) { - combine( - dao.observeAll(), - observeIsNsfwDisabled(), - ) { entities, skipNsfw -> - val result = EnumSet.copyOf(remoteSources) - for (e in entities) { - result.remove(MangaSource(e.source)) - } - if (skipNsfw) { - result.removeAll { x -> x.isNsfw() } - } - result - }.distinctUntilChanged() + fun observeHasNewSources(): Flow = observeIsNsfwDisabled().map { skipNsfw -> + val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null) + sources.isNotEmpty() + }.onStart { assimilateNewSources() } + + fun observeHasNewSourcesForBadge(): Flow = combine( + settings.observeAsFlow(AppSettings.KEY_SOURCES_VERSION) { sourcesVersion }, + observeIsNsfwDisabled(), + ) { version, skipNsfw -> + if (version < BuildConfig.VERSION_CODE) { + val sources = dao.findAllFromVersion(version).toSources(skipNsfw, null) + sources.isNotEmpty() } else { - assimilateNewSources() - flowOf(emptySet()) + false } + }.onStart { assimilateNewSources() } + + fun clearNewSourcesBadge() { + settings.sourcesVersion = BuildConfig.VERSION_CODE } - @Deprecated("") - suspend fun assimilateNewSources(): Set { + private suspend fun assimilateNewSources(): Boolean { + if (isNewSourcesAssimilated.getAndSet(true)) { + return false + } val new = getNewSources() if (new.isEmpty()) { - return emptySet() + return false } var maxSortKey = dao.getMaxSortKey() val entities = new.map { x -> @@ -172,14 +212,11 @@ class MangaSourcesRepository @Inject constructor( ) } dao.insertIfAbsent(entities) - if (settings.isNsfwContentDisabled) { - new.removeAll { x -> x.isNsfw() } - } - return new + return true } suspend fun isSetupRequired(): Boolean { - return dao.findAll().isEmpty() + return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty() } private suspend fun setSourcesEnabledImpl(sources: Collection, isEnabled: Boolean) { @@ -198,7 +235,7 @@ class MangaSourcesRepository @Inject constructor( val entities = dao.findAll() val result = EnumSet.copyOf(remoteSources) for (e in entities) { - result.remove(MangaSource(e.source)) + result.remove(e.source.toMangaSourceOrNull() ?: continue) } return result } @@ -206,10 +243,10 @@ class MangaSourcesRepository @Inject constructor( private fun List.toSources( skipNsfwSources: Boolean, sortOrder: SourcesSortOrder?, - ): List { + ): MutableList { val result = ArrayList(size) for (entity in this) { - val source = MangaSource(entity.source) + val source = entity.source.toMangaSourceOrNull() ?: continue if (skipNsfwSources && source.isNsfw()) { continue } @@ -227,11 +264,9 @@ class MangaSourcesRepository @Inject constructor( isNsfwContentDisabled } - private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) { - isNewSourcesTipEnabled - } - private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) { sourcesSortOrder } + + private fun String.toMangaSourceOrNull(): MangaSource? = MangaSource.entries.find { it.name == this } } 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 5733409c8..a9c34b87b 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 @@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver -import org.koitharu.kotatsu.core.ui.widgets.TipView import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate import org.koitharu.kotatsu.core.util.ext.observe @@ -40,13 +39,11 @@ import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.model.ListHeader -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.mapNotNullToSet import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.settings.SettingsActivity -import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity import javax.inject.Inject @@ -56,7 +53,7 @@ class ExploreFragment : BaseFragment(), RecyclerViewOwner, ExploreListEventListener, - OnListItemClickListener, TipView.OnButtonClickListener, ListSelectionController.Callback2 { + OnListItemClickListener, ListSelectionController.Callback2 { @Inject lateinit var coil: ImageLoader @@ -74,7 +71,7 @@ class ExploreFragment : override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) - exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this, this) { manga, view -> + exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this) { manga, view -> startActivity(DetailsActivity.newIntent(view.context, manga)) } sourceSelectionController = ListSelectionController( @@ -124,18 +121,6 @@ class ExploreFragment : } } - override fun onPrimaryButtonClick(tipView: TipView) { - when ((tipView.tag as? TipModel)?.key) { - ExploreViewModel.TIP_NEW_SOURCES -> NewSourcesDialogFragment.show(childFragmentManager) - } - } - - override fun onSecondaryButtonClick(tipView: TipView) { - when ((tipView.tag as? TipModel)?.key) { - ExploreViewModel.TIP_NEW_SOURCES -> viewModel.discardNewSources() - } - } - override fun onClick(v: View) { val intent = when (v.id) { R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL) 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 ec8cc3da9..30eba8c14 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 @@ -102,12 +102,6 @@ class ExploreViewModel @Inject constructor( } } - fun discardNewSources() { - launchJob(Dispatchers.Default) { - sourcesRepository.assimilateNewSources() - } - } - fun requestPinShortcut(source: MangaSource) { launchLoadingJob(Dispatchers.Default) { shortcutManager.requestPinShortcut(source) @@ -124,7 +118,7 @@ class ExploreViewModel @Inject constructor( getSuggestionFlow(), isGrid, isRandomLoading, - sourcesRepository.observeNewSources(), + sourcesRepository.observeHasNewSourcesForBadge(), ) { content, suggestions, grid, randomLoading, newSources -> buildList(content, suggestions, grid, randomLoading, newSources) }.withErrorHandling() @@ -134,7 +128,7 @@ class ExploreViewModel @Inject constructor( recommendation: List, isGrid: Boolean, randomLoading: Boolean, - newSources: Set, + hasNewSources: Boolean, ): List { val result = ArrayList(sources.size + 3) result += ExploreButtons(randomLoading) @@ -146,7 +140,7 @@ class ExploreViewModel @Inject constructor( result += ListHeader( textRes = R.string.remote_sources, buttonTextRes = R.string.catalog, - badge = if (newSources.isNotEmpty()) "" else null, + badge = if (hasNewSources) "" else null, ) sources.mapTo(result) { MangaSourceItem(it, isGrid) } } else { @@ -191,6 +185,5 @@ class ExploreViewModel @Inject constructor( private const val TIP_SUGGESTIONS = "suggestions" private const val SUGGESTIONS_COUNT = 8 - const val TIP_NEW_SOURCES = "new_sources" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt index ad6834281..f275ff345 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt @@ -4,13 +4,11 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.ui.widgets.TipView import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD -import org.koitharu.kotatsu.list.ui.adapter.tipAD import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga @@ -18,7 +16,6 @@ class ExploreAdapter( coil: ImageLoader, lifecycleOwner: LifecycleOwner, listener: ExploreListEventListener, - tipClickListener: TipView.OnButtonClickListener, clickListener: OnListItemClickListener, mangaClickListener: OnListItemClickListener, ) : BaseListAdapter() { @@ -34,6 +31,5 @@ class ExploreAdapter( addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner)) addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) - addDelegate(ListItemType.TIP, tipAD(tipClickListener)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index ae467b8af..e664b7785 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -257,7 +257,7 @@ class FilterCoordinator @Inject constructor( } oldValue.copy( tagsExclude = newTags, - tags = oldValue.tags - newTags + tags = oldValue.tags - newTags, ) } } @@ -308,7 +308,7 @@ class FilterCoordinator @Inject constructor( currentState.update { oldValue -> oldValue.copy( tags = tags, - tagsExclude = oldValue.tagsExclude - tags + tagsExclude = oldValue.tagsExclude - tags, ) } } @@ -391,9 +391,7 @@ class FilterCoordinator @Inject constructor( val result = LinkedList() for (tag in tags) { val model = ChipsView.ChipModel( - tint = 0, title = tag.title, - icon = 0, isCheckable = true, isChecked = selectedTags.remove(tag), data = tag, @@ -406,9 +404,7 @@ class FilterCoordinator @Inject constructor( } for (tag in selectedTags) { val model = ChipsView.ChipModel( - tint = 0, title = tag.title, - icon = 0, isCheckable = true, isChecked = true, data = tag, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index 546c3892a..c7a6f0ef9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -61,10 +61,7 @@ class FilterHeaderFragment : BaseFragment(), ChipsV } private fun moreTagsChip() = ChipsView.ChipModel( - tint = 0, title = getString(R.string.more), icon = materialR.drawable.abc_ic_menu_overflow_material, - isCheckable = false, - isChecked = false, ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index ac9bb8b27..724f77101 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -144,9 +144,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), val chips = ArrayList(value.selectedItems.size + value.availableItems.size + 1) value.selectedItems.mapTo(chips) { tag -> ChipsView.ChipModel( - tint = 0, title = tag.title, - icon = 0, isCheckable = true, isChecked = true, data = tag, @@ -155,9 +153,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), value.availableItems.mapNotNullTo(chips) { tag -> if (tag !in value.selectedItems) { ChipsView.ChipModel( - tint = 0, title = tag.title, - icon = 0, isCheckable = true, isChecked = false, data = tag, @@ -168,12 +164,8 @@ class FilterSheetFragment : BaseAdaptiveSheet(), } chips.add( ChipsView.ChipModel( - tint = 0, title = getString(R.string.more), icon = materialR.drawable.abc_ic_menu_overflow_material, - isCheckable = false, - isChecked = false, - data = null, ), ) b.chipsGenres.setChips(chips) @@ -200,9 +192,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), value.availableItems.mapNotNullTo(chips) { tag -> if (tag !in value.selectedItems) { ChipsView.ChipModel( - tint = 0, title = tag.title, - icon = 0, isCheckable = true, isChecked = false, data = tag, @@ -213,12 +203,8 @@ class FilterSheetFragment : BaseAdaptiveSheet(), } chips.add( ChipsView.ChipModel( - tint = 0, title = getString(R.string.more), icon = materialR.drawable.abc_ic_menu_overflow_material, - isCheckable = false, - isChecked = false, - data = null, ), ) b.chipsGenresExclude.setChips(chips) @@ -233,9 +219,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), } val chips = value.availableItems.map { state -> ChipsView.ChipModel( - tint = 0, title = getString(state.titleResId), - icon = 0, isCheckable = true, isChecked = state in value.selectedItems, data = state, @@ -253,9 +237,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), } val chips = value.availableItems.map { contentRating -> ChipsView.ChipModel( - tint = 0, title = getString(contentRating.titleResId), - icon = 0, isCheckable = true, isChecked = contentRating in value.selectedItems, data = contentRating, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt index 833734e2b..42a847b46 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -37,9 +37,6 @@ suspend fun Manga.toListDetailedModel( ChipsView.ChipModel( tint = extraProvider?.getTagTint(it) ?: 0, title = it.title, - icon = 0, - isCheckable = false, - isChecked = false, data = it, ) }, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt index 01632a90f..360190b46 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewViewModel.kt @@ -85,10 +85,7 @@ class PreviewViewModel @Inject constructor( ChipsView.ChipModel( title = tag.title, tint = extraProvider.getTagTint(tag), - icon = 0, data = tag, - isCheckable = false, - isChecked = false, ) } }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt index a5f25dcef..b31c9b012 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeSheet.kt @@ -91,9 +91,7 @@ class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipC chips.setChips( value.availableItems.map { ChipsView.ChipModel( - tint = 0, title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages), - icon = 0, isCheckable = true, isChecked = it in value.selectedItems, data = it, @@ -107,9 +105,7 @@ class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipC chips.setChips( value.availableItems.map { ChipsView.ChipModel( - tint = 0, title = getString(it.titleResId), - icon = 0, isCheckable = true, isChecked = it in value.selectedItems, data = it, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt index fa5ee1b71..c3c6800f8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/welcome/WelcomeViewModel.kt @@ -61,7 +61,7 @@ class WelcomeViewModel @Inject constructor( selectedItems = selectedLocales, isLoading = false, ) - repository.assimilateNewSources() + repository.clearNewSourcesBadge() commit() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt index 5ebf1f6b4..315cb8ec8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt @@ -172,12 +172,8 @@ class SearchSuggestionViewModel @Inject constructor( private fun mapTags(tags: List): List = tags.map { tag -> ChipsView.ChipModel( - tint = 0, title = tag.title, - icon = 0, data = tag, - isCheckable = false, - isChecked = false, ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt deleted file mode 100644 index 455663d66..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.koitharu.kotatsu.settings.newsources - -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels -import coil.ImageLoader -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.AlertDialogFragment -import org.koitharu.kotatsu.core.util.ext.observe -import org.koitharu.kotatsu.databinding.DialogOnboardBinding -import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener -import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -import javax.inject.Inject - -@AndroidEntryPoint -class NewSourcesDialogFragment : - AlertDialogFragment(), - SourceConfigListener, - DialogInterface.OnClickListener { - - @Inject - lateinit var coil: ImageLoader - - private val viewModel by viewModels() - - override fun onCreateViewBinding( - inflater: LayoutInflater, - container: ViewGroup?, - ): DialogOnboardBinding { - return DialogOnboardBinding.inflate(inflater, container, false) - } - - override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - val adapter = SourcesSelectAdapter(this, coil, viewLifecycleOwner) - binding.recyclerView.adapter = adapter - binding.textViewTitle.setText(R.string.new_sources_text) - - viewModel.content.observe(viewLifecycleOwner) { adapter.items = it } - } - - override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { - return super.onBuildDialog(builder) - .setPositiveButton(R.string.done, this) - .setCancelable(true) - .setTitle(R.string.remote_sources) - } - - override fun onClick(dialog: DialogInterface, which: Int) { - dialog.dismiss() - } - - override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit - - override fun onItemLiftClick(item: SourceConfigItem.SourceItem) = Unit - - override fun onItemShortcutClick(item: SourceConfigItem.SourceItem) = Unit - - override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { - viewModel.onItemEnabledChanged(item, isEnabled) - } - - override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit - - companion object { - - private const val TAG = "NewSources" - - fun show(fm: FragmentManager) = NewSourcesDialogFragment().show(fm, TAG) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt deleted file mode 100644 index afe91f76f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.koitharu.kotatsu.settings.newsources - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.plus -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.ui.BaseViewModel -import org.koitharu.kotatsu.explore.data.MangaSourcesRepository -import org.koitharu.kotatsu.parsers.model.ContentType -import org.koitharu.kotatsu.parsers.util.SuspendLazy -import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -import javax.inject.Inject - -@HiltViewModel -class NewSourcesViewModel @Inject constructor( - private val repository: MangaSourcesRepository, - private val settings: AppSettings, -) : BaseViewModel() { - - private val newSources = SuspendLazy { - repository.assimilateNewSources() - } - val content: StateFlow> = repository.observeAll() - .map { sources -> - val new = newSources.get() - val skipNsfw = settings.isNsfwContentDisabled - sources.mapNotNull { (source, enabled) -> - if (source in new) { - SourceConfigItem.SourceItem( - source = source, - isEnabled = enabled, - isDraggable = false, - isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI, - ) - } else { - null - } - } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList()) - - fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { - launchJob(Dispatchers.Default) { - repository.setSourcesEnabled(setOf(item.source), isEnabled) - } - } -} - diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt deleted file mode 100644 index 30fc3e448..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.koitharu.kotatsu.settings.newsources - -import androidx.lifecycle.LifecycleOwner -import coil.ImageLoader -import org.koitharu.kotatsu.core.ui.BaseListAdapter -import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener -import org.koitharu.kotatsu.settings.sources.adapter.sourceConfigItemCheckableDelegate -import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem - -class SourcesSelectAdapter( - listener: SourceConfigListener, - coil: ImageLoader, - lifecycleOwner: LifecycleOwner, -) : BaseListAdapter() { - - init { - delegatesManager.addDelegate(sourceConfigItemCheckableDelegate(listener, coil, lifecycleOwner)) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt index 34c84e2c4..d5ce18d8e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItem.kt @@ -9,7 +9,6 @@ sealed interface SourceCatalogItem : ListModel { data class Source( val source: MangaSource, - val showSummary: Boolean, ) : SourceCatalogItem { override fun areItemsTheSame(other: ListModel): Boolean { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt index 4b267125a..360f33d61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt @@ -34,17 +34,15 @@ fun sourceCatalogItemSourceAD( ) { binding.imageViewAdd.setOnClickListener { v -> + listener.onItemLongClick(item, v) + } + binding.root.setOnClickListener { v -> listener.onItemClick(item, v) } bind { binding.textViewTitle.text = item.source.getTitle(context) - if (item.showSummary) { - binding.textViewDescription.text = item.source.getSummary(context) - binding.textViewDescription.isVisible = true - } else { - binding.textViewDescription.isVisible = false - } + binding.textViewDescription.text = item.source.getSummary(context) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { crossfade(context) 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 865c4544f..9896ddd76 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 @@ -1,22 +1,27 @@ package org.koitharu.kotatsu.settings.sources.catalog import android.os.Bundle +import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets import androidx.core.view.updatePadding import coil.ImageLoader import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.ui.widgets.ChipsView.ChipModel +import org.koitharu.kotatsu.core.util.LocaleComparator import org.koitharu.kotatsu.core.util.ext.getDisplayName import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent @@ -25,7 +30,7 @@ import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.ContentType -import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment +import org.koitharu.kotatsu.search.ui.MangaListActivity import javax.inject.Inject @AndroidEntryPoint @@ -36,8 +41,6 @@ class SourcesCatalogActivity : BaseActivity(), @Inject lateinit var coil: ImageLoader - private var newSourcesSnackbar: Snackbar? = null - override val appBar: AppBarLayout get() = viewBinding.appbar @@ -55,16 +58,12 @@ class SourcesCatalogActivity : BaseActivity(), } viewBinding.chipsFilter.onChipClickListener = this viewModel.content.observe(this, sourcesAdapter) - viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged) viewModel.onActionDone.observeEvent( this, ReversibleActionObserver(viewBinding.recyclerView), ) - viewModel.appliedFilter.observe(this) { - supportActionBar?.subtitle = it.locale?.toLocale().getDisplayName(this) - } - viewModel.filter.observe(this) { - viewBinding.chipsFilter.setChips(it) + combine(viewModel.appliedFilter, viewModel.hasNewSources, ::Pair).observe(this) { + updateFilers(it.first, it.second) } addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this)) } @@ -79,11 +78,18 @@ class SourcesCatalogActivity : BaseActivity(), override fun onChipClick(chip: Chip, data: Any?) { when (data) { is ContentType -> viewModel.setContentType(data, chip.isChecked) + is Boolean -> viewModel.setNewOnly(chip.isChecked) + else -> showLocalesMenu(chip) } } override fun onItemClick(item: SourceCatalogItem.Source, view: View) { + startActivity(MangaListActivity.newIntent(this, item.source)) + } + + override fun onItemLongClick(item: SourceCatalogItem.Source, view: View): Boolean { viewModel.addSource(item.source) + return false } override fun onMenuItemActionExpand(item: MenuItem): Boolean { @@ -97,30 +103,52 @@ class SourcesCatalogActivity : BaseActivity(), return true } - private fun onHasNewSourcesChanged(hasNewSources: Boolean) { + private fun updateFilers( + appliedFilter: SourcesCatalogFilter, + hasNewSources: Boolean, + ) { + val chips = ArrayList(ContentType.entries.size + 2) + chips += ChipModel( + title = appliedFilter.locale?.toLocale().getDisplayName(this), + icon = R.drawable.ic_language, + isDropdown = true, + ) if (hasNewSources) { - if (newSourcesSnackbar?.isShownOrQueued == true) { - return - } - val snackbar = Snackbar.make(viewBinding.recyclerView, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE) - snackbar.setAction(R.string.explore) { - NewSourcesDialogFragment.show(supportFragmentManager) + chips += ChipModel( + title = getString(R.string._new), + icon = R.drawable.ic_updated_selector, + isCheckable = true, + isChecked = appliedFilter.isNewOnly, + data = true, + ) + } + for (type in ContentType.entries) { + if (type == ContentType.HENTAI && viewModel.isNsfwDisabled) { + continue } - snackbar.addCallback( - object : Snackbar.Callback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (event == DISMISS_EVENT_SWIPE) { - viewModel.skipNewSources() - } - } - }, + chips += ChipModel( + title = getString(type.titleResId), + isCheckable = true, + isChecked = type in appliedFilter.types, + data = type, ) - snackbar.show() - newSourcesSnackbar = snackbar - } else { - newSourcesSnackbar?.dismiss() - newSourcesSnackbar = null } + viewBinding.chipsFilter.setChips(chips) + } + + private fun showLocalesMenu(anchor: View) { + val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) { + it to it?.toLocale() + } + locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second }) + val menu = PopupMenu(this, anchor) + for ((i, lc) in locales.withIndex()) { + menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(this)) + } + menu.setOnMenuItemClickListener { + viewModel.setLocale(locales.getOrNull(it.order)?.first) + true + } + menu.show() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogFilter.kt index 979ed677a..ba6d0df31 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogFilter.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.settings.sources.catalog import org.koitharu.kotatsu.parsers.model.ContentType -import java.util.Locale data class SourcesCatalogFilter( val types: Set, val locale: String?, + val isNewOnly: Boolean, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt deleted file mode 100644 index 3d11ff033..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogListProducer.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.koitharu.kotatsu.settings.sources.catalog - -import androidx.room.InvalidationTracker -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.ViewModelLifecycle -import dagger.hilt.android.lifecycle.RetainedLifecycle -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.TABLE_SOURCES -import org.koitharu.kotatsu.core.db.removeObserverAsync -import org.koitharu.kotatsu.core.util.ext.lifecycleScope -import org.koitharu.kotatsu.explore.data.MangaSourcesRepository -import org.koitharu.kotatsu.parsers.model.ContentType - -@Deprecated("") -class SourcesCatalogListProducer @AssistedInject constructor( - @Assisted private val locale: String?, - @Assisted private val contentType: ContentType, - @Assisted lifecycle: ViewModelLifecycle, - private val repository: MangaSourcesRepository, - private val database: MangaDatabase, -) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener { - - private val scope = lifecycle.lifecycleScope - private var query: String? = null - val list = MutableStateFlow(emptyList()) - - private var job = scope.launch(Dispatchers.Default) { - list.value = buildList() - } - - init { - scope.launch(Dispatchers.Default) { - database.invalidationTracker.addObserver(this@SourcesCatalogListProducer) - } - lifecycle.addOnClearedListener(this) - } - - override fun onCleared() { - database.invalidationTracker.removeObserverAsync(this) - } - - override fun onInvalidated(tables: Set) { - val prevJob = job - job = scope.launch(Dispatchers.Default) { - prevJob.cancelAndJoin() - list.update { buildList() } - } - } - - fun setQuery(value: String?) { - this.query = value - onInvalidated(emptySet()) - } - - private suspend fun buildList(): List { - val sources = repository.getDisabledSources().toMutableList() - when (val q = query) { - null -> sources.retainAll { it.contentType == contentType && it.locale == locale } - "" -> return emptyList() - else -> sources.retainAll { it.title.contains(q, ignoreCase = true) } - } - return if (sources.isEmpty()) { - listOf( - if (query == null) { - SourceCatalogItem.Hint( - icon = R.drawable.ic_empty_feed, - title = R.string.no_manga_sources, - text = R.string.no_manga_sources_catalog_text, - ) - } else { - SourceCatalogItem.Hint( - icon = R.drawable.ic_empty_feed, - title = R.string.nothing_found, - text = R.string.no_manga_sources_found, - ) - }, - ) - } else { - sources.sortBy { it.title } - sources.map { - SourceCatalogItem.Source( - source = it, - showSummary = query != null, - ) - } - } - } - - @AssistedFactory - interface Factory { - - fun create( - locale: String?, - contentType: ContentType, - lifecycle: ViewModelLifecycle, - ): SourcesCatalogListProducer - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt index 67154760e..355d7a831 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogMenuProvider.kt @@ -4,14 +4,9 @@ import android.app.Activity import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import android.view.View -import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.core.view.MenuProvider import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.util.LocaleComparator -import org.koitharu.kotatsu.core.util.ext.getDisplayName -import org.koitharu.kotatsu.core.util.ext.toLocale import org.koitharu.kotatsu.main.ui.owners.AppBarOwner class SourcesCatalogMenuProvider( @@ -32,14 +27,7 @@ class SourcesCatalogMenuProvider( searchView.queryHint = searchMenuItem.title } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.action_locales -> { - showLocalesMenu() - true - } - - else -> false - } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false override fun onMenuItemActionExpand(item: MenuItem): Boolean { (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) @@ -57,24 +45,4 @@ class SourcesCatalogMenuProvider( viewModel.performSearch(newText?.trim().orEmpty()) return true } - - private fun showLocalesMenu() { - val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) { - it to it?.toLocale() - } - locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second }) - - val anchor: View = (activity as AppBarOwner).appBar.let { - it.findViewById(R.id.toolbar) ?: it - } - val menu = PopupMenu(activity, anchor) - for ((i, lc) in locales.withIndex()) { - menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(activity)) - } - menu.setOnMenuItemClickListener { - viewModel.setLocale(locales.getOrNull(it.order)?.first) - true - } - menu.show() - } } 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 c8f4ffe99..a451cad34 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 @@ -7,16 +7,16 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.util.ReversibleAction -import org.koitharu.kotatsu.core.ui.widgets.ChipsView.ChipModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.explore.data.MangaSourcesRepository +import org.koitharu.kotatsu.explore.data.SourcesSortOrder import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet @@ -27,6 +27,7 @@ import javax.inject.Inject @HiltViewModel class SourcesCatalogViewModel @Inject constructor( private val repository: MangaSourcesRepository, + private val settings: AppSettings, ) : BaseViewModel() { val onActionDone = MutableEventFlow() @@ -37,16 +38,14 @@ class SourcesCatalogViewModel @Inject constructor( SourcesCatalogFilter( types = emptySet(), locale = Locale.getDefault().language.takeIf { it in locales }, + isNewOnly = false, ), ) - val hasNewSources = repository.observeNewSources() - .map { it.isNotEmpty() } - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) + val isNsfwDisabled = settings.isNsfwContentDisabled - val filter: StateFlow> = appliedFilter.map { - buildFilter(it) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, buildFilter(appliedFilter.value)) + val hasNewSources = repository.observeHasNewSources() + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) val content: StateFlow> = combine( searchQuery, @@ -55,6 +54,10 @@ class SourcesCatalogViewModel @Inject constructor( buildSourcesList(f, q) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) + init { + repository.clearNewSourcesBadge() + } + fun performSearch(query: String?) { searchQuery.value = query?.trim() } @@ -70,12 +73,6 @@ class SourcesCatalogViewModel @Inject constructor( } } - fun skipNewSources() { - launchJob { - repository.assimilateNewSources() - } - } - fun setContentType(value: ContentType, isAdd: Boolean) { val filter = appliedFilter.value val types = EnumSet.noneOf(ContentType::class.java) @@ -88,29 +85,19 @@ class SourcesCatalogViewModel @Inject constructor( appliedFilter.value = filter.copy(types = types) } - private fun buildFilter(applied: SourcesCatalogFilter): List = buildList(ContentType.entries.size) { - for (ct in ContentType.entries) { - add( - ChipModel( - tint = 0, - title = ct.name, - icon = 0, - isCheckable = true, - isChecked = ct in applied.types, - data = ct, - ), - ) - } + fun setNewOnly(value: Boolean) { + appliedFilter.value = appliedFilter.value.copy(isNewOnly = value) } private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List { - val sources = repository.getDisabledSources().toMutableList() - sources.retainAll { - (filter.types.isEmpty() || it.contentType in filter.types) && it.locale == filter.locale - } - if (!query.isNullOrEmpty()) { - sources.retainAll { it.title.contains(query, ignoreCase = true) } - } + val sources = repository.getAvailableSources( + isDisabledOnly = true, + isNewOnly = filter.isNewOnly, + excludeBroken = false, + types = filter.types, + query = query, + sortOrder = SourcesSortOrder.ALPHABETIC, + ).filter { it.locale == filter.locale } return if (sources.isEmpty()) { listOf( if (query == null) { @@ -128,12 +115,8 @@ class SourcesCatalogViewModel @Inject constructor( }, ) } else { - sources.sortBy { it.title } sources.map { - SourceCatalogItem.Source( - source = it, - showSummary = query != null, - ) + SourceCatalogItem.Source(source = it) } } } diff --git a/app/src/main/res/layout/item_source_catalog.xml b/app/src/main/res/layout/item_source_catalog.xml index b7c73edd0..2c99ffc8f 100644 --- a/app/src/main/res/layout/item_source_catalog.xml +++ b/app/src/main/res/layout/item_source_catalog.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:windowBackground" + android:background="?selectableItemBackground" android:gravity="center_vertical" android:minHeight="?listPreferredItemHeightSmall" android:orientation="horizontal" @@ -52,10 +52,17 @@ + + - - Do not show notifications about NSFW manga updates Checking for new chapters log Debug information about background checks for new chapters + + New diff --git a/app/src/main/res/xml/pref_sources.xml b/app/src/main/res/xml/pref_sources.xml index cc2126336..19f0b5c03 100644 --- a/app/src/main/res/xml/pref_sources.xml +++ b/app/src/main/res/xml/pref_sources.xml @@ -31,10 +31,4 @@ android:summary="@string/disable_nsfw_summary" android:title="@string/disable_nsfw" /> - -