diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt index 495e42f45..8f1fbb50d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/MigrateUseCase.kt @@ -69,7 +69,6 @@ class MigrateUseCase @Inject constructor( lastCheckTime = System.currentTimeMillis(), lastChapterDate = lastChapter?.uploadDate ?: 0L, lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, - lastError = null, ) tracksDao.delete(oldDetails.id) tracksDao.upsert(newTrack) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt index 02229b7de..53e26189d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt @@ -84,7 +84,6 @@ class JsonDeserializer(private val json: JSONObject) { source = json.getString("source"), isEnabled = json.getBoolean("enabled"), sortKey = json.getInt("sort_key"), - addedIn = json.getIntOrDefault("added_in", 0), ) fun toMap(): Map { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt index bda4584aa..c0b3d6efa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -33,7 +33,6 @@ import org.koitharu.kotatsu.core.db.migrations.Migration17To18 import org.koitharu.kotatsu.core.db.migrations.Migration18To19 import org.koitharu.kotatsu.core.db.migrations.Migration19To20 import org.koitharu.kotatsu.core.db.migrations.Migration1To2 -import org.koitharu.kotatsu.core.db.migrations.Migration20To21 import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration4To5 @@ -59,7 +58,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TracksDao -const val DATABASE_VERSION = 21 +const val DATABASE_VERSION = 20 @Database( entities = [ @@ -119,7 +118,6 @@ fun getDatabaseMigrations(context: Context): Array = arrayOf( Migration17To18(), Migration18To19(), Migration19To20(), - Migration20To21(), ) fun MangaDatabase(context: Context): MangaDatabase = Room 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..a9a9e1f02 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 @@ -11,7 +11,6 @@ import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow import org.intellij.lang.annotations.Language -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.explore.data.SourcesSortOrder @@ -69,7 +68,6 @@ abstract class MangaSourcesDao { source = source, isEnabled = isEnabled, sortKey = getMaxSortKey() + 1, - addedIn = BuildConfig.VERSION_CODE, ) upsert(entity) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt index 8c8784a46..00243e8df 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt @@ -14,5 +14,4 @@ data class MangaSourceEntity( val source: String, @ColumnInfo(name = "enabled") val isEnabled: Boolean, @ColumnInfo(name = "sort_key", index = true) val sortKey: Int, - @ColumnInfo(name = "added_in") val addedIn: Int, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration20To21.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration20To21.kt deleted file mode 100644 index 462b77261..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration20To21.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.koitharu.kotatsu.core.db.migrations - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -class Migration20To21 : Migration(20, 21) { - - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE tracks ADD COLUMN `last_error` TEXT DEFAULT NULL") - db.execSQL("ALTER TABLE sources ADD COLUMN `added_in` INTEGER NOT NULL DEFAULT 0") - } -} 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..48e01376b 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 @@ -156,7 +156,6 @@ class MangaSourcesRepository @Inject constructor( } } - @Deprecated("") suspend fun assimilateNewSources(): Set { val new = getNewSources() if (new.isEmpty()) { @@ -168,7 +167,6 @@ class MangaSourcesRepository @Inject constructor( source = x.name, isEnabled = false, sortKey = ++maxSortKey, - addedIn = BuildConfig.VERSION_CODE, ) } dao.insertIfAbsent(entities) 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..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 @@ -6,32 +6,30 @@ import android.view.View import androidx.activity.viewModels import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets +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.chip.Chip 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 -import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.util.ext.getDisplayName import org.koitharu.kotatsu.core.util.ext.observe 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.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 javax.inject.Inject @AndroidEntryPoint class SourcesCatalogActivity : BaseActivity(), OnListItemClickListener, - AppBarOwner, MenuItem.OnActionExpandListener, ChipsView.OnChipClickListener { + AppBarOwner, MenuItem.OnActionExpandListener { @Inject lateinit var coil: ImageLoader @@ -47,24 +45,18 @@ class SourcesCatalogActivity : BaseActivity(), super.onCreate(savedInstanceState) setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater)) supportActionBar?.setDisplayHomeAsUpEnabled(true) - val sourcesAdapter = SourcesCatalogAdapter(this, coil, this) - with(viewBinding.recyclerView) { - setHasFixedSize(true) - addItemDecoration(TypedListSpacingDecoration(context, false)) - adapter = sourcesAdapter - } - viewBinding.chipsFilter.onChipClickListener = this - viewModel.content.observe(this, sourcesAdapter) + val pagerAdapter = SourcesCatalogPagerAdapter(this, coil, this) + viewBinding.pager.adapter = pagerAdapter + 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.recyclerView), + ReversibleActionObserver(viewBinding.pager), ) - viewModel.appliedFilter.observe(this) { - supportActionBar?.subtitle = it.locale?.toLocale().getDisplayName(this) - } - viewModel.filter.observe(this) { - viewBinding.chipsFilter.setChips(it) + viewModel.locale.observe(this) { + supportActionBar?.subtitle = it?.toLocale().getDisplayName(this) } addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this)) } @@ -76,23 +68,21 @@ class SourcesCatalogActivity : BaseActivity(), ) } - override fun onChipClick(chip: Chip, data: Any?) { - when (data) { - is ContentType -> viewModel.setContentType(data, chip.isChecked) - } - } - override fun onItemClick(item: SourceCatalogItem.Source, view: View) { viewModel.addSource(item.source) } override fun onMenuItemActionExpand(item: MenuItem): Boolean { + viewBinding.tabs.isVisible = false + viewBinding.pager.isUserInputEnabled = false val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty() viewModel.performSearch(sq) return true } override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + viewBinding.tabs.isVisible = true + viewBinding.pager.isUserInputEnabled = true viewModel.performSearch(null) return true } @@ -102,7 +92,7 @@ class SourcesCatalogActivity : BaseActivity(), if (newSourcesSnackbar?.isShownOrQueued == true) { return } - val snackbar = Snackbar.make(viewBinding.recyclerView, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE) + val snackbar = Snackbar.make(viewBinding.pager, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE) snackbar.setAction(R.string.explore) { NewSourcesDialogFragment.show(supportFragmentManager) } 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 deleted file mode 100644 index 979ed677a..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourcesCatalogFilter.kt +++ /dev/null @@ -1,9 +0,0 @@ -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?, -) 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 index 3d11ff033..40c42c3e2 100644 --- 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 @@ -19,7 +19,6 @@ 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, 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..6d573ac34 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 @@ -1,25 +1,29 @@ package org.koitharu.kotatsu.settings.sources.catalog +import androidx.annotation.MainThread import androidx.lifecycle.viewModelScope +import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest 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.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet +import java.util.EnumMap import java.util.EnumSet import java.util.Locale import javax.inject.Inject @@ -27,40 +31,41 @@ import javax.inject.Inject @HiltViewModel class SourcesCatalogViewModel @Inject constructor( private val repository: MangaSourcesRepository, + private val listProducerFactory: SourcesCatalogListProducer.Factory, + private val settings: AppSettings, ) : BaseViewModel() { + private val lifecycle = RetainedLifecycleImpl() + private var searchQuery: String? = null val onActionDone = MutableEventFlow() val locales = repository.allMangaSources.mapToSet { it.locale } - - private val searchQuery = MutableStateFlow(null) - val appliedFilter = MutableStateFlow( - SourcesCatalogFilter( - types = emptySet(), - locale = 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) - val filter: StateFlow> = appliedFilter.map { - buildFilter(it) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, buildFilter(appliedFilter.value)) + private val listProducers = locale.map { lc -> + createListProducers(lc) + }.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value)) - val content: StateFlow> = combine( - searchQuery, - appliedFilter, - ) { q, f -> - buildSourcesList(f, q) + val content: StateFlow> = listProducers.flatMapLatest { + val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } } + combine>(flows, Array::toList) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) + override fun onCleared() { + super.onCleared() + lifecycle.dispatchOnCleared() + } + fun performSearch(query: String?) { - searchQuery.value = query?.trim() + searchQuery = query + listProducers.value.forEach { (_, v) -> v.setQuery(query) } } fun setLocale(value: String?) { - appliedFilter.value = appliedFilter.value.copy(locale = value) + locale.value = value } fun addSource(source: MangaSource) { @@ -76,64 +81,15 @@ class SourcesCatalogViewModel @Inject constructor( } } - fun setContentType(value: ContentType, isAdd: Boolean) { - val filter = appliedFilter.value - val types = EnumSet.noneOf(ContentType::class.java) - types.addAll(filter.types) - if (isAdd) { - types.add(value) - } else { - types.remove(value) - } - 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, - ), - ) - } - } - - 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) } + @MainThread + private fun createListProducers(lc: String?): Map { + val types = EnumSet.allOf(ContentType::class.java) + if (settings.isNsfwContentDisabled) { + types.remove(ContentType.HENTAI) } - 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, - ) + return types.associateWithTo(EnumMap(ContentType::class.java)) { type -> + listProducerFactory.create(lc, type, lifecycle).also { + it.setQuery(searchQuery) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt index 127a60b4c..c152253ac 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt @@ -25,7 +25,6 @@ class TrackEntity( @ColumnInfo(name = "last_check_time") val lastCheckTime: Long, @ColumnInfo(name = "last_chapter_date") val lastChapterDate: Long, @ColumnInfo(name = "last_result") val lastResult: Int, - @ColumnInfo(name = "last_error") val lastError: String?, ) { companion object { @@ -43,7 +42,6 @@ class TrackEntity( lastCheckTime = 0L, lastChapterDate = 0, lastResult = RESULT_NONE, - lastError = null, ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 011d50a0e..628b5ea29 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -174,7 +174,6 @@ class TrackingRepository @Inject constructor( lastCheckTime = tracking.lastCheck?.toEpochMilli() ?: 0L, lastChapterDate = tracking.lastChapterDate?.toEpochMilli() ?: 0L, lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, - lastError = null, ) db.getTracksDao().upsert(entity) } @@ -231,7 +230,6 @@ class TrackingRepository @Inject constructor( lastCheckTime = System.currentTimeMillis(), lastChapterDate = lastChapterDate, lastResult = TrackEntity.RESULT_FAILED, - lastError = updates.error?.toString(), ) is MangaUpdates.Success -> TrackEntity( @@ -241,7 +239,6 @@ class TrackingRepository @Inject constructor( lastCheckTime = System.currentTimeMillis(), lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate }, lastResult = if (updates.isNotEmpty()) TrackEntity.RESULT_HAS_UPDATE else TrackEntity.RESULT_NO_UPDATE, - lastError = null, ) } } diff --git a/app/src/main/res/layout/activity_sources_catalog.xml b/app/src/main/res/layout/activity_sources_catalog.xml index 528ad1107..7b844cac0 100644 --- a/app/src/main/res/layout/activity_sources_catalog.xml +++ b/app/src/main/res/layout/activity_sources_catalog.xml @@ -19,36 +19,19 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" /> - - - - - + app:tabGravity="start" + app:tabMode="scrollable" /> -