diff --git a/app/build.gradle b/app/build.gradle
index d8ccb45bf..de3d3fa5b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -87,9 +87,9 @@ dependencies {
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
- implementation 'androidx.room:room-runtime:2.4.1'
- implementation 'androidx.room:room-ktx:2.4.1'
- kapt 'androidx.room:room-compiler:2.4.1'
+ implementation 'androidx.room:room-runtime:2.4.2'
+ implementation 'androidx.room:room-ktx:2.4.2'
+ kapt 'androidx.room:room-compiler:2.4.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okio:okio:3.0.0'
@@ -115,6 +115,6 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
- androidTestImplementation 'androidx.room:room-testing:2.4.1'
+ androidTestImplementation 'androidx.room:room-testing:2.4.2'
androidTestImplementation 'com.google.truth:truth:1.1.3'
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a666a295e..d42998bc7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -64,6 +64,7 @@
{
+ return db.tagsDao.findTags(source.name).mapToSet {
+ it.toMangaTag()
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
index b3df5277a..8d1c5e279 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt
@@ -33,7 +33,7 @@ abstract class BaseViewModel : ViewModel() {
}
}
- private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
+ protected fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
if (BuildConfig.DEBUG) {
throwable.printStackTrace()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt
deleted file mode 100644
index f17b31f84..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/AnimatedToolbar.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.koitharu.kotatsu.base.ui.widgets
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import android.view.View
-import androidx.appcompat.widget.Toolbar
-import androidx.core.view.isGone
-import com.google.android.material.R
-import com.google.android.material.appbar.MaterialToolbar
-import java.lang.reflect.Field
-
-class AnimatedToolbar @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = R.attr.toolbarStyle,
-) : MaterialToolbar(context, attrs, defStyleAttr) {
-
- private var navButtonView: View? = null
- get() {
- if (field == null) {
- runCatching {
- field = navButtonViewField?.get(this) as? View
- }
- }
- return field
- }
-
- override fun setNavigationIcon(icon: Drawable?) {
- super.setNavigationIcon(icon)
- navButtonView?.isGone = (icon == null)
- }
-
- private companion object {
-
- val navButtonViewField: Field? = runCatching {
- Toolbar::class.java.getDeclaredField("mNavButtonView")
- .also { it.isAccessible = true }
- }.getOrNull()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt
index 497d18499..885e99b70 100644
--- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt
@@ -29,6 +29,10 @@ class BrowserActivity : BaseActivity(), BrowserCallback
javaScriptEnabled = true
}
binding.webView.webViewClient = BrowserClient(this)
+ binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
+ if (savedInstanceState != null) {
+ return
+ }
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finishAfterTransition()
@@ -41,6 +45,16 @@ class BrowserActivity : BaseActivity(), BrowserCallback
}
}
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ binding.webView.saveState(outState)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ binding.webView.restoreState(savedInstanceState)
+ }
+
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_browser, menu)
return super.onCreateOptionsMenu(menu)
@@ -82,6 +96,11 @@ class BrowserActivity : BaseActivity(), BrowserCallback
binding.webView.onResume()
}
+ override fun onDestroy() {
+ super.onDestroy()
+ binding.webView.destroy()
+ }
+
override fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt b/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt
new file mode 100644
index 000000000..0d890397e
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt
@@ -0,0 +1,31 @@
+package org.koitharu.kotatsu.browser
+
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import androidx.core.view.isVisible
+import com.google.android.material.progressindicator.BaseProgressIndicator
+import org.koitharu.kotatsu.utils.ext.setIndeterminateCompat
+
+private const val PROGRESS_MAX = 100
+
+class ProgressChromeClient(
+ private val progressIndicator: BaseProgressIndicator<*>,
+) : WebChromeClient() {
+
+ init {
+ progressIndicator.max = PROGRESS_MAX
+ }
+
+ override fun onProgressChanged(view: WebView?, newProgress: Int) {
+ super.onProgressChanged(view, newProgress)
+ if (!progressIndicator.isVisible) {
+ return
+ }
+ if (newProgress in 1 until PROGRESS_MAX) {
+ progressIndicator.setIndeterminateCompat(false)
+ progressIndicator.setProgressCompat(newProgress.coerceAtMost(PROGRESS_MAX), true)
+ } else {
+ progressIndicator.setIndeterminateCompat(true)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt
index 04b1ba764..89d90665e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt
@@ -10,6 +10,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
+import org.koitharu.kotatsu.suggestions.data.SuggestionDao
+import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
@Database(
entities = [
@@ -35,4 +37,6 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val tracksDao: TracksDao
abstract val trackLogsDao: TrackLogsDao
+
+ abstract val suggestionDao: SuggestionDao
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt
index 0cd94ba37..7f9655d19 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt
@@ -6,8 +6,8 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
abstract class TagsDao {
- @Query("SELECT * FROM tags")
- abstract suspend fun getAllTags(): List
+ @Query("SELECT * FROM tags WHERE source = :source")
+ abstract suspend fun findTags(source: String): List
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(tag: TagEntity): Long
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt
index 3e25b0bed..6b3b6a94b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt
@@ -6,6 +6,7 @@ import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode
+import org.koitharu.kotatsu.utils.ext.toTitleCase
@Entity(tableName = "tags")
class TagEntity(
@@ -18,7 +19,7 @@ class TagEntity(
fun toMangaTag() = MangaTag(
key = this.key,
- title = this.title,
+ title = this.title.toTitleCase(),
source = MangaSource.valueOf(this.source)
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt
deleted file mode 100644
index 498492f24..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.koitharu.kotatsu.core.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class MangaFilter(
- val sortOrder: SortOrder?,
- val tags: Set,
-) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
index 4a2c2be82..f66e4b14f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt
@@ -4,7 +4,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaTag
-import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.SourceSettings
abstract class RemoteMangaRepository(
@@ -20,8 +19,6 @@ abstract class RemoteMangaRepository(
val title: String
get() = source.title
- override val sortOrders: Set get() = emptySet()
-
override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain()
override suspend fun getTags(): Set = emptySet()
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
index e71378eec..beb969daf 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
@@ -237,7 +237,6 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
when {
c == '-' -> {
builder.setCharAt(i, ' ')
- capitalize = true
}
capitalize -> {
builder.setCharAt(i, c.uppercaseChar())
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt
index 82a0a3268..ceb4ee024 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt
@@ -61,7 +61,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
tags = runCatching {
row.selectFirst("div.genre")?.select("a")?.mapToSet {
MangaTag(
- title = it.text(),
+ title = it.text().toTitleCase(),
key = it.attr("href").substringAfterLast('/').urlEncoded(),
source = source
)
@@ -136,7 +136,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
return root.select("li.sidetag").mapToSet { li ->
val a = li.children().last() ?: throw ParseException("a is null")
MangaTag(
- title = a.text().toCamelCase(),
+ title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt
index 0b5fd9b65..308f209b0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt
@@ -85,7 +85,7 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
tags = json.getJSONArray("genres").mapToSet {
MangaTag(
key = it.getString("text"),
- title = it.getString("russian"),
+ title = it.getString("russian").toTitleCase(),
source = manga.source
)
},
@@ -133,7 +133,7 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
MangaTag(
source = source,
key = it.selectFirst("input")?.attr("data-genre") ?: parseFailed(),
- title = it.selectFirst("label")?.text() ?: parseFailed()
+ title = it.selectFirst("label")?.text()?.toTitleCase() ?: parseFailed()
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt
index 41b86750e..8fb6ee6a7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt
@@ -17,6 +17,8 @@ class ExHentaiRepository(
override val source = MangaSource.EXHENTAI
+ override val sortOrders: Set = emptySet()
+
override val defaultDomain: String
get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED
@@ -85,7 +87,7 @@ class ExHentaiRepository(
val tagsDiv = glink.nextElementSibling() ?: parseFailed("tags div not found")
val mainTag = td2.selectFirst("div.cn")?.let { div ->
MangaTag(
- title = div.text(),
+ title = div.text().toTitleCase(),
key = tagIdByClass(div.classNames()) ?: return@let null,
source = source,
)
@@ -181,7 +183,7 @@ class ExHentaiRepository(
val id = div.id().substringAfterLast('_').toIntOrNull()
?: return@mapNotNullToSet null
MangaTag(
- title = div.text(),
+ title = div.text().toTitleCase(),
key = id.toString(),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
index 598a43bf0..c1f49d804 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
@@ -89,7 +89,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
tileInfo?.select("a.element-link")
?.mapToSet {
MangaTag(
- title = it.text(),
+ title = it.text().toTitleCase(),
key = it.attr("href").substringAfterLast('/'),
source = source
)
@@ -119,7 +119,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
.mapNotNull {
val a = it.selectFirst("a.element-link") ?: return@mapNotNull null
MangaTag(
- title = a.text(),
+ title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
@@ -183,7 +183,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
?.selectFirst("table.table") ?: parseFailed("Cannot find root")
return root.select("a.element-link").mapToSet { a ->
MangaTag(
- title = a.text().toCamelCase(),
+ title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt
index 072c7611b..2c25870b9 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt
@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.parseHtml
+import org.koitharu.kotatsu.utils.ext.toTitleCase
class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {
@@ -36,7 +37,7 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
tags = root.selectFirst("div.sidetags")?.select("li.sidetag")?.mapToSet {
val a = it.children().last() ?: parseFailed("Invalid tag")
MangaTag(
- title = a.text(),
+ title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt
index 2b289212b..57e57c025 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt
@@ -94,7 +94,8 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
MangaTag(
title = tag.getJSONObject("attributes")
.getJSONObject("name")
- .firstStringValue(),
+ .firstStringValue()
+ .toTitleCase(),
key = tag.getString("id"),
source = source,
)
@@ -194,7 +195,7 @@ class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
.getJSONArray("data")
return tags.mapToSet { jo ->
MangaTag(
- title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue(),
+ title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue().toTitleCase(),
key = jo.getString("id"),
source = source,
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt
index ed58f073c..8ebbf2c44 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt
@@ -139,7 +139,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
tags = info?.selectFirst("div.media-tags")
?.select("a.media-tag-item")?.mapToSet { a ->
MangaTag(
- title = a.text().toCamelCase(),
+ title = a.text().toTitleCase(),
key = a.attr("href").substringAfterLast('='),
source = source
)
@@ -203,7 +203,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
result += MangaTag(
source = source,
key = x.getInt("id").toString(),
- title = x.getString("name").toCamelCase()
+ title = x.getString("name").toTitleCase(),
)
}
return result
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
index 5e5429d95..a9c0030d2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
@@ -91,7 +91,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
.mapNotNull {
val a = it.selectFirst("a") ?: return@mapNotNull null
MangaTag(
- title = a.text(),
+ title = a.text().toTitleCase(),
key = a.attr("href"),
source = source
)
@@ -144,7 +144,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
return root.mapToSet { p ->
val a = p.selectFirst("a") ?: parseFailed("a is null")
MangaTag(
- title = a.text().toCamelCase(),
+ title = a.text().toTitleCase(),
key = a.attr("href"),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
index afe3750c3..973bb77bb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
@@ -80,7 +80,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
},
tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNullToSet tags@{ x ->
MangaTag(
- title = x.attr("title"),
+ title = x.attr("title").toTitleCase(),
key = x.attr("href").parseTagKey() ?: return@tags null,
source = MangaSource.MANGATOWN
)
@@ -104,7 +104,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
x.selectFirst("b")?.ownText() == "Genre(s):"
}?.select("a")?.mapNotNull { a ->
MangaTag(
- title = a.attr("title"),
+ title = a.attr("title").toTitleCase(),
key = a.attr("href").parseTagKey() ?: return@mapNotNull null,
source = MangaSource.MANGATOWN
)
@@ -172,7 +172,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
MangaTag(
source = MangaSource.MANGATOWN,
key = key,
- title = a.text()
+ title = a.text().toTitleCase()
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt
index 2b2f9b8cd..6aa94cb98 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt
@@ -62,7 +62,7 @@ class MangareadRepository(
tags = summary?.selectFirst(".mg_genres")?.select("a")?.mapToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
- title = a.text(),
+ title = a.text().toTitleCase(),
source = MangaSource.MANGAREAD
)
}.orEmpty(),
@@ -91,7 +91,7 @@ class MangareadRepository(
}
MangaTag(
key = href,
- title = a.text(),
+ title = a.text().toTitleCase(),
source = MangaSource.MANGAREAD
)
}
@@ -113,7 +113,7 @@ class MangareadRepository(
?.mapNotNullToSet { a ->
MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
- title = a.text(),
+ title = a.text().toTitleCase(),
source = MangaSource.MANGAREAD
)
} ?: manga.tags,
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt
index 7b782ab1c..351467882 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt
@@ -94,7 +94,7 @@ abstract class NineMangaRepository(
tags = infoRoot.getElementsByAttributeValue("itemprop", "genre").first()
?.select("a")?.mapToSet { a ->
MangaTag(
- title = a.text(),
+ title = a.text().toTitleCase(),
key = a.attr("href").substringBetween("/", "."),
source = source,
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
index ebea9bc94..d3925d62a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
@@ -73,7 +73,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
author = null,
tags = jo.optJSONArray("genres")?.mapToSet { g ->
MangaTag(
- title = g.getString("name"),
+ title = g.getString("name").toTitleCase(),
key = g.getInt("id").toString(),
source = MangaSource.REMANGA
)
@@ -109,7 +109,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
},
tags = content.getJSONArray("genres").mapToSet { g ->
MangaTag(
- title = g.getString("name"),
+ title = g.getString("name").toTitleCase(),
key = g.getInt("id").toString(),
source = MangaSource.REMANGA
)
@@ -175,7 +175,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
.parseJson().getJSONObject("content").getJSONArray("genres")
return content.mapToSet { jo ->
MangaTag(
- title = jo.getString("name"),
+ title = jo.getString("name").toTitleCase(),
key = jo.getInt("id").toString(),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt
index 64ce67264..0efa45c92 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt
@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs
enum class AppSection {
- LOCAL, FAVOURITES, HISTORY, FEED
+ LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index f20afcc48..c6dfdb5c6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
@@ -142,6 +142,12 @@ class AppSettings(context: Context) {
}
}
+ val isSuggestionsEnabled: Boolean
+ get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
+
+ val isSuggestionsExcludeNsfw: Boolean
+ get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
+
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
NETWORK_ALWAYS -> true
@@ -241,6 +247,8 @@ class AppSettings(context: Context) {
const val KEY_PAGES_NUMBERS = "pages_numbers"
const val KEY_SCREENSHOTS_POLICY = "screenshots_policy"
const val KEY_PAGES_PRELOAD = "pages_preload"
+ const val KEY_SUGGESTIONS = "suggestions"
+ const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
// About
const val KEY_APP_UPDATE = "app_update"
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
index 9ee2642fb..0b973aa64 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
+import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
@@ -22,6 +23,9 @@ abstract class HistoryDao {
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
abstract suspend fun findAllManga(): List
+ @Query("SELECT * FROM tags WHERE tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_id IN (SELECT manga_id FROM history))")
+ abstract suspend fun findAllTags(): List
+
@Query("SELECT * FROM history WHERE manga_id = :id")
abstract suspend fun find(id: Long): HistoryEntity?
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
index 220f06dca..c492e0e0b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
+import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
@@ -89,4 +90,8 @@ class HistoryRepository(
db.historyDao.delete(manga.id)
}
}
+
+ suspend fun getAllTags(): Set {
+ return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
index 97fa18c4c..97664dd2f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt
@@ -85,7 +85,7 @@ class HistoryListViewModel(
val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
var prevDate: DateTimeAgo? = null
if (!grouped) {
- result += ListHeader(null, R.string.history)
+ result += ListHeader(null, R.string.history, null)
}
for ((manga, history) in list) {
if (grouped) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
index a91d3924b..00bd769fd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
@@ -4,13 +4,9 @@ import android.os.Bundle
import android.view.*
import androidx.annotation.CallSuper
import androidx.appcompat.widget.PopupMenu
-import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.GravityCompat
-import androidx.core.view.isGone
-import androidx.core.view.isVisible
import androidx.core.view.updatePadding
-import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -30,8 +26,6 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
-import org.koitharu.kotatsu.list.ui.filter.FilterAdapter2
-import org.koitharu.kotatsu.list.ui.filter.FilterItem
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.main.ui.MainActivity
@@ -43,7 +37,6 @@ abstract class MangaListFragment : BaseFragment(),
SwipeRefreshLayout.OnRefreshListener {
private var listAdapter: MangaListAdapter? = null
- private var filterAdapter: FilterAdapter2? = null
private var paginationListener: PaginationScrollListener? = null
private val spanResolver = MangaListSpanResolver()
private val spanSizeLookup = SpanSizeLookup()
@@ -51,7 +44,6 @@ abstract class MangaListFragment : BaseFragment(),
spanSizeLookup.invalidateCache()
}
open val isSwipeRefreshEnabled = true
- private var drawer: DrawerLayout? = null
protected abstract val viewModel: MangaListViewModel
@@ -67,16 +59,14 @@ abstract class MangaListFragment : BaseFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- drawer = binding.root as? DrawerLayout
- drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
listAdapter = MangaListAdapter(
coil = get(),
lifecycleOwner = viewLifecycleOwner,
clickListener = this,
onRetryClick = ::resolveException,
- onTagRemoveClick = viewModel::onRemoveFilterTag
+ onTagRemoveClick = viewModel::onRemoveFilterTag,
+ onFilterClickListener = this::onFilterClick,
)
- filterAdapter = FilterAdapter2(viewModel)
paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) {
setHasFixedSize(true)
@@ -89,17 +79,12 @@ abstract class MangaListFragment : BaseFragment(),
setOnRefreshListener(this@MangaListFragment)
isEnabled = isSwipeRefreshEnabled
}
- with(binding.recyclerViewFilter) {
- setHasFixedSize(true)
- adapter = filterAdapter
- }
(parentFragment as? RecycledViewPoolHolder)?.let {
binding.recyclerView.setRecycledViewPool(it.recycledViewPool)
}
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
- viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
@@ -107,9 +92,7 @@ abstract class MangaListFragment : BaseFragment(),
}
override fun onDestroyView() {
- drawer = null
listAdapter = null
- filterAdapter = null
paginationListener = null
spanSizeLookup.invalidateCache()
super.onDestroyView()
@@ -125,19 +108,9 @@ abstract class MangaListFragment : BaseFragment(),
ListModeSelectDialog.show(childFragmentManager)
true
}
- R.id.action_filter -> {
- drawer?.toggleDrawer(GravityCompat.END)
- true
- }
else -> super.onOptionsItemSelected(item)
}
- override fun onPrepareOptionsMenu(menu: Menu) {
- menu.findItem(R.id.action_filter).isVisible = drawer != null &&
- drawer?.getDrawerLockMode(GravityCompat.END) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED
- super.onPrepareOptionsMenu(menu)
- }
-
override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item))
}
@@ -200,27 +173,8 @@ abstract class MangaListFragment : BaseFragment(),
}
}
- protected fun onInitFilter(filter: List) {
- filterAdapter?.items = filter
- drawer?.setDrawerLockMode(
- if (filter.isEmpty()) {
- DrawerLayout.LOCK_MODE_LOCKED_CLOSED
- } else {
- DrawerLayout.LOCK_MODE_UNLOCKED
- }
- ) ?: binding.dividerFilter?.let {
- it.isGone = filter.isEmpty()
- binding.recyclerViewFilter.isVisible = it.isVisible
- }
- activity?.invalidateOptionsMenu()
- }
-
override fun onWindowInsetsChanged(insets: Insets) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
- binding.recyclerViewFilter.updatePadding(
- top = headerHeight,
- bottom = insets.bottom
- )
binding.root.updatePadding(
left = insets.left,
right = insets.right
@@ -238,6 +192,8 @@ abstract class MangaListFragment : BaseFragment(),
}
}
+ protected open fun onFilterClick() = Unit
+
private fun onGridScaleChanged(scale: Float) {
spanSizeLookup.invalidateCache()
spanResolver.setGridSize(scale, binding.recyclerView)
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
index e2a463f4e..6a04449b6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
@@ -1,32 +1,22 @@
package org.koitharu.kotatsu.list.ui
-import androidx.annotation.CallSuper
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.*
-import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
-import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
-import org.koitharu.kotatsu.list.domain.AvailableFilters
-import org.koitharu.kotatsu.list.ui.filter.FilterItem
-import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
abstract class MangaListViewModel(
private val settings: AppSettings,
-) : BaseViewModel(), OnFilterChangedListener {
+) : BaseViewModel() {
abstract val content: LiveData>
- val filter = MutableLiveData>()
val listMode = MutableLiveData()
val gridScale = settings.observe()
.filter { it == AppSettings.KEY_GRID_SIZE }
@@ -35,6 +25,8 @@ abstract class MangaListViewModel(
settings.gridSize / 100f
}
+ open fun onRemoveFilterTag(tag: MangaTag) = Unit
+
protected fun createListModeFlow() = settings.observe()
.filter { it == AppSettings.KEY_LIST_MODE }
.map { settings.listMode }
@@ -46,63 +38,6 @@ abstract class MangaListViewModel(
}
}
- protected var currentFilter: MangaFilter = MangaFilter(null, emptySet())
- private set(value) {
- field = value
- onFilterChanged()
- }
- protected var availableFilters: AvailableFilters? = null
- private var filterJob: Job? = null
-
- final override fun onSortItemClick(item: FilterItem.Sort) {
- currentFilter = currentFilter.copy(sortOrder = item.order)
- }
-
- final override fun onTagItemClick(item: FilterItem.Tag) {
- val tags = if (item.isChecked) {
- currentFilter.tags - item.tag
- } else {
- currentFilter.tags + item.tag
- }
- currentFilter = currentFilter.copy(tags = tags)
- }
-
- fun onRemoveFilterTag(tag: MangaTag) {
- val tags = currentFilter.tags
- if (tag !in tags) {
- return
- }
- currentFilter = currentFilter.copy(tags = tags - tag)
- }
-
- @CallSuper
- open fun onFilterChanged() {
- val previousJob = filterJob
- filterJob = launchJob(Dispatchers.Default) {
- previousJob?.cancelAndJoin()
- filter.postValue(
- availableFilters?.run {
- val list = ArrayList(size + 2)
- if (sortOrders.isNotEmpty()) {
- val selectedSort = currentFilter.sortOrder ?: sortOrders.first()
- list += FilterItem.Header(R.string.sort_order)
- sortOrders.sortedBy { it.ordinal }.mapTo(list) {
- FilterItem.Sort(it, isSelected = it == selectedSort)
- }
- }
- if (tags.isNotEmpty()) {
- list += FilterItem.Header(R.string.genres)
- tags.sortedBy { it.title }.mapTo(list) {
- FilterItem.Tag(it, isChecked = it in currentFilter.tags)
- }
- }
- ensureActive()
- list
- }.orEmpty()
- )
- }
- }
-
abstract fun onRefresh()
abstract fun onRetry()
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt
index 4d25060ac..53ac01484 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt
@@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui.adapter
import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.databinding.ItemHeaderWithFilterBinding
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
-fun listHeaderAD() = adapterDelegate(R.layout.item_header) {
+fun listHeaderAD() = adapterDelegate(
+ layout = R.layout.item_header,
+ on = { item, _, _ -> item is ListHeader && item.sortOrder == null },
+) {
bind {
val textView = (itemView as TextView)
@@ -16,4 +21,25 @@ fun listHeaderAD() = adapterDelegate(R.layout.item_header
textView.setText(item.textRes)
}
}
+}
+
+fun listHeaderWithFilterAD(
+ onFilterClickListener: () -> Unit,
+) = adapterDelegateViewBinding(
+ viewBinding = { inflater, parent -> ItemHeaderWithFilterBinding.inflate(inflater, parent, false) },
+ on = { item, _, _ -> item is ListHeader && item.sortOrder != null },
+) {
+
+ binding.textViewFilter.setOnClickListener {
+ onFilterClickListener()
+ }
+
+ bind {
+ if (item.text != null) {
+ binding.textViewTitle.text = item.text
+ } else {
+ binding.textViewTitle.setText(item.textRes)
+ }
+ binding.textViewFilter.setText(requireNotNull(item.sortOrder).titleRes)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
index 61cd60c03..714f04473 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
@@ -20,6 +20,7 @@ class MangaListAdapter(
clickListener: OnListItemClickListener,
onRetryClick: (Throwable) -> Unit,
onTagRemoveClick: (MangaTag) -> Unit,
+ onFilterClickListener: () -> Unit,
) : AsyncListDifferDelegationAdapter(DiffCallback()) {
init {
@@ -41,6 +42,7 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD())
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick))
+ .addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(onFilterClickListener))
}
private class DiffCallback : DiffUtil.ItemCallback() {
@@ -79,5 +81,6 @@ class MangaListAdapter(
const val ITEM_TYPE_EMPTY = 8
const val ITEM_TYPE_HEADER = 9
const val ITEM_TYPE_FILTER = 10
+ const val ITEM_TYPE_HEADER_FILTER = 11
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
similarity index 81%
rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt
rename to app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
index 67b4d3585..19b3f11f7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.list.ui.filter
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
-class FilterAdapter2(
+class FilterAdapter(
listener: OnFilterChangedListener,
) : AsyncListDifferDelegationAdapter(
FilterDiffCallback(),
filterSortDelegate(listener),
filterTagDelegate(listener),
filterHeaderDelegate(),
+ filterLoadingDelegate(),
+ filterErrorDelegate(),
)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt
index 8b926d768..073de2c9d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt
@@ -1,6 +1,9 @@
package org.koitharu.kotatsu.list.ui.filter
+import android.widget.TextView
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
@@ -44,4 +47,13 @@ fun filterHeaderDelegate() = adapterDelegateViewBinding(R.layout.item_loading_footer) {}
+
+fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) {
+
+ bind {
+ (itemView as TextView).setText(item.textResId)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt
new file mode 100644
index 000000000..70aef4326
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt
@@ -0,0 +1,84 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentManager
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from
+import org.koin.androidx.viewmodel.ext.android.sharedViewModel
+import org.koin.core.parameter.parametersOf
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.ui.BaseBottomSheet
+import org.koitharu.kotatsu.core.model.MangaSource
+import org.koitharu.kotatsu.databinding.SheetFilterBinding
+import org.koitharu.kotatsu.utils.ext.withArgs
+
+class FilterBottomSheet : BaseBottomSheet() {
+
+ private val viewModel by sharedViewModel(
+ owner = { from(requireParentFragment(), requireParentFragment()) }
+ ) {
+ parametersOf(
+ requireArguments().getParcelable(ARG_SOURCE),
+ requireArguments().getParcelable(ARG_STATE),
+ )
+ }
+
+ override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
+ return SheetFilterBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.toolbar.setNavigationOnClickListener { dismiss() }
+ if (!resources.getBoolean(R.bool.is_tablet)) {
+ binding.toolbar.navigationIcon = null
+ }
+ val adapter = FilterAdapter(viewModel)
+ binding.recyclerView.adapter = adapter
+ viewModel.filter.observe(viewLifecycleOwner, adapter::setItems)
+ viewModel.result.observe(viewLifecycleOwner) {
+ parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(ARG_STATE to it))
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also {
+ val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also
+ behavior.addBottomSheetCallback(
+ object : BottomSheetBehavior.BottomSheetCallback() {
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
+
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_EXPANDED) {
+ binding.toolbar.setNavigationIcon(R.drawable.ic_cross)
+ } else {
+ binding.toolbar.navigationIcon = null
+ }
+ }
+ }
+ )
+ }
+
+ companion object {
+
+ const val REQUEST_KEY = "filter"
+
+ const val ARG_STATE = "state"
+ private const val TAG = "FilterBottomSheet"
+ private const val ARG_SOURCE = "source"
+
+ fun show(
+ fm: FragmentManager,
+ source: MangaSource,
+ state: FilterState,
+ ) = FilterBottomSheet().withArgs(2) {
+ putParcelable(ARG_SOURCE, source)
+ putParcelable(ARG_STATE, state)
+ }.show(fm, TAG)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt
index 1ccd4e813..73e3db315 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt
@@ -6,6 +6,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when {
+ oldItem === newItem -> true
oldItem.javaClass != newItem.javaClass -> false
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
oldItem.titleResId == newItem.titleResId
@@ -16,13 +17,18 @@ class FilterDiffCallback : DiffUtil.ItemCallback() {
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
oldItem.order == newItem.order
}
+ oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
+ oldItem.textResId == newItem.textResId
+ }
else -> false
}
}
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when {
+ oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
oldItem is FilterItem.Header && newItem is FilterItem.Header -> true
+ oldItem is FilterItem.Error && newItem is FilterItem.Error -> true
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
oldItem.isChecked == newItem.isChecked
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt
index a74d93b1d..75b29e60d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt
@@ -19,4 +19,10 @@ sealed interface FilterItem {
val tag: MangaTag,
val isChecked: Boolean,
) : FilterItem
+
+ object Loading : FilterItem
+
+ class Error(
+ @StringRes val textResId: Int,
+ ) : FilterItem
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt
new file mode 100644
index 000000000..1c1c8a9cf
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt
@@ -0,0 +1,12 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import org.koitharu.kotatsu.core.model.MangaTag
+import org.koitharu.kotatsu.core.model.SortOrder
+
+@Parcelize
+class FilterState(
+ val sortOrder: SortOrder?,
+ val tags: Set,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt
new file mode 100644
index 000000000..06c4b029e
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterViewModel.kt
@@ -0,0 +1,114 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.*
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.domain.MangaDataRepository
+import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.core.model.MangaTag
+import org.koitharu.kotatsu.core.model.SortOrder
+import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import java.util.*
+
+class FilterViewModel(
+ private val repository: RemoteMangaRepository,
+ dataRepository: MangaDataRepository,
+ state: FilterState,
+) : BaseViewModel(), OnFilterChangedListener {
+
+ val filter = MutableLiveData>()
+ val result = MutableLiveData()
+ private var job: Job? = null
+ private var selectedSortOrder: SortOrder? = state.sortOrder
+ private val selectedTags = HashSet(state.tags)
+ private val localTagsDeferred = viewModelScope.async(Dispatchers.Default) {
+ dataRepository.findTags(repository.source)
+ }
+ private var availableTagsDeferred = loadTagsAsync()
+
+ init {
+ showFilter()
+ }
+
+ override fun onSortItemClick(item: FilterItem.Sort) {
+ selectedSortOrder = item.order
+ updateFilters()
+ }
+
+ override fun onTagItemClick(item: FilterItem.Tag) {
+ val isModified = if (item.isChecked) {
+ selectedTags.remove(item.tag)
+ } else {
+ selectedTags.add(item.tag)
+ }
+ if (isModified) {
+ updateFilters()
+ }
+ }
+
+ private fun updateFilters() {
+ val previousJob = job
+ job = launchJob(Dispatchers.Default) {
+ previousJob?.cancelAndJoin()
+ val tags = tryLoadTags()
+ val localTags = localTagsDeferred.await()
+ val sortOrders = repository.sortOrders
+ val list = ArrayList(sortOrders.size + (tags?.size ?: 1) + 2)
+ list.add(FilterItem.Header(R.string.sort_order))
+ sortOrders.sortedBy { it.ordinal }.mapTo(list) {
+ FilterItem.Sort(it, isSelected = it == selectedSortOrder)
+ }
+ if (tags == null || tags.isNotEmpty() || selectedTags.isNotEmpty()) {
+ list.add(FilterItem.Header(R.string.genres))
+ val mappedTags = TreeSet(compareBy({ !it.isChecked }, { it.tag.title }))
+ localTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) }
+ tags?.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = it in selectedTags) }
+ selectedTags.mapTo(mappedTags) { FilterItem.Tag(it, isChecked = true) }
+ list.addAll(mappedTags)
+ if (tags == null) {
+ list.add(FilterItem.Error(R.string.filter_load_error))
+ }
+ }
+ ensureActive()
+ filter.postValue(list)
+ }
+ result.value = FilterState(selectedSortOrder, selectedTags)
+ }
+
+ private fun showFilter() {
+ job = launchJob(Dispatchers.Default) {
+ val sortOrders = repository.sortOrders
+ val list = ArrayList(sortOrders.size + selectedTags.size + 3)
+ list.add(FilterItem.Header(R.string.sort_order))
+ sortOrders.sortedBy { it.ordinal }.mapTo(list) {
+ FilterItem.Sort(it, isSelected = it == selectedSortOrder)
+ }
+ if (selectedTags.isNotEmpty()) {
+ list.add(FilterItem.Header(R.string.genres))
+ selectedTags.sortedBy { it.title }.mapTo(list) {
+ FilterItem.Tag(it, isChecked = it in selectedTags)
+ }
+ }
+ list.add(FilterItem.Loading)
+ filter.postValue(list)
+ updateFilters()
+ }
+ }
+
+ private suspend fun tryLoadTags(): Set? {
+ val shouldRetryOnError = availableTagsDeferred.isCompleted
+ val result = availableTagsDeferred.await()
+ if (result == null && shouldRetryOnError) {
+ availableTagsDeferred = loadTagsAsync()
+ return availableTagsDeferred.await()
+ }
+ return result
+ }
+
+ private fun loadTagsAsync() = viewModelScope.async(Dispatchers.Default) {
+ kotlin.runCatching {
+ repository.getTags()
+ }.getOrNull()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt
index 209c7227f..a14db0f3a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt
@@ -1,8 +1,10 @@
package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.StringRes
+import org.koitharu.kotatsu.core.model.SortOrder
data class ListHeader(
val text: CharSequence?,
@StringRes val textRes: Int,
+ val sortOrder: SortOrder?,
) : ListModel
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
index f678b83b7..791e9985a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
@@ -7,10 +7,7 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
-import org.koitharu.kotatsu.utils.ext.getBooleanOrDefault
-import org.koitharu.kotatsu.utils.ext.getLongOrDefault
-import org.koitharu.kotatsu.utils.ext.getStringOrNull
-import org.koitharu.kotatsu.utils.ext.mapToSet
+import org.koitharu.kotatsu.utils.ext.*
class MangaIndex(source: String?) {
@@ -61,7 +58,7 @@ class MangaIndex(source: String?) {
description = json.getStringOrNull("description"),
tags = json.getJSONArray("tags").mapToSet { x ->
MangaTag(
- title = x.getString("title"),
+ title = x.getString("title").toTitleCase(),
key = x.getString("key"),
source = source
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
index 3f721355a..b68af3a0a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
@@ -32,7 +32,7 @@ class LocalListViewModel(
val importProgress = MutableLiveData