- table.select("div.manga2")
- }.map { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a ->
- val href = a?.relUrl("href") ?: return@mapIndexedNotNull null
+ chapters = root.select("table.table_cha tr:gt(1)").reversed().mapIndexedNotNull { i, tr ->
+ val href = tr?.selectFirst("a")?.relUrl("href") ?: return@mapIndexedNotNull null
MangaChapter(
id = generateUid(href),
- name = a.text().trim(),
+ name = tr.selectFirst("a")?.text().orEmpty(),
number = i + 1,
url = href,
- source = source
+ scanlator = null,
+ branch = null,
+ uploadDate = dateFormat.tryParse(tr.selectFirst("div.date")?.text()),
+ source = source,
)
}
)
@@ -116,8 +119,9 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
MangaPage(
id = generateUid(url),
url = url,
+ preview = null,
referer = fullUrl,
- source = source
+ source = source,
)
}
}
@@ -154,4 +158,5 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
SortOrder.NEWEST -> "datedesc"
else -> "favdesc"
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt
index 73b223b82..0b5fd9b65 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt
@@ -93,12 +93,17 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
description = json.getString("description"),
chapters = chaptersList.mapIndexed { i, it ->
val chid = it.getLong("id")
+ val volChap = "Том " + it.optString("vol", "0") + ". " + "Глава " + it.optString("ch", "0")
+ val title = it.optString("title", "null").takeUnless { it == "null" }
MangaChapter(
id = generateUid(chid),
source = manga.source,
url = "$baseChapterUrl$chid",
- name = it.getStringOrNull("title") ?: "${manga.title} #${it.getDouble("ch")}",
- number = totalChapters - i
+ uploadDate = it.getLong("date") * 1000,
+ name = if (title.isNullOrEmpty()) volChap else "$volChap: $title",
+ number = totalChapters - i,
+ scanlator = null,
+ branch = null,
)
}.reversed()
)
@@ -113,8 +118,9 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
MangaPage(
id = generateUid(jo.getLong("id")),
referer = fullUrl,
+ preview = null,
source = chapter.source,
- url = jo.getString("img")
+ url = jo.getString("img"),
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt
index bef2af960..41b86750e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt
@@ -141,8 +141,10 @@ class ExHentaiRepository(
name = "${manga.title} #$i",
number = i,
url = url,
- branch = null,
+ uploadDate = 0L,
source = source,
+ scanlator = null,
+ branch = null,
)
}
chapters
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
index 2d35111e8..598a43bf0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.parser.site
+import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Response
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
@@ -7,6 +8,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
+import java.text.SimpleDateFormat
import java.util.*
abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
@@ -39,14 +41,14 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
getSortKey(
sortOrder
)
- }&offset=${offset upBy PAGE_SIZE}"
+ }&offset=${offset upBy PAGE_SIZE}", HEADER
)
tags.size == 1 -> loaderContext.httpGet(
"https://$domain/list/genre/${tags.first().key}?sortType=${
getSortKey(
sortOrder
)
- }&offset=${offset upBy PAGE_SIZE}"
+ }&offset=${offset upBy PAGE_SIZE}", HEADER
)
offset > 0 -> return emptyList()
else -> advancedSearch(domain, tags)
@@ -104,14 +106,15 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
}
override suspend fun getDetails(manga: Manga): Manga {
- val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml()
+ val doc = loaderContext.httpGet(manga.url.withDomain(), HEADER).parseHtml()
val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")
?: throw ParseException("Cannot find root")
+ val dateFormat = SimpleDateFormat("dd.MM.yy", Locale.US)
+ val coverImg = root.selectFirst("div.subject-cover")?.selectFirst("img")
return manga.copy(
description = root.selectFirst("div.manga-description")?.html(),
- largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr(
- "data-full"
- ),
+ largeCoverUrl = coverImg?.attr("data-full"),
+ coverUrl = coverImg?.attr("data-thumb") ?: manga.coverUrl,
tags = manga.tags + root.select("div.subject-meta").select("span.elem_genre ")
.mapNotNull {
val a = it.selectFirst("a.element-link") ?: return@mapNotNull null
@@ -122,21 +125,32 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
)
},
chapters = root.selectFirst("div.chapters-link")?.selectFirst("table")
- ?.select("a")?.asReversed()?.mapIndexed { i, a ->
+ ?.select("tr:has(td > a)")?.asReversed()?.mapIndexedNotNull { i, tr ->
+ val a = tr.selectFirst("a") ?: return@mapIndexedNotNull null
val href = a.relUrl("href")
+ var translators = ""
+ val translatorElement = a.attr("title")
+ if (!translatorElement.isNullOrBlank()) {
+ translators = translatorElement
+ .replace("(Переводчик),", "&")
+ .removeSuffix(" (Переводчик)")
+ }
MangaChapter(
id = generateUid(href),
- name = a.ownText().removePrefix(manga.title).trim(),
+ name = tr.selectFirst("a")?.text().orEmpty().removePrefix(manga.title).trim(),
number = i + 1,
url = href,
- source = source
+ uploadDate = dateFormat.tryParse(tr.selectFirst("td.d-none")?.text()),
+ scanlator = translators,
+ source = source,
+ branch = null,
)
}
)
}
override suspend fun getPages(chapter: MangaChapter): List
{
- val doc = loaderContext.httpGet(chapter.url.withDomain() + "?mtr=1").parseHtml()
+ val doc = loaderContext.httpGet(chapter.url.withDomain() + "?mtr=1", HEADER).parseHtml()
val scripts = doc.select("script")
for (script in scripts) {
val data = script.html()
@@ -154,8 +168,9 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
MangaPage(
id = generateUid(url),
url = url,
+ preview = null,
referer = chapter.url,
- source = source
+ source = source,
)
}
}
@@ -163,7 +178,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
}
override suspend fun getTags(): Set {
- val doc = loaderContext.httpGet("https://${getDomain()}/list/genres/sort_name").parseHtml()
+ val doc = loaderContext.httpGet("https://${getDomain()}/list/genres/sort_name", HEADER).parseHtml()
val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")
?.selectFirst("table.table") ?: parseFailed("Cannot find root")
return root.select("a.element-link").mapToSet { a ->
@@ -188,7 +203,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
private suspend fun advancedSearch(domain: String, tags: Set): Response {
val url = "https://$domain/search/advanced"
// Step 1: map catalog genres names to advanced-search genres ids
- val tagsIndex = loaderContext.httpGet(url).parseHtml()
+ val tagsIndex = loaderContext.httpGet(url, HEADER).parseHtml()
.body().selectFirst("form.search-form")
?.select("div.form-group")
?.get(1) ?: parseFailed("Genres filter element not found")
@@ -226,5 +241,9 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
private const val PAGE_SIZE = 70
private const val PAGE_SIZE_SEARCH = 50
+ private val HEADER = Headers.Builder()
+ .add("User-Agent", "readmangafun")
+ .build()
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt
index a358f50df..072c7611b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt
@@ -18,12 +18,10 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
sortOrder: SortOrder?
): List {
return super.getList2(offset, query, tags, sortOrder).map {
- val cover = it.coverUrl
- if (cover.contains("_blur")) {
- it.copy(coverUrl = cover.replace("_blur", ""))
- } else {
- it
- }
+ it.copy(
+ coverUrl = it.coverUrl.replace("_blur", ""),
+ isNsfw = true,
+ )
}
}
@@ -49,7 +47,10 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
url = readLink,
source = source,
number = 1,
- name = manga.title
+ uploadDate = 0L,
+ name = manga.title,
+ scanlator = null,
+ branch = null,
)
)
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt
new file mode 100644
index 000000000..2b289212b
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaDexRepository.kt
@@ -0,0 +1,215 @@
+package org.koitharu.kotatsu.core.parser.site
+
+import android.os.Build
+import androidx.core.os.LocaleListCompat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import org.json.JSONObject
+import org.koitharu.kotatsu.base.domain.MangaLoaderContext
+import org.koitharu.kotatsu.core.model.*
+import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.utils.ext.*
+import java.text.SimpleDateFormat
+import java.util.*
+
+private const val PAGE_SIZE = 20
+private const val CONTENT_RATING =
+ "contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic"
+private const val LOCALE_FALLBACK = "en"
+
+class MangaDexRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
+
+ override val source = MangaSource.MANGADEX
+ override val defaultDomain = "mangadex.org"
+
+ override val sortOrders: EnumSet = EnumSet.of(
+ SortOrder.UPDATED,
+ SortOrder.ALPHABETICAL,
+ SortOrder.NEWEST,
+ SortOrder.POPULARITY,
+ )
+
+ override suspend fun getList2(
+ offset: Int,
+ query: String?,
+ tags: Set?,
+ sortOrder: SortOrder?,
+ ): List {
+ val domain = getDomain()
+ val url = buildString {
+ append("https://api.")
+ append(domain)
+ append("/manga?limit=")
+ append(PAGE_SIZE)
+ append("&offset=")
+ append(offset)
+ append("&includes[]=cover_art&includes[]=author&includes[]=artist&")
+ tags?.forEach { tag ->
+ append("includedTags[]=")
+ append(tag.key)
+ append('&')
+ }
+ if (!query.isNullOrEmpty()) {
+ append("title=")
+ append(query.urlEncoded())
+ append('&')
+ }
+ append(CONTENT_RATING)
+ append("&order")
+ append(when (sortOrder) {
+ null,
+ SortOrder.UPDATED,
+ -> "[latestUploadedChapter]=desc"
+ SortOrder.ALPHABETICAL -> "[title]=asc"
+ SortOrder.NEWEST -> "[createdAt]=desc"
+ SortOrder.POPULARITY -> "[followedCount]=desc"
+ else -> "[followedCount]=desc"
+ })
+ }
+ val json = loaderContext.httpGet(url).parseJson().getJSONArray("data")
+ return json.map { jo ->
+ val id = jo.getString("id")
+ val attrs = jo.getJSONObject("attributes")
+ val relations = jo.getJSONArray("relationships").associateByKey("type")
+ val cover = relations["cover_art"]
+ ?.getJSONObject("attributes")
+ ?.getString("fileName")
+ ?.let {
+ "https://uploads.$domain/covers/$id/$it"
+ }
+ Manga(
+ id = generateUid(id),
+ title = requireNotNull(attrs.getJSONObject("title").selectByLocale()) {
+ "Title should not be null"
+ },
+ altTitle = attrs.optJSONObject("altTitles")?.selectByLocale(),
+ url = id,
+ publicUrl = "https://$domain/title/$id",
+ rating = Manga.NO_RATING,
+ isNsfw = attrs.getStringOrNull("contentRating") == "erotica",
+ coverUrl = cover?.plus(".256.jpg").orEmpty(),
+ largeCoverUrl = cover,
+ description = attrs.optJSONObject("description")?.selectByLocale(),
+ tags = attrs.getJSONArray("tags").mapToSet { tag ->
+ MangaTag(
+ title = tag.getJSONObject("attributes")
+ .getJSONObject("name")
+ .firstStringValue(),
+ key = tag.getString("id"),
+ source = source,
+ )
+ },
+ state = when (jo.getStringOrNull("status")) {
+ "ongoing" -> MangaState.ONGOING
+ "completed" -> MangaState.FINISHED
+ else -> null
+ },
+ author = (relations["author"] ?: relations["artist"])
+ ?.getJSONObject("attributes")
+ ?.getStringOrNull("name"),
+ source = source,
+ )
+ }
+ }
+
+ override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
+ val domain = getDomain()
+ val attrsDeferred = async {
+ loaderContext.httpGet(
+ "https://api.$domain/manga/${manga.url}?includes[]=artist&includes[]=author&includes[]=cover_art"
+ ).parseJson().getJSONObject("data").getJSONObject("attributes")
+ }
+ val feedDeferred = async {
+ val url = buildString {
+ append("https://api.")
+ append(domain)
+ append("/manga/")
+ append(manga.url)
+ append("/feed")
+ append("?limit=96&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=0&")
+ append(CONTENT_RATING)
+ }
+ loaderContext.httpGet(url).parseJson().getJSONArray("data")
+ }
+ val mangaAttrs = attrsDeferred.await()
+ val feed = feedDeferred.await()
+ //2022-01-02T00:27:11+00:00
+ val dateFormat = SimpleDateFormat(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ "yyyy-MM-dd'T'HH:mm:ssX"
+ } else {
+ "yyyy-MM-dd'T'HH:mm:ss'+00:00'"
+ },
+ Locale.ROOT
+ )
+ manga.copy(
+ description = mangaAttrs.getJSONObject("description").selectByLocale()
+ ?: manga.description,
+ chapters = feed.mapNotNull { jo ->
+ val id = jo.getString("id")
+ val attrs = jo.getJSONObject("attributes")
+ if (!attrs.isNull("externalUrl")) {
+ return@mapNotNull null
+ }
+ val locale = Locale.forLanguageTag(attrs.getString("translatedLanguage"))
+ val relations = jo.getJSONArray("relationships").associateByKey("type")
+ val number = attrs.optInt("chapter", 0)
+ MangaChapter(
+ id = generateUid(id),
+ name = attrs.getStringOrNull("title")?.takeUnless(String::isEmpty)
+ ?: "Chapter #$number",
+ number = number,
+ url = id,
+ scanlator = relations["scanlation_group"]?.getStringOrNull("name"),
+ uploadDate = dateFormat.tryParse(attrs.getString("publishAt")),
+ branch = locale.getDisplayName(locale).toTitleCase(locale),
+ source = source,
+ )
+ }
+ )
+ }
+
+ override suspend fun getPages(chapter: MangaChapter): List {
+ val domain = getDomain()
+ val chapter = loaderContext.httpGet("https://api.$domain/at-home/server/${chapter.url}?forcePort443=false")
+ .parseJson()
+ .getJSONObject("chapter")
+ val pages = chapter.getJSONArray("data")
+ val prefix = "https://uploads.$domain/data/${chapter.getString("hash")}/"
+ val referer = "https://$domain/"
+ return List(pages.length()) { i ->
+ val url = prefix + pages.getString(i)
+ MangaPage(
+ id = generateUid(url),
+ url = url,
+ referer = referer,
+ preview = null, // TODO prefix + dataSaver.getString(i),
+ source = source,
+ )
+ }
+ }
+
+ override suspend fun getTags(): Set {
+ val tags = loaderContext.httpGet("https://api.${getDomain()}/manga/tag").parseJson()
+ .getJSONArray("data")
+ return tags.mapToSet { jo ->
+ MangaTag(
+ title = jo.getJSONObject("attributes").getJSONObject("name").firstStringValue(),
+ key = jo.getString("id"),
+ source = source,
+ )
+ }
+ }
+
+ private fun JSONObject.firstStringValue() = values().next() as String
+
+ private fun JSONObject.selectByLocale(): String? {
+ val preferredLocales = LocaleListCompat.getAdjustedDefault()
+ repeat(preferredLocales.size()) { i ->
+ val locale = preferredLocales.get(i)
+ getStringOrNull(locale.language)?.let { return it }
+ getStringOrNull(locale.toLanguageTag())?.let { return it }
+ }
+ return getStringOrNull(LOCALE_FALLBACK) ?: values().nextOrNull() as? String
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt
index ba0ea771d..ed58f073c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
+import java.text.SimpleDateFormat
import java.util.*
open class MangaLibRepository(loaderContext: MangaLoaderContext) :
@@ -79,6 +80,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
val info = root.selectFirst("div.media-content")
val chaptersDoc = loaderContext.httpGet("$fullUrl?section=chapters").parseHtml()
val scripts = chaptersDoc.select("script")
+ val dateFormat = SimpleDateFormat("yyy-MM-dd", Locale.US)
var chapters: ArrayList? = null
scripts@ for (script in scripts) {
val raw = script.html().lines()
@@ -91,7 +93,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
for (i in 0 until total) {
val item = list.getJSONObject(i)
val chapterId = item.getLong("chapter_id")
- val branchName = item.getStringOrNull("username")
+ val scanlator = item.getStringOrNull("username")
val url = buildString {
append(manga.url)
append("/v")
@@ -102,19 +104,22 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
append('/')
append(item.optString("chapter_string"))
}
- var name = item.getStringOrNull("chapter_name")
- if (name.isNullOrBlank() || name == "null") {
- name = "Том " + item.getInt("chapter_volume") +
- " Глава " + item.getString("chapter_number")
- }
+ val nameChapter = item.getStringOrNull("chapter_name")
+ val volume = item.getInt("chapter_volume")
+ val number = item.getString("chapter_number")
+ val fullNameChapter = "Том $volume. Глава $number"
chapters.add(
MangaChapter(
id = generateUid(chapterId),
url = url,
source = source,
- branch = branchName,
number = total - i,
- name = name
+ uploadDate = dateFormat.tryParse(
+ item.getString("chapter_created_at").substringBefore(" ")
+ ),
+ scanlator = scanlator,
+ branch = null,
+ name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter",
)
)
}
@@ -174,8 +179,9 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
MangaPage(
id = generateUid(pageUrl),
url = pageUrl,
+ preview = null,
referer = fullUrl,
- source = source
+ source = source,
)
}
}
@@ -235,8 +241,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
.toFloatOrNull()?.div(5f) ?: Manga.NO_RATING,
state = null,
source = source,
- coverUrl = "https://$domain${covers.getString("thumbnail")}",
- largeCoverUrl = "https://$domain${covers.getString("default")}"
+ coverUrl = covers.getString("thumbnail"),
+ largeCoverUrl = covers.getString("default")
)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
new file mode 100644
index 000000000..5e5429d95
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
@@ -0,0 +1,169 @@
+package org.koitharu.kotatsu.core.parser.site
+
+import android.util.Base64
+import org.koitharu.kotatsu.base.domain.MangaLoaderContext
+import org.koitharu.kotatsu.core.exceptions.ParseException
+import org.koitharu.kotatsu.core.model.*
+import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.utils.ext.*
+import java.text.SimpleDateFormat
+import java.util.*
+
+class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
+
+ override val source = MangaSource.MANGAOWL
+
+ override val defaultDomain = "mangaowls.com"
+
+ override val sortOrders: Set = EnumSet.of(
+ SortOrder.POPULARITY,
+ SortOrder.NEWEST,
+ SortOrder.UPDATED
+ )
+
+ override suspend fun getList2(
+ offset: Int,
+ query: String?,
+ tags: Set?,
+ sortOrder: SortOrder?,
+ ): List {
+ val page = (offset / 36f).toIntUp().inc()
+ val link = buildString {
+ append("https://")
+ append(getDomain())
+ when {
+ !query.isNullOrEmpty() -> {
+ append("/search/${page}?search=")
+ append(query.urlEncoded())
+ }
+ !tags.isNullOrEmpty() -> {
+ for (tag in tags) {
+ append(tag.key)
+ }
+ append("/${page}?type=${getAlternativeSortKey(sortOrder)}")
+ }
+ else -> {
+ append("/${getSortKey(sortOrder)}/${page}")
+ }
+ }
+ }
+ val doc = loaderContext.httpGet(link).parseHtml()
+ val slides = doc.body().select("ul.slides") ?: parseFailed("An error occurred while parsing")
+ val items = slides.select("div.col-md-2")
+ return items.mapNotNull { item ->
+ val href = item.selectFirst("h6 a")?.relUrl("href") ?: return@mapNotNull null
+ Manga(
+ id = generateUid(href),
+ title = item.selectFirst("h6 a")?.text() ?: return@mapNotNull null,
+ coverUrl = item.select("div.img-responsive").attr("abs:data-background-image"),
+ altTitle = null,
+ author = null,
+ rating = runCatching {
+ item.selectFirst("div.block-stars")
+ ?.text()
+ ?.toFloatOrNull()
+ ?.div(10f)
+ }.getOrNull() ?: Manga.NO_RATING,
+ url = href,
+ publicUrl = href.withDomain(),
+ source = source
+ )
+ }
+ }
+
+ override suspend fun getDetails(manga: Manga): Manga {
+ val doc = loaderContext.httpGet(manga.publicUrl).parseHtml()
+ val info = doc.body().selectFirst("div.single_detail") ?: parseFailed("An error occurred while parsing")
+ val table = doc.body().selectFirst("div.single-grid-right") ?: parseFailed("An error occurred while parsing")
+ val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US)
+ val trRegex = "window\\['tr'] = '([^']*)';".toRegex(RegexOption.IGNORE_CASE)
+ val trElement = doc.getElementsByTag("script").find { trRegex.find(it.data()) != null } ?: parseFailed("Oops, tr not found")
+ val tr = trRegex.find(trElement.data())!!.groups[1]!!.value
+ val s = Base64.encodeToString(defaultDomain.toByteArray(), Base64.NO_PADDING)
+ return manga.copy(
+ description = info.selectFirst(".description")?.html(),
+ largeCoverUrl = info.select("img").first()?.let { img ->
+ if (img.hasAttr("data-src")) img.attr("abs:data-src") else img.attr("abs:src")
+ },
+ author = info.selectFirst("p.fexi_header_para a.author_link")?.text(),
+ state = parseStatus(info.select("p.fexi_header_para:contains(status)").first()?.ownText()),
+ tags = manga.tags + info.select("div.col-xs-12.col-md-8.single-right-grid-right > p > a[href*=genres]")
+ .mapNotNull {
+ val a = it.selectFirst("a") ?: return@mapNotNull null
+ MangaTag(
+ title = a.text(),
+ key = a.attr("href"),
+ source = source
+ )
+ },
+ chapters = table.select("div.table.table-chapter-list").select("li.list-group-item.chapter_list").asReversed().mapIndexed { i, li ->
+ val a = li.select("a")
+ val href = a.attr("data-href").ifEmpty {
+ parseFailed("Link is missing")
+ }
+ MangaChapter(
+ id = generateUid(href),
+ name = a.select("label").text(),
+ number = i + 1,
+ url = "$href?tr=$tr&s=$s",
+ scanlator = null,
+ branch = null,
+ uploadDate = dateFormat.tryParse(li.selectFirst("small:last-of-type")?.text()),
+ source = MangaSource.MANGAOWL,
+ )
+ }
+ )
+ }
+
+ override suspend fun getPages(chapter: MangaChapter): List {
+ val fullUrl = chapter.url.withDomain()
+ val doc = loaderContext.httpGet(fullUrl).parseHtml()
+ val root = doc.body().select("div.item img.owl-lazy") ?: throw ParseException("Root not found")
+ return root.map { div ->
+ val url = div?.relUrl("data-src") ?: parseFailed("Page image not found")
+ MangaPage(
+ id = generateUid(url),
+ url = url,
+ preview = null,
+ referer = url,
+ source = MangaSource.MANGAOWL,
+ )
+ }
+ }
+
+ private fun parseStatus(status: String?) = when {
+ status == null -> null
+ status.contains("Ongoing") -> MangaState.ONGOING
+ status.contains("Completed") -> MangaState.FINISHED
+ else -> null
+ }
+
+ override suspend fun getTags(): Set {
+ val doc = loaderContext.httpGet("https://${getDomain()}/").parseHtml()
+ val root = doc.body().select("ul.dropdown-menu.multi-column.columns-3").select("li")
+ return root.mapToSet { p ->
+ val a = p.selectFirst("a") ?: parseFailed("a is null")
+ MangaTag(
+ title = a.text().toCamelCase(),
+ key = a.attr("href"),
+ source = source
+ )
+ }
+ }
+
+ private fun getSortKey(sortOrder: SortOrder?) =
+ when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
+ SortOrder.POPULARITY -> "popular"
+ SortOrder.NEWEST -> "new_release"
+ SortOrder.UPDATED -> "lastest"
+ else -> "lastest"
+ }
+
+ private fun getAlternativeSortKey(sortOrder: SortOrder?) =
+ when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
+ SortOrder.POPULARITY -> "0"
+ SortOrder.NEWEST -> "2"
+ SortOrder.UPDATED -> "3"
+ else -> "3"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
index bf5ce1b8f..afe3750c3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
@@ -7,6 +7,8 @@ import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.*
+import java.text.DateFormat
+import java.text.SimpleDateFormat
import java.util.*
class MangaTownRepository(loaderContext: MangaLoaderContext) :
@@ -96,6 +98,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
val info = root.selectFirst("div.detail_info")?.selectFirst("ul")
val chaptersList = root.selectFirst("div.chapter_content")
?.selectFirst("ul.chapter_list")?.select("li")?.asReversed()
+ val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US)
return manga.copy(
tags = manga.tags + info?.select("li")?.find { x ->
x.selectFirst("b")?.ownText() == "Genre(s):"
@@ -117,9 +120,15 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
url = href,
source = MangaSource.MANGATOWN,
number = i + 1,
- name = name.ifEmpty { "${manga.title} - ${i + 1}" }
+ uploadDate = parseChapterDate(
+ dateFormat,
+ li.selectFirst("span.time")?.text()
+ ),
+ name = name.ifEmpty { "${manga.title} - ${i + 1}" },
+ scanlator = null,
+ branch = null,
)
- }
+ } ?: bypassLicensedChapters(manga)
)
}
@@ -136,8 +145,9 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
MangaPage(
id = generateUid(href),
url = href,
+ preview = null,
referer = fullUrl,
- source = MangaSource.MANGATOWN
+ source = MangaSource.MANGATOWN,
)
} ?: parseFailed("Pages list not found")
}
@@ -167,11 +177,46 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
}
}
+ private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
+ return when {
+ date.isNullOrEmpty() -> 0L
+ date.contains("Today") -> Calendar.getInstance().timeInMillis
+ date.contains("Yesterday") -> Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis
+ else -> dateFormat.tryParse(date)
+ }
+ }
+
override fun onCreatePreferences(map: MutableMap) {
super.onCreatePreferences(map)
map[SourceSettings.KEY_USE_SSL] = true
}
+ private suspend fun bypassLicensedChapters(manga: Manga): List {
+ val doc = loaderContext.httpGet(manga.url.withDomain("m")).parseHtml()
+ val list = doc.body().selectFirst("ul.detail-ch-list") ?: return emptyList()
+ val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US)
+ return list.select("li").asReversed().mapIndexedNotNull { i, li ->
+ val a = li.selectFirst("a") ?: return@mapIndexedNotNull null
+ val href = a.relUrl("href")
+ val name = a.selectFirst("span.vol")?.text().orEmpty().ifEmpty {
+ a.ownText()
+ }
+ MangaChapter(
+ id = generateUid(href),
+ url = href,
+ source = MangaSource.MANGATOWN,
+ number = i + 1,
+ uploadDate = parseChapterDate(
+ dateFormat,
+ li.selectFirst("span.time")?.text()
+ ),
+ name = name.ifEmpty { "${manga.title} - ${i + 1}" },
+ scanlator = null,
+ branch = null,
+ )
+ }
+ }
+
private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it }
private companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt
index 0a94c3d04..2b2f9b8cd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt
@@ -4,7 +4,10 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.utils.WordSet
import org.koitharu.kotatsu.utils.ext.*
+import java.text.DateFormat
+import java.text.SimpleDateFormat
import java.util.*
class MangareadRepository(
@@ -52,7 +55,7 @@ class MangareadRepository(
id = generateUid(href),
url = href,
publicUrl = href.inContextOf(div),
- coverUrl = div.selectFirst("img")?.absUrl("src").orEmpty(),
+ coverUrl = div.selectFirst("img")?.absUrl("data-src").orEmpty(),
title = summary?.selectFirst("h3")?.text().orEmpty(),
rating = div.selectFirst("span.total_votes")?.ownText()
?.toFloatOrNull()?.div(5f) ?: -1f,
@@ -104,16 +107,7 @@ class MangareadRepository(
val root2 = doc.body().selectFirst("div.content-area")
?.selectFirst("div.c-page")
?: throw ParseException("Root2 not found")
- val mangaId = doc.getElementsByAttribute("data-post").firstOrNull()
- ?.attr("data-post")?.toLongOrNull()
- ?: throw ParseException("Cannot obtain manga id")
- val doc2 = loaderContext.httpPost(
- "https://${getDomain()}/wp-admin/admin-ajax.php",
- mapOf(
- "action" to "manga_get_chapters",
- "manga" to mangaId.toString()
- )
- ).parseHtml()
+ val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
return manga.copy(
tags = root.selectFirst("div.genres-content")?.select("a")
?.mapNotNullToSet { a ->
@@ -128,7 +122,7 @@ class MangareadRepository(
?.select("p")
?.filterNot { it.ownText().startsWith("A brief description") }
?.joinToString { it.html() },
- chapters = doc2.select("li").asReversed().mapIndexed { i, li ->
+ chapters = root2.select("li").asReversed().mapIndexed { i, li ->
val a = li.selectFirst("a")
val href = a?.relUrl("href").orEmpty().ifEmpty {
parseFailed("Link is missing")
@@ -138,7 +132,13 @@ class MangareadRepository(
name = a!!.ownText(),
number = i + 1,
url = href,
- source = MangaSource.MANGAREAD
+ uploadDate = parseChapterDate(
+ dateFormat,
+ li.selectFirst("span.chapter-release-date i")?.text()
+ ),
+ source = MangaSource.MANGAREAD,
+ scanlator = null,
+ branch = null,
)
}
)
@@ -151,17 +151,85 @@ class MangareadRepository(
?.selectFirst("div.reading-content")
?: throw ParseException("Root not found")
return root.select("div.page-break").map { div ->
- val img = div.selectFirst("img")
- val url = img?.relUrl("src") ?: parseFailed("Page image not found")
+ val img = div.selectFirst("img") ?: parseFailed("Page image not found")
+ val url = img.relUrl("data-src").ifEmpty {
+ img.relUrl("src")
+ }
MangaPage(
id = generateUid(url),
url = url,
+ preview = null,
referer = fullUrl,
- source = MangaSource.MANGAREAD
+ source = MangaSource.MANGAREAD,
)
}
}
+ private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
+
+ date ?: return 0
+ return when {
+ date.endsWith(" ago", ignoreCase = true) -> {
+ parseRelativeDate(date)
+ }
+ // Handle translated 'ago' in Portuguese.
+ date.endsWith(" atrás", ignoreCase = true) -> {
+ parseRelativeDate(date)
+ }
+ // Handle translated 'ago' in Turkish.
+ date.endsWith(" önce", ignoreCase = true) -> {
+ parseRelativeDate(date)
+ }
+ // Handle 'yesterday' and 'today', using midnight
+ date.startsWith("year", ignoreCase = true) -> {
+ Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, -1) // yesterday
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }.timeInMillis
+ }
+ date.startsWith("today", ignoreCase = true) -> {
+ Calendar.getInstance().apply {
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }.timeInMillis
+ }
+ date.contains(Regex("""\d(st|nd|rd|th)""")) -> {
+ // Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
+ date.split(" ").map {
+ if (it.contains(Regex("""\d\D\D"""))) {
+ it.replace(Regex("""\D"""), "")
+ } else {
+ it
+ }
+ }
+ .let { dateFormat.tryParse(it.joinToString(" ")) }
+ }
+ else -> dateFormat.tryParse(date)
+ }
+ }
+
+ // Parses dates in this form:
+ // 21 hours ago
+ private fun parseRelativeDate(date: String): Long {
+ val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
+ val cal = Calendar.getInstance()
+
+ return when {
+ WordSet("hari", "gün", "jour", "día", "dia", "day").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
+ WordSet("jam", "saat", "heure", "hora", "hour").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
+ WordSet("menit", "dakika", "min", "minute", "minuto").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
+ WordSet("detik", "segundo", "second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
+ WordSet("month").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
+ WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
+ else -> 0
+ }
+ }
+
private companion object {
private const val PAGE_SIZE = 12
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt
index 9c67b2146..7b782ab1c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt
@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
+import java.text.SimpleDateFormat
import java.util.*
abstract class NineMangaRepository(
@@ -40,7 +41,7 @@ abstract class NineMangaRepository(
append("&page=")
}
!tags.isNullOrEmpty() -> {
- append("/search/&category_id=")
+ append("/search/?category_id=")
for (tag in tags) {
append(tag.key)
append(',')
@@ -99,19 +100,22 @@ abstract class NineMangaRepository(
)
}.orEmpty(),
author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.text(),
+ state = parseStatus(infoRoot.select("li a.red").text()),
description = infoRoot.getElementsByAttributeValue("itemprop", "description").first()
?.html()?.substringAfter(""),
- chapters = root.selectFirst("div.chapterbox")?.selectFirst("ul")
- ?.select("li")?.asReversed()?.mapIndexed { i, li ->
- val a = li.selectFirst("a")
- val href = a?.relUrl("href") ?: parseFailed("Link not found")
+ chapters = root.selectFirst("div.chapterbox")?.select("ul.sub_vol_ul > li")
+ ?.asReversed()?.mapIndexed { i, li ->
+ val a = li.selectFirst("a.chapter_list_a")
+ val href = a?.relUrl("href")?.replace("%20", " ") ?: parseFailed("Link not found")
MangaChapter(
id = generateUid(href),
name = a.text(),
number = i + 1,
url = href,
- branch = null,
+ uploadDate = parseChapterDateByLang(li.selectFirst("span")?.text().orEmpty()),
source = source,
+ scanlator = null,
+ branch = null,
)
}
)
@@ -153,6 +157,50 @@ abstract class NineMangaRepository(
} ?: parseFailed("Root not found")
}
+ private fun parseStatus(status: String) = when {
+ status.contains("Ongoing") -> MangaState.ONGOING
+ status.contains("Completed") -> MangaState.FINISHED
+ else -> null
+ }
+
+ private fun parseChapterDateByLang(date: String): Long {
+ val dateWords = date.split(" ")
+
+ if (dateWords.size == 3) {
+ if (dateWords[1].contains(",")) {
+ SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).tryParse(date)
+ } else {
+ val timeAgo = Integer.parseInt(dateWords[0])
+ return Calendar.getInstance().apply {
+ when (dateWords[1]) {
+ "minutes" -> Calendar.MINUTE // EN-FR
+ "hours" -> Calendar.HOUR // EN
+
+ "minutos" -> Calendar.MINUTE // ES
+ "horas" -> Calendar.HOUR
+
+ // "minutos" -> Calendar.MINUTE // BR
+ "hora" -> Calendar.HOUR
+
+ "минут" -> Calendar.MINUTE // RU
+ "часа" -> Calendar.HOUR
+
+ "Stunden" -> Calendar.HOUR // DE
+
+ "minuti" -> Calendar.MINUTE // IT
+ "ore" -> Calendar.HOUR
+
+ "heures" -> Calendar.HOUR // FR ("minutes" also French word)
+ else -> null
+ }?.let {
+ add(it, -timeAgo)
+ }
+ }.timeInMillis
+ }
+ }
+ return 0L
+ }
+
class English(loaderContext: MangaLoaderContext) : NineMangaRepository(
loaderContext,
MangaSource.NINEMANGA_EN,
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ReadmangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ReadmangaRepository.kt
index 3871a966f..ab95903d5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ReadmangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ReadmangaRepository.kt
@@ -5,6 +5,6 @@ import org.koitharu.kotatsu.core.model.MangaSource
class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {
- override val defaultDomain = "readmanga.live"
+ override val defaultDomain = "readmanga.io"
override val source = MangaSource.READMANGA_RU
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
index 17a3dd5d7..2319dfcd2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
@@ -6,15 +6,20 @@ import org.json.JSONObject
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
+import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
+import java.text.SimpleDateFormat
import java.util.*
-class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
+class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext),
+ MangaRepositoryAuthProvider {
override val source = MangaSource.REMANGA
override val defaultDomain = "remanga.org"
+ override val authUrl: String
+ get() = "https://${getDomain()}/user/login"
override val sortOrders: Set = EnumSet.of(
SortOrder.UPDATED,
@@ -29,6 +34,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
tags: Set?,
sortOrder: SortOrder?
): List {
+ copyCookies()
val domain = getDomain()
val urlBuilder = StringBuilder()
.append("https://api.")
@@ -77,6 +83,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
}
override suspend fun getDetails(manga: Manga): Manga {
+ copyCookies()
val domain = getDomain()
val slug = manga.url.find(LAST_URL_PATH_REGEX)
?: throw ParseException("Cannot obtain slug from ${manga.url}")
@@ -93,6 +100,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
val chapters = loaderContext.httpGet(
url = "https://api.$domain/api/titles/chapters/?branch_id=$branchId"
).parseJson().getJSONArray("content")
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
return manga.copy(
description = content.getString("description"),
state = when (content.optJSONObject("status")?.getInt("id")) {
@@ -109,20 +117,27 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
},
chapters = chapters.mapIndexed { i, jo ->
val id = jo.getLong("id")
- val name = jo.getString("name")
+ val name = jo.getString("name").toTitleCase(Locale.ROOT)
+ val publishers = jo.getJSONArray("publishers")
MangaChapter(
id = generateUid(id),
url = "/api/titles/chapters/$id/",
number = chapters.length() - i,
name = buildString {
+ append("Том ")
+ append(jo.optString("tome", "0"))
+ append(". ")
append("Глава ")
- append(jo.getString("chapter"))
+ append(jo.optString("chapter", "0"))
if (name.isNotEmpty()) {
append(" - ")
append(name)
}
},
- source = MangaSource.REMANGA
+ uploadDate = dateFormat.tryParse(jo.getString("upload_date")),
+ scanlator = publishers.optJSONObject(0)?.getStringOrNull("name"),
+ source = MangaSource.REMANGA,
+ branch = null,
)
}.asReversed()
)
@@ -156,6 +171,17 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
}
}
+ override fun isAuthorized(): Boolean {
+ return loaderContext.cookieJar.getCookies(getDomain()).any {
+ it.name == "user"
+ }
+ }
+
+ private fun copyCookies() {
+ val domain = getDomain()
+ loaderContext.cookieJar.copyCookies(domain, "api.$domain")
+ }
+
private fun getSortKey(order: SortOrder?) = when (order) {
SortOrder.UPDATED -> "-chapter_date"
SortOrder.POPULARITY -> "-rating"
@@ -167,8 +193,9 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
private fun parsePage(jo: JSONObject, referer: String) = MangaPage(
id = generateUid(jo.getLong("id")),
url = jo.getString("link"),
+ preview = null,
referer = referer,
- source = source
+ source = source,
)
private companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/YaoiChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/YaoiChanRepository.kt
index 076da352d..77edbb5b1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/YaoiChanRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/YaoiChanRepository.kt
@@ -29,7 +29,10 @@ class YaoiChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loa
name = a.text().trim(),
number = i + 1,
url = href,
- source = source
+ uploadDate = 0L,
+ source = source,
+ scanlator = null,
+ branch = null,
)
}
)
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index a2f2fbc59..8fac79601 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt
@@ -2,18 +2,23 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.content.SharedPreferences
+import android.os.Build
import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf
import androidx.core.content.edit
import androidx.preference.PreferenceManager
+import com.google.android.material.color.DynamicColors
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.channels.sendBlocking
+import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.delegates.prefs.*
import java.io.File
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
class AppSettings private constructor(private val prefs: SharedPreferences) :
SharedPreferences by prefs {
@@ -39,6 +44,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
+ val isDynamicTheme by BoolPreferenceDelegate(KEY_DYNAMIC_THEME, defaultValue = false)
+
val isAmoledTheme by BoolPreferenceDelegate(KEY_THEME_AMOLED, defaultValue = false)
val isToolbarHideWhenScrolling by BoolPreferenceDelegate(KEY_HIDE_TOOLBAR, defaultValue = true)
@@ -76,6 +83,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
var historyGrouping by BoolPreferenceDelegate(KEY_HISTORY_GROUPING, true)
+ var isHistoryExcludeNsfw by BoolPreferenceDelegate(KEY_HISTORY_EXCLUDE_NSFW, false)
+
var chaptersReverse by BoolPreferenceDelegate(KEY_REVERSE_CHAPTERS, false)
val zoomMode by EnumPreferenceDelegate(
@@ -104,6 +113,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
val isSourcesSelected: Boolean
get() = KEY_SOURCES_HIDDEN in prefs
+ val isPagesNumbersEnabled by BoolPreferenceDelegate(KEY_PAGES_NUMBERS, false)
+
fun getStorageDir(context: Context): File? {
val value = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
File(it)
@@ -121,6 +132,12 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
}
}
+ fun dateFormat(format: String? = prefs.getString(KEY_DATE_FORMAT, "")): DateFormat =
+ when (format) {
+ "" -> DateFormat.getDateInstance(DateFormat.SHORT)
+ else -> SimpleDateFormat(format, Locale.getDefault())
+ }
+
@Deprecated("Use observe()")
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
@@ -132,7 +149,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
fun observe() = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
- sendBlocking(key)
+ trySendBlocking(key)
}
prefs.registerOnSharedPreferenceChangeListener(listener)
awaitClose {
@@ -151,7 +168,9 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val KEY_LIST_MODE = "list_mode_2"
const val KEY_APP_SECTION = "app_section"
const val KEY_THEME = "theme"
+ const val KEY_DYNAMIC_THEME = "dynamic_theme"
const val KEY_THEME_AMOLED = "amoled_theme"
+ const val KEY_DATE_FORMAT = "date_format"
const val KEY_HIDE_TOOLBAR = "hide_toolbar"
const val KEY_SOURCES_ORDER = "sources_order"
const val KEY_SOURCES_HIDDEN = "sources_hidden"
@@ -182,6 +201,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val KEY_RESTORE = "restore"
const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
+ const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
+ const val KEY_PAGES_NUMBERS = "pages_numbers"
// About
const val KEY_APP_UPDATE = "app_update"
@@ -191,5 +212,12 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val KEY_FEEDBACK_4PDA = "about_feedback_4pda"
const val KEY_FEEDBACK_GITHUB = "about_feedback_github"
const val KEY_SUPPORT_DEVELOPER = "about_support_developer"
+
+ val isDynamicColorAvailable: Boolean
+ get() = DynamicColors.isDynamicColorAvailable() ||
+ (isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+
+ private val isSamsung
+ get() = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt
index 8deefacfe..0d38d95ce 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt
@@ -14,16 +14,35 @@ sealed class DateTimeAgo : ListModel {
}
}
- data class MinutesAgo(val minutes: Int) : DateTimeAgo() {
+ class MinutesAgo(val minutes: Int) : DateTimeAgo() {
+
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes)
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as MinutesAgo
+ return minutes == other.minutes
+ }
+
+ override fun hashCode(): Int = minutes
}
- data class HoursAgo(val hours: Int) : DateTimeAgo() {
+ class HoursAgo(val hours: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.hours_ago, hours, hours)
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as HoursAgo
+ return hours == other.hours
+ }
+
+ override fun hashCode(): Int = hours
}
object Today : DateTimeAgo() {
@@ -38,10 +57,19 @@ sealed class DateTimeAgo : ListModel {
}
}
- data class DaysAgo(val days: Int) : DateTimeAgo() {
+ class DaysAgo(val days: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.days_ago, days, days)
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as DaysAgo
+ return days == other.days
+ }
+
+ override fun hashCode(): Int = days
}
object LongAgo : DateTimeAgo() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
index 8032d9783..148fa409d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt
@@ -5,6 +5,7 @@ import coil.ImageLoader
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
+import org.koitharu.kotatsu.core.parser.FaviconMapper
import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule
@@ -15,6 +16,7 @@ val uiModule
.componentRegistry(
ComponentRegistry.Builder()
.add(CbzFetcher())
+ .add(FaviconMapper())
.build()
).build()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
index c2205bf30..6ea44a8ac 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
@@ -9,8 +9,8 @@ import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
-import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.divider.MaterialDividerItemDecoration
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -51,12 +51,7 @@ class ChaptersFragment : BaseFragment(),
chaptersAdapter = ChaptersAdapter(this)
selectionDecoration = ChaptersSelectionDecoration(view.context)
with(binding.recyclerViewChapters) {
- addItemDecoration(
- DividerItemDecoration(
- view.context,
- RecyclerView.VERTICAL
- )
- )
+ addItemDecoration(MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL))
addItemDecoration(selectionDecoration!!)
setHasFixedSize(true)
adapter = chaptersAdapter
@@ -117,7 +112,7 @@ class ChaptersFragment : BaseFragment(),
}
return
}
- if (item.isMissing) {
+ if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
return
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index 84745b56d..6b0b78279 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -6,15 +6,17 @@ import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
+import android.view.ViewGroup
import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.Insets
import androidx.core.net.toFile
+import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -44,7 +46,7 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class DetailsActivity : BaseActivity(),
TabLayoutMediator.TabConfigurationStrategy {
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ private val viewModel by viewModel {
parametersOf(MangaIntent.from(intent))
}
@@ -85,18 +87,24 @@ class DetailsActivity : BaseActivity(),
finishAfterTransition()
}
else -> {
- Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
- .show()
+ binding.snackbar.show(e.getDisplayMessage(resources))
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
- binding.toolbar.updatePadding(
- top = insets.top,
- left = insets.left,
- right = insets.right
+ binding.snackbar.updatePadding(
+ bottom = insets.bottom
)
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
if (binding.tabs.parent !is Toolbar) {
binding.tabs.updatePadding(
left = insets.left,
@@ -147,7 +155,7 @@ class DetailsActivity : BaseActivity(),
}
R.id.action_delete -> {
viewModel.manga.value?.let { m ->
- AlertDialog.Builder(this)
+ MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, m.title))
.setPositiveButton(R.string.delete) { _, _ ->
@@ -162,7 +170,7 @@ class DetailsActivity : BaseActivity(),
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) {
- AlertDialog.Builder(this)
+ MaterialAlertDialogBuilder(this)
.setTitle(R.string.save_manga)
.setMessage(
getString(
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
index a427f2b3e..3d6ce19f0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.details.ui
+import android.app.ActivityOptions
import android.os.Bundle
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import coil.ImageLoader
+import coil.request.ImageRequest
import coil.util.CoilUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -23,13 +26,15 @@ import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource
+import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
+import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
+import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.*
-import kotlin.random.Random
class DetailsFragment : BaseFragment(), View.OnClickListener,
View.OnLongClickListener {
@@ -39,11 +44,16 @@ class DetailsFragment : BaseFragment(), View.OnClickList
override fun onInflateView(
inflater: LayoutInflater,
- container: ViewGroup?
+ container: ViewGroup?,
) = FragmentDetailsBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ binding.textViewAuthor.setOnClickListener(this)
+ binding.buttonFavorite.setOnClickListener(this)
+ binding.buttonRead.setOnClickListener(this)
+ binding.buttonRead.setOnLongClickListener(this)
+ binding.coverCard.setOnClickListener(this)
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
@@ -52,12 +62,8 @@ class DetailsFragment : BaseFragment(), View.OnClickList
private fun onMangaUpdated(manga: Manga) {
with(binding) {
- imageViewCover.newImageRequest(manga.largeCoverUrl ?: manga.coverUrl)
- .referer(manga.publicUrl)
- .fallback(R.drawable.ic_placeholder)
- .placeholderMemoryCacheKey(CoilUtils.metadata(imageViewCover)?.memoryCacheKey)
- .lifecycle(viewLifecycleOwner)
- .enqueueWith(coil)
+ // Main
+ loadCover(manga)
textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitle
textViewAuthor.textAndVisible = manga.author
@@ -66,6 +72,27 @@ class DetailsFragment : BaseFragment(), View.OnClickList
textViewDescription.text =
manga.description?.parseAsHtml()?.takeUnless(Spanned::isBlank)
?: getString(R.string.no_description)
+ when (manga.state) {
+ MangaState.FINISHED -> {
+ textViewState.apply {
+ textAndVisible = resources.getString(R.string.state_finished)
+ drawableStart = ResourcesCompat.getDrawable(resources,
+ R.drawable.ic_state_finished,
+ context.theme)
+ }
+ }
+ MangaState.ONGOING -> {
+ textViewState.apply {
+ textAndVisible = resources.getString(R.string.state_ongoing)
+ drawableStart = ResourcesCompat.getDrawable(resources,
+ R.drawable.ic_state_ongoing,
+ context.theme)
+ }
+ }
+ else -> textViewState.isVisible = false
+ }
+
+ // Info containers
if (manga.chapters?.isNotEmpty() == true) {
chaptersContainer.isVisible = true
textViewChapters.text = manga.chapters.let {
@@ -96,10 +123,11 @@ class DetailsFragment : BaseFragment(), View.OnClickList
} else {
sizeContainer.isVisible = false
}
- buttonFavorite.setOnClickListener(this@DetailsFragment)
- buttonRead.setOnClickListener(this@DetailsFragment)
- buttonRead.setOnLongClickListener(this@DetailsFragment)
+
+ // Buttons
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
+
+ // Chips
bindTags(manga)
}
}
@@ -154,6 +182,26 @@ class DetailsFragment : BaseFragment(), View.OnClickList
)
}
}
+ R.id.textView_author -> {
+ startActivity(
+ SearchActivity.newIntent(
+ context = v.context,
+ source = manga.source,
+ query = manga.author ?: return,
+ )
+ )
+ }
+ R.id.cover_card -> {
+ val options = ActivityOptions.makeSceneTransitionAnimation(
+ requireActivity(),
+ binding.imageViewCover,
+ binding.imageViewCover.transitionName,
+ )
+ startActivity(
+ ImageActivity.newIntent(v.context, manga.largeCoverUrl ?: manga.coverUrl),
+ options.toBundle()
+ )
+ }
}
}
@@ -204,4 +252,22 @@ class DetailsFragment : BaseFragment(), View.OnClickList
}
)
}
+
+ private fun loadCover(manga: Manga) {
+ val currentCover = binding.imageViewCover.drawable
+ val request = ImageRequest.Builder(context ?: return)
+ .target(binding.imageViewCover)
+ if (currentCover != null) {
+ request.data(manga.largeCoverUrl ?: return)
+ .placeholderMemoryCacheKey(CoilUtils.metadata(binding.imageViewCover)?.memoryCacheKey)
+ .fallback(currentCover)
+ } else {
+ request.crossfade(true)
+ .data(manga.coverUrl)
+ .fallback(R.drawable.ic_placeholder)
+ }
+ request.referer(manga.publicUrl)
+ .lifecycle(viewLifecycleOwner)
+ .enqueueWith(coil)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index ebd377a50..1e8e9d063 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui
+import androidx.core.os.LocaleListCompat
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
@@ -18,12 +19,13 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
-import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
+import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.mapToSet
+import org.koitharu.kotatsu.utils.ext.toTitleCase
import java.io.IOException
class DetailsViewModel(
@@ -58,16 +60,6 @@ class DetailsViewModel(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val remoteManga = MutableStateFlow(null)
- /*private val remoteManga = mangaData.mapLatest {
- if (it?.source == MangaSource.LOCAL) {
- runCatching {
- val m = localMangaRepository.getRemoteManga(it) ?: return@mapLatest null
- MangaRepository(m.source).getDetails(m)
- }.getOrNull()
- } else {
- null
- }
- }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)*/
private val chaptersReversed = settings.observe()
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
@@ -107,10 +99,10 @@ class DetailsViewModel(
selectedBranch
) { chapters, sourceManga, currentId, newCount, branch ->
val sourceChapters = sourceManga?.chapters
- if (sourceChapters.isNullOrEmpty()) {
- mapChapters(chapters, currentId, newCount, branch)
- } else {
+ if (sourceManga?.source != MangaSource.LOCAL && !sourceChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch)
+ } else {
+ mapChapters(chapters, sourceChapters, currentId, newCount, branch)
}
}.combine(chaptersReversed) { list, reversed ->
if (reversed) list.asReversed() else list
@@ -121,23 +113,23 @@ class DetailsViewModel(
var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
- manga = manga.source.repository.getDetails(manga)
+ manga = MangaRepository(manga.source).getDetails(manga)
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = if (hist != null) {
manga.chapters?.find { it.id == hist.chapterId }?.branch
} else {
- manga.chapters
- ?.groupBy { it.branch }
- ?.maxByOrNull { it.value.size }?.key
+ predictBranch(manga.chapters)
}
mangaData.value = manga
- if (manga.source == MangaSource.LOCAL) {
- remoteManga.value = runCatching {
+ remoteManga.value = runCatching {
+ if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
MangaRepository(m.source).getDetails(m)
- }.getOrNull()
- }
+ } else {
+ localMangaRepository.findSavedManga(manga)
+ }
+ }.getOrNull()
}
}
@@ -166,26 +158,28 @@ class DetailsViewModel(
private fun mapChapters(
chapters: List,
+ downloadedChapters: List?,
currentId: Long?,
newCount: Int,
branch: String?,
): List {
val result = ArrayList(chapters.size)
+ val dateFormat = settings.dateFormat()
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
+ val downloadedIds = downloadedChapters?.mapToSet { it.id }
for (i in chapters.indices) {
val chapter = chapters[i]
if (chapter.branch != branch) {
continue
}
result += chapter.toListItem(
- extra = when {
- i >= firstNewIndex -> ChapterExtra.NEW
- i == currentIndex -> ChapterExtra.CURRENT
- i < currentIndex -> ChapterExtra.READ
- else -> ChapterExtra.UNREAD
- },
- isMissing = false
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = false,
+ isDownloaded = downloadedIds?.contains(chapter.id) == true,
+ dateFormat = dateFormat,
)
}
return result
@@ -202,6 +196,7 @@ class DetailsViewModel(
val result = ArrayList(sourceChapters.size)
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
val firstNewIndex = sourceChapters.size - newCount
+ val dateFormat = settings.dateFormat()
for (i in sourceChapters.indices) {
val chapter = sourceChapters[i]
if (chapter.branch != branch) {
@@ -209,30 +204,53 @@ class DetailsViewModel(
}
val localChapter = chaptersMap.remove(chapter.id)
result += localChapter?.toListItem(
- extra = when {
- i >= firstNewIndex -> ChapterExtra.NEW
- i == currentIndex -> ChapterExtra.CURRENT
- i < currentIndex -> ChapterExtra.READ
- else -> ChapterExtra.UNREAD
- },
- isMissing = false
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
) ?: chapter.toListItem(
- extra = when {
- i >= firstNewIndex -> ChapterExtra.NEW
- i == currentIndex -> ChapterExtra.CURRENT
- i < currentIndex -> ChapterExtra.READ
- else -> ChapterExtra.UNREAD
- },
- isMissing = true
+ isCurrent = i == currentIndex,
+ isUnread = i > currentIndex,
+ isNew = i >= firstNewIndex,
+ isMissing = true,
+ isDownloaded = false,
+ dateFormat = dateFormat,
)
}
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
result.ensureCapacity(result.size + chaptersMap.size)
chaptersMap.values.mapTo(result) {
- it.toListItem(ChapterExtra.UNREAD, false)
+ it.toListItem(
+ isCurrent = false,
+ isUnread = true,
+ isNew = false,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
+ )
}
result.sortBy { it.chapter.number }
}
return result
}
+
+ private fun predictBranch(chapters: List?): String? {
+ if (chapters.isNullOrEmpty()) {
+ return null
+ }
+ val groups = chapters.groupBy { it.branch }
+ for (locale in LocaleListCompat.getAdjustedDefault()) {
+ var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
+ if (groups.containsKey(language)) {
+ return language
+ }
+ language = locale.getDisplayName(locale).toTitleCase(locale)
+ if (groups.containsKey(language)) {
+ return language
+ }
+ }
+ return groups.maxByOrNull { it.value.size }?.key
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt
index 983f322a8..9a423b2e2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt
@@ -1,12 +1,19 @@
package org.koitharu.kotatsu.details.ui.adapter
+import android.view.View
+import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
-import org.koitharu.kotatsu.history.domain.ChapterExtra
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import org.koitharu.kotatsu.utils.ext.getThemeColor
+import org.koitharu.kotatsu.utils.ext.textAndVisible
fun chapterListItemAD(
clickListener: OnListItemClickListener,
@@ -14,35 +21,40 @@ fun chapterListItemAD(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
) {
- itemView.setOnClickListener {
- clickListener.onItemClick(item, it)
- }
- itemView.setOnLongClickListener {
- clickListener.onItemLongClick(item, it)
+ val eventListener = object : View.OnClickListener, View.OnLongClickListener {
+ override fun onClick(v: View) = clickListener.onItemClick(item, v)
+ override fun onLongClick(v: View) = clickListener.onItemLongClick(item, v)
}
- bind { payload ->
- binding.textViewTitle.text = item.chapter.name
- binding.textViewNumber.text = item.chapter.number.toString()
- when (item.extra) {
- ChapterExtra.UNREAD -> {
+ itemView.setOnClickListener(eventListener)
+ itemView.setOnLongClickListener(eventListener)
+
+ bind { payloads ->
+ if (payloads.isEmpty()) {
+ binding.textViewTitle.text = item.chapter.name
+ binding.textViewNumber.text = item.chapter.number.toString()
+ binding.textViewDescription.textAndVisible = item.description()
+ }
+ when (item.status) {
+ FLAG_UNREAD -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
}
- ChapterExtra.READ -> {
- binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
- binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
- }
- ChapterExtra.CURRENT -> {
- binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline_accent)
- binding.textViewNumber.setTextColor(context.getThemeColor(androidx.appcompat.R.attr.colorAccent))
- }
- ChapterExtra.NEW -> {
+ FLAG_CURRENT -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
+ else -> {
+ binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
+ binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
+ }
}
- binding.textViewTitle.alpha = if (item.isMissing) 0.3f else 1f
- binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f
+ val isMissing = item.hasFlag(FLAG_MISSING)
+ binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
+ binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
+ binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
+
+ binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
+ binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt
index 46dc930d1..033b9ed92 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt
@@ -19,10 +19,6 @@ class ChaptersAdapter(
return items[position].chapter.id
}
- fun setItems(newItems: List, callback: Runnable) {
- differ.submitList(newItems, callback)
- }
-
private class DiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
@@ -37,8 +33,8 @@ class ChaptersAdapter(
}
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
- if (oldItem.extra != newItem.extra && oldItem.chapter == newItem.chapter) {
- return newItem.extra
+ if (oldItem.flags != newItem.flags && oldItem.chapter == newItem.chapter) {
+ return newItem.flags
}
return null
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt
index 69ad32f06..dbd64b9b7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt
@@ -4,20 +4,14 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
-import androidx.collection.ArraySet
-import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
-import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.getThemeColor
-import org.koitharu.kotatsu.utils.ext.resolveDp
class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoration() {
- private val icon = ContextCompat.getDrawable(context, R.drawable.ic_check)
- private val padding = context.resources.resolveDp(16)
private val bounds = Rect()
- private val selection = ArraySet()
+ private val selection = HashSet()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
@@ -54,7 +48,6 @@ class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoratio
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
- icon ?: return
canvas.save()
if (parent.clipToPadding) {
canvas.clipRect(
@@ -73,36 +66,4 @@ class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoratio
}
canvas.restore()
}
-
- override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
- icon ?: return
- canvas.save()
- val left: Int
- val right: Int
- if (parent.clipToPadding) {
- left = parent.paddingLeft
- right = parent.width - parent.paddingRight
- canvas.clipRect(
- left, parent.paddingTop, right,
- parent.height - parent.paddingBottom
- )
- } else {
- left = 0
- right = parent.width
- }
-
- for (child in parent.children) {
- val itemId = parent.getChildItemId(child)
- if (itemId in selection) {
- parent.getDecoratedBoundsWithMargins(child, bounds)
- bounds.offset(child.translationX.toInt(), child.translationY.toInt())
- val hh = (bounds.height() - icon.intrinsicHeight) / 2
- val top: Int = bounds.top + hh
- val bottom: Int = bounds.bottom - hh
- icon.setBounds(right - icon.intrinsicWidth - padding, top, right - padding, bottom)
- icon.draw(canvas)
- }
- }
- canvas.restore()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt
index 82f00decf..2e1ac64bd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt
@@ -1,10 +1,57 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
-import org.koitharu.kotatsu.history.domain.ChapterExtra
-data class ChapterListItem(
+class ChapterListItem(
val chapter: MangaChapter,
- val extra: ChapterExtra,
- val isMissing: Boolean,
-)
+ val flags: Int,
+ val uploadDate: String?,
+) {
+
+ val status: Int
+ get() = flags and MASK_STATUS
+
+ fun hasFlag(flag: Int): Boolean {
+ return (flags and flag) == flag
+ }
+
+ fun description(): CharSequence? {
+ val scanlator = chapter.scanlator?.takeUnless { it.isBlank() }
+ return when {
+ uploadDate != null && scanlator != null -> "$uploadDate • $scanlator"
+ scanlator != null -> scanlator
+ else -> uploadDate
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ChapterListItem
+
+ if (chapter != other.chapter) return false
+ if (flags != other.flags) return false
+ if (uploadDate != other.uploadDate) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = chapter.hashCode()
+ result = 31 * result + flags
+ result = 31 * result + uploadDate.hashCode()
+ return result
+ }
+
+
+ companion object {
+
+ const val FLAG_UNREAD = 2
+ const val FLAG_CURRENT = 4
+ const val FLAG_NEW = 8
+ const val FLAG_MISSING = 16
+ const val FLAG_DOWNLOADED = 32
+ const val MASK_STATUS = FLAG_UNREAD or FLAG_CURRENT
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt
index 0a1609989..98a7f9c4b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt
@@ -1,13 +1,30 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaChapter
-import org.koitharu.kotatsu.history.domain.ChapterExtra
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
+import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
+import java.text.DateFormat
fun MangaChapter.toListItem(
- extra: ChapterExtra,
+ isCurrent: Boolean,
+ isUnread: Boolean,
+ isNew: Boolean,
isMissing: Boolean,
-) = ChapterListItem(
- chapter = this,
- extra = extra,
- isMissing = isMissing,
-)
\ No newline at end of file
+ isDownloaded: Boolean,
+ dateFormat: DateFormat,
+): ChapterListItem {
+ var flags = 0
+ if (isCurrent) flags = flags or FLAG_CURRENT
+ if (isUnread) flags = flags or FLAG_UNREAD
+ if (isNew) flags = flags or FLAG_NEW
+ if (isMissing) flags = flags or FLAG_MISSING
+ if (isDownloaded) flags = flags or FLAG_DOWNLOADED
+ return ChapterListItem(
+ chapter = this,
+ flags = flags,
+ uploadDate = if (uploadDate != 0L) dateFormat.format(uploadDate) else null
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
index 75905aa51..1a36ec7fc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt
@@ -145,7 +145,7 @@ class DownloadManager(
while (true) {
try {
val response = call.clone().await()
- withContext(Dispatchers.IO) {
+ runInterruptible(Dispatchers.IO) {
file.outputStream().use { out ->
checkNotNull(response.body).byteStream().copyTo(out)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt
index 649b67342..20724d769 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.download.ui
import androidx.core.view.isVisible
+import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -10,12 +11,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.utils.JobStateFlow
-import org.koitharu.kotatsu.utils.ext.format
-import org.koitharu.kotatsu.utils.ext.getDisplayMessage
-import org.koitharu.kotatsu.utils.ext.setIndeterminateCompat
+import org.koitharu.kotatsu.utils.ext.*
fun downloadItemAD(
scope: CoroutineScope,
+ coil: ImageLoader,
) = adapterDelegateViewBinding, JobStateFlow, ItemDownloadBinding>(
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }
) {
@@ -24,11 +24,16 @@ fun downloadItemAD(
bind {
job?.cancel()
- job = item.onEach { state ->
+ job = item.onFirst { state ->
+ binding.imageViewCover.newImageRequest(state.manga.coverUrl)
+ .referer(state.manga.publicUrl)
+ .placeholder(state.cover)
+ .fallback(R.drawable.ic_placeholder)
+ .error(R.drawable.ic_placeholder)
+ .allowRgb565(true)
+ .enqueueWith(coil)
+ }.onEach { state ->
binding.textViewTitle.text = state.manga.title
- binding.imageViewCover.setImageDrawable(
- state.cover ?: getDrawable(R.drawable.ic_placeholder)
- )
when (state) {
is DownloadManager.State.Cancelling -> {
binding.textViewStatus.setText(R.string.cancelling_)
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
index 5b39881ed..d590a724d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt
@@ -3,14 +3,17 @@ package org.koitharu.kotatsu.download.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService
@@ -22,7 +25,7 @@ class DownloadsActivity : BaseActivity() {
super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- val adapter = DownloadsAdapter(lifecycleScope)
+ val adapter = DownloadsAdapter(lifecycleScope, get())
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
LifecycleAwareServiceConnection.bindService(
@@ -44,11 +47,15 @@ class DownloadsActivity : BaseActivity() {
right = insets.right,
bottom = insets.bottom
)
- binding.toolbar.updatePadding(
- left = insets.left,
- right = insets.right,
- top = insets.top
- )
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
}
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt
index e6998f894..325180a79 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt
@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.download.ui
import androidx.recyclerview.widget.DiffUtil
+import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlinx.coroutines.CoroutineScope
import org.koitharu.kotatsu.download.domain.DownloadManager
@@ -8,10 +9,11 @@ import org.koitharu.kotatsu.utils.JobStateFlow
class DownloadsAdapter(
scope: CoroutineScope,
+ coil: ImageLoader,
) : AsyncListDifferDelegationAdapter>(DiffCallback()) {
init {
- delegatesManager.addDelegate(downloadItemAD(scope))
+ delegatesManager.addDelegate(downloadItemAD(scope, coil))
setHasStableIds(true)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
index d44204533..03712d889 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
@@ -16,7 +16,6 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -79,6 +78,11 @@ class DownloadService : BaseService() {
return binder ?: DownloadBinder(this).also { binder = it }
}
+ override fun onUnbind(intent: Intent?): Boolean {
+ binder = null
+ return super.onUnbind(intent)
+ }
+
override fun onDestroy() {
unregisterReceiver(controlReceiver)
binder = null
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt
index f66c87fae..aa054c441 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.model.SortOrder
import java.util.*
@Entity(tableName = "favourite_categories")
-data class FavouriteCategoryEntity(
+class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long,
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt
index 95ae66e87..d79660a12 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt
@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
)
]
)
-data class FavouriteEntity(
+class FavouriteEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt
index 5b74bc08f..e98c84904 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt
@@ -7,7 +7,7 @@ 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 FavouriteManga(
+class FavouriteManga(
@Embedded val favourite: FavouriteEntity,
@Relation(
parentColumn = "manga_id",
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt
index 399314311..096d26cfd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt
@@ -30,9 +30,7 @@ class FavouritesContainerFragment : BaseFragment(),
override val recycledViewPool = RecyclerView.RecycledViewPool()
- private val viewModel by viewModel(
- mode = LazyThreadSafetyMode.NONE
- )
+ private val viewModel by viewModel()
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt
index 17639f06f..f08a83032 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt
@@ -2,8 +2,6 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.content.Intent
-import android.content.res.ColorStateList
-import android.graphics.Color
import android.os.Bundle
import android.view.Menu
import android.view.View
@@ -12,9 +10,9 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
-import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
@@ -24,15 +22,14 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.showPopupMenu
class CategoriesActivity : BaseActivity(),
OnListItemClickListener,
View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
- private val viewModel by viewModel(
- mode = LazyThreadSafetyMode.NONE
- )
+ private val viewModel by viewModel()
private lateinit var adapter: CategoriesAdapter
private lateinit var reorderHelper: ItemTouchHelper
@@ -42,10 +39,9 @@ class CategoriesActivity : BaseActivity(),
super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- binding.fabAdd.imageTintList = ColorStateList.valueOf(Color.WHITE)
adapter = CategoriesAdapter(this)
editDelegate = CategoriesEditDelegate(this, this)
- binding.recyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
+ binding.recyclerView.addItemDecoration(MaterialDividerItemDecoration(this, RecyclerView.VERTICAL))
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
binding.fabAdd.setOnClickListener(this)
@@ -95,13 +91,17 @@ class CategoriesActivity : BaseActivity(),
binding.recyclerView.updatePadding(
left = insets.left,
right = insets.right,
- bottom = insets.bottom
- )
- binding.toolbar.updatePadding(
- left = insets.left,
- right = insets.right,
- top = insets.top
+ bottom = 2 * insets.bottom + binding.fabAdd.measureHeight()
)
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
}
private fun onCategoriesChanged(categories: List) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt
index a9a80ce59..bae47cc45 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt
@@ -2,7 +2,8 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.text.InputType
-import androidx.appcompat.app.AlertDialog
+import android.widget.Toast
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -13,7 +14,7 @@ class CategoriesEditDelegate(
) {
fun deleteCategory(category: FavouriteCategory) {
- AlertDialog.Builder(context)
+ MaterialAlertDialogBuilder(context)
.setMessage(context.getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null)
@@ -32,7 +33,12 @@ class CategoriesEditDelegate(
.setNegativeButton(android.R.string.cancel)
.setMaxLength(MAX_TITLE_LENGTH, false)
.setPositiveButton(R.string.rename) { _, name ->
- callback.onRenameCategory(category, name)
+ val trimmed = name.trim()
+ if (trimmed.isEmpty()) {
+ Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show()
+ } else {
+ callback.onRenameCategory(category, name)
+ }
}.create()
.show()
}
@@ -45,7 +51,12 @@ class CategoriesEditDelegate(
.setNegativeButton(android.R.string.cancel)
.setMaxLength(MAX_TITLE_LENGTH, false)
.setPositiveButton(R.string.add) { _, name ->
- callback.onCreateCategory(name)
+ val trimmed = name.trim()
+ if (trimmed.isEmpty()) {
+ Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show()
+ } else {
+ callback.onCreateCategory(trimmed)
+ }
}.create()
.show()
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt
index 3736b959a..c1496aa4c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt
@@ -25,7 +25,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet, CategoriesEditDelegate.CategoriesEditCallback,
View.OnClickListener {
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ private val viewModel by viewModel {
parametersOf(requireNotNull(arguments?.getParcelable(MangaIntent.KEY_MANGA)))
}
@@ -36,7 +36,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet(mode = LazyThreadSafetyMode.NONE) {
+ override val viewModel by viewModel {
parametersOf(categoryId)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt
index 6ca026947..bf6ea6304 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt
@@ -8,6 +8,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel
val historyModule
get() = module {
- single { HistoryRepository(get(), get()) }
+ single { HistoryRepository(get(), get(), get()) }
viewModel { HistoryListViewModel(get(), get(), get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt
index 34a75b6a5..770181595 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt
@@ -18,14 +18,14 @@ import java.util.*
)
]
)
-data class HistoryEntity(
+class HistoryEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int,
- @ColumnInfo(name = "scroll") val scroll: Float
+ @ColumnInfo(name = "scroll") val scroll: Float,
) {
fun toMangaHistory() = MangaHistory(
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryWithManga.kt
index d1c8ee2b0..55f41adc6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryWithManga.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryWithManga.kt
@@ -7,7 +7,7 @@ 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 HistoryWithManga(
+class HistoryWithManga(
@Embedded val history: HistoryEntity,
@Relation(
parentColumn = "manga_id",
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/ChapterExtra.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/ChapterExtra.kt
deleted file mode 100644
index 32178c19d..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/history/domain/ChapterExtra.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.koitharu.kotatsu.history.domain
-
-enum class ChapterExtra {
-
- READ, CURRENT, UNREAD, NEW
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
index ade87172b..e34a4fd92 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt
@@ -9,6 +9,7 @@ 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
import org.koitharu.kotatsu.utils.ext.mapItems
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.utils.ext.mapToSet
class HistoryRepository(
private val db: MangaDatabase,
private val trackingRepository: TrackingRepository,
+ private val settings: AppSettings,
) {
suspend fun getList(offset: Int, limit: Int = 20): List {
@@ -46,6 +48,9 @@ class HistoryRepository(
}
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
+ if (manga.isNsfw && settings.isHistoryExcludeNsfw) {
+ return
+ }
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction {
db.tagsDao.upsert(tags)
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
index 311adb720..550fab1a5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt
@@ -5,7 +5,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
-import androidx.appcompat.app.AlertDialog
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.utils.ext.ellipsize
class HistoryListFragment : MangaListFragment() {
- override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE)
+ override val viewModel by viewModel()
override val isSwipeRefreshEnabled = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -42,7 +42,7 @@ class HistoryListFragment : MangaListFragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_clear_history -> {
- AlertDialog.Builder(context ?: return false)
+ MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.clear_history)
.setMessage(R.string.text_clear_history_prompt)
.setNegativeButton(android.R.string.cancel, null)
diff --git a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt b/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt
new file mode 100644
index 000000000..8e674701a
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt
@@ -0,0 +1,98 @@
+package org.koitharu.kotatsu.image.ui
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.graphics.Insets
+import androidx.core.graphics.drawable.toBitmap
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
+import coil.ImageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
+import coil.target.PoolableViewTarget
+import com.davemorrissey.labs.subscaleview.ImageSource
+import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+import org.koin.android.ext.android.inject
+import org.koitharu.kotatsu.base.ui.BaseActivity
+import org.koitharu.kotatsu.databinding.ActivityImageBinding
+import org.koitharu.kotatsu.utils.ext.enqueueWith
+import org.koitharu.kotatsu.utils.ext.indicator
+
+class ImageActivity : BaseActivity() {
+
+ private val coil: ImageLoader by inject()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(ActivityImageBinding.inflate(layoutInflater))
+ supportActionBar?.run {
+ setDisplayHomeAsUpEnabled(true)
+ setDisplayShowTitleEnabled(false)
+ }
+ loadImage(intent.data)
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
+ }
+
+ private fun loadImage(url: Uri?) {
+ ImageRequest.Builder(this)
+ .data(url)
+ .memoryCachePolicy(CachePolicy.DISABLED)
+ .lifecycle(this)
+ .target(SsivTarget(binding.ssiv))
+ .indicator(binding.progressBar)
+ .enqueueWith(coil)
+ }
+
+ private class SsivTarget(
+ override val view: SubsamplingScaleImageView,
+ ) : PoolableViewTarget {
+
+ override fun onStart(placeholder: Drawable?) = setDrawable(placeholder)
+
+ override fun onError(error: Drawable?) = setDrawable(error)
+
+ override fun onSuccess(result: Drawable) = setDrawable(result)
+
+ override fun onClear() = setDrawable(null)
+
+ override fun equals(other: Any?): Boolean {
+ return (this === other) || (other is SsivTarget && view == other.view)
+ }
+
+ override fun hashCode() = view.hashCode()
+
+ override fun toString() = "SsivTarget(view=$view)"
+
+ private fun setDrawable(drawable: Drawable?) {
+ if (drawable != null) {
+ view.setImage(ImageSource.bitmap(drawable.toBitmap()))
+ } else {
+ view.recycle()
+ }
+ }
+ }
+
+ companion object {
+
+ fun newIntent(context: Context, url: String): Intent {
+ return Intent(context, ImageActivity::class.java)
+ .setData(Uri.parse(url))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/domain/AvailableFilters.kt b/app/src/main/java/org/koitharu/kotatsu/list/domain/AvailableFilters.kt
new file mode 100644
index 000000000..414a219db
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/domain/AvailableFilters.kt
@@ -0,0 +1,30 @@
+package org.koitharu.kotatsu.list.domain
+
+import org.koitharu.kotatsu.core.model.MangaTag
+import org.koitharu.kotatsu.core.model.SortOrder
+
+class AvailableFilters(
+ val sortOrders: Set,
+ val tags: Set,
+) {
+
+ val size: Int
+ get() = sortOrders.size + tags.size
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as AvailableFilters
+ if (sortOrders != other.sortOrders) return false
+ if (tags != other.tags) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = sortOrders.hashCode()
+ result = 31 * result + tags.hashCode()
+ return result
+ }
+
+ fun isEmpty(): Boolean = sortOrders.isEmpty() && tags.isEmpty()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
index 56dc55d19..46dfaefd5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt
@@ -4,37 +4,31 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.SeekBar
-import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
+import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.DialogListModeBinding
+import org.koitharu.kotatsu.utils.ext.setValueRounded
+import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
-class ListModeSelectDialog : AlertDialogFragment(), View.OnClickListener,
- SeekBar.OnSeekBarChangeListener {
+class ListModeSelectDialog : AlertDialogFragment(),
+ CheckableButtonGroup.OnCheckedChangeListener, Slider.OnSliderTouchListener {
private val settings by inject(mode = LazyThreadSafetyMode.NONE)
- private var mode: ListMode = ListMode.GRID
- private var pendingGridSize: Int = 100
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- mode = settings.listMode
- pendingGridSize = settings.gridSize
- }
-
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
) = DialogListModeBinding.inflate(inflater, container, false)
- override fun onBuildDialog(builder: AlertDialog.Builder) {
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder.setTitle(R.string.list_mode)
.setPositiveButton(R.string.done, null)
.setCancelable(true)
@@ -42,51 +36,42 @@ class ListModeSelectDialog : AlertDialogFragment(), View.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ val mode = settings.listMode
binding.buttonList.isChecked = mode == ListMode.LIST
binding.buttonListDetailed.isChecked = mode == ListMode.DETAILED_LIST
binding.buttonGrid.isChecked = mode == ListMode.GRID
binding.textViewGridTitle.isVisible = mode == ListMode.GRID
- binding.seekbarGrid.isVisible = mode == ListMode.GRID
+ binding.sliderGrid.isVisible = mode == ListMode.GRID
- with(binding.seekbarGrid) {
- progress = pendingGridSize - 50
- setOnSeekBarChangeListener(this@ListModeSelectDialog)
- }
+ binding.sliderGrid.setLabelFormatter(IntPercentLabelFormatter())
+ binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
+ binding.sliderGrid.addOnSliderTouchListener(this)
- binding.buttonList.setOnClickListener(this)
- binding.buttonGrid.setOnClickListener(this)
- binding.buttonListDetailed.setOnClickListener(this)
+ binding.checkableGroup.onCheckedChangeListener = this
}
- override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
- pendingGridSize = progress + 50
- }
-
- override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
-
- override fun onStopTrackingTouch(seekBar: SeekBar?) {
- settings.gridSize = pendingGridSize
- }
-
- override fun onClick(v: View) {
- when (v.id) {
- R.id.button_list -> mode = ListMode.LIST
- R.id.button_list_detailed -> mode = ListMode.DETAILED_LIST
- R.id.button_grid -> mode = ListMode.GRID
+ override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
+ val mode = when (checkedId) {
+ R.id.button_list -> ListMode.LIST
+ R.id.button_list_detailed -> ListMode.DETAILED_LIST
+ R.id.button_grid -> ListMode.GRID
+ else -> return
}
binding.textViewGridTitle.isVisible = mode == ListMode.GRID
- binding.seekbarGrid.isVisible = mode == ListMode.GRID
+ binding.sliderGrid.isVisible = mode == ListMode.GRID
settings.listMode = mode
}
+ override fun onStartTrackingTouch(slider: Slider) = Unit
+
+ override fun onStopTrackingTouch(slider: Slider) {
+ settings.gridSize = slider.value.toInt()
+ }
+
companion object {
private const val TAG = "ListModeSelectDialog"
- fun show(fm: FragmentManager) = ListModeSelectDialog()
- .show(
- fm,
- TAG
- )
+ fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaFilterConfig.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaFilterConfig.kt
deleted file mode 100644
index e2147aa97..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaFilterConfig.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.koitharu.kotatsu.list.ui
-
-import org.koitharu.kotatsu.core.model.MangaFilter
-import org.koitharu.kotatsu.core.model.MangaTag
-import org.koitharu.kotatsu.core.model.SortOrder
-
-data class MangaFilterConfig(
- val sortOrders: List,
- val tags: List,
- val currentFilter: MangaFilter?
-)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
index 1fa44344c..bee787480 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt
@@ -21,20 +21,17 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
-import org.koitharu.kotatsu.base.ui.list.decor.ItemTypeDividerDecoration
-import org.koitharu.kotatsu.base.ui.list.decor.SectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.Manga
-import org.koitharu.kotatsu.core.model.MangaFilter
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.FilterAdapter
-import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
+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
@@ -42,10 +39,11 @@ import org.koitharu.kotatsu.utils.RecycledViewPoolHolder
import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListFragment : BaseFragment(),
- PaginationScrollListener.Callback, OnListItemClickListener, OnFilterChangedListener,
- SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
+ PaginationScrollListener.Callback, OnListItemClickListener,
+ 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()
@@ -78,6 +76,7 @@ abstract class MangaListFragment : BaseFragment(),
onRetryClick = ::resolveException,
onTagRemoveClick = viewModel::onRemoveFilterTag
)
+ filterAdapter = FilterAdapter2(viewModel)
paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) {
setHasFixedSize(true)
@@ -85,17 +84,14 @@ abstract class MangaListFragment : BaseFragment(),
addOnScrollListener(paginationListener!!)
}
with(binding.swipeRefreshLayout) {
- setColorSchemeColors(
- ContextCompat.getColor(context, R.color.color_primary),
- ContextCompat.getColor(context, R.color.color_primary_variant)
- )
+ setProgressBackgroundColorSchemeColor(context.getThemeColor(com.google.android.material.R.attr.colorPrimary))
+ setColorSchemeColors(context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary))
setOnRefreshListener(this@MangaListFragment)
isEnabled = isSwipeRefreshEnabled
}
with(binding.recyclerViewFilter) {
setHasFixedSize(true)
- addItemDecoration(ItemTypeDividerDecoration(view.context))
- addItemDecoration(SectionItemDecoration(false, this@MangaListFragment))
+ adapter = filterAdapter
}
(parentFragment as? RecycledViewPoolHolder)?.let {
@@ -113,6 +109,7 @@ abstract class MangaListFragment : BaseFragment(),
override fun onDestroyView() {
drawer = null
listAdapter = null
+ filterAdapter = null
paginationListener = null
spanSizeLookup.invalidateCache()
super.onDestroyView()
@@ -203,28 +200,21 @@ abstract class MangaListFragment : BaseFragment(),
}
}
- protected fun onInitFilter(config: MangaFilterConfig) {
- binding.recyclerViewFilter.adapter = FilterAdapter(
- sortOrders = config.sortOrders,
- tags = config.tags,
- state = config.currentFilter,
- listener = this
- )
+ protected fun onInitFilter(filter: List) {
+ filterAdapter?.items = filter
drawer?.setDrawerLockMode(
- if (config.sortOrders.isEmpty() && config.tags.isEmpty()) {
+ if (filter.isEmpty()) {
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
} else {
DrawerLayout.LOCK_MODE_UNLOCKED
}
) ?: binding.dividerFilter?.let {
- it.isGone = config.sortOrders.isEmpty() && config.tags.isEmpty()
+ it.isGone = filter.isEmpty()
binding.recyclerViewFilter.isVisible = it.isVisible
}
activity?.invalidateOptionsMenu()
}
- override fun onFilterChanged(filter: MangaFilter) = Unit
-
override fun onWindowInsetsChanged(insets: Insets) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
binding.recyclerViewFilter.updatePadding(
@@ -284,20 +274,6 @@ abstract class MangaListFragment : BaseFragment(),
}
}
- final override fun isSection(position: Int): Boolean {
- return position == 0 || binding.recyclerViewFilter.adapter?.run {
- getItemViewType(position) != getItemViewType(position - 1)
- } ?: false
- }
-
- final override fun getSectionTitle(position: Int): CharSequence? {
- return when (binding.recyclerViewFilter.adapter?.getItemViewType(position)) {
- FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order)
- FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genres)
- else -> null
- }
- }
-
protected open fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) = Unit
protected open fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = false
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
index 3d94d1b65..e2a463f4e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt
@@ -1,23 +1,32 @@
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() {
+ private val settings: AppSettings,
+) : BaseViewModel(), OnFilterChangedListener {
abstract val content: LiveData>
- val filter = MutableLiveData()
+ val filter = MutableLiveData>()
val listMode = MutableLiveData()
val gridScale = settings.observe()
.filter { it == AppSettings.KEY_GRID_SIZE }
@@ -37,7 +46,62 @@ abstract class MangaListViewModel(
}
}
- open fun onRemoveFilterTag(tag: MangaTag) = Unit
+ protected var currentFilter: MangaFilter = MangaFilter(null, emptySet())
+ private set(value) {
+ field = value
+ onFilterChanged()
+ }
+ protected var availableFilters: AvailableFilters? = null
+ private var filterJob: Job? = null
+
+ final override fun onSortItemClick(item: FilterItem.Sort) {
+ currentFilter = currentFilter.copy(sortOrder = item.order)
+ }
+
+ final override fun onTagItemClick(item: FilterItem.Tag) {
+ val tags = if (item.isChecked) {
+ currentFilter.tags - item.tag
+ } else {
+ currentFilter.tags + item.tag
+ }
+ currentFilter = currentFilter.copy(tags = tags)
+ }
+
+ fun onRemoveFilterTag(tag: MangaTag) {
+ val tags = currentFilter.tags
+ if (tag !in tags) {
+ return
+ }
+ currentFilter = currentFilter.copy(tags = tags - tag)
+ }
+
+ @CallSuper
+ open fun onFilterChanged() {
+ val previousJob = filterJob
+ filterJob = launchJob(Dispatchers.Default) {
+ previousJob?.cancelAndJoin()
+ filter.postValue(
+ availableFilters?.run {
+ val list = ArrayList(size + 2)
+ if (sortOrders.isNotEmpty()) {
+ val selectedSort = currentFilter.sortOrder ?: sortOrders.first()
+ list += FilterItem.Header(R.string.sort_order)
+ sortOrders.sortedBy { it.ordinal }.mapTo(list) {
+ FilterItem.Sort(it, isSelected = it == selectedSort)
+ }
+ }
+ if (tags.isNotEmpty()) {
+ list += FilterItem.Header(R.string.genres)
+ tags.sortedBy { it.title }.mapTo(list) {
+ FilterItem.Tag(it, isChecked = it in currentFilter.tags)
+ }
+ }
+ ensureActive()
+ list
+ }.orEmpty()
+ )
+ }
+ }
abstract fun onRefresh()
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
index cc4c80967..61cd60c03 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt
@@ -43,10 +43,6 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick))
}
- fun setItems(list: List, commitCallback: Runnable) {
- differ.submitList(list, commitCallback)
- }
-
private class DiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
deleted file mode 100644
index 983fdf0f1..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package org.koitharu.kotatsu.list.ui.filter
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
-import org.koitharu.kotatsu.core.model.MangaFilter
-import org.koitharu.kotatsu.core.model.MangaTag
-import org.koitharu.kotatsu.core.model.SortOrder
-
-class FilterAdapter(
- private val sortOrders: List = emptyList(),
- private val tags: List = emptyList(),
- state: MangaFilter?,
- private val listener: OnFilterChangedListener
-) : RecyclerView.Adapter>() {
-
- private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), emptySet())
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
- VIEW_TYPE_SORT -> FilterSortHolder(parent).apply {
- itemView.setOnClickListener {
- setCheckedSort(requireData())
- }
- }
- VIEW_TYPE_TAG -> FilterTagHolder(parent).apply {
- itemView.setOnClickListener {
- setCheckedTag(boundData ?: return@setOnClickListener, !isChecked)
- }
- }
- else -> throw IllegalArgumentException("Unknown viewType $viewType")
- }
-
- override fun getItemCount() = sortOrders.size + tags.size
-
- override fun onBindViewHolder(holder: BaseViewHolder<*, Boolean, *>, position: Int) {
- when (holder) {
- is FilterSortHolder -> {
- val item = sortOrders[position]
- holder.bind(item, item == currentState.sortOrder)
- }
- is FilterTagHolder -> {
- val item = tags[position - sortOrders.size]
- holder.bind(item, item in currentState.tags)
- }
- }
- }
-
- override fun getItemViewType(position: Int) = when (position) {
- in sortOrders.indices -> VIEW_TYPE_SORT
- else -> VIEW_TYPE_TAG
- }
-
- fun setCheckedTag(tag: MangaTag, isChecked: Boolean) {
- currentState = if (tag in currentState.tags) {
- if (!isChecked) {
- currentState.copy(tags = currentState.tags - tag)
- } else {
- return
- }
- } else {
- if (isChecked) {
- currentState.copy(tags = currentState.tags + tag)
- } else {
- return
- }
- }
- val index = tags.indexOf(tag)
- if (index in tags.indices) {
- notifyItemChanged(sortOrders.size + index)
- }
- listener.onFilterChanged(currentState)
- }
-
- fun setCheckedSort(sort: SortOrder) {
- if (sort != currentState.sortOrder) {
- val oldItemPos = sortOrders.indexOf(currentState.sortOrder)
- val newItemPos = sortOrders.indexOf(sort)
- currentState = currentState.copy(sortOrder = sort)
- if (oldItemPos in sortOrders.indices) {
- notifyItemChanged(oldItemPos)
- }
- if (newItemPos in sortOrders.indices) {
- notifyItemChanged(newItemPos)
- }
- listener.onFilterChanged(currentState)
- }
- }
-
- companion object {
-
- const val VIEW_TYPE_SORT = 0
- const val VIEW_TYPE_TAG = 1
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt
new file mode 100644
index 000000000..67b4d3585
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter2.kt
@@ -0,0 +1,12 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
+
+class FilterAdapter2(
+ listener: OnFilterChangedListener,
+) : AsyncListDifferDelegationAdapter(
+ FilterDiffCallback(),
+ filterSortDelegate(listener),
+ filterTagDelegate(listener),
+ filterHeaderDelegate(),
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt
new file mode 100644
index 000000000..8b926d768
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt
@@ -0,0 +1,47 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
+import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
+import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
+
+fun filterSortDelegate(
+ listener: OnFilterChangedListener,
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) }
+) {
+
+ itemView.setOnClickListener {
+ listener.onSortItemClick(item)
+ }
+
+ bind {
+ binding.root.setText(item.order.titleRes)
+ binding.root.isChecked = item.isSelected
+ }
+}
+
+fun filterTagDelegate(
+ listener: OnFilterChangedListener,
+) = adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }
+) {
+
+ itemView.setOnClickListener {
+ listener.onTagItemClick(item)
+ }
+
+ bind {
+ binding.root.text = item.tag.title
+ binding.root.isChecked = item.isChecked
+ }
+}
+
+fun filterHeaderDelegate() = adapterDelegateViewBinding(
+ { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
+) {
+
+ bind {
+ binding.root.setText(item.titleResId)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt
new file mode 100644
index 000000000..1ccd4e813
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt
@@ -0,0 +1,48 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import androidx.recyclerview.widget.DiffUtil
+
+class FilterDiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
+ return when {
+ oldItem.javaClass != newItem.javaClass -> false
+ oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
+ oldItem.titleResId == newItem.titleResId
+ }
+ oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
+ oldItem.tag == newItem.tag
+ }
+ oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
+ oldItem.order == newItem.order
+ }
+ else -> false
+ }
+ }
+
+ override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
+ return when {
+ oldItem is FilterItem.Header && newItem is FilterItem.Header -> true
+ oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
+ oldItem.isChecked == newItem.isChecked
+ }
+ oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
+ oldItem.isSelected == newItem.isSelected
+ }
+ else -> false
+ }
+ }
+
+ override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
+ val isCheckedChanged = when {
+ oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
+ oldItem.isChecked != newItem.isChecked
+ }
+ oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
+ oldItem.isSelected != newItem.isSelected
+ }
+ else -> false
+ }
+ return if (isCheckedChanged) Unit else super.getChangePayload(oldItem, newItem)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt
new file mode 100644
index 000000000..a74d93b1d
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt
@@ -0,0 +1,22 @@
+package org.koitharu.kotatsu.list.ui.filter
+
+import androidx.annotation.StringRes
+import org.koitharu.kotatsu.core.model.MangaTag
+import org.koitharu.kotatsu.core.model.SortOrder
+
+sealed interface FilterItem {
+
+ class Header(
+ @StringRes val titleResId: Int,
+ ) : FilterItem
+
+ class Sort(
+ val order: SortOrder,
+ val isSelected: Boolean,
+ ) : FilterItem
+
+ class Tag(
+ val tag: MangaTag,
+ val isChecked: Boolean,
+ ) : FilterItem
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt
deleted file mode 100644
index 9275ae831..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.koitharu.kotatsu.list.ui.filter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
-import org.koitharu.kotatsu.core.model.SortOrder
-import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
-
-class FilterSortHolder(parent: ViewGroup) :
- BaseViewHolder(
- ItemCheckableSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- ) {
-
- override fun onBind(data: SortOrder, extra: Boolean) {
- binding.root.setText(data.titleRes)
- binding.root.isChecked = extra
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt
deleted file mode 100644
index 2054d4cb9..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.koitharu.kotatsu.list.ui.filter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
-import org.koitharu.kotatsu.core.model.MangaTag
-import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
-
-class FilterTagHolder(parent: ViewGroup) :
- BaseViewHolder(
- ItemCheckableMultipleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- ) {
-
- val isChecked: Boolean
- get() = binding.root.isChecked
-
- override fun onBind(data: MangaTag, extra: Boolean) {
- binding.root.text = data.title
- binding.root.isChecked = extra
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt
index 93a1b7db5..a28596c9f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt
@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.list.ui.filter
-import org.koitharu.kotatsu.core.model.MangaFilter
+interface OnFilterChangedListener {
-fun interface OnFilterChangedListener {
+ fun onSortItemClick(item: FilterItem.Sort)
- fun onFilterChanged(filter: MangaFilter)
+ fun onTagItemClick(item: FilterItem.Tag)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
index cd4f5ea83..4e2746cec 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt
@@ -9,23 +9,24 @@ import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import okio.buffer
import okio.source
import java.util.zip.ZipFile
class CbzFetcher : Fetcher {
- @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch(
pool: BitmapPool,
data: Uri,
size: Size,
options: Options,
- ): FetchResult {
+ ): FetchResult = runInterruptible(Dispatchers.IO) {
val zip = ZipFile(data.schemeSpecificPart)
val entry = zip.getEntry(data.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
- return SourceResult(
+ SourceResult(
source = ExtraCloseableBufferedSource(
zip.getInputStream(entry).source().buffer(),
zip,
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
index 45df69340..f678b83b7 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
@@ -7,6 +7,8 @@ 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
@@ -20,9 +22,11 @@ class MangaIndex(source: String?) {
json.put("title_alt", manga.altTitle)
json.put("url", manga.url)
json.put("public_url", manga.publicUrl)
+ json.put("author", manga.author)
json.put("cover", manga.coverUrl)
json.put("description", manga.description)
json.put("rating", manga.rating)
+ json.put("nsfw", manga.isNsfw)
json.put("source", manga.source.name)
json.put("cover_large", manga.largeCoverUrl)
json.put("tags", JSONArray().also { a ->
@@ -48,8 +52,11 @@ class MangaIndex(source: String?) {
altTitle = json.getStringOrNull("title_alt"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
+ author = json.getStringOrNull("author"),
+ largeCoverUrl = json.getStringOrNull("cover_large"),
source = source,
rating = json.getDouble("rating").toFloat(),
+ isNsfw = json.getBooleanOrDefault("nsfw", false),
coverUrl = json.getString("cover"),
description = json.getStringOrNull("description"),
tags = json.getJSONArray("tags").mapToSet { x ->
@@ -59,7 +66,7 @@ class MangaIndex(source: String?) {
source = source
)
},
- chapters = getChapters(json.getJSONObject("chapters"), source)
+ chapters = getChapters(json.getJSONObject("chapters"), source),
)
}.getOrNull()
@@ -72,6 +79,8 @@ class MangaIndex(source: String?) {
jo.put("number", chapter.number)
jo.put("url", chapter.url)
jo.put("name", chapter.name)
+ jo.put("uploadDate", chapter.uploadDate)
+ jo.put("scanlator", chapter.scanlator)
jo.put("branch", chapter.branch)
jo.put("entries", "%03d\\d{3}".format(chapter.number))
chapters.put(chapter.id.toString(), jo)
@@ -98,8 +107,10 @@ class MangaIndex(source: String?) {
name = v.getString("name"),
url = v.getString("url"),
number = v.getInt("number"),
+ uploadDate = v.getLongOrDefault("uploadDate", 0L),
+ scanlator = v.getStringOrNull("scanlator"),
branch = v.getStringOrNull("branch"),
- source = source
+ source = source,
)
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt
index 2904910d6..c9d93f147 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt
@@ -31,7 +31,7 @@ class MangaZip(val file: File) {
return writableCbz.flush()
}
- fun addCover(file: File, ext: String) {
+ suspend fun addCover(file: File, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(0, 0))
if (ext.isNotEmpty() && ext.length <= 4) {
@@ -39,11 +39,11 @@ class MangaZip(val file: File) {
append(ext)
}
}
- writableCbz[name] = file
+ writableCbz.put(name, file)
index.setCoverEntry(name)
}
- fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
+ suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
val name = buildString {
append(FILENAME_PATTERN.format(chapter.number, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) {
@@ -51,7 +51,7 @@ class MangaZip(val file: File) {
append(ext)
}
}
- writableCbz[name] = file
+ writableCbz.put(name, file)
index.addChapter(chapter)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt
index 5a591740f..fbc2637aa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt
@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.*
+import org.koitharu.kotatsu.utils.ext.deleteAwait
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@@ -14,7 +14,6 @@ class WritableCbzFile(private val file: File) {
private val dir = File(file.parentFile, file.nameWithoutExtension)
- @Suppress("BlockingMethodInNonBlockingContext")
suspend fun prepare() = withContext(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty"
@@ -27,11 +26,13 @@ class WritableCbzFile(private val file: File) {
}
ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry
- while (entry != null) {
+ while (entry != null && currentCoroutineContext().isActive) {
val target = File(dir.path + File.separator + entry.name)
- target.parentFile?.mkdirs()
- target.outputStream().use { out ->
- zip.copyTo(out)
+ runInterruptible {
+ target.parentFile?.mkdirs()
+ target.outputStream().use { out ->
+ zip.copyTo(out)
+ }
}
zip.closeEntry()
entry = zip.nextEntry
@@ -44,52 +45,50 @@ class WritableCbzFile(private val file: File) {
}
@CheckResult
- @Suppress("BlockingMethodInNonBlockingContext")
suspend fun flush() = withContext(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) {
- tempFile.delete()
+ tempFile.deleteAwait()
}
try {
- ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
- dir.listFiles()?.forEach {
- zipFile(it, it.name, zip)
+ runInterruptible {
+ ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
+ dir.listFiles()?.forEach {
+ zipFile(it, it.name, zip)
+ }
+ zip.flush()
}
- zip.flush()
}
tempFile.renameTo(file)
} finally {
if (tempFile.exists()) {
- tempFile.delete()
+ tempFile.deleteAwait()
}
}
}
operator fun get(name: String) = File(dir, name)
- operator fun set(name: String, file: File) {
+ suspend fun put(name: String, file: File) = runInterruptible(Dispatchers.IO) {
file.copyTo(this[name], overwrite = true)
}
- companion object {
-
- private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
- if (fileToZip.isDirectory) {
- if (fileName.endsWith("/")) {
- zipOut.putNextEntry(ZipEntry(fileName))
- } else {
- zipOut.putNextEntry(ZipEntry("$fileName/"))
- }
- zipOut.closeEntry()
- fileToZip.listFiles()?.forEach { childFile ->
- zipFile(childFile, "$fileName/${childFile.name}", zipOut)
- }
+ private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
+ if (fileToZip.isDirectory) {
+ if (fileName.endsWith("/")) {
+ zipOut.putNextEntry(ZipEntry(fileName))
} else {
- FileInputStream(fileToZip).use { fis ->
- val zipEntry = ZipEntry(fileName)
- zipOut.putNextEntry(zipEntry)
- fis.copyTo(zipOut)
- }
+ zipOut.putNextEntry(ZipEntry("$fileName/"))
+ }
+ zipOut.closeEntry()
+ fileToZip.listFiles()?.forEach { childFile ->
+ zipFile(childFile, "$fileName/${childFile.name}", zipOut)
+ }
+ } else {
+ FileInputStream(fileToZip).use { fis ->
+ val zipEntry = ZipEntry(fileName)
+ zipOut.putNextEntry(zipEntry)
+ fis.copyTo(zipOut)
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
index 3d4c51571..2dfb798f2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt
@@ -8,6 +8,7 @@ import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -23,6 +24,7 @@ import java.util.zip.ZipFile
class LocalMangaRepository(private val context: Context) : MangaRepository {
+ override val source = MangaSource.LOCAL
private val filenameFilter = CbzFilter()
override suspend fun getList2(
@@ -42,37 +44,39 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
getFromFile(Uri.parse(manga.url).toFile())
} else manga
- @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getPages(chapter: MangaChapter): List {
- val uri = Uri.parse(chapter.url)
- val file = uri.toFile()
- val zip = ZipFile(file)
- val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
- var entries = zip.entries().asSequence()
- entries = if (index != null) {
- val pattern = index.getChapterNamesPattern(chapter)
- entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
- } else {
- val parent = uri.fragment.orEmpty()
- entries.filter { x ->
- !x.isDirectory && x.name.substringBeforeLast(
- File.separatorChar,
- ""
- ) == parent
+ return runInterruptible(Dispatchers.IO){
+ val uri = Uri.parse(chapter.url)
+ val file = uri.toFile()
+ val zip = ZipFile(file)
+ val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
+ var entries = zip.entries().asSequence()
+ entries = if (index != null) {
+ val pattern = index.getChapterNamesPattern(chapter)
+ entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
+ } else {
+ val parent = uri.fragment.orEmpty()
+ entries.filter { x ->
+ !x.isDirectory && x.name.substringBeforeLast(
+ File.separatorChar,
+ ""
+ ) == parent
+ }
}
+ entries
+ .toList()
+ .sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
+ .map { x ->
+ val entryUri = zipUri(file, x.name)
+ MangaPage(
+ id = entryUri.longHashCode(),
+ url = entryUri,
+ preview = null,
+ referer = chapter.url,
+ source = MangaSource.LOCAL,
+ )
+ }
}
- return entries
- .toList()
- .sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
- .map { x ->
- val entryUri = zipUri(file, x.name)
- MangaPage(
- id = entryUri.longHashCode(),
- url = entryUri,
- referer = chapter.url,
- source = MangaSource.LOCAL
- )
- }
}
suspend fun delete(manga: Manga): Boolean {
@@ -123,7 +127,10 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
name = s.ifEmpty { title },
number = i + 1,
source = MangaSource.LOCAL,
- url = uriBuilder.fragment(s).build().toString()
+ uploadDate = 0L,
+ url = uriBuilder.fragment(s).build().toString(),
+ scanlator = null,
+ branch = null,
)
}
)
@@ -133,20 +140,18 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
val file = runCatching {
Uri.parse(localManga.url).toFile()
}.getOrNull() ?: return null
- return withContext(Dispatchers.IO) {
- @Suppress("BlockingMethodInNonBlockingContext")
+ return runInterruptible(Dispatchers.IO) {
ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
- val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null
- index.getMangaInfo()
+ val index = entry?.let(zip::readText)?.let(::MangaIndex)
+ index?.getMangaInfo()
}
}
}
- suspend fun findSavedManga(remoteManga: Manga): Manga? = withContext(Dispatchers.IO) {
+ suspend fun findSavedManga(remoteManga: Manga): Manga? = runInterruptible(Dispatchers.IO) {
val files = getAllFiles()
for (file in files) {
- @Suppress("BlockingMethodInNonBlockingContext")
val index = ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
entry?.let(zip::readText)?.let(::MangaIndex)
@@ -154,7 +159,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
val info = index.getMangaInfo() ?: continue
if (info.id == remoteManga.id) {
val fileUri = file.toUri().toString()
- return@withContext info.copy(
+ return@runInterruptible info.copy(
source = MangaSource.LOCAL,
url = fileUri,
chapters = info.chapters?.map { c -> c.copy(url = fileUri) }
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
index 319cf5619..586e96bfc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt
@@ -9,7 +9,7 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.BuildConfig
@@ -19,9 +19,9 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.utils.ext.ellipsize
-class LocalListFragment : MangaListFragment(), ActivityResultCallback {
+class LocalListFragment : MangaListFragment(), ActivityResultCallback {
- override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE)
+ override val viewModel by viewModel()
private val importCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(),
this
@@ -98,7 +98,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback {
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
return when (item.itemId) {
R.id.action_delete -> {
- AlertDialog.Builder(context ?: return false)
+ MaterialAlertDialogBuilder(context ?: return false)
.setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, data.title))
.setPositiveButton(R.string.delete) { _, _ ->
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
index 5c12ad115..b1fc2493e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
@@ -16,9 +17,9 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
-import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.ext.resolveName
import java.io.File
import java.io.IOException
@@ -74,17 +75,18 @@ class LocalListViewModel(
launchLoadingJob {
val contentResolver = context.contentResolver
withContext(Dispatchers.IO) {
- val name = MediaStoreCompat(contentResolver).getName(uri)
+ val name = contentResolver.resolveName(uri)
?: throw IOException("Cannot fetch name from uri: $uri")
if (!LocalMangaRepository.isFileSupported(name)) {
throw UnsupportedFileException("Unsupported file on $uri")
}
val dest = settings.getStorageDir(context)?.let { File(it, name) }
?: throw IOException("External files dir unavailable")
- @Suppress("BlockingMethodInNonBlockingContext")
- contentResolver.openInputStream(uri)?.use { source ->
- dest.outputStream().use { output ->
- source.copyTo(output)
+ runInterruptible {
+ contentResolver.openInputStream(uri)?.use { source ->
+ dest.outputStream().use { output ->
+ source.copyTo(output)
+ }
}
} ?: throw IOException("Cannot open input stream: $uri")
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
index d333fddb7..6b59897ad 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -1,17 +1,13 @@
package org.koitharu.kotatsu.main.ui
import android.app.ActivityOptions
-import android.content.res.ColorStateList
import android.content.res.Configuration
-import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.appcompat.app.ActionBarDrawerToggle
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.*
@@ -21,8 +17,8 @@ import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
-import androidx.transition.TransitionManager
import com.google.android.material.appbar.AppBarLayout
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get
@@ -32,7 +28,6 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSection
-import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.databinding.NavigationHeaderBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
@@ -59,14 +54,11 @@ class MainActivity : BaseActivity(),
NavigationView.OnNavigationItemSelectedListener, AppBarOwner,
View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener {
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE)
- private val searchSuggestionViewModel by viewModel(
- mode = LazyThreadSafetyMode.NONE
- )
+ private val viewModel by viewModel()
+ private val searchSuggestionViewModel by viewModel()
private lateinit var navHeaderBinding: NavigationHeaderBinding
private lateinit var drawerToggle: ActionBarDrawerToggle
- private var searchViewElevation = 0f
override val appBar: AppBarLayout
get() = binding.appbar
@@ -74,7 +66,6 @@ class MainActivity : BaseActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater))
- searchViewElevation = binding.toolbarCard.cardElevation
navHeaderBinding = NavigationHeaderBinding.inflate(layoutInflater)
drawerToggle = ActionBarDrawerToggle(
this,
@@ -91,13 +82,6 @@ class MainActivity : BaseActivity(),
binding.drawer.addDrawerListener(drawerToggle)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- if (get().isAmoledTheme && get().theme == AppCompatDelegate.MODE_NIGHT_YES) {
- binding.appbar.setBackgroundColor(Color.BLACK)
- binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_background))
- } else {
- binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_surface))
- }
-
with(binding.searchView) {
onFocusChangeListener = this@MainActivity
searchSuggestionListener = this@MainActivity
@@ -114,17 +98,13 @@ class MainActivity : BaseActivity(),
insets
}
addHeaderView(navHeaderBinding.root)
- itemBackground = navigationItemBackground(context)
setNavigationItemSelectedListener(this@MainActivity)
}
- with(binding.fab) {
- imageTintList = ColorStateList.valueOf(Color.WHITE)
- setOnClickListener(this@MainActivity)
- }
+ binding.fab.setOnClickListener(this@MainActivity)
supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let {
- binding.fab.isVisible = it is HistoryListFragment
+ if (it is HistoryListFragment) binding.fab.show() else binding.fab.hide()
} ?: run {
openDefaultSection()
}
@@ -271,7 +251,7 @@ class MainActivity : BaseActivity(),
}
override fun onClearSearchHistory() {
- AlertDialog.Builder(this)
+ MaterialAlertDialogBuilder(this)
.setTitle(R.string.clear_search_history)
.setMessage(R.string.text_clear_search_history_prompt)
.setNegativeButton(android.R.string.cancel, null)
@@ -302,7 +282,7 @@ class MainActivity : BaseActivity(),
binding.fab.isEnabled = !isLoading
if (isLoading) {
binding.fab.setImageDrawable(CircularProgressDrawable(this).also {
- it.setColorSchemeColors(Color.WHITE)
+ it.setColorSchemeColors(R.color.kotatsu_onPrimaryContainer)
it.strokeWidth = resources.resolveDp(2f)
it.start()
})
@@ -316,6 +296,7 @@ class MainActivity : BaseActivity(),
submenu.removeGroup(R.id.group_remote_sources)
remoteSources.forEachIndexed { index, source ->
submenu.add(R.id.group_remote_sources, source.ordinal, index, source.title)
+ .setIcon(R.drawable.ic_manga_source)
}
submenu.setGroupCheckable(R.id.group_remote_sources, true, true)
}
@@ -349,48 +330,17 @@ class MainActivity : BaseActivity(),
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment, TAG_PRIMARY)
.commit()
- binding.fab.isVisible = fragment is HistoryListFragment
+ if (fragment is HistoryListFragment) binding.fab.show() else binding.fab.hide()
}
private fun onSearchOpened() {
binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
drawerToggle.isDrawerIndicatorEnabled = false
- TransitionManager.beginDelayedTransition(binding.appbar)
- // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/grey/black
- if (isDarkAmoledTheme()) {
- binding.toolbar.setBackgroundColor(Color.BLACK)
- } else {
- binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_surface))
- }
- binding.toolbarCard.apply {
- cardElevation = 0f
- // Remove margin
- updateLayoutParams {
- leftMargin = 0
- rightMargin = 0
- }
-
- }
- binding.appbar.elevation = searchViewElevation
}
private fun onSearchClosed() {
binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
drawerToggle.isDrawerIndicatorEnabled = true
- if (isDarkAmoledTheme()) {
- binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_background))
- }
- TransitionManager.beginDelayedTransition(binding.appbar)
- // Returning transparent color
- binding.appbar.setBackgroundColor(Color.TRANSPARENT)
- binding.appbar.elevation = 0f
- binding.toolbarCard.apply {
- cardElevation = searchViewElevation
- updateLayoutParams {
- leftMargin = resources.resolveDp(16)
- rightMargin = resources.resolveDp(16)
- }
- }
}
private fun onFirstStart() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt
index 19ad00c4b..97ca1df3a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt
@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class ProtectActivity : BaseActivity(), TextView.OnEditorActionListener,
TextWatcher, View.OnClickListener {
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE)
+ private val viewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt
index 74ea5b618..a7a806fef 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt
@@ -13,6 +13,6 @@ val readerModule
single { PagesCache(get()) }
viewModel { params ->
- ReaderViewModel(params[0], params[1], get(), get(), get(), get())
+ ReaderViewModel(params[0], params[1], get(), get(), get(), get(), get())
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt
index 38cfe450e..8e4e8316d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt
@@ -50,7 +50,7 @@ class PageLoader(
private fun loadAsync(page: MangaPage): Deferred {
var repo = repository
- if (repo?.javaClass != page.source.cls) {
+ if (repo?.source != page.source) {
repo = mangaRepositoryOf(page.source)
repository = repo
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt
index 78d713fff..4575e12fa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt
@@ -4,20 +4,21 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
-import androidx.recyclerview.widget.DividerItemDecoration
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.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.withArgs
class ChaptersDialog : AlertDialogFragment(),
@@ -28,7 +29,7 @@ class ChaptersDialog : AlertDialogFragment(),
container: ViewGroup?,
) = DialogChaptersBinding.inflate(inflater, container, false)
- override fun onBuildDialog(builder: AlertDialog.Builder) {
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder.setTitle(R.string.chapters)
.setNegativeButton(R.string.close, null)
.setCancelable(true)
@@ -36,7 +37,7 @@ class ChaptersDialog : AlertDialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.recyclerViewChapters.addItemDecoration(
- DividerItemDecoration(requireContext(), RecyclerView.VERTICAL)
+ MaterialDividerItemDecoration(view.context, RecyclerView.VERTICAL)
)
val chapters = arguments?.getParcelableArrayList(ARG_CHAPTERS)
if (chapters == null) {
@@ -45,15 +46,16 @@ class ChaptersDialog : AlertDialogFragment(),
}
val currentId = arguments?.getLong(ARG_CURRENT_ID, 0L) ?: 0L
val currentPosition = chapters.indexOfFirst { it.id == currentId }
+ val dateFormat = get().dateFormat()
binding.recyclerViewChapters.adapter = ChaptersAdapter(this).apply {
setItems(chapters.mapIndexed { index, chapter ->
chapter.toListItem(
- when {
- index < currentPosition -> ChapterExtra.READ
- index == currentPosition -> ChapterExtra.CURRENT
- else -> ChapterExtra.UNREAD
- },
- isMissing = false
+ isCurrent = index == currentPosition,
+ isUnread = index > currentPosition,
+ isNew = false,
+ isMissing = false,
+ isDownloaded = false,
+ dateFormat = dateFormat,
)
}) {
if (currentPosition >= 0) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
index b8b7bcb19..dc57a4fb2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
@@ -10,12 +10,15 @@ import android.view.*
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
-import androidx.core.view.*
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.core.view.postDelayed
+import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
@@ -52,7 +55,7 @@ class ReaderActivity : BaseFullscreenActivity(),
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
ActivityResultCallback, ReaderControlDelegate.OnInteractionListener {
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ private val viewModel by viewModel {
parametersOf(MangaIntent.from(intent), intent?.getParcelableExtra(EXTRA_STATE))
}
@@ -192,7 +195,8 @@ class ReaderActivity : BaseFullscreenActivity(),
override fun onActivityResult(result: Boolean) {
if (result) {
- viewModel.saveCurrentPage(contentResolver)
+ viewModel.saveCurrentState(reader?.getCurrentState())
+ viewModel.saveCurrentPage()
}
}
@@ -207,7 +211,7 @@ class ReaderActivity : BaseFullscreenActivity(),
}
private fun onError(e: Throwable) {
- val dialog = AlertDialog.Builder(this)
+ val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_occurred)
.setMessage(e.getDisplayMessage(resources))
.setPositiveButton(R.string.close, null)
@@ -234,8 +238,8 @@ class ReaderActivity : BaseFullscreenActivity(),
) {
false
} else {
- val targets = binding.root.hitTest(rawX, rawY)
- targets.none { it.hasOnClickListeners() }
+ val touchables = window.peekDecorView()?.touchables
+ touchables?.none { it.hasGlobalPoint(rawX, rawY) } ?: true
}
}
@@ -277,7 +281,7 @@ class ReaderActivity : BaseFullscreenActivity(),
private fun onPageSaved(uri: Uri?) {
if (uri != null) {
- Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_LONG)
+ Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_INDEFINITE)
.setAnchorView(binding.appbarBottom)
.setAction(R.string.share) {
ShareHelper(this).shareImage(uri)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt
index 42824267d..85f326c68 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt
@@ -5,16 +5,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
+import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.databinding.DialogReaderConfigBinding
import org.koitharu.kotatsu.utils.ext.withArgs
class ReaderConfigDialog : AlertDialogFragment(),
- View.OnClickListener {
+ CheckableButtonGroup.OnCheckedChangeListener {
private lateinit var mode: ReaderMode
@@ -30,7 +31,7 @@ class ReaderConfigDialog : AlertDialogFragment(),
?: ReaderMode.STANDARD
}
- override fun onBuildDialog(builder: AlertDialog.Builder) {
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder.setTitle(R.string.read_mode)
.setPositiveButton(R.string.done, null)
.setCancelable(true)
@@ -42,9 +43,7 @@ class ReaderConfigDialog : AlertDialogFragment(),
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
- binding.buttonStandard.setOnClickListener(this)
- binding.buttonReversed.setOnClickListener(this)
- binding.buttonWebtoon.setOnClickListener(this)
+ binding.checkableGroup.onCheckedChangeListener = this
}
override fun onDismiss(dialog: DialogInterface) {
@@ -53,11 +52,12 @@ class ReaderConfigDialog : AlertDialogFragment(),
super.onDismiss(dialog)
}
- override fun onClick(v: View) {
- when (v.id) {
- R.id.button_standard -> mode = ReaderMode.STANDARD
- R.id.button_webtoon -> mode = ReaderMode.WEBTOON
- R.id.button_reversed -> mode = ReaderMode.REVERSED
+ override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
+ mode = when (checkedId) {
+ R.id.button_standard -> ReaderMode.STANDARD
+ R.id.button_webtoon -> ReaderMode.WEBTOON
+ R.id.button_reversed -> ReaderMode.REVERSED
+ else -> return
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index adb4a5914..fa6203f55 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -1,9 +1,7 @@
package org.koitharu.kotatsu.reader.ui
-import android.content.ContentResolver
import android.net.Uri
import android.util.LongSparseArray
-import android.webkit.URLUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
@@ -19,15 +17,17 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.os.ShortcutsRepository
+import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.history.domain.HistoryRepository
-import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
-import org.koitharu.kotatsu.utils.MediaStoreCompat
+import org.koitharu.kotatsu.utils.DownloadManagerHelper
import org.koitharu.kotatsu.utils.SingleLiveEvent
-import org.koitharu.kotatsu.utils.ext.*
+import org.koitharu.kotatsu.utils.ext.IgnoreErrors
+import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import org.koitharu.kotatsu.utils.ext.processLifecycleScope
class ReaderViewModel(
intent: MangaIntent,
@@ -35,7 +35,8 @@ class ReaderViewModel(
private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository,
- private val settings: AppSettings
+ private val settings: AppSettings,
+ private val downloadManagerHelper: DownloadManagerHelper,
) : BaseViewModel() {
private var loadingJob: Job? = null
@@ -75,7 +76,7 @@ class ReaderViewModel(
var manga = dataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga
- val repo = manga.source.repository
+ val repo = MangaRepository(manga.source)
manga = repo.getDetails(manga)
manga.chapters?.forEach {
chapters.put(it.id, it)
@@ -147,22 +148,17 @@ class ReaderViewModel(
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
}
- fun saveCurrentPage(resolver: ContentResolver) {
+ fun saveCurrentPage() {
launchJob(Dispatchers.Default) {
try {
val state = currentState.value ?: error("Undefined state")
val page = content.value?.pages?.find {
it.chapterId == state.chapterId && it.index == state.page
}?.toMangaPage() ?: error("Page not found")
- val repo = page.source.repository
+ val repo = MangaRepository(page.source)
val pageUrl = repo.getPageUrl(page)
- val file = get()[pageUrl] ?: error("Page not found in cache")
- val uri = file.inputStream().use { input ->
- val fileName = URLUtil.guessFileName(pageUrl, null, null)
- MediaStoreCompat(resolver).insertImage(fileName) {
- input.copyTo(it)
- }
- }
+ val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
+ val uri = downloadManagerHelper.awaitDownload(downloadId)
onPageSaved.postCall(uri)
} catch (e: CancellationException) {
} catch (e: Exception) {
@@ -209,7 +205,7 @@ class ReaderViewModel(
private suspend fun loadChapter(chapterId: Long): List {
val manga = checkNotNull(mangaData.value) { "Manga is null" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
- val repo = manga.source.repository
+ val repo = MangaRepository(manga.source)
return repo.getPages(chapter).mapIndexed { index, page ->
ReaderPage.from(page, index, chapterId)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt
index 0113bb475..8efab6827 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt
@@ -4,7 +4,9 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
+import android.view.ViewGroup
import androidx.core.graphics.Insets
+import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import org.koitharu.kotatsu.BuildConfig
@@ -38,11 +40,15 @@ class SimpleSettingsActivity : BaseActivity() {
}
override fun onWindowInsetsChanged(insets: Insets) {
- binding.toolbar.updatePadding(
- top = insets.top,
- left = insets.left,
- right = insets.right
- )
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
}
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
index c44a5a1c0..b6adc87b1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
@@ -47,10 +47,6 @@ abstract class BaseReaderAdapter>(
viewType: Int
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
- fun setItems(items: List, callback: Runnable) {
- differ.submitList(items, callback)
- }
-
suspend fun setItems(items: List) = suspendCoroutine { cont ->
differ.submitList(items) {
cont.resume(Unit)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt
index 1da4a9d39..87ea32b7f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt
@@ -5,32 +5,27 @@ import androidx.viewpager2.widget.ViewPager2
class ReversedPageAnimTransformer : ViewPager2.PageTransformer {
- override fun transformPage(page: View, position: Float) {
- with(page) {
- val pageWidth = width
- when {
- position > 1 -> alpha = 0f
- position >= 0 -> {
- alpha = 1f
- translationX = 0f
- translationZ = 0f
- scaleX = 1 + FACTOR * position
- scaleY = 1f
- }
- position >= -1 -> {
- alpha = 1f
- translationX = pageWidth * -position
- translationZ = -1f
- scaleX = 1f
- scaleY = 1f
- }
- else -> alpha = 0f
+ override fun transformPage(page: View, position: Float) = with(page) {
+ translationX = -position * width
+ pivotX = width.toFloat()
+ pivotY = height / 2f
+ cameraDistance = 20000f
+ when {
+ position < -1f || position > 1f -> {
+ alpha = 0f
+ rotationY = 0f
+ translationZ = -1f
+ }
+ position <= 0f -> {
+ alpha = 1f
+ rotationY = 0f
+ translationZ = 0f
+ }
+ position > 0f -> {
+ alpha = 1f
+ rotationY = 120 * position
+ translationZ = 2f
}
}
}
-
- private companion object {
-
- const val FACTOR = 0.1f
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
index edf5b205b..33920e631 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.graphics.PointF
+import android.view.Gravity
+import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
@@ -16,6 +18,11 @@ class ReversedPageHolder(
exceptionResolver: ExceptionResolver
) : PageHolder(binding, loader, settings, exceptionResolver) {
+ init {
+ (binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
+ .gravity = Gravity.START or Gravity.BOTTOM
+ }
+
override fun onImageShowing(zoom: ZoomMode) {
with(binding.ssiv) {
maxScale = 2f * maxOf(
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt
index cd03ce8eb..1aad3c8b0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt
@@ -5,32 +5,27 @@ import androidx.viewpager2.widget.ViewPager2
class PageAnimTransformer : ViewPager2.PageTransformer {
- override fun transformPage(page: View, position: Float) {
- page.apply {
- val pageWidth = width
- when {
- position < -1 -> alpha = 0f
- position <= 0 -> { // [-1,0]
- alpha = 1f
- translationX = 0f
- translationZ = 0f
- scaleX = 1 + FACTOR * position
- scaleY = 1f
- }
- position <= 1 -> { // (0,1]
- alpha = 1f
- translationX = pageWidth * -position
- translationZ = -1f
- scaleX = 1f
- scaleY = 1f
- }
- else -> alpha = 0f
+ override fun transformPage(page: View, position: Float) = with(page) {
+ translationX = -position * width
+ pivotX = 0f
+ pivotY = height / 2f
+ cameraDistance = 20000f
+ when {
+ position < -1f || position > 1f -> {
+ alpha = 0f
+ rotationY = 0f
+ translationZ = -1f
+ }
+ position > 0f -> {
+ alpha = 1f
+ rotationY = 0f
+ translationZ = 0f
+ }
+ position <= 0f -> {
+ alpha = 1f
+ rotationY = 120 * position
+ translationZ = 2f
}
}
}
-
- private companion object {
-
- const val FACTOR = 0.1f
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
index 369bccf48..bd630b8d0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
@@ -20,17 +20,20 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
open class PageHolder(
binding: ItemPageBinding,
loader: PageLoader,
- settings: AppSettings, exceptionResolver: ExceptionResolver
+ settings: AppSettings,
+ exceptionResolver: ExceptionResolver,
) : BasePageHolder(binding, loader, settings, exceptionResolver),
View.OnClickListener {
init {
binding.ssiv.setOnImageEventListener(delegate)
binding.buttonRetry.setOnClickListener(this)
+ binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
}
override fun onBind(data: ReaderPage) {
delegate.onBind(data.toMangaPage())
+ binding.textViewNumber.text = (data.index + 1).toString()
}
override fun onRecycled() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
index c93a27ef0..d2d2a6b50 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
@@ -12,7 +12,10 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
-import org.koitharu.kotatsu.utils.ext.*
+import org.koitharu.kotatsu.utils.ext.doOnPageChanged
+import org.koitharu.kotatsu.utils.ext.recyclerView
+import org.koitharu.kotatsu.utils.ext.resetTransformations
+import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import kotlin.math.absoluteValue
class PagerReaderFragment : BaseReader() {
@@ -37,8 +40,8 @@ class PagerReaderFragment : BaseReader() {
val transformer = if (it) PageAnimTransformer() else null
binding.pager.setPageTransformer(transformer)
if (transformer == null) {
- binding.pager.recyclerView?.children?.forEach {
- it.resetTransformations()
+ binding.pager.recyclerView?.children?.forEach { view ->
+ view.resetTransformations()
}
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt
index 09cb044c0..a762e05ef 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt
@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon
+import android.app.Activity
import android.content.Context
import android.graphics.PointF
import android.util.AttributeSet
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.utils.ext.toIntUp
-class WebtoonImageView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null) :
- SubsamplingScaleImageView(context, attr) {
+class WebtoonImageView @JvmOverloads constructor(
+ context: Context,
+ attr: AttributeSet? = null,
+) : SubsamplingScaleImageView(context, attr) {
private val ct = PointF()
- private val displayHeight = resources.displayMetrics.heightPixels
+ private val displayHeight = (context as Activity).window.decorView.height
private var scrollPos = 0
private var scrollRange = SCROLL_UNKNOWN
@@ -55,6 +58,30 @@ class WebtoonImageView @JvmOverloads constructor(context: Context, attr: Attribu
return desiredHeight
}
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
+ val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
+ val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
+ val parentHeight = MeasureSpec.getSize(heightMeasureSpec)
+ val resizeWidth = widthSpecMode != MeasureSpec.EXACTLY
+ val resizeHeight = heightSpecMode != MeasureSpec.EXACTLY
+ var width = parentWidth
+ var height = parentHeight
+ if (sWidth > 0 && sHeight > 0) {
+ if (resizeWidth && resizeHeight) {
+ width = sWidth
+ height = sHeight
+ } else if (resizeHeight) {
+ height = (sHeight.toDouble() / sWidth.toDouble() * width).toInt()
+ } else if (resizeWidth) {
+ width = (sWidth.toDouble() / sHeight.toDouble() * height).toInt()
+ }
+ }
+ width = width.coerceAtLeast(suggestedMinimumWidth)
+ height = height.coerceIn(suggestedMinimumHeight, displayHeight)
+ setMeasuredDimension(width, height)
+ }
+
private fun scrollToInternal(pos: Int) {
scrollPos = pos
ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt
index f4535d7be..dfb0bad8c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt
@@ -34,7 +34,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
consumed[0] = 0
consumed[1] = consumedY
}
- return consumedY != 0
+ return consumedY != 0 || dy == 0
}
private fun consumeVerticalScroll(dy: Int): Int {
diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
index 1f9b543a6..5ae3a92da 100644
--- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt
@@ -6,7 +6,6 @@ import android.view.MenuItem
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.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
@@ -15,7 +14,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class RemoteListFragment : MangaListFragment() {
- override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ override val viewModel by viewModel {
parametersOf(source)
}
@@ -29,10 +28,6 @@ class RemoteListFragment : MangaListFragment() {
return source.title
}
- override fun onFilterChanged(filter: MangaFilter) {
- viewModel.applyFilter(filter)
- }
-
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_list_remote, menu)
diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
index d8df693a3..b9b1f3d3e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt
@@ -9,12 +9,10 @@ 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.model.MangaFilter
-import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
-import org.koitharu.kotatsu.list.ui.MangaFilterConfig
+import org.koitharu.kotatsu.list.domain.AvailableFilters
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -27,7 +25,6 @@ class RemoteListViewModel(
private val mangaList = MutableStateFlow?>(null)
private val hasNextPage = MutableStateFlow(false)
private val listError = MutableStateFlow(null)
- private var appliedFilter: MangaFilter? = null
private var loadingJob: Job? = null
private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0)
@@ -68,16 +65,6 @@ class RemoteListViewModel(
loadList(append = !mangaList.value.isNullOrEmpty())
}
- override fun onRemoveFilterTag(tag: MangaTag) {
- val filter = appliedFilter ?: return
- if (tag !in filter.tags) {
- return
- }
- applyFilter(
- filter.copy(tags = filter.tags - tag)
- )
- }
-
fun loadNextPage() {
if (hasNextPage.value && listError.value == null) {
loadList(append = true)
@@ -93,8 +80,8 @@ class RemoteListViewModel(
listError.value = null
val list = repository.getList2(
offset = if (append) mangaList.value?.size ?: 0 else 0,
- sortOrder = appliedFilter?.sortOrder,
- tags = appliedFilter?.tags,
+ sortOrder = currentFilter.sortOrder,
+ tags = currentFilter.tags,
)
if (!append) {
mangaList.value = list
@@ -111,26 +98,29 @@ class RemoteListViewModel(
}
}
- fun applyFilter(newFilter: MangaFilter) {
- appliedFilter = newFilter
+ override fun onFilterChanged() {
+ super.onFilterChanged()
mangaList.value = null
hasNextPage.value = false
loadList(false)
- filter.value?.run {
- filter.value = copy(currentFilter = newFilter)
- }
}
- private fun createFilterModel() = appliedFilter?.run {
- CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) })
+ private fun createFilterModel(): CurrentFilterModel? {
+ val tags = currentFilter.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.sortedBy { it.ordinal }
- val tags = repository.getTags().sortedBy { it.title }
- filter.postValue(MangaFilterConfig(sorts, tags, appliedFilter))
+ val sorts = repository.sortOrders
+ val tags = repository.getTags()
+ availableFilters = AvailableFilters(sorts, tags)
+ onFilterChanged()
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt
index d099f54c5..efb736d7b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.SortOrder
+import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.levenshteinDistance
@@ -29,7 +30,7 @@ class MangaSearchRepository(
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source ->
runCatching {
- source.repository.getList2(
+ MangaRepository(source).getList2(
offset = 0,
query = query,
sortOrder = SortOrder.POPULARITY
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
index 7221f6d11..725a11303 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt
@@ -4,8 +4,10 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
+import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
+import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -18,9 +20,7 @@ import org.koitharu.kotatsu.utils.ext.showKeyboard
class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
- private val searchSuggestionViewModel by viewModel(
- mode = LazyThreadSafetyMode.NONE
- )
+ private val searchSuggestionViewModel by viewModel()
private lateinit var source: MangaSource
override fun onCreate(savedInstanceState: Bundle?) {
@@ -46,10 +46,17 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery
}
override fun onWindowInsetsChanged(insets: Insets) {
- binding.toolbar.updatePadding(
- top = insets.top,
- left = insets.left,
- right = insets.right
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
+ binding.container.updatePadding(
+ bottom = insets.bottom
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt
index 3ebafa633..6f2621475 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt
@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class SearchFragment : MangaListFragment() {
- override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ override val viewModel by viewModel {
parametersOf(source, query)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt
index b1228d4bf..ad23f0b98 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchActivity.kt
@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.search.ui.global
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.view.ViewGroup
import androidx.core.graphics.Insets
+import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
@@ -31,11 +33,15 @@ class GlobalSearchActivity : BaseActivity() {
}
override fun onWindowInsetsChanged(insets: Insets) {
- binding.toolbar.updatePadding(
- top = insets.top,
- left = insets.left,
- right = insets.right
- )
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
}
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt
index 4680fa8b1..2f6ca1ae3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/global/GlobalSearchFragment.kt
@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class GlobalSearchFragment : MangaListFragment() {
- override val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ override val viewModel by viewModel {
parametersOf(query)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
index 0b6b67e5e..90e9f9bbe 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt
@@ -7,8 +7,8 @@ import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
-import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -77,7 +77,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
@MainThread
private fun showUpdateDialog(version: AppVersion) {
- AlertDialog.Builder(activity)
+ MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_update_available)
.setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name))
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
index 99ec51e9c..82abffced 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
@@ -2,8 +2,8 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
-import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -119,7 +119,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
private fun clearSearchHistory(preference: Preference) {
- AlertDialog.Builder(context ?: return)
+ MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.clear_search_history)
.setMessage(R.string.text_clear_search_history_prompt)
.setNegativeButton(android.R.string.cancel, null)
@@ -138,7 +138,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
private fun clearCookies() {
- AlertDialog.Builder(context ?: return)
+ MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.clear_cookies)
.setMessage(R.string.text_clear_cookies_prompt)
.setNegativeButton(android.R.string.cancel, null)
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt
index a61eaa361..1bc8be9fc 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt
@@ -1,35 +1,45 @@
package org.koitharu.kotatsu.settings
-import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
-import android.text.InputType
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
-import androidx.preference.*
-import com.google.android.material.snackbar.Snackbar
-import kotlinx.coroutines.launch
-import org.koitharu.kotatsu.BuildConfig
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreference
+import leakcanary.LeakCanary
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
-import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
-import org.koitharu.kotatsu.utils.ext.*
+import org.koitharu.kotatsu.settings.utils.SliderPreference
+import org.koitharu.kotatsu.utils.ext.getStorageName
+import org.koitharu.kotatsu.utils.ext.names
+import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import java.io.File
+import java.util.*
class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
SharedPreferences.OnSharedPreferenceChangeListener,
StorageSelectDialog.OnStorageSelectListener {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_main)
- findPreference(AppSettings.KEY_GRID_SIZE)?.run {
+ findPreference(AppSettings.KEY_GRID_SIZE)?.run {
summary = "%d%%".format(value)
setOnPreferenceChangeListener { preference, newValue ->
preference.summary = "%d%%".format(newValue)
@@ -40,6 +50,22 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name)
}
+ findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
+ AppSettings.isDynamicColorAvailable
+ findPreference(AppSettings.KEY_DATE_FORMAT)?.run {
+ entryValues = arrayOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd", "dd MMM yyyy", "MMM dd, yyyy")
+ val now = Date().time
+ entries = entryValues.map { value ->
+ val formattedDate = settings.dateFormat(value.toString()).format(now)
+ if (value == "") {
+ "${context.getString(R.string.system_default)} ($formattedDate)"
+ } else {
+ "$value ($formattedDate)"
+ }
+ }.toTypedArray()
+ setDefaultValueCompat("")
+ summary = "%s"
+ }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -53,6 +79,21 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
settings.subscribe(this)
}
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+ inflater.inflate(R.menu.opt_settings, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.action_leaks -> {
+ startActivity(LeakCanary.newLeakDisplayActivityIntent())
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
@@ -63,6 +104,9 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
AppSettings.KEY_THEME -> {
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
+ AppSettings.KEY_DYNAMIC_THEME -> {
+ findPreference(key)?.setSummary(R.string.restart_required)
+ }
AppSettings.KEY_THEME_AMOLED -> {
findPreference(key)?.setSummary(R.string.restart_required)
}
@@ -92,12 +136,12 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
}
}
- override fun onPreferenceTreeClick(preference: Preference?): Boolean {
- return when (preference?.key) {
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ return when (preference.key) {
AppSettings.KEY_LOCAL_STORAGE -> {
val ctx = context ?: return false
StorageSelectDialog.Builder(ctx, settings.getStorageDir(ctx), this)
- .setTitle(preference.title)
+ .setTitle(preference.title ?: "")
.setNegativeButton(android.R.string.cancel)
.create()
.show()
@@ -121,53 +165,4 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
settings.setStorageDir(context ?: return, file)
}
- private fun enableAppProtection(preference: SwitchPreference) {
- val ctx = preference.context ?: return
- val cancelListener =
- object : DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
-
- override fun onCancel(dialog: DialogInterface?) {
- settings.appPassword = null
- preference.isChecked = false
- preference.isEnabled = true
- }
-
- override fun onClick(dialog: DialogInterface?, which: Int) = onCancel(dialog)
- }
- preference.isEnabled = false
- TextInputDialog.Builder(ctx)
- .setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
- .setHint(R.string.enter_password)
- .setNegativeButton(android.R.string.cancel, cancelListener)
- .setOnCancelListener(cancelListener)
- .setPositiveButton(android.R.string.ok) { d, password ->
- if (password.isBlank()) {
- cancelListener.onCancel(d)
- return@setPositiveButton
- }
- TextInputDialog.Builder(ctx)
- .setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
- .setHint(R.string.repeat_password)
- .setNegativeButton(android.R.string.cancel, cancelListener)
- .setOnCancelListener(cancelListener)
- .setPositiveButton(android.R.string.ok) { d2, password2 ->
- if (password == password2) {
- settings.appPassword = password.md5()
- preference.isChecked = true
- preference.isEnabled = true
- } else {
- cancelListener.onCancel(d2)
- Snackbar.make(
- listView,
- R.string.passwords_mismatch,
- Snackbar.LENGTH_SHORT
- ).show()
- }
- }.setTitle(preference.title)
- .create()
- .show()
- }.setTitle(preference.title)
- .create()
- .show()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt
index 923a7e8bf..5c021a39f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt
@@ -37,8 +37,8 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif
}
}
- override fun onPreferenceTreeClick(preference: Preference?): Boolean {
- return when (preference?.key) {
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ return when (preference.key) {
AppSettings.KEY_NOTIFICATIONS_SOUND -> {
ringtonePickContract.launch(settings.notificationSound.toUriOrNull())
true
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt
index 5382630a7..562462b82 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt
@@ -3,9 +3,12 @@ package org.koitharu.kotatsu.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.view.ViewGroup
import androidx.core.graphics.Insets
+import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.preference.Preference
@@ -16,7 +19,8 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
class SettingsActivity : BaseActivity(),
- PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
+ PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
+ FragmentManager.OnBackStackChangedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -30,13 +34,26 @@ class SettingsActivity : BaseActivity(),
}
}
- @Suppress("DEPRECATION")
+ override fun onStart() {
+ super.onStart()
+ supportFragmentManager.addOnBackStackChangedListener(this)
+ }
+
+ override fun onStop() {
+ supportFragmentManager.removeOnBackStackChangedListener(this)
+ super.onStop()
+ }
+
+ override fun onBackStackChanged() {
+ binding.appbar.setExpanded(true, true)
+ }
+
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
val fm = supportFragmentManager
- val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment)
+ val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false)
fragment.arguments = pref.extras
fragment.setTargetFragment(caller, 0)
openFragment(fragment)
@@ -61,11 +78,15 @@ class SettingsActivity : BaseActivity(),
}
override fun onWindowInsetsChanged(insets: Insets) {
- binding.toolbar.updatePadding(
- top = insets.top,
- left = insets.left,
- right = insets.right
- )
+ with(binding.toolbar) {
+ updatePadding(
+ left = insets.left,
+ right = insets.right
+ )
+ updateLayoutParams {
+ topMargin = insets.top
+ }
+ }
}
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt
index 24fe705d3..6d9f543c4 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.onboard.OnboardViewModel
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
+import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
val settingsModule
get() = module {
@@ -25,4 +26,5 @@ val settingsModule
}
viewModel { ProtectSetupViewModel(get()) }
viewModel { OnboardViewModel(get()) }
+ viewModel { SourcesSettingsViewModel(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
index da9115e80..750d66013 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
@@ -51,8 +51,8 @@ class SourceSettingsFragment : PreferenceFragmentCompat() {
}
}
- override fun onPreferenceTreeClick(preference: Preference?): Boolean {
- return when (preference?.key) {
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ return when (preference.key) {
SourceSettings.KEY_AUTH -> {
startActivity(
SourceAuthActivity.newIntent(
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt
index a4c489f8b..4c7870e57 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt
@@ -35,8 +35,8 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.new_chapters_che
}
}
- override fun onPreferenceTreeClick(preference: Preference?): Boolean {
- return when (preference?.key) {
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ return when (preference.key) {
AppSettings.KEY_NOTIFICATIONS_SETTINGS -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt
index 19107be54..e295d3bfa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt
@@ -30,8 +30,8 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
}
- override fun onPreferenceTreeClick(preference: Preference?): Boolean {
- return when (preference?.key) {
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ return when (preference.key) {
AppSettings.KEY_APP_VERSION -> {
checkForUpdates()
true
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt
index 50c448149..b1d63684b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt
@@ -7,8 +7,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
@@ -21,7 +21,7 @@ import java.io.FileOutputStream
class BackupDialogFragment : AlertDialogFragment() {
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE)
+ private val viewModel by viewModel()
private var backup: File? = null
private val saveFileContract =
@@ -49,13 +49,13 @@ class BackupDialogFragment : AlertDialogFragment() {
viewModel.onError.observe(viewLifecycleOwner, this::onError)
}
- override fun onBuildDialog(builder: AlertDialog.Builder) {
+ override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder.setCancelable(false)
.setNegativeButton(android.R.string.cancel, null)
}
private fun onError(e: Throwable) {
- AlertDialog.Builder(context ?: return)
+ MaterialAlertDialogBuilder(context ?: return)
.setNegativeButton(R.string.close, null)
.setTitle(R.string.error)
.setMessage(e.getDisplayMessage(resources))
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
index 93bde0e64..b41ebb205 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt
@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
- ActivityResultCallback {
+ ActivityResultCallback {
private val backupSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(),
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt
index 5353cdefc..9ccb5e1b8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt
@@ -5,8 +5,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
@@ -22,10 +22,10 @@ class RestoreDialogFragment : AlertDialogFragment() {
override fun onInflateView(
inflater: LayoutInflater,
- container: ViewGroup?
+ container: ViewGroup?,
) = DialogProgressBinding.inflate(inflater, container, false)
- private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) {
+ private val viewModel by viewModel {
parametersOf(arguments?.getString(ARG_FILE)?.toUriOrNull())
}
@@ -39,12 +39,12 @@ class RestoreDialogFragment : AlertDialogFragment