Merge branch 'devel' into feature/page-preload
This commit is contained in:
@@ -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'
|
||||
}
|
||||
@@ -64,6 +64,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.reader.readerModule
|
||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
||||
import org.koitharu.kotatsu.search.searchModule
|
||||
import org.koitharu.kotatsu.settings.settingsModule
|
||||
import org.koitharu.kotatsu.suggestions.suggestionsModule
|
||||
import org.koitharu.kotatsu.tracker.trackerModule
|
||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
||||
@@ -67,6 +68,7 @@ class KotatsuApp : Application() {
|
||||
settingsModule,
|
||||
readerModule,
|
||||
appWidgetModule,
|
||||
suggestionsModule,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
class MangaDataRepository(private val db: MangaDatabase) {
|
||||
|
||||
@@ -45,4 +48,10 @@ class MangaDataRepository(private val db: MangaDatabase) {
|
||||
db.mangaDao.upsert(MangaEntity.from(manga), tags)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findTags(source: MangaSource): Set<MangaTag> {
|
||||
return db.tagsDao.findTags(source.name).mapToSet {
|
||||
it.toMangaTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ abstract class BaseViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||
protected fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), 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<ActivityBrowserBinding>(), 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<ActivityBrowserBinding>(), BrowserCallback
|
||||
binding.webView.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding.webView.destroy()
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<TagEntity>
|
||||
@Query("SELECT * FROM tags WHERE source = :source")
|
||||
abstract suspend fun findTags(source: String): List<TagEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(tag: TagEntity): Long
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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<MangaTag>,
|
||||
) : Parcelable
|
||||
@@ -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<SortOrder> get() = emptySet()
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain()
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> = emptySet()
|
||||
|
||||
@@ -237,7 +237,6 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
|
||||
when {
|
||||
c == '-' -> {
|
||||
builder.setCharAt(i, ' ')
|
||||
capitalize = true
|
||||
}
|
||||
capitalize -> {
|
||||
builder.setCharAt(i, c.uppercaseChar())
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ class ExHentaiRepository(
|
||||
|
||||
override val source = MangaSource.EXHENTAI
|
||||
|
||||
override val sortOrders: Set<SortOrder> = 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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
enum class AppSection {
|
||||
|
||||
LOCAL, FAVOURITES, HISTORY, FEED
|
||||
LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<MangaEntity>
|
||||
|
||||
@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<TagEntity>
|
||||
|
||||
@Query("SELECT * FROM history WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): HistoryEntity?
|
||||
|
||||
|
||||
@@ -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<MangaTag> {
|
||||
return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() }
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ class HistoryListViewModel(
|
||||
val result = ArrayList<ListModel>(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) {
|
||||
|
||||
@@ -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<FragmentListBinding>(),
|
||||
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<FragmentListBinding>(),
|
||||
spanSizeLookup.invalidateCache()
|
||||
}
|
||||
open val isSwipeRefreshEnabled = true
|
||||
private var drawer: DrawerLayout? = null
|
||||
|
||||
protected abstract val viewModel: MangaListViewModel
|
||||
|
||||
@@ -67,16 +59,14 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
|
||||
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<FragmentListBinding>(),
|
||||
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<FragmentListBinding>(),
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
drawer = null
|
||||
listAdapter = null
|
||||
filterAdapter = null
|
||||
paginationListener = null
|
||||
spanSizeLookup.invalidateCache()
|
||||
super.onDestroyView()
|
||||
@@ -125,19 +108,9 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
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<FragmentListBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
protected fun onInitFilter(filter: List<FilterItem>) {
|
||||
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<FragmentListBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onFilterClick() = Unit
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver.setGridSize(scale, binding.recyclerView)
|
||||
|
||||
@@ -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<List<ListModel>>
|
||||
val filter = MutableLiveData<List<FilterItem>>()
|
||||
val listMode = MutableLiveData<ListMode>()
|
||||
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<FilterItem>(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()
|
||||
|
||||
@@ -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<ListHeader, ListModel>(R.layout.item_header) {
|
||||
fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>(
|
||||
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<ListHeader, ListModel>(R.layout.item_header
|
||||
textView.setText(item.textRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun listHeaderWithFilterAD(
|
||||
onFilterClickListener: () -> Unit,
|
||||
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderWithFilterBinding>(
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class MangaListAdapter(
|
||||
clickListener: OnListItemClickListener<Manga>,
|
||||
onRetryClick: (Throwable) -> Unit,
|
||||
onTagRemoveClick: (MangaTag) -> Unit,
|
||||
onFilterClickListener: () -> Unit,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(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<ListModel>() {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.list.ui.filter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
|
||||
class FilterAdapter2(
|
||||
class FilterAdapter(
|
||||
listener: OnFilterChangedListener,
|
||||
) : AsyncListDifferDelegationAdapter<FilterItem>(
|
||||
FilterDiffCallback(),
|
||||
filterSortDelegate(listener),
|
||||
filterTagDelegate(listener),
|
||||
filterHeaderDelegate(),
|
||||
filterLoadingDelegate(),
|
||||
filterErrorDelegate(),
|
||||
)
|
||||
@@ -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<FilterItem.Header, Filte
|
||||
bind {
|
||||
binding.root.setText(item.titleResId)
|
||||
}
|
||||
}
|
||||
|
||||
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
|
||||
|
||||
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).setText(item.textResId)
|
||||
}
|
||||
}
|
||||
@@ -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<SheetFilterBinding>() {
|
||||
|
||||
private val viewModel by sharedViewModel<FilterViewModel>(
|
||||
owner = { from(requireParentFragment(), requireParentFragment()) }
|
||||
) {
|
||||
parametersOf(
|
||||
requireArguments().getParcelable<MangaSource>(ARG_SOURCE),
|
||||
requireArguments().getParcelable<FilterState>(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)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
|
||||
|
||||
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<FilterItem>() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -19,4 +19,10 @@ sealed interface FilterItem {
|
||||
val tag: MangaTag,
|
||||
val isChecked: Boolean,
|
||||
) : FilterItem
|
||||
|
||||
object Loading : FilterItem
|
||||
|
||||
class Error(
|
||||
@StringRes val textResId: Int,
|
||||
) : FilterItem
|
||||
}
|
||||
|
||||
@@ -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<MangaTag>,
|
||||
) : Parcelable
|
||||
@@ -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<List<FilterItem>>()
|
||||
val result = MutableLiveData<FilterState>()
|
||||
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<FilterItem>(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<FilterItem.Tag>(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<FilterItem>(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<MangaTag>? {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ class LocalListViewModel(
|
||||
val importProgress = MutableLiveData<Progress?>(null)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val headerModel = ListHeader(null, R.string.local_storage)
|
||||
private val headerModel = ListHeader(null, R.string.local_storage, null)
|
||||
private var importJob: Job? = null
|
||||
|
||||
override val content = combine(
|
||||
|
||||
@@ -49,6 +49,8 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
import org.koitharu.kotatsu.tracker.ui.FeedFragment
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
@@ -122,6 +124,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.remoteSources.observe(this, this::updateSideMenu)
|
||||
viewModel.isSuggestionsEnabled.observe(this, this::setSuggestionsEnabled)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
@@ -187,6 +190,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
viewModel.defaultSection = AppSection.LOCAL
|
||||
setPrimaryFragment(LocalListFragment.newInstance())
|
||||
}
|
||||
R.id.nav_suggestions -> {
|
||||
viewModel.defaultSection = AppSection.SUGGESTIONS
|
||||
setPrimaryFragment(SuggestionsFragment.newInstance())
|
||||
}
|
||||
R.id.nav_feed -> {
|
||||
viewModel.defaultSection = AppSection.FEED
|
||||
setPrimaryFragment(FeedFragment.newInstance())
|
||||
@@ -285,7 +292,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
if (isLoading) {
|
||||
binding.fab.setImageDrawable(CircularProgressDrawable(this).also {
|
||||
it.setColorSchemeColors(R.color.kotatsu_onPrimaryContainer)
|
||||
it.strokeWidth = resources.resolveDp(2f)
|
||||
it.strokeWidth = resources.resolveDp(3.5f)
|
||||
it.start()
|
||||
})
|
||||
} else {
|
||||
@@ -303,6 +310,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
submenu.setGroupCheckable(R.id.group_remote_sources, true, true)
|
||||
}
|
||||
|
||||
private fun setSuggestionsEnabled(isEnabled: Boolean) {
|
||||
val item = binding.navigationView.menu.findItem(R.id.nav_suggestions) ?: return
|
||||
if (!isEnabled && item.isChecked) {
|
||||
binding.navigationView.setCheckedItem(R.id.nav_history)
|
||||
}
|
||||
item.isVisible = isEnabled
|
||||
}
|
||||
|
||||
private fun openDefaultSection() {
|
||||
when (viewModel.defaultSection) {
|
||||
AppSection.LOCAL -> {
|
||||
@@ -321,6 +336,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
binding.navigationView.setCheckedItem(R.id.nav_feed)
|
||||
setPrimaryFragment(FeedFragment.newInstance())
|
||||
}
|
||||
AppSection.SUGGESTIONS -> {
|
||||
binding.navigationView.setCheckedItem(R.id.nav_suggestions)
|
||||
setPrimaryFragment(SuggestionsFragment.newInstance())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +363,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
private fun onFirstStart() {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
AppUpdateChecker(this@MainActivity).checkIfNeeded()
|
||||
if (!get<AppSettings>().isSourcesSelected) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -21,6 +21,12 @@ class MainViewModel(
|
||||
val onOpenReader = SingleLiveEvent<Manga>()
|
||||
var defaultSection by settings::defaultSection
|
||||
|
||||
val isSuggestionsEnabled = settings.observe()
|
||||
.filter { it == AppSettings.KEY_SUGGESTIONS }
|
||||
.onStart { emit("") }
|
||||
.map { settings.isSuggestionsEnabled }
|
||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val remoteSources = settings.observe()
|
||||
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
|
||||
.onStart { emit("") }
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.SheetChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemClickListener<ChapterListItem> {
|
||||
|
||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersBinding {
|
||||
return SheetChaptersBinding.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
|
||||
}
|
||||
binding.recyclerView.addItemDecoration(
|
||||
MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL)
|
||||
)
|
||||
val chapters = arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
}
|
||||
val currentId = requireArguments().getLong(ARG_CURRENT_ID, 0L)
|
||||
val currentPosition = chapters.indexOfFirst { it.id == currentId }
|
||||
val dateFormat = get<AppSettings>().getDateFormat()
|
||||
val items = chapters.mapIndexed { index, chapter ->
|
||||
chapter.toListItem(
|
||||
isCurrent = index == currentPosition,
|
||||
isUnread = index > currentPosition,
|
||||
isNew = false,
|
||||
isMissing = false,
|
||||
isDownloaded = false,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter ->
|
||||
if (currentPosition >= 0) {
|
||||
val targetPosition = (currentPosition - 1).coerceAtLeast(0)
|
||||
adapter.setItems(items, Scroller(binding.recyclerView, targetPosition))
|
||||
} else {
|
||||
adapter.items = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
||||
((parentFragment as? OnChapterChangeListener) ?: (activity as? OnChapterChangeListener))?.let {
|
||||
dismiss()
|
||||
it.onChapterChanged(item.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun interface OnChapterChangeListener {
|
||||
|
||||
fun onChapterChanged(chapter: MangaChapter)
|
||||
}
|
||||
|
||||
private class Scroller(private val recyclerView: RecyclerView, private val position: Int) : Runnable {
|
||||
override fun run() {
|
||||
val offset = recyclerView.resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) / 2
|
||||
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(position, offset)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_CHAPTERS = "chapters"
|
||||
private const val ARG_CURRENT_ID = "current_id"
|
||||
|
||||
private const val TAG = "ChaptersBottomSheet"
|
||||
|
||||
fun show(
|
||||
fm: FragmentManager,
|
||||
chapters: List<MangaChapter>,
|
||||
currentId: Long,
|
||||
) = ChaptersBottomSheet().withArgs(2) {
|
||||
putParcelableArrayList(ARG_CHAPTERS, chapters.asArrayList())
|
||||
putLong(ARG_CURRENT_ID, currentId)
|
||||
}.show(fm, TAG)
|
||||
|
||||
private fun <T> List<T>.asArrayList(): ArrayList<T> {
|
||||
return this as? ArrayList<T> ?: ArrayList(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.DialogChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
||||
OnListItemClickListener<ChapterListItem> {
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = DialogChaptersBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
|
||||
builder.setTitle(R.string.chapters)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setCancelable(true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.recyclerViewChapters.addItemDecoration(
|
||||
MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL)
|
||||
)
|
||||
val chapters = arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)
|
||||
if (chapters == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
}
|
||||
val currentId = arguments?.getLong(ARG_CURRENT_ID, 0L) ?: 0L
|
||||
val currentPosition = chapters.indexOfFirst { it.id == currentId }
|
||||
val dateFormat = get<AppSettings>().getDateFormat()
|
||||
binding.recyclerViewChapters.adapter = ChaptersAdapter(this).apply {
|
||||
setItems(chapters.mapIndexed { index, chapter ->
|
||||
chapter.toListItem(
|
||||
isCurrent = index == currentPosition,
|
||||
isUnread = index > currentPosition,
|
||||
isNew = false,
|
||||
isMissing = false,
|
||||
isDownloaded = false,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}) {
|
||||
if (currentPosition >= 0) {
|
||||
with(binding.recyclerViewChapters) {
|
||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||
currentPosition,
|
||||
height / 3
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
||||
((parentFragment as? OnChapterChangeListener)
|
||||
?: (activity as? OnChapterChangeListener))?.let {
|
||||
dismiss()
|
||||
it.onChapterChanged(item.chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun interface OnChapterChangeListener {
|
||||
|
||||
fun onChapterChanged(chapter: MangaChapter)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "ChaptersDialog"
|
||||
|
||||
private const val ARG_CHAPTERS = "chapters"
|
||||
private const val ARG_CURRENT_ID = "current_id"
|
||||
|
||||
fun show(fm: FragmentManager, chapters: List<MangaChapter>, currentId: Long = 0L) =
|
||||
ChaptersDialog().withArgs(2) {
|
||||
putParcelableArrayList(ARG_CHAPTERS, ArrayList(chapters))
|
||||
putLong(ARG_CURRENT_ID, currentId)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ import org.koitharu.kotatsu.utils.anim.Motion
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
ChaptersDialog.OnChapterChangeListener,
|
||||
ChaptersBottomSheet.OnChapterChangeListener,
|
||||
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
|
||||
ActivityResultCallback<Boolean>, ReaderControlDelegate.OnInteractionListener {
|
||||
|
||||
@@ -152,7 +152,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
||||
startActivity(SimpleSettingsActivity.newReaderSettingsIntent(this))
|
||||
}
|
||||
R.id.action_chapters -> {
|
||||
ChaptersDialog.show(
|
||||
ChaptersBottomSheet.show(
|
||||
supportFragmentManager,
|
||||
viewModel.manga?.chapters.orEmpty(),
|
||||
viewModel.getCurrentState()?.chapterId ?: 0L
|
||||
|
||||
@@ -14,10 +14,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding
|
||||
import org.koitharu.kotatsu.settings.MainSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.NetworkSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.ReaderSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.*
|
||||
|
||||
class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
|
||||
|
||||
@@ -27,9 +24,11 @@ class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportFragmentManager.commit {
|
||||
replace(
|
||||
R.id.container, when (intent?.action) {
|
||||
R.id.container,
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment()
|
||||
ACTION_READER -> ReaderSettingsFragment()
|
||||
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
intent.getParcelableExtra(EXTRA_SOURCE) ?: MangaSource.LOCAL
|
||||
)
|
||||
@@ -55,6 +54,8 @@ class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
|
||||
|
||||
private const val ACTION_READER =
|
||||
"${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
|
||||
private const val ACTION_SUGGESTIONS =
|
||||
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
||||
private const val ACTION_SOURCE =
|
||||
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
@@ -63,6 +64,10 @@ class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
|
||||
Intent(context, SimpleSettingsActivity::class.java)
|
||||
.setAction(ACTION_READER)
|
||||
|
||||
fun newSuggestionsSettingsIntent(context: Context) =
|
||||
Intent(context, SimpleSettingsActivity::class.java)
|
||||
.setAction(ACTION_SUGGESTIONS)
|
||||
|
||||
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
|
||||
Intent(context, SimpleSettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCE)
|
||||
|
||||
@@ -4,12 +4,26 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterViewModel
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||
|
||||
val remoteListModule
|
||||
get() = module {
|
||||
|
||||
viewModel { source ->
|
||||
RemoteListViewModel(get(named(source.get<MangaSource>())), get())
|
||||
viewModel { params ->
|
||||
RemoteListViewModel(
|
||||
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
|
||||
settings = get(),
|
||||
)
|
||||
}
|
||||
|
||||
viewModel { params ->
|
||||
FilterViewModel(
|
||||
repository = get<MangaRepository>(named(params.get<MangaSource>())) as RemoteMangaRepository,
|
||||
dataRepository = get(),
|
||||
state = params.get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
package org.koitharu.kotatsu.remotelist.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet
|
||||
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.parcelableArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class RemoteListFragment : MangaListFragment() {
|
||||
class RemoteListFragment : MangaListFragment(), FragmentResultListener {
|
||||
|
||||
override val viewModel by viewModel<RemoteListViewModel> {
|
||||
parametersOf(source)
|
||||
@@ -20,6 +24,11 @@ class RemoteListFragment : MangaListFragment() {
|
||||
|
||||
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
childFragmentManager.setFragmentResultListener(FilterBottomSheet.REQUEST_KEY, viewLifecycleOwner, this)
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
@@ -44,10 +53,26 @@ class RemoteListFragment : MangaListFragment() {
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.action_filter -> {
|
||||
onFilterClick()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilterClick() {
|
||||
FilterBottomSheet.show(childFragmentManager, source, viewModel.filter)
|
||||
}
|
||||
|
||||
override fun onFragmentResult(requestKey: String, result: Bundle) {
|
||||
when (requestKey) {
|
||||
FilterBottomSheet.REQUEST_KEY -> viewModel.applyFilter(
|
||||
result.getParcelable(FilterBottomSheet.ARG_STATE) ?: return
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_SOURCE = "provider"
|
||||
|
||||
@@ -9,38 +9,43 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.domain.AvailableFilters
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterState
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
|
||||
class RemoteListViewModel(
|
||||
private val repository: MangaRepository,
|
||||
private val repository: RemoteMangaRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
var filter = FilterState(repository.sortOrders.firstOrNull(), emptySet())
|
||||
private set
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private var loadingJob: Job? = null
|
||||
private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0)
|
||||
private val headerModel = MutableStateFlow(
|
||||
ListHeader(repository.title, 0, filter.sortOrder)
|
||||
)
|
||||
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
headerModel,
|
||||
listError,
|
||||
hasNextPage
|
||||
) { list, mode, error, hasNext ->
|
||||
) { list, mode, header, error, hasNext ->
|
||||
when {
|
||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string.empty))
|
||||
else -> {
|
||||
val result = ArrayList<ListModel>(list.size + 3)
|
||||
result += headerModel
|
||||
result += header
|
||||
createFilterModel()?.let { result.add(it) }
|
||||
list.toUi(result, mode)
|
||||
when {
|
||||
@@ -54,7 +59,6 @@ class RemoteListViewModel(
|
||||
|
||||
init {
|
||||
loadList(false)
|
||||
loadFilter()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
@@ -65,12 +69,28 @@ class RemoteListViewModel(
|
||||
loadList(append = !mangaList.value.isNullOrEmpty())
|
||||
}
|
||||
|
||||
override fun onRemoveFilterTag(tag: MangaTag) {
|
||||
val tags = filter.tags
|
||||
if (tag !in tags) {
|
||||
return
|
||||
}
|
||||
applyFilter(FilterState(filter.sortOrder, tags - tag))
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (hasNextPage.value && listError.value == null) {
|
||||
loadList(append = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyFilter(newFilter: FilterState) {
|
||||
filter = newFilter
|
||||
headerModel.value = ListHeader(repository.title, 0, newFilter.sortOrder)
|
||||
mangaList.value = null
|
||||
hasNextPage.value = false
|
||||
loadList(false)
|
||||
}
|
||||
|
||||
private fun loadList(append: Boolean) {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
@@ -80,8 +100,8 @@ class RemoteListViewModel(
|
||||
listError.value = null
|
||||
val list = repository.getList2(
|
||||
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
||||
sortOrder = currentFilter.sortOrder,
|
||||
tags = currentFilter.tags,
|
||||
sortOrder = filter.sortOrder,
|
||||
tags = filter.tags,
|
||||
)
|
||||
if (!append) {
|
||||
mangaList.value = list
|
||||
@@ -98,34 +118,12 @@ class RemoteListViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilterChanged() {
|
||||
super.onFilterChanged()
|
||||
mangaList.value = null
|
||||
hasNextPage.value = false
|
||||
loadList(false)
|
||||
}
|
||||
|
||||
private fun createFilterModel(): CurrentFilterModel? {
|
||||
val tags = currentFilter.tags
|
||||
val tags = filter.tags
|
||||
return if (tags.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFilter() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
val sorts = repository.sortOrders
|
||||
val tags = repository.getTags()
|
||||
availableFilters = AvailableFilters(sorts, tags)
|
||||
onFilterChanged()
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import kotlinx.coroutines.launch
|
||||
import leakcanary.LeakCanary
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -56,7 +56,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
entryValues = ListMode.values().names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
|
||||
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
|
||||
AppSettings.isDynamicColorAvailable
|
||||
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
|
||||
entryValues = arrayOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd", "dd MMM yyyy", "MMM dd, yyyy")
|
||||
@@ -72,12 +72,15 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
setDefaultValueCompat("")
|
||||
summary = "%s"
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
|
||||
!settings.appPassword.isNullOrEmpty()
|
||||
settings.subscribe(this)
|
||||
}
|
||||
@@ -114,15 +117,20 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_HIDE_TOOLBAR -> {
|
||||
findPreference<SwitchPreference>(key)?.setSummary(R.string.restart_required)
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
AppSettings.KEY_APP_PASSWORD -> {
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
AppSettings.KEY_SUGGESTIONS -> {
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +156,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
val pref = (preference as? SwitchPreference ?: return false)
|
||||
val pref = (preference as? TwoStatePreference ?: return false)
|
||||
if (pref.isChecked) {
|
||||
pref.isChecked = false
|
||||
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
|
||||
class SuggestionsSettingsFragment : BasePreferenceFragment(R.string.suggestions),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val repository by inject<SuggestionRepository>(mode = LazyThreadSafetyMode.NONE)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_suggestions)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
settings.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == AppSettings.KEY_SUGGESTIONS && settings.isSuggestionsEnabled) {
|
||||
onSuggestionsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSuggestionsEnabled() {
|
||||
lifecycleScope.launch {
|
||||
if (repository.isEmpty()) {
|
||||
SuggestionsWorker.startNow(context ?: return@launch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.suggestions
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsViewModel
|
||||
|
||||
val suggestionsModule
|
||||
get() = module {
|
||||
|
||||
factory { SuggestionRepository(get()) }
|
||||
|
||||
viewModel { SuggestionsViewModel(get(), get()) }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.koitharu.kotatsu.suggestions.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class SuggestionDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM suggestions ORDER BY relevance DESC")
|
||||
abstract fun observeAll(): Flow<List<SuggestionWithManga>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM suggestions")
|
||||
abstract suspend fun count(): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(entity: SuggestionEntity): Long
|
||||
|
||||
@Update
|
||||
abstract suspend fun update(entity: SuggestionEntity): Int
|
||||
|
||||
@Query("DELETE FROM suggestions")
|
||||
abstract suspend fun deleteAll()
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(entity: SuggestionEntity) {
|
||||
if (update(entity) == 0) {
|
||||
insert(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.suggestions.data
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "suggestions",
|
||||
@@ -19,6 +21,7 @@ import androidx.room.PrimaryKey
|
||||
class SuggestionEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
@ColumnInfo(name = "relevance") val relevance: Float,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.suggestions.data
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
data class SuggestionWithManga(
|
||||
@Embedded val suggestion: SuggestionEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "manga_id"
|
||||
)
|
||||
val manga: MangaEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "tag_id",
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.suggestions.domain
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
|
||||
data class MangaSuggestion(
|
||||
val manga: Manga,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
val relevance: Float,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.suggestions.domain
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
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.suggestions.data.SuggestionEntity
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
class SuggestionRepository(
|
||||
private val db: MangaDatabase,
|
||||
) {
|
||||
|
||||
fun observeAll(): Flow<List<Manga>> {
|
||||
return db.suggestionDao.observeAll().mapItems {
|
||||
it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
db.suggestionDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun isEmpty(): Boolean {
|
||||
return db.suggestionDao.count() == 0
|
||||
}
|
||||
|
||||
suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
|
||||
db.withTransaction {
|
||||
db.suggestionDao.deleteAll()
|
||||
suggestions.forEach { x ->
|
||||
val tags = x.manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(MangaEntity.from(x.manga), tags)
|
||||
db.suggestionDao.upsert(
|
||||
SuggestionEntity(
|
||||
mangaId = x.manga.id,
|
||||
relevance = x.relevance,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.suggestions.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
|
||||
|
||||
class SuggestionsFragment : MangaListFragment() {
|
||||
|
||||
override val viewModel by viewModel<SuggestionsViewModel>()
|
||||
override val isSwipeRefreshEnabled = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_suggestions, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_update -> {
|
||||
SuggestionsWorker.startNow(requireContext())
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
R.string.feed_will_update_soon,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
true
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
startActivity(SimpleSettingsActivity.newSuggestionsSettingsIntent(requireContext()))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
return context?.getString(R.string.suggestions)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = SuggestionsFragment()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.koitharu.kotatsu.suggestions.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class SuggestionsViewModel(
|
||||
repository: SuggestionRepository,
|
||||
settings: AppSettings,
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
private val headerModel = ListHeader(null, R.string.suggestions, null)
|
||||
|
||||
override val content = combine(
|
||||
repository.observeAll(),
|
||||
createListModeFlow()
|
||||
) { list, mode ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(EmptyState(
|
||||
icon = R.drawable.ic_book_cross,
|
||||
textPrimary = R.string.nothing_found,
|
||||
textSecondary = R.string.text_suggestion_holder,
|
||||
))
|
||||
else -> buildList<ListModel>(list.size + 1) {
|
||||
add(headerModel)
|
||||
list.toUi(this, mode)
|
||||
}
|
||||
}
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.catch {
|
||||
it.toErrorState(canRetry = false)
|
||||
}.asLiveDataDistinct(
|
||||
viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
listOf(LoadingState)
|
||||
)
|
||||
|
||||
override fun onRefresh() = Unit
|
||||
|
||||
override fun onRetry() = Unit
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.koitharu.kotatsu.suggestions.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.pow
|
||||
|
||||
class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
|
||||
CoroutineWorker(appContext, params), KoinComponent {
|
||||
|
||||
private val suggestionRepository by inject<SuggestionRepository>()
|
||||
private val historyRepository by inject<HistoryRepository>()
|
||||
private val appSettings by inject<AppSettings>()
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
val count = doWorkImpl()
|
||||
Result.success(workDataOf(DATA_COUNT to count))
|
||||
} catch (t: Throwable) {
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
private suspend fun doWorkImpl(): Int {
|
||||
if (!appSettings.isSuggestionsEnabled) {
|
||||
suggestionRepository.clear()
|
||||
return 0
|
||||
}
|
||||
val rawResults = ArrayList<Manga>()
|
||||
val allTags = historyRepository.getAllTags()
|
||||
if (allTags.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
val tagsBySources = allTags.groupBy { x -> x.source }
|
||||
for ((source, tags) in tagsBySources) {
|
||||
val repo = mangaRepositoryOf(source)
|
||||
tags.flatMapTo(rawResults) { tag ->
|
||||
repo.getList2(
|
||||
offset = 0,
|
||||
sortOrder = SortOrder.UPDATED,
|
||||
tags = setOf(tag),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (appSettings.isSuggestionsExcludeNsfw) {
|
||||
rawResults.removeAll { it.isNsfw }
|
||||
}
|
||||
if (rawResults.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
val suggestions = rawResults.distinctBy { manga ->
|
||||
manga.id
|
||||
}.map { manga ->
|
||||
val jointTags = manga.tags intersect allTags
|
||||
MangaSuggestion(
|
||||
manga = manga,
|
||||
relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(),
|
||||
)
|
||||
}.sortedBy { it.relevance }.take(LIMIT)
|
||||
suggestionRepository.replace(suggestions)
|
||||
return suggestions.size
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "suggestions"
|
||||
private const val TAG_ONESHOT = "suggestions_oneshot"
|
||||
private const val LIMIT = 140
|
||||
private const val DATA_COUNT = "count"
|
||||
|
||||
fun setup(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
val request = PeriodicWorkRequestBuilder<SuggestionsWorker>(6, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
fun startNow(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<SuggestionsWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG_ONESHOT)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueue(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
R.string.feed_will_update_soon,
|
||||
Snackbar.LENGTH_SHORT
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
private const val DATA_PROGRESS = "progress"
|
||||
private const val DATA_TOTAL = "total"
|
||||
private const val TAG = "tracking"
|
||||
private const val TAG_ONESHOT = "tracking_oneshot"
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(context: Context) {
|
||||
@@ -276,7 +277,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<TrackWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.addTag(TAG_ONESHOT)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueue(request)
|
||||
|
||||
12
app/src/main/res/drawable/ic_suggestion.xml
Normal file
12
app/src/main/res/drawable/ic_suggestion.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,2A7,7 0 0,1 19,9C19,11.38 17.81,13.47 16,14.74V17A1,1 0 0,1 15,18H9A1,1 0 0,1 8,17V14.74C6.19,13.47 5,11.38 5,9A7,7 0 0,1 12,2M9,21V20H15V21A1,1 0 0,1 14,22H10A1,1 0 0,1 9,21M12,4A5,5 0 0,0 7,9C7,11.05 8.23,12.81 10,13.58V16H14V13.58C15.77,12.81 17,11.05 17,9A5,5 0 0,0 12,4Z" />
|
||||
</vector>
|
||||
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/grid_spacing_outer"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_filter"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorOutline"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?android:windowBackground"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -6,22 +6,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="0dp"
|
||||
@@ -43,4 +27,20 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -22,7 +22,7 @@
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="@null"
|
||||
android:stateListAnimator="@null">
|
||||
|
||||
<FrameLayout
|
||||
@@ -39,7 +39,7 @@
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="@null"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
@@ -74,6 +74,7 @@
|
||||
android:contentDescription="@string/_continue"
|
||||
android:src="@drawable/ic_read_fill"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="?attr/colorContainer"
|
||||
app:fabSize="normal"
|
||||
app:layout_anchor="@id/container"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
@@ -89,7 +90,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:insetForeground="@android:color/transparent"
|
||||
app:menu="@menu/nav_drawer" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
@@ -10,7 +10,6 @@
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabGravity="center"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/grid_spacing_outer"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
android:layout_width="240dp"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?android:windowBackground"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
android:padding="@dimen/grid_spacing_outer"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable" />
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/chapter_list_item_height"
|
||||
@@ -54,7 +55,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:src="@drawable/ic_new" />
|
||||
android:src="@drawable/ic_new"
|
||||
app:tint="?colorError" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_downloaded"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="?android:listChoiceIndicatorMultiple"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_vertical|start"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="?android:listChoiceIndicatorSingle"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_vertical|start"
|
||||
|
||||
36
app/src/main/res/layout/item_header_with_filter.xml
Normal file
36
app/src/main/res/layout/item_header_with_filter.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/textView_filter"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||
tools:text="@tools:sample/lorem[21]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/list_selector"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="6dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||
app:drawableEndCompat="@drawable/ic_drop_down"
|
||||
app:drawableTint="?android:attr/textColorSecondary"
|
||||
tools:ignore="RtlSymmetry"
|
||||
tools:text="@string/popular" />
|
||||
|
||||
</RelativeLayout>
|
||||
34
app/src/main/res/layout/sheet_chapters.xml
Normal file
34
app/src/main/res/layout/sheet_chapters.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:navigationIcon="@drawable/ic_cross"
|
||||
app:title="@string/chapters" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_chapter" />
|
||||
|
||||
</LinearLayout>
|
||||
33
app/src/main/res/layout/sheet_filter.xml
Normal file
33
app/src/main/res/layout/sheet_filter.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:navigationIcon="@drawable/ic_cross"
|
||||
app:title="@string/filter" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable" />
|
||||
</LinearLayout>
|
||||
@@ -14,6 +14,10 @@
|
||||
android:id="@+id/nav_history"
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/history" />
|
||||
<item
|
||||
android:id="@+id/nav_suggestions"
|
||||
android:icon="@drawable/ic_suggestion"
|
||||
android:title="@string/suggestions" />
|
||||
<item
|
||||
android:id="@+id/nav_feed"
|
||||
android:icon="@drawable/ic_feed"
|
||||
|
||||
@@ -9,9 +9,4 @@
|
||||
android:title="@string/list_mode"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_filter"
|
||||
android:orderInCategory="30"
|
||||
android:title="@string/filter"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
@@ -3,6 +3,12 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_filter"
|
||||
android:orderInCategory="30"
|
||||
android:title="@string/filter"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_source_settings"
|
||||
android:orderInCategory="50"
|
||||
|
||||
18
app/src/main/res/menu/opt_suggestions.xml
Normal file
18
app/src/main/res/menu/opt_suggestions.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_update"
|
||||
android:orderInCategory="50"
|
||||
android:title="@string/update"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="90"
|
||||
android:title="@string/settings"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
@@ -54,11 +54,11 @@
|
||||
<string name="automatic">Аўтаматычна</string>
|
||||
<string name="pages">Старонкi</string>
|
||||
<string name="clear">Ачысціць</string>
|
||||
<string name="text_clear_history_prompt">Вы ўпэўненыя, што жадаеце ачысціць гісторыю\? Гэта дзеянне нельга будзе адмяніць.</string>
|
||||
<string name="text_clear_history_prompt">Вы ўпэўненыя, што жадаеце ачысціць гісторыю\?</string>
|
||||
<string name="remove">Выдаліць</string>
|
||||
<string name="_s_removed_from_history">\"%s\" выдалена з гiсторыi</string>
|
||||
<string name="_s_deleted_from_local_storage">\"%s\" выдалена з прылады</string>
|
||||
<string name="wait_for_loading_finish">Дачакайцеся заканчэння загрузкі</string>
|
||||
<string name="wait_for_loading_finish">Дачакайцеся заканчэння загрузкі…</string>
|
||||
<string name="save_page">Захаваць старонку</string>
|
||||
<string name="page_saved">Старонка захавана</string>
|
||||
<string name="share_image">Падзяліцца выявай</string>
|
||||
@@ -79,8 +79,7 @@
|
||||
<string name="delete_manga">Выдаліць мангу</string>
|
||||
<string name="reader_settings">Налады чытання</string>
|
||||
<string name="switch_pages">Гартанне старонак</string>
|
||||
<string name="text_delete_local_manga">Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады\?
|
||||
\nГэта дзеянне нельга будзе адмяніць.</string>
|
||||
<string name="text_delete_local_manga">Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады\?</string>
|
||||
<string name="taps_on_edges">Націск па краях</string>
|
||||
<string name="volume_buttons">Кнопкі гучнасці</string>
|
||||
<string name="_continue">Працягнцуць</string>
|
||||
@@ -207,7 +206,7 @@
|
||||
<string name="password_length_hint">Пароль павінен змяшчаць не менш за 4 сімвалы</string>
|
||||
<string name="hide_toolbar">Схаваць загаловак пры прагортцы</string>
|
||||
<string name="search_only_on_s">Пошук толькі па %s</string>
|
||||
<string name="text_clear_search_history_prompt">Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты\? Гэта дзеянне нельга будзе адмяніць.</string>
|
||||
<string name="text_clear_search_history_prompt">Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты\?</string>
|
||||
<string name="description">Апісанне</string>
|
||||
<string name="read_more">Падрабязна</string>
|
||||
<string name="tracker_warning">Некаторыя вытворцы могуць змяняць паводзіны сістэмы, што можа парушаць выкананне фонавых задач.</string>
|
||||
@@ -227,7 +226,7 @@
|
||||
<string name="queued">У чарзе</string>
|
||||
<string name="about_license">Ліцэнзія</string>
|
||||
<string name="about_copyright_and_licenses">Аўтарскія правы і ліцэнзіі</string>
|
||||
<string name="about_gratitudes_summary">Гэтыя людзі робяць Kotatsu лепш!</string>
|
||||
<string name="about_gratitudes_summary">Гэтыя людзі робяць Kotatsu лепш</string>
|
||||
<string name="about_gratitudes">Падзякі</string>
|
||||
<string name="about_support_developer_summary">Калі вам падабаецца гэтая праграма, вы можаце дапамагчы фінансава з дапамогай ЮMoney (был. Яндекс.Деньги)</string>
|
||||
<string name="about_support_developer">Падтрымаць распрацоўшчыка</string>
|
||||
@@ -251,4 +250,19 @@
|
||||
<string name="available_sources">Даступныя крыніцы</string>
|
||||
<string name="dynamic_theme">Дынамічная тэма</string>
|
||||
<string name="dynamic_theme_summary">Ужывае тэму праграмы, заснаваную на каляровай палітры шпалер на прыладзе</string>
|
||||
<string name="computing_">Вылічэнні…</string>
|
||||
<string name="importing_progress">Імпарт мангі: %1$d of %2$d</string>
|
||||
<string name="screenshots_allow">Дазваляць</string>
|
||||
<string name="screenshots_policy">Палітыка скрыншотаў</string>
|
||||
<string name="screenshots_block_all">Заўсёды блакуйце</string>
|
||||
<string name="screenshots_block_nsfw">Блок на NSFW</string>
|
||||
<string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string>
|
||||
<string name="disabled">Непрацаздольны</string>
|
||||
<string name="enabled">Уключаны</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Не прапануйце мангу NSFW</string>
|
||||
<string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string>
|
||||
<string name="suggestions_info">Усе дадзеныя аналізуюцца лакальна на гэтай прыладзе. Перадача вашых персанальных дадзеных якім-небудзь сэрвісам не ажыццяўляецца</string>
|
||||
<string name="suggestions_summary">Прапануеце мангу, заснаваную на вашых перавагах</string>
|
||||
<string name="suggestions_enable">Уключыць прапановы</string>
|
||||
<string name="suggestions">Прапанова</string>
|
||||
</resources>
|
||||
@@ -256,4 +256,13 @@
|
||||
<string name="screenshots_policy">Bildschirmfoto-Richtlinie</string>
|
||||
<string name="screenshots_block_nsfw">Für NSFW blockieren</string>
|
||||
<string name="screenshots_block_all">Immer blockieren</string>
|
||||
<string name="suggestions">Vorschläge</string>
|
||||
<string name="suggestions_enable">Vorschläge einschalten</string>
|
||||
<string name="suggestions_summary">Manga nach deinen Vorlieben vorschlagen</string>
|
||||
<string name="suggestions_info">Alle Daten werden lokal auf diesem Gerät ausgewertet. Es findet keine Übertragung Ihrer persönlichen Daten an andere Dienste statt</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Keine NSFW-Manga vorschlagen</string>
|
||||
<string name="enabled">Aktiviert</string>
|
||||
<string name="text_suggestion_holder">Fang an, Manga zu lesen und du bekommst personalisierte Vorschläge</string>
|
||||
<string name="disabled">Deaktiviert</string>
|
||||
<string name="filter_load_error">Liste der Genres kann nicht geladen werden</string>
|
||||
</resources>
|
||||
@@ -54,7 +54,7 @@
|
||||
<string name="automatic">De acuerdo al sistema</string>
|
||||
<string name="pages">Páginas</string>
|
||||
<string name="clear">Borrar</string>
|
||||
<string name="text_clear_history_prompt">¿Realmente quieres borrar todo tu historial de lectura\? Esta acción no se puede deshacer.</string>
|
||||
<string name="text_clear_history_prompt">Borrar todo el historial de lectura de forma permanente\?</string>
|
||||
<string name="remove">Eliminar</string>
|
||||
<string name="_s_removed_from_history">«%s» retirado del historial</string>
|
||||
<string name="_s_deleted_from_local_storage">«%s» borrado del almacenamiento local</string>
|
||||
@@ -72,7 +72,7 @@
|
||||
<string name="cache">Caché</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="standard">Estándar</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="webtoon">Sitio web</string>
|
||||
<string name="read_mode">Modo de lectura</string>
|
||||
<string name="grid_size">Tamaño de la cuadrícula</string>
|
||||
<string name="search_on_s">Buscar en %s</string>
|
||||
@@ -158,7 +158,7 @@
|
||||
<string name="update_check_failed">Fallo en la comprobación de actualizaciones</string>
|
||||
<string name="no_update_available">No hay actualizaciones disponibles</string>
|
||||
<string name="right_to_left">Derecha a izquierda (←)</string>
|
||||
<string name="prefer_rtl_reader">Preferir lector de derecha a izquierda (→)</string>
|
||||
<string name="prefer_rtl_reader">Preferir lector de derecha a izquierda (←)</string>
|
||||
<string name="prefer_rtl_reader_summary">Puedes configurar el modo de lectura para cada manga por separado</string>
|
||||
<string name="create_category">Nueva categoría</string>
|
||||
<string name="report_github">Crear incidencia en GitHub</string>
|
||||
@@ -177,7 +177,7 @@
|
||||
<string name="preparing_">Preparando…</string>
|
||||
<string name="file_not_found">Archivo no encontrado</string>
|
||||
<string name="data_restored_success">Todos los datos fueron restaurados con éxito</string>
|
||||
<string name="data_restored_with_errors">Los datos fueron restaurados, pero hay errores.</string>
|
||||
<string name="data_restored_with_errors">Los datos fueron restaurados, pero hay errores</string>
|
||||
<string name="backup_information">Puedes crear una copia de seguridad de tu historial y favoritos para restaurarla</string>
|
||||
<string name="just_now">Ahora mismo</string>
|
||||
<string name="yesterday">Ayer</string>
|
||||
@@ -213,7 +213,7 @@
|
||||
<string name="about_support_developer_summary">Si te gusta esta aplicación, puedes ayudar económicamente a través de Yoomoney (ex. Yandex.Money)</string>
|
||||
<string name="about_support_developer">Apoyar al desarrollador</string>
|
||||
<string name="search_only_on_s">Buscar sólo en %s</string>
|
||||
<string name="about_gratitudes_summary">Todas estas personas hicieron que Kotatsu fuera mejor.</string>
|
||||
<string name="about_gratitudes_summary">Todas estas personas hicieron que Kotatsu fuera mejor</string>
|
||||
<string name="about_license">Licencia</string>
|
||||
<string name="about_copyright_and_licenses">Derechos de autor y licencias</string>
|
||||
<string name="chapter_is_missing">Falta un capítulo</string>
|
||||
@@ -231,7 +231,7 @@
|
||||
<string name="other">Otro</string>
|
||||
<string name="genres">Géneros</string>
|
||||
<string name="text_search_holder_secondary">Intenta reformular la consulta.</string>
|
||||
<string name="text_clear_search_history_prompt">¿Realmente quiere eliminar todas las consultas de búsqueda recientes\? Esta acción no se puede deshacer.</string>
|
||||
<string name="text_clear_search_history_prompt">¿Realmente quiere eliminar todas las consultas de búsqueda recientes\?</string>
|
||||
<string name="state_finished">Terminado</string>
|
||||
<string name="state_ongoing">En curso</string>
|
||||
<string name="hide_toolbar">Ocultar la barra de herramientas al desplazarse</string>
|
||||
@@ -243,11 +243,26 @@
|
||||
<string name="tracker_warning">Algunos fabricantes pueden cambiar el comportamiento del sistema, lo que podría interrumpir las tareas en segundo plano.</string>
|
||||
<string name="error_empty_name">El nombre no debe estar vacío</string>
|
||||
<string name="auth_not_supported_by">No se admite iniciar sesión en %s</string>
|
||||
<string name="text_clear_cookies_prompt">Serás desconectado de todas las fuentes.</string>
|
||||
<string name="text_clear_cookies_prompt">Serás desconectado de todas las fuentes</string>
|
||||
<string name="exclude_nsfw_from_history">Excluye manga NSFW del historial</string>
|
||||
<string name="show_pages_numbers">Mostrar los números de páginas</string>
|
||||
<string name="enabled_sources">Fuentes activadas</string>
|
||||
<string name="available_sources">Fuentes disponibles</string>
|
||||
<string name="dynamic_theme">Tema dinámico</string>
|
||||
<string name="dynamic_theme_summary">Aplica un tema creado a partir del esquema de colores de su fondo de pantalla</string>
|
||||
<string name="computing_">Informática…</string>
|
||||
<string name="importing_progress">Importando manga: %1$d de %2$d</string>
|
||||
<string name="screenshots_policy">Política de capturas de pantalla</string>
|
||||
<string name="screenshots_allow">Permitir</string>
|
||||
<string name="screenshots_block_all">Bloquear siempre</string>
|
||||
<string name="suggestions">Sugerencias</string>
|
||||
<string name="suggestions_enable">Activar sugerencias</string>
|
||||
<string name="suggestions_summary">Sugiere mangas según tus preferencias</string>
|
||||
<string name="suggestions_info">Todos los datos se analizan localmente en este dispositivo. No hay transferencia de sus datos personales a ningún servicio</string>
|
||||
<string name="text_suggestion_holder">Empieza a leer manga y recibirás sugerencias personalizadas</string>
|
||||
<string name="exclude_nsfw_from_suggestions">No sugerir manga NSFW</string>
|
||||
<string name="enabled">Activado</string>
|
||||
<string name="disabled">Desactivado</string>
|
||||
<string name="filter_load_error">No se puede cargar la lista de géneros</string>
|
||||
<string name="screenshots_block_nsfw">Bloqueo en NSFW</string>
|
||||
</resources>
|
||||
@@ -256,4 +256,13 @@
|
||||
<string name="computing_">Lasketaan…</string>
|
||||
<string name="available_sources">Käytettävissä olevat lähteet</string>
|
||||
<string name="dynamic_theme">Dynaaminen teema</string>
|
||||
<string name="suggestions">Ehdotukset</string>
|
||||
<string name="suggestions_summary">Ehdota mangaa mieltymystesi perusteella</string>
|
||||
<string name="suggestions_info">Kaikki tiedot analysoidaan paikallisesti tässä laitteessa. Henkilötietojasi ei siirretä mihinkään palveluihin</string>
|
||||
<string name="suggestions_enable">Ota ehdotukset käyttöön</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Älä ehdota NSFW-mangaa</string>
|
||||
<string name="text_suggestion_holder">Aloita mangan lukeminen ja saat henkilökohtaisia ehdotuksia</string>
|
||||
<string name="enabled">Käytössä</string>
|
||||
<string name="disabled">Pois päältä</string>
|
||||
<string name="filter_load_error">Genreluetteloa ei voida ladata</string>
|
||||
</resources>
|
||||
@@ -56,7 +56,7 @@
|
||||
<string name="report_github">Signaler un problème sur GitHub</string>
|
||||
<string name="create_category">Nouvelle catégorie</string>
|
||||
<string name="prefer_rtl_reader_summary">Le mode de lecture peut être configuré séparément pour chaque série</string>
|
||||
<string name="prefer_rtl_reader">Préférer le lecteur de droite à gauche (→)</string>
|
||||
<string name="prefer_rtl_reader">Préférer le lecteur de droite à gauche (←)</string>
|
||||
<string name="right_to_left">De droite à gauche (←)</string>
|
||||
<string name="no_update_available">Aucune mise à jour disponible</string>
|
||||
<string name="update_check_failed">Échec de la recherche de mise à jour</string>
|
||||
@@ -256,4 +256,13 @@
|
||||
<string name="screenshots_block_all">Toujours bloquer</string>
|
||||
<string name="screenshots_policy">Politique relative aux captures d\'écran</string>
|
||||
<string name="screenshots_allow">Autoriser</string>
|
||||
<string name="suggestions">Suggestions</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Ne pas suggérer de mangas osés</string>
|
||||
<string name="suggestions_enable">Activer les suggestions</string>
|
||||
<string name="suggestions_summary">Suggérer des mangas en fonction de vos préférences</string>
|
||||
<string name="suggestions_info">Toutes les données sont analysées localement sur cet appareil. Vos données personnelles ne sont pas transférées à d\'autres services</string>
|
||||
<string name="text_suggestion_holder">Commencez à lire des mangas et vous recevrez des suggestions personnalisées</string>
|
||||
<string name="filter_load_error">Impossible de charger la liste des genres</string>
|
||||
<string name="enabled">Activé</string>
|
||||
<string name="disabled">Désactivé</string>
|
||||
</resources>
|
||||
@@ -256,4 +256,13 @@
|
||||
<string name="screenshots_allow">Permetti</string>
|
||||
<string name="screenshots_block_nsfw">Blocca per NSFW</string>
|
||||
<string name="screenshots_block_all">Blocca sempre</string>
|
||||
<string name="filter_load_error">Impossibile caricare la lista dei generi</string>
|
||||
<string name="suggestions_enable">Abilita i suggerimenti</string>
|
||||
<string name="suggestions_summary">Suggerisci manga in base alle tue preferenze</string>
|
||||
<string name="suggestions_info">Tutti i dati sono analizzati localmente su questo dispositivo. Non c\'è trasferimento dei suoi dati personali a nessun servizio</string>
|
||||
<string name="text_suggestion_holder">Inizia a leggere manga e riceverai suggerimenti personalizzati</string>
|
||||
<string name="suggestions">Suggerimenti</string>
|
||||
<string name="enabled">Abilitato</string>
|
||||
<string name="disabled">Disabilitato</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Non suggerire manga NSFW</string>
|
||||
</resources>
|
||||
@@ -164,7 +164,7 @@
|
||||
<string name="update_check_failed">アップデートを見つける事が出来ませんでした</string>
|
||||
<string name="no_update_available">利用可能なアップデートはありません</string>
|
||||
<string name="right_to_left">右から左(←)</string>
|
||||
<string name="prefer_rtl_reader">右から左(→)の読書を好む</string>
|
||||
<string name="prefer_rtl_reader">右から左(←)の読書を好む</string>
|
||||
<string name="about_feedback">フィードバック</string>
|
||||
<string name="about_feedback_4pda">4PDAに関する話題</string>
|
||||
<string name="about_support_developer">開発者をサポートします(Yoomoneyが開きます)</string>
|
||||
@@ -256,4 +256,13 @@
|
||||
<string name="screenshots_block_all">常にブロック</string>
|
||||
<string name="screenshots_policy">スクリーンショットポリシー</string>
|
||||
<string name="screenshots_block_nsfw">NSFWでブロック</string>
|
||||
<string name="suggestions">提案</string>
|
||||
<string name="suggestions_info">すべてのデータは、このデバイス上でローカルに分析されます。お客様のデータが他のサービスに転送されることはありません</string>
|
||||
<string name="suggestions_enable">サジェスト機能を有効</string>
|
||||
<string name="suggestions_summary">あなたの好みに合わせて漫画を提案</string>
|
||||
<string name="filter_load_error">ジャンルリストを読み込めません</string>
|
||||
<string name="disabled">無効</string>
|
||||
<string name="text_suggestion_holder">マンガを読み始めると、個人的な提案を受けることができます</string>
|
||||
<string name="enabled">有効</string>
|
||||
<string name="exclude_nsfw_from_suggestions">NSFWのマンガを提案しない</string>
|
||||
</resources>
|
||||
@@ -29,7 +29,7 @@
|
||||
<string name="report_github">Opprett feilrapport på GitHub</string>
|
||||
<string name="prefer_rtl_reader_summary">Lesemodus kan settes opp for hver serie</string>
|
||||
<string name="right_to_left">Høyre-til-venstre (←)</string>
|
||||
<string name="prefer_rtl_reader">Foretrekk høyre-til-venstre (→)-leser</string>
|
||||
<string name="prefer_rtl_reader">Foretrekk høyre-til-venstre (←)-leser</string>
|
||||
<string name="no_update_available">Ingen tilgjengelige oppdateringer</string>
|
||||
<string name="update_check_failed">Kunne ikke se etter oppdateringer</string>
|
||||
<string name="checking_for_updates">Ser etter oppdateringer …</string>
|
||||
@@ -241,7 +241,7 @@
|
||||
<string name="auth_not_supported_by">Innlogging på %s støttes ikke</string>
|
||||
<string name="text_clear_cookies_prompt">Du vil bli utlogget fra alle kilder</string>
|
||||
<string name="genres">Sjangere</string>
|
||||
<string name="exclude_nsfw_from_history">Utelat NSFW-manga fra historikk</string>
|
||||
<string name="exclude_nsfw_from_history">Utelat sensurerbar-manga fra historikk</string>
|
||||
<string name="date_format">Datoformat</string>
|
||||
<string name="system_default">Forvalg</string>
|
||||
<string name="error_empty_name">Du må angi ett navn</string>
|
||||
@@ -251,5 +251,18 @@
|
||||
<string name="dynamic_theme">Dynamisk tema</string>
|
||||
<string name="dynamic_theme_summary">Bruker et tema basert på fargene til bakgrunnen din</string>
|
||||
<string name="computing_">Beregner …</string>
|
||||
<string name="importing_progress">Importerer manga: %1$d av %2$d</string>
|
||||
<string name="importing_progress">Importere manga: %1$d av %2$d</string>
|
||||
<string name="screenshots_allow">Tillat</string>
|
||||
<string name="screenshots_block_nsfw">Blokker for sensurerbare</string>
|
||||
<string name="screenshots_block_all">Alltid blokker</string>
|
||||
<string name="screenshots_policy">Skjermavbildningspraksis</string>
|
||||
<string name="suggestions_enable">Skru på forslag</string>
|
||||
<string name="suggestions">Forslag</string>
|
||||
<string name="text_suggestion_holder">Du vil få personaliserte forslag når du begynner å lese manga</string>
|
||||
<string name="suggestions_info">Alle data analyseres lokalt på denne enheten. Det er ingen overføring av dine personlige data til noen tjenester</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Ikke foreslå sensurerbar manga</string>
|
||||
<string name="filter_load_error">Kunne ikke laste inn sjangerliste</string>
|
||||
<string name="suggestions_summary">Foreslå manga basert på vaner</string>
|
||||
<string name="enabled">Påskrudd</string>
|
||||
<string name="disabled">Avskrudd</string>
|
||||
</resources>
|
||||
@@ -202,7 +202,7 @@
|
||||
<string name="chapter_is_missing">O capítulo está em falta</string>
|
||||
<string name="auth_complete">Autorizado</string>
|
||||
<string name="auth_not_supported_by">O login em %s não é suportado</string>
|
||||
<string name="genres">Géneros</string>
|
||||
<string name="genres">Gêneros</string>
|
||||
<string name="about_app_translation">Tradução</string>
|
||||
<string name="text_clear_cookies_prompt">Você será desconectado de todas as fontes</string>
|
||||
<string name="vibration">Vibração</string>
|
||||
@@ -215,7 +215,7 @@
|
||||
<string name="recent_manga">Recente</string>
|
||||
<string name="other_storage">Outro armazenamento</string>
|
||||
<string name="text_search_holder_secondary">Tente reformular a consulta.</string>
|
||||
<string name="prefer_rtl_reader">Prefira o leitor da direita para a esquerda (→)</string>
|
||||
<string name="prefer_rtl_reader">Prefira o leitor da direita para a esquerda (←)</string>
|
||||
<string name="not_available">Não disponível</string>
|
||||
<string name="size_s">Tamanho: %s</string>
|
||||
<string name="text_history_holder_primary">O que você ler será exibido aqui</string>
|
||||
@@ -250,4 +250,19 @@
|
||||
<string name="system_default">Padrão</string>
|
||||
<string name="dynamic_theme">Tema dinâmico</string>
|
||||
<string name="dynamic_theme_summary">Aplica um tema criado no esquema de cores do seu papel de parede</string>
|
||||
<string name="computing_">Computando…</string>
|
||||
<string name="importing_progress">Importando mangá: %1$d de %2$d</string>
|
||||
<string name="screenshots_allow">Permitir</string>
|
||||
<string name="screenshots_block_nsfw">Bloquear no NSFW</string>
|
||||
<string name="screenshots_policy">Política de captura de tela</string>
|
||||
<string name="screenshots_block_all">Sempre bloquear</string>
|
||||
<string name="suggestions_summary">Sugira mangá com base em suas preferências</string>
|
||||
<string name="suggestions_info">Todos os dados são analisados localmente neste dispositivo. Não há transferência de seus dados pessoais para nenhum serviço</string>
|
||||
<string name="text_suggestion_holder">Comece a ler mangá e você receberá sugestões personalizadas</string>
|
||||
<string name="suggestions">Sugestões</string>
|
||||
<string name="suggestions_enable">Ativar sugestões</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Não sugira mangá NSFW</string>
|
||||
<string name="enabled">Habilitado</string>
|
||||
<string name="disabled">Desabilitado</string>
|
||||
<string name="filter_load_error">Não foi possível carregar a lista de gêneros</string>
|
||||
</resources>
|
||||
@@ -249,8 +249,20 @@
|
||||
<string name="available_sources">Доступные источники</string>
|
||||
<string name="dynamic_theme">Динамическая тема</string>
|
||||
<string name="dynamic_theme_summary">Применяет тему приложения, основанную на цветовой палитре обоев на устройстве</string>
|
||||
<string name="screenshots_policy">Разрешить скриншоты</string>
|
||||
<string name="screenshots_allow">Разрешить</string>
|
||||
<string name="screenshots_block_nsfw">Запретить для NSFW</string>
|
||||
<string name="screenshots_block_all">Запретить всегда</string>
|
||||
<string name="screenshots_policy">Политика скриншотов</string>
|
||||
<string name="screenshots_allow">Разрешить</string>
|
||||
<string name="screenshots_block_nsfw">Запретить для NSFW</string>
|
||||
<string name="screenshots_block_all">Всегда блокировать</string>
|
||||
<string name="suggestions">Рекомендации</string>
|
||||
<string name="suggestions_enable">Включить рекомендации</string>
|
||||
<string name="suggestions_summary">Предлагать мангу на основе Ваших предпочтений</string>
|
||||
<string name="suggestions_info">Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы</string>
|
||||
<string name="text_suggestion_holder">Начните читать мангу, чтобы получать персональные предложения</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string>
|
||||
<string name="enabled">Включено</string>
|
||||
<string name="disabled">Выключено</string>
|
||||
<string name="filter_load_error">Не удалось загрузить список жанров</string>
|
||||
<string name="computing_">Вычисления…</string>
|
||||
<string name="report_github">Создать проблему на GitHub</string>
|
||||
<string name="importing_progress">Импорт манги: %1$d из %2$d</string>
|
||||
</resources>
|
||||
@@ -204,7 +204,7 @@
|
||||
<string name="search_results">Arama sonuçları</string>
|
||||
<string name="waiting_for_network">Ağ bekleniyor…</string>
|
||||
<string name="repeat_password">Parolayı tekrarla</string>
|
||||
<string name="prefer_rtl_reader">Sağdan sola (→) okuyucuyu tercih et</string>
|
||||
<string name="prefer_rtl_reader">Sağdan sola (←) okuyucuyu tercih et</string>
|
||||
<string name="dont_check">Denetleme</string>
|
||||
<string name="wrong_password">Yanlış parola</string>
|
||||
<string name="report_github">GitHub\'da sorun oluştur</string>
|
||||
@@ -256,4 +256,14 @@
|
||||
<string name="screenshots_block_nsfw">Uygunsuzlarda engelle</string>
|
||||
<string name="screenshots_block_all">Her zaman engelle</string>
|
||||
<string name="screenshots_allow">İzin ver</string>
|
||||
<string name="check_for_new_chapters">Yeni bölümleri denetle</string>
|
||||
<string name="suggestions">Öneriler</string>
|
||||
<string name="suggestions_enable">Önerileri etkinleştir</string>
|
||||
<string name="suggestions_summary">Tercihlerinize göre manga önerileri alın</string>
|
||||
<string name="suggestions_info">Tüm veriler aygıt üzerinde yerel olarak işlenir. Kişisel verilerinizin herhangi bir hizmete aktarılması söz konusu değildir</string>
|
||||
<string name="text_suggestion_holder">Manga okumaya başladıktan sonra kişiselleştirilmiş öneriler alacaksınız</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Uygunsuz manga önerme</string>
|
||||
<string name="enabled">Etkin</string>
|
||||
<string name="disabled">Devre dışı</string>
|
||||
<string name="filter_load_error">Türler listesi yüklenemiyor</string>
|
||||
</resources>
|
||||
@@ -164,7 +164,7 @@
|
||||
<string name="update_check_failed">Could not look for updates</string>
|
||||
<string name="no_update_available">No updates available</string>
|
||||
<string name="right_to_left">Right-to-left (←)</string>
|
||||
<string name="prefer_rtl_reader">Prefer right-to-left (→) reader</string>
|
||||
<string name="prefer_rtl_reader">Prefer right-to-left (←) reader</string>
|
||||
<string name="prefer_rtl_reader_summary">Reading mode can be set up separately for each series</string>
|
||||
<string name="create_category">New category</string>
|
||||
<string name="scale_mode">Scale mode</string>
|
||||
@@ -180,7 +180,7 @@
|
||||
<string name="restore_backup">Restore from backup</string>
|
||||
<string name="data_restored">Restored</string>
|
||||
<string name="preparing_">Preparing…</string>
|
||||
<string name="report_github">Create issue on GitHub</string>
|
||||
<string name="report_github">Create issue on GitHub</string>
|
||||
<string name="file_not_found">File not found</string>
|
||||
<string name="data_restored_success">All data was restored</string>
|
||||
<string name="data_restored_with_errors">The data was restored, but there are errors</string>
|
||||
@@ -251,10 +251,19 @@
|
||||
<string name="dynamic_theme">Dynamic theme</string>
|
||||
<string name="dynamic_theme_summary">Applies a theme created on the color scheme of your wallpaper</string>
|
||||
<string name="importing_progress">Importing manga: %1$d of %2$d</string>
|
||||
<string name="screenshots_policy">Screenshots policy</string>
|
||||
<string name="screenshots_policy">Screenshot policy</string>
|
||||
<string name="screenshots_allow">Allow</string>
|
||||
<string name="screenshots_block_nsfw">Block on NSFW</string>
|
||||
<string name="screenshots_block_all">Block always</string>
|
||||
<string name="screenshots_block_all">Always block</string>
|
||||
<string name="suggestions">Suggestions</string>
|
||||
<string name="suggestions_enable">Enable suggestions</string>
|
||||
<string name="suggestions_summary">Suggest manga based on your preferences</string>
|
||||
<string name="suggestions_info">All data is analyzed locally on this device. There is no transfer of your personal data to any services</string>
|
||||
<string name="text_suggestion_holder">Start reading manga and you will get personalized suggestions</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Do not suggest NSFW manga</string>
|
||||
<string name="enabled">Enabled</string>
|
||||
<string name="disabled">Disabled</string>
|
||||
<string name="filter_load_error">Unable to load genres list</string>
|
||||
<string name="never">Never</string>
|
||||
<string name="only_using_wifi">Only using WiFi</string>
|
||||
<string name="always">Always</string>
|
||||
|
||||
@@ -61,6 +61,13 @@
|
||||
app:allowDividerAbove="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.SuggestionsSettingsFragment"
|
||||
android:key="suggestions"
|
||||
android:persistent="false"
|
||||
android:title="@string/suggestions"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="local_storage"
|
||||
android:title="@string/manga_save_location"
|
||||
@@ -71,7 +78,7 @@
|
||||
android:title="@string/history_and_cache"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreference
|
||||
<SwitchPreferenceCompat
|
||||
android:key="protect_app"
|
||||
android:persistent="false"
|
||||
android:summary="@string/protect_application_summary"
|
||||
|
||||
27
app/src/main/res/xml/pref_suggestions.xml
Normal file
27
app/src/main/res/xml/pref_suggestions.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="suggestions"
|
||||
android:summary="@string/suggestions_summary"
|
||||
android:title="@string/suggestions_enable"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:dependency="suggestions"
|
||||
android:key="suggestions_exclude_nsfw"
|
||||
android:title="@string/exclude_nsfw_from_suggestions"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_info_outline"
|
||||
android:key="track_warning"
|
||||
android:persistent="false"
|
||||
android:selectable="false"
|
||||
android:summary="@string/suggestions_info"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user