@@ -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)
|
||||
|
||||
@@ -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<String, Any?> {
|
||||
|
||||
@@ -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<Migration> = arrayOf(
|
||||
Migration17To18(),
|
||||
Migration18To19(),
|
||||
Migration19To20(),
|
||||
Migration20To21(),
|
||||
)
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,6 @@ class MangaSourcesRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
suspend fun assimilateNewSources(): Set<MangaSource> {
|
||||
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)
|
||||
|
||||
@@ -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<ActivitySourcesCatalogBinding>(),
|
||||
OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
AppBarOwner, MenuItem.OnActionExpandListener, ChipsView.OnChipClickListener {
|
||||
AppBarOwner, MenuItem.OnActionExpandListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
@@ -47,24 +45,18 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
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<ActivitySourcesCatalogBinding>(),
|
||||
)
|
||||
}
|
||||
|
||||
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<ActivitySourcesCatalogBinding>(),
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<ContentType>,
|
||||
val locale: String?,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ReversibleAction>()
|
||||
val locales = repository.allMangaSources.mapToSet { it.locale }
|
||||
|
||||
private val searchQuery = MutableStateFlow<String?>(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<List<ChipModel>> = 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<List<SourceCatalogItem>> = combine(
|
||||
searchQuery,
|
||||
appliedFilter,
|
||||
) { q, f ->
|
||||
buildSourcesList(f, q)
|
||||
val content: StateFlow<List<SourceCatalogPage>> = listProducers.flatMapLatest {
|
||||
val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } }
|
||||
combine<SourceCatalogPage, List<SourceCatalogPage>>(flows, Array<SourceCatalogPage>::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)
|
||||
@MainThread
|
||||
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> {
|
||||
val types = EnumSet.allOf(ContentType::class.java)
|
||||
if (settings.isNsfwContentDisabled) {
|
||||
types.remove(ContentType.HENTAI)
|
||||
}
|
||||
appliedFilter.value = filter.copy(types = types)
|
||||
}
|
||||
|
||||
private fun buildFilter(applied: SourcesCatalogFilter): List<ChipModel> = 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<SourceCatalogItem> {
|
||||
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) }
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,36 +19,19 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scrollView_chips"
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="@dimen/list_spacing_large"
|
||||
android:scrollbars="none">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
|
||||
app:selectionRequired="false"
|
||||
app:singleLine="true"
|
||||
app:singleSelection="false" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
app:tabGravity="start"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
Reference in New Issue
Block a user