Add chapter description (date, scanlator)

This commit is contained in:
Zakhar Timoshenko
2021-10-17 22:00:26 +03:00
committed by Koitharu
parent ad76d6d414
commit 2380d69b11
23 changed files with 358 additions and 37 deletions

View File

@@ -9,6 +9,8 @@ data class MangaChapter(
val name: String, val name: String,
val number: Int, val number: Int,
val url: String, val url: String,
val scanlator: String? = null,
val date_upload: Long,
val branch: String? = null, val branch: String? = null,
val source: MangaSource val source: MangaSource
) : Parcelable ) : Parcelable

View File

@@ -84,9 +84,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
?.toRelativeUrl(getDomain()) ?: return@mapIndexedNotNull null ?.toRelativeUrl(getDomain()) ?: return@mapIndexedNotNull null
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
name = a.selectFirst("a")?.text().orEmpty(), name = "Глава " + a.selectFirst("a")?.text().orEmpty(),
number = i + 1, number = i + 1,
url = href, url = href,
date_upload = 0L,
source = source source = source
) )
} }

View File

@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository( abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(
@@ -79,15 +80,14 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
return manga.copy( return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"), description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.absUrl("src"), largeCoverUrl = root.getElementById("cover")?.absUrl("src"),
chapters = root.select("table.table_cha").flatMap { table -> chapters = root.select("table.table_cha tr:gt(1)").reversed().mapIndexedNotNull { i, tr ->
table.select("div.manga2") val href = tr?.selectFirst("a")?.relUrl("href") ?: return@mapIndexedNotNull null
}.map { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a ->
val href = a?.relUrl("href") ?: return@mapIndexedNotNull null
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
name = a.text().trim(), name = tr.select("a").text().trim(),
number = i + 1, number = i + 1,
url = href, url = href,
date_upload = parseChapterDate(tr.select("div.date").text()),
source = source source = source
) )
} }
@@ -154,4 +154,18 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
SortOrder.NEWEST -> "datedesc" SortOrder.NEWEST -> "datedesc"
else -> "favdesc" else -> "favdesc"
} }
private fun parseChapterDate(string: String): Long {
return try {
dateFormat.parse(string)?.time ?: 0
} catch (_: ParseException) {
0
}
}
companion object {
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.US)
}
}
} }

View File

@@ -93,11 +93,14 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
description = json.getString("description"), description = json.getString("description"),
chapters = chaptersList.mapIndexed { i, it -> chapters = chaptersList.mapIndexed { i, it ->
val chid = it.getLong("id") val chid = it.getLong("id")
val volChap = "Том " + it.getString("vol") + ". " + "Глава " + it.getString("ch")
val title = if (it.getString("title") == "null") "" else it.getString("title")
MangaChapter( MangaChapter(
id = generateUid(chid), id = generateUid(chid),
source = manga.source, source = manga.source,
url = "$baseChapterUrl$chid", url = "$baseChapterUrl$chid",
name = it.getStringOrNull("title") ?: "${manga.title} #${it.getDouble("ch")}", date_upload = it.getLong("date") * 1000,
name = if (title.isEmpty()) volChap else "$volChap: $title",
number = totalChapters - i number = totalChapters - i
) )
}.reversed() }.reversed()

View File

@@ -141,7 +141,7 @@ class ExHentaiRepository(
name = "${manga.title} #$i", name = "${manga.title} #$i",
number = i, number = i,
url = url, url = url,
branch = null, date_upload = 0L,
source = source, source = source,
) )
} }

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
abstract class GroupleRepository(loaderContext: MangaLoaderContext) : abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
@@ -123,13 +124,22 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
) )
}, },
chapters = root.selectFirst("div.chapters-link")?.selectFirst("table") chapters = root.selectFirst("div.chapters-link")?.selectFirst("table")
?.select("a")?.asReversed()?.mapIndexed { i, a -> ?.select("tr:has(td > a)")?.asReversed()?.mapIndexed { i, tr ->
val href = a.relUrl("href") val href = tr.selectFirst("a")?.relUrl("href")
var translators = ""
val translatorElement = tr.select("a").attr("title")
if (!translatorElement.isNullOrBlank()) {
translators = translatorElement
.replace("(Переводчик),", "&")
.removeSuffix(" (Переводчик)")
}
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href!!),
name = a.ownText().removePrefix(manga.title).trim(), name = tr.select("a").text().removePrefix(manga.title).trim(),
number = i + 1, number = i + 1,
url = href, url = href,
date_upload = parseChapterDate(tr.select("td.d-none").text()),
scanlator = translators,
source = source source = source
) )
} }
@@ -223,13 +233,24 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
return loaderContext.httpPost(url, payload) return loaderContext.httpPost(url, payload)
} }
private fun parseChapterDate(string: String): Long {
return try {
dateFormat.parse(string)?.time ?: 0
} catch (_: ParseException) {
0
}
}
private companion object { private companion object {
private const val PAGE_SIZE = 70 private const val PAGE_SIZE = 70
private const val PAGE_SIZE_SEARCH = 50 private const val PAGE_SIZE_SEARCH = 50
val HEADER = Headers.Builder() private val HEADER = Headers.Builder()
.add("User-Agent", "readmangafun") .add("User-Agent", "readmangafun")
.build() .build()
private val dateFormat by lazy {
SimpleDateFormat("dd.MM.yy", Locale.US)
}
} }
} }

View File

@@ -49,6 +49,7 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
url = readLink, url = readLink,
source = source, source = source,
number = 1, number = 1,
date_upload = 0L,
name = manga.title name = manga.title
) )
) )

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
open class MangaLibRepository(loaderContext: MangaLoaderContext) : open class MangaLibRepository(loaderContext: MangaLoaderContext) :
@@ -91,7 +92,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
for (i in 0 until total) { for (i in 0 until total) {
val item = list.getJSONObject(i) val item = list.getJSONObject(i)
val chapterId = item.getLong("chapter_id") val chapterId = item.getLong("chapter_id")
val branchName = item.getStringOrNull("username") val scanlator = item.getStringOrNull("username")
val url = buildString { val url = buildString {
append(manga.url) append(manga.url)
append("/v") append("/v")
@@ -102,19 +103,19 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
append('/') append('/')
append(item.optString("chapter_string")) append(item.optString("chapter_string"))
} }
var name = item.getStringOrNull("chapter_name") val nameChapter = item.getStringOrNull("chapter_name")
if (name.isNullOrBlank() || name == "null") { val volume = item.getInt("chapter_volume")
name = "Том " + item.getInt("chapter_volume") + val number = item.getString("chapter_number")
" Глава " + item.getString("chapter_number") val fullNameChapter = "Том $volume. Глава $number"
}
chapters.add( chapters.add(
MangaChapter( MangaChapter(
id = generateUid(chapterId), id = generateUid(chapterId),
url = url, url = url,
source = source, source = source,
branch = branchName,
number = total - i, number = total - i,
name = name date_upload = parseChapterDate(item.getString("chapter_created_at").substringBefore(" ")),
scanlator = scanlator,
name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
) )
) )
} }
@@ -235,9 +236,23 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
.toFloatOrNull()?.div(5f) ?: Manga.NO_RATING, .toFloatOrNull()?.div(5f) ?: Manga.NO_RATING,
state = null, state = null,
source = source, source = source,
coverUrl = "https://$domain${covers.getString("thumbnail")}", coverUrl = covers.getString("thumbnail"),
largeCoverUrl = "https://$domain${covers.getString("default")}" largeCoverUrl = covers.getString("default")
) )
} }
} }
private fun parseChapterDate(string: String): Long {
return try {
dateFormat.parse(string)?.time ?: 0
} catch (_: ParseException) {
0
}
}
companion object {
private val dateFormat by lazy {
SimpleDateFormat("yyy-MM-dd", Locale.US)
}
}
} }

View File

@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
@@ -99,6 +100,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
name = a.select("label").text(), name = a.select("label").text(),
number = i + 1, number = i + 1,
url = href, url = href,
date_upload = parseChapterDate(li.select("small:last-of-type").text()),
source = MangaSource.MANGAOWL source = MangaSource.MANGAOWL
) )
} }
@@ -156,4 +158,18 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
else -> "3" else -> "3"
} }
private fun parseChapterDate(string: String): Long {
return try {
dateFormat.parse(string)?.time ?: 0
} catch (_: ParseException) {
0
}
}
companion object {
private val dateFormat by lazy {
SimpleDateFormat("MM/dd/yyyy", Locale.US)
}
}
} }

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
class MangaTownRepository(loaderContext: MangaLoaderContext) : class MangaTownRepository(loaderContext: MangaLoaderContext) :
@@ -117,6 +118,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
url = href, url = href,
source = MangaSource.MANGATOWN, source = MangaSource.MANGATOWN,
number = i + 1, number = i + 1,
date_upload = parseChapterDate(li.select("span.time").text()),
name = name.ifEmpty { "${manga.title} - ${i + 1}" } name = name.ifEmpty { "${manga.title} - ${i + 1}" }
) )
} }
@@ -167,6 +169,20 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
} }
} }
private fun parseChapterDate(date: String): Long {
return when {
date.contains("Today") -> Calendar.getInstance().timeInMillis
date.contains("Yesterday") -> Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis
else -> {
try {
SimpleDateFormat("MMM dd,yyyy", Locale.US).parse(date)?.time ?: 0L
} catch (e: Exception) {
0L
}
}
}
}
override fun onCreatePreferences(map: MutableMap<String, Any>) { override fun onCreatePreferences(map: MutableMap<String, Any>) {
super.onCreatePreferences(map) super.onCreatePreferences(map)
map[SourceSettings.KEY_USE_SSL] = true map[SourceSettings.KEY_USE_SSL] = true

View File

@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
class MangareadRepository( class MangareadRepository(
@@ -138,6 +139,7 @@ class MangareadRepository(
name = a!!.ownText(), name = a!!.ownText(),
number = i + 1, number = i + 1,
url = href, url = href,
date_upload = parseChapterDate(doc2.select("span.chapter-release-date i").firstOrNull()?.text()),
source = MangaSource.MANGAREAD source = MangaSource.MANGAREAD
) )
} }
@@ -162,6 +164,79 @@ class MangareadRepository(
} }
} }
private fun parseChapterDate(date: String?): Long {
date ?: return 0
fun SimpleDateFormat.tryParse(string: String): Long {
return try {
parse(string)?.time ?: 0
} catch (_: ParseException) {
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 companion object {
private const val PAGE_SIZE = 12 private const val PAGE_SIZE = 12
@@ -173,5 +248,11 @@ class MangareadRepository(
val pos = it.indexOf('=') val pos = it.indexOf('=')
it.substring(0, pos) to it.substring(pos + 1) it.substring(0, pos) to it.substring(pos + 1)
}.toMutableMap() }.toMutableMap()
private val dateFormat by lazy {
SimpleDateFormat("MMMM dd, yyyy", Locale.US)
}
} }
} }
class WordSet(private vararg val words: String) { fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) } }

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
abstract class NineMangaRepository( abstract class NineMangaRepository(
@@ -99,18 +100,19 @@ abstract class NineMangaRepository(
) )
}.orEmpty(), }.orEmpty(),
author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.text(), author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.text(),
state = parseStatus(infoRoot.select("li a.red").text()),
description = infoRoot.getElementsByAttributeValue("itemprop", "description").first() description = infoRoot.getElementsByAttributeValue("itemprop", "description").first()
?.html()?.substringAfter("</b>"), ?.html()?.substringAfter("</b>"),
chapters = root.selectFirst("div.chapterbox")?.selectFirst("ul") chapters = root.selectFirst("div.chapterbox")?.select("ul.sub_vol_ul > li")
?.select("li")?.asReversed()?.mapIndexed { i, li -> ?.asReversed()?.mapIndexed { i, li ->
val a = li.selectFirst("a") val a = li.selectFirst("a.chapter_list_a")
val href = a?.relUrl("href") ?: parseFailed("Link not found") val href = a?.relUrl("href")?.replace("%20", " ") ?: parseFailed("Link not found")
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
name = a.text(), name = a.text(),
number = i + 1, number = i + 1,
url = href, url = href,
branch = null, date_upload = parseChapterDateByLang(li.select("span").text()),
source = source, source = source,
) )
} }
@@ -153,7 +155,55 @@ abstract class NineMangaRepository(
} ?: parseFailed("Root not found") } ?: parseFailed("Root not found")
} }
class English(loaderContext: MangaLoaderContext) : NineMangaRepository( private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> MangaState.ONGOING
status.contains("Completed") -> MangaState.FINISHED
else -> null
}
fun parseChapterDateByLang(date: String): Long {
val dateWords = date.split(" ")
if (dateWords.size == 3) {
if (dateWords[1].contains(",")) {
return try {
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date)?.time ?: 0L
} catch (e: ParseException) {
0L
}
} 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
}
open class English(loaderContext: MangaLoaderContext) : NineMangaRepository(
loaderContext, loaderContext,
MangaSource.NINEMANGA_EN, MangaSource.NINEMANGA_EN,
"www.ninemanga.com", "www.ninemanga.com",

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
@@ -109,12 +110,16 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
}, },
chapters = chapters.mapIndexed { i, jo -> chapters = chapters.mapIndexed { i, jo ->
val id = jo.getLong("id") val id = jo.getLong("id")
val name = jo.getString("name") val name = jo.getString("name").capitalize(Locale.ROOT)
val publishers = jo.getJSONArray("publishers")
MangaChapter( MangaChapter(
id = generateUid(id), id = generateUid(id),
url = "/api/titles/chapters/$id/", url = "/api/titles/chapters/$id/",
number = chapters.length() - i, number = chapters.length() - i,
name = buildString { name = buildString {
append("Том ")
append(jo.getString("tome"))
append(". ")
append("Глава ") append("Глава ")
append(jo.getString("chapter")) append(jo.getString("chapter"))
if (name.isNotEmpty()) { if (name.isNotEmpty()) {
@@ -122,6 +127,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
append(name) append(name)
} }
}, },
date_upload = parseChapterDate(jo.getString("upload_date")),
scanlator = publishers.optJSONObject(0)?.getStringOrNull("name"),
source = MangaSource.REMANGA source = MangaSource.REMANGA
) )
}.asReversed() }.asReversed()
@@ -171,6 +178,14 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
source = source source = source
) )
private fun parseChapterDate(string: String): Long {
return try {
dateFormat.parse(string)?.time ?: 0
} catch (_: ParseException) {
0
}
}
private companion object { private companion object {
const val PAGE_SIZE = 30 const val PAGE_SIZE = 30
@@ -179,5 +194,9 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
const val STATUS_FINISHED = 0 const val STATUS_FINISHED = 0
val LAST_URL_PATH_REGEX = Regex("/[^/]+/?$") val LAST_URL_PATH_REGEX = Regex("/[^/]+/?$")
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
}
} }
} }

View File

@@ -29,6 +29,7 @@ class YaoiChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loa
name = a.text().trim(), name = a.text().trim(),
number = i + 1, number = i + 1,
url = href, url = href,
date_upload = 0L,
source = source source = source
) )
} }

View File

@@ -14,6 +14,9 @@ import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.delegates.prefs.* import org.koitharu.kotatsu.utils.delegates.prefs.*
import java.io.File import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class AppSettings private constructor(private val prefs: SharedPreferences) : class AppSettings private constructor(private val prefs: SharedPreferences) :
SharedPreferences by prefs { SharedPreferences by prefs {
@@ -121,6 +124,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()") @Deprecated("Use observe()")
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) { fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener) prefs.registerOnSharedPreferenceChangeListener(listener)
@@ -152,6 +161,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val KEY_APP_SECTION = "app_section" const val KEY_APP_SECTION = "app_section"
const val KEY_THEME = "theme" const val KEY_THEME = "theme"
const val KEY_THEME_AMOLED = "amoled_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_HIDE_TOOLBAR = "hide_toolbar"
const val KEY_SOURCES_ORDER = "sources_order" const val KEY_SOURCES_ORDER = "sources_order"
const val KEY_SOURCES_HIDDEN = "sources_hidden" const val KEY_SOURCES_HIDDEN = "sources_hidden"

View File

@@ -1,12 +1,16 @@
package org.koitharu.kotatsu.details.ui.adapter package org.koitharu.kotatsu.details.ui.adapter
import android.text.SpannableStringBuilder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koin.core.context.GlobalContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemChapterBinding import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.getThemeColor import org.koitharu.kotatsu.utils.ext.getThemeColor
import java.util.*
fun chapterListItemAD( fun chapterListItemAD(
clickListener: OnListItemClickListener<ChapterListItem>, clickListener: OnListItemClickListener<ChapterListItem>,
@@ -24,6 +28,20 @@ fun chapterListItemAD(
bind { payload -> bind { payload ->
binding.textViewTitle.text = item.chapter.name binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString() binding.textViewNumber.text = item.chapter.number.toString()
val settings = GlobalContext.get().get<AppSettings>()
val descriptions = mutableListOf<CharSequence>()
val dateFormat = settings.dateFormat()
if (item.chapter.date_upload > 0) {
descriptions.add(dateFormat.format(Date(item.chapter.date_upload)))
}
if (!item.chapter.scanlator.isNullOrBlank()) {
descriptions.add(item.chapter.scanlator!!)
}
if (descriptions.isNotEmpty()) {
binding.textViewDescription.text = descriptions.joinTo(SpannableStringBuilder(), "")
} else {
binding.textViewDescription.text = ""
}
when (item.extra) { when (item.extra) {
ChapterExtra.UNREAD -> { ChapterExtra.UNREAD -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default) binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
@@ -43,6 +61,7 @@ fun chapterListItemAD(
} }
} }
binding.textViewTitle.alpha = if (item.isMissing) 0.3f else 1f binding.textViewTitle.alpha = if (item.isMissing) 0.3f else 1f
binding.textViewDescription.alpha = if (item.isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f
} }
} }

View File

@@ -72,6 +72,8 @@ class MangaIndex(source: String?) {
jo.put("number", chapter.number) jo.put("number", chapter.number)
jo.put("url", chapter.url) jo.put("url", chapter.url)
jo.put("name", chapter.name) jo.put("name", chapter.name)
jo.put("date_upload", chapter.date_upload)
jo.put("scanlator", chapter.scanlator)
jo.put("branch", chapter.branch) jo.put("branch", chapter.branch)
jo.put("entries", "%03d\\d{3}".format(chapter.number)) jo.put("entries", "%03d\\d{3}".format(chapter.number))
chapters.put(chapter.id.toString(), jo) chapters.put(chapter.id.toString(), jo)
@@ -98,6 +100,8 @@ class MangaIndex(source: String?) {
name = v.getString("name"), name = v.getString("name"),
url = v.getString("url"), url = v.getString("url"),
number = v.getInt("number"), number = v.getInt("number"),
date_upload = v.getLong("date_upload"),
scanlator = v.getStringOrNull("scanlator"),
branch = v.getStringOrNull("branch"), branch = v.getStringOrNull("branch"),
source = source source = source
) )

View File

@@ -123,6 +123,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
name = s.ifEmpty { title }, name = s.ifEmpty { title },
number = i + 1, number = i + 1,
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
date_upload = 0L,
url = uriBuilder.fragment(s).build().toString() url = uriBuilder.fragment(s).build().toString()
) )
} }

View File

@@ -9,8 +9,6 @@ import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.* import androidx.preference.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
@@ -21,6 +19,7 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.io.File import java.io.File
import java.util.*
class MainSettingsFragment : BasePreferenceFragment(R.string.settings), class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
@@ -40,6 +39,20 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
entryValues = ListMode.values().names() entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name) setDefaultValueCompat(ListMode.GRID.name)
} }
findPreference<ListPreference>(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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -13,25 +13,50 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginEnd="10dp"
android:background="@drawable/bg_badge_default" android:background="@drawable/bg_badge_default"
android:gravity="center" android:gravity="center"
android:minWidth="26dp" android:minWidth="26dp"
android:textAlignment="center" android:textAlignment="center"
android:textColor="?android:textColorSecondaryInverse" android:textColor="?android:textColorSecondaryInverse"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="13" /> tools:text="13" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:layout_toEndOf="@id/textView_number" android:layout_toEndOf="@id/textView_number"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="1"
android:text="?android:textColorPrimary" android:text="?android:textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@+id/textView_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView_number"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem[15]" /> tools:text="@tools:sample/lorem[15]" />
</RelativeLayout> <TextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="?attr/colorControlNormal"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView_number"
app:layout_constraintTop_toBottomOf="@+id/textView_title"
tools:text="05.10.2021 • Scanlator" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -241,4 +241,6 @@
<string name="genres">Жанры</string> <string name="genres">Жанры</string>
<string name="state_finished">Завершено</string> <string name="state_finished">Завершено</string>
<string name="state_ongoing">Онгоинг</string> <string name="state_ongoing">Онгоинг</string>
<string name="date_format">Формат даты</string>
<string name="system_default">По умолчанию</string>
</resources> </resources>

View File

@@ -242,4 +242,6 @@
<string name="genres">Genres</string> <string name="genres">Genres</string>
<string name="state_finished">Finished</string> <string name="state_finished">Finished</string>
<string name="state_ongoing">Ongoing</string> <string name="state_ongoing">Ongoing</string>
<string name="date_format">Date format</string>
<string name="system_default">Default</string>
</resources> </resources>

View File

@@ -20,6 +20,11 @@
android:title="@string/black_dark_theme" android:title="@string/black_dark_theme"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<ListPreference
android:key="date_format"
android:title="@string/date_format"
app:iconSpaceReserved="false" />
<SwitchPreference <SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="hide_toolbar" android:key="hide_toolbar"