diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt
new file mode 100644
index 000000000..8f41c695d
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.koitharu.kotatsu.base.ui.widgets
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.core.view.postDelayed
+import org.koitharu.kotatsu.R
+
+/**
+ * A custom snackbar implementation allowing more control over placement and entry/exit animations.
+ *
+ * Xtimms: Well, my sufferings over the Snackbar in [DetailsActivity] will go away forever... Thanks, Google.
+ *
+ * https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/widget/FadingSnackbar.kt
+ */
+class FadingSnackbar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val message: TextView
+ private val action: Button
+
+ init {
+ val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true)
+ message = view.findViewById(R.id.snackbar_text)
+ action = view.findViewById(R.id.snackbar_action)
+ }
+
+ fun dismiss() {
+ if (visibility == VISIBLE && alpha == 1f) {
+ animate()
+ .alpha(0f)
+ .withEndAction { visibility = GONE }
+ .duration = EXIT_DURATION
+ }
+ }
+
+ fun show(
+ messageText: CharSequence? = null,
+ @StringRes actionId: Int? = null,
+ longDuration: Boolean = true,
+ actionClick: () -> Unit = { dismiss() },
+ dismissListener: () -> Unit = { }
+ ) {
+ message.text = messageText
+ if (actionId != null) {
+ action.run {
+ visibility = VISIBLE
+ text = context.getString(actionId)
+ setOnClickListener {
+ actionClick()
+ }
+ }
+ } else {
+ action.visibility = GONE
+ }
+ alpha = 0f
+ visibility = VISIBLE
+ animate()
+ .alpha(1f)
+ .duration = ENTER_DURATION
+ val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION
+ postDelayed(showDuration) {
+ dismiss()
+ dismissListener()
+ }
+ }
+
+ companion object {
+ private const val ENTER_DURATION = 300L
+ private const val EXIT_DURATION = 200L
+ private const val SHORT_DURATION = 1_500L
+ private const val LONG_DURATION = 2_750L
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaChapter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaChapter.kt
index 0682eadc6..f0a56361c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaChapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaChapter.kt
@@ -9,6 +9,8 @@ data class MangaChapter(
val name: String,
val number: Int,
val url: String,
+ val scanlator: String? = null,
+ val uploadDate: Long,
val branch: String? = null,
val source: MangaSource
) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
index bb3622afc..507dab2da 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt
@@ -84,9 +84,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
?.toRelativeUrl(getDomain()) ?: return@mapIndexedNotNull null
MangaChapter(
id = generateUid(href),
- name = a.selectFirst("a")?.text().orEmpty(),
+ name = "Глава " + a.selectFirst("a")?.text().orEmpty(),
number = i + 1,
url = href,
+ uploadDate = 0L,
source = source
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt
index 629082010..86c6b884d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt
@@ -5,6 +5,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 ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(
@@ -79,15 +80,14 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("
- 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,
+ uploadDate = parseChapterDate(tr.selectFirst("div.date")?.text().orEmpty()),
source = source
)
}
@@ -154,4 +154,9 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
SortOrder.NEWEST -> "datedesc"
else -> "favdesc"
}
+
+ private fun parseChapterDate(string: String): Long {
+ return SimpleDateFormat("yyyy-MM-dd", Locale.US).tryParse(string)
+ }
+
}
\ 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..b765d09aa 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,11 +93,14 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
description = json.getString("description"),
chapters = chaptersList.mapIndexed { i, it ->
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(
id = generateUid(chid),
source = manga.source,
url = "$baseChapterUrl$chid",
- name = it.getStringOrNull("title") ?: "${manga.title} #${it.getDouble("ch")}",
+ uploadDate = it.getLong("date") * 1000,
+ name = if (title.isEmpty()) volChap else "$volChap: $title",
number = totalChapters - i
)
}.reversed()
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..084ce47a2 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,7 +141,7 @@ class ExHentaiRepository(
name = "${manga.title} #$i",
number = i,
url = url,
- branch = null,
+ uploadDate = 0L,
source = source,
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt
index 1f5e666c3..6bcce48f6 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
@@ -8,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) :
@@ -123,13 +124,23 @@ 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,
+ uploadDate = parseChapterDate(tr.select("td.d-none").text()),
+ scanlator = translators,
source = source
)
}
@@ -223,11 +234,15 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
return loaderContext.httpPost(url, payload)
}
+ private fun parseChapterDate(string: String): Long {
+ return SimpleDateFormat("dd.MM.yy", Locale.US).tryParse(string)
+ }
+
private companion object {
private const val PAGE_SIZE = 70
private const val PAGE_SIZE_SEARCH = 50
- val HEADER = Headers.Builder()
+ private val HEADER = Headers.Builder()
.add("User-Agent", "readmangafun")
.build()
}
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..a802986a3 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
@@ -49,6 +49,7 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
url = readLink,
source = source,
number = 1,
+ uploadDate = 0L,
name = manga.title
)
)
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..b74aaabb5 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) :
@@ -91,7 +92,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 +103,19 @@ 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 = parseChapterDate(item.getString("chapter_created_at").substringBefore(" ")),
+ scanlator = scanlator,
+ name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
)
)
}
@@ -235,9 +236,14 @@ 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")
)
}
}
+
+ private fun parseChapterDate(string: String): Long {
+ return SimpleDateFormat("yyy-MM-dd", Locale.US).tryParse(string)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
index d8f8e4e00..b47712ad8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaOwlRepository.kt
@@ -5,6 +5,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.*
class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
@@ -99,6 +100,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
name = a.select("label").text(),
number = i + 1,
url = href,
+ uploadDate = parseChapterDate(li.select("small:last-of-type").text()),
source = MangaSource.MANGAOWL
)
}
@@ -156,4 +158,8 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
else -> "3"
}
+ private fun parseChapterDate(string: String): Long {
+ return SimpleDateFormat("MM/dd/yyyy", Locale.US).tryParse(string)
+ }
+
}
\ 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..d40bdf96f 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,7 @@ 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.SimpleDateFormat
import java.util.*
class MangaTownRepository(loaderContext: MangaLoaderContext) :
@@ -117,6 +118,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
url = href,
source = MangaSource.MANGATOWN,
number = i + 1,
+ uploadDate = parseChapterDate(li.selectFirst("span.time")?.text().orEmpty()),
name = name.ifEmpty { "${manga.title} - ${i + 1}" }
)
}
@@ -167,6 +169,14 @@ 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 -> SimpleDateFormat("MMM dd,yyyy", Locale.US).tryParse(date)
+ }
+ }
+
override fun onCreatePreferences(map: MutableMap
) {
super.onCreatePreferences(map)
map[SourceSettings.KEY_USE_SSL] = true
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..068016887 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,9 @@ 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.SimpleDateFormat
import java.util.*
class MangareadRepository(
@@ -138,6 +140,7 @@ class MangareadRepository(
name = a!!.ownText(),
number = i + 1,
url = href,
+ uploadDate = parseChapterDate(doc2.selectFirst("span.chapter-release-date i")?.text()),
source = MangaSource.MANGAREAD
)
}
@@ -162,6 +165,70 @@ class MangareadRepository(
}
}
+ private fun parseChapterDate(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
@@ -173,5 +240,9 @@ class MangareadRepository(
val pos = it.indexOf('=')
it.substring(0, pos) to it.substring(pos + 1)
}.toMutableMap()
+
+ private val dateFormat by lazy {
+ SimpleDateFormat("MMMM dd, yyyy", Locale.US)
+ }
}
}
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..5c0ad5185 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(
@@ -99,18 +100,19 @@ 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,
)
}
@@ -153,6 +155,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
+ }
+
+ 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/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt
index 17a3dd5d7..3a817b76b 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
@@ -8,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.*
class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
@@ -109,12 +110,16 @@ 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").capitalize(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.getString("tome"))
+ append(". ")
append("Глава ")
append(jo.getString("chapter"))
if (name.isNotEmpty()) {
@@ -122,6 +127,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
append(name)
}
},
+ uploadDate = parseChapterDate(jo.getString("upload_date")),
+ scanlator = publishers.optJSONObject(0)?.getStringOrNull("name"),
source = MangaSource.REMANGA
)
}.asReversed()
@@ -171,6 +178,10 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
source = source
)
+ private fun parseChapterDate(string: String): Long {
+ return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).tryParse(string)
+ }
+
private companion object {
const val PAGE_SIZE = 30
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..97a762c21 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,6 +29,7 @@ class YaoiChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loa
name = a.text().trim(),
number = i + 1,
url = href,
+ uploadDate = 0L,
source = source
)
}
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..992d656f2 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
@@ -14,6 +14,9 @@ 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 {
@@ -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()")
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
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_THEME = "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"
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 af987e208..57213f9df 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
@@ -85,14 +85,13 @@ class DetailsActivity : BaseActivity(),
finishAfterTransition()
}
else -> {
- Snackbar.make(binding.coordinator, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
- .show()
+ binding.snackbar.show(e.getDisplayMessage(resources))
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
- binding.coordinator.updatePadding(
+ binding.snackbar.updatePadding(
bottom = insets.bottom
)
binding.toolbar.updatePadding(
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..9db3208e8 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,16 @@
package org.koitharu.kotatsu.details.ui.adapter
+import android.text.SpannableStringBuilder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
+import org.koin.core.context.GlobalContext
import org.koitharu.kotatsu.R
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.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.history.domain.ChapterExtra
import org.koitharu.kotatsu.utils.ext.getThemeColor
+import java.util.*
fun chapterListItemAD(
clickListener: OnListItemClickListener,
@@ -24,6 +28,20 @@ fun chapterListItemAD(
bind { payload ->
binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString()
+ val settings = GlobalContext.get().get()
+ val descriptions = mutableListOf()
+ val dateFormat = settings.dateFormat()
+ if (item.chapter.uploadDate > 0) {
+ descriptions.add(dateFormat.format(Date(item.chapter.uploadDate)))
+ }
+ 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) {
ChapterExtra.UNREAD -> {
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.textViewDescription.alpha = if (item.isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt
index 45df69340..e13d2eafb 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
@@ -72,6 +72,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,6 +100,8 @@ class MangaIndex(source: String?) {
name = v.getString("name"),
url = v.getString("url"),
number = v.getInt("number"),
+ uploadDate = v.getLong("uploadDate"),
+ scanlator = v.getStringOrNull("scanlator"),
branch = v.getStringOrNull("branch"),
source = source
)
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..0be6da157 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
@@ -123,6 +123,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
name = s.ifEmpty { title },
number = i + 1,
source = MangaSource.LOCAL,
+ uploadDate = 0L,
url = uriBuilder.fragment(s).build().toString()
)
}
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..c8d024c13 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt
@@ -9,8 +9,6 @@ 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 org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
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.utils.ext.*
import java.io.File
+import java.util.*
class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
@@ -40,6 +39,20 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name)
}
+ 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?) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/WordSet.kt b/app/src/main/java/org/koitharu/kotatsu/utils/WordSet.kt
new file mode 100644
index 000000000..214c934dd
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/WordSet.kt
@@ -0,0 +1,9 @@
+package org.koitharu.kotatsu.utils
+
+class WordSet(private vararg val words: String) {
+
+ fun anyWordIn(dateString: String): Boolean = words.any {
+ dateString.contains(it, ignoreCase = true)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt
index 7968b5ba2..89e02566f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt
@@ -10,6 +10,7 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.select.Elements
+import java.text.SimpleDateFormat
fun Response.parseHtml(): Document {
try {
@@ -97,4 +98,8 @@ fun Element.css(property: String): String? {
val regex = Regex("${Regex.escape(property)}\\s*:\\s*[^;]+")
val css = attr("style").find(regex) ?: return null
return css.substringAfter(':').removeSuffix(';').trim()
-}
\ No newline at end of file
+}
+
+fun SimpleDateFormat.tryParse(str: String): Long = runCatching {
+ parse(str)?.time ?: 0L
+}.getOrDefault(0L)
\ No newline at end of file
diff --git a/app/src/main/res/drawable/fading_snackbar_background.xml b/app/src/main/res/drawable/fading_snackbar_background.xml
new file mode 100644
index 000000000..b439322e0
--- /dev/null
+++ b/app/src/main/res/drawable/fading_snackbar_background.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-w600dp-land/fragment_details.xml b/app/src/main/res/layout-w600dp-land/fragment_details.xml
index 4ce68a753..c7295768c 100644
--- a/app/src/main/res/layout-w600dp-land/fragment_details.xml
+++ b/app/src/main/res/layout-w600dp-land/fragment_details.xml
@@ -116,7 +116,7 @@
android:dividerPadding="8dp"
android:orientation="horizontal"
android:showDividers="middle"
- app:layout_constraintTop_toBottomOf="@+id/textView_author">
+ app:layout_constraintTop_toBottomOf="@+id/textView_state">
+ app:layout_constraintTop_toBottomOf="@+id/textView_state">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml
index da7df642a..39f3f7d81 100644
--- a/app/src/main/res/layout/activity_details.xml
+++ b/app/src/main/res/layout/activity_details.xml
@@ -6,7 +6,6 @@
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:clipToPadding="false"
tools:context=".details.ui.DetailsActivity">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_checkbox.xml b/app/src/main/res/layout/dialog_checkbox.xml
index b4b938045..b55dbc6d7 100644
--- a/app/src/main/res/layout/dialog_checkbox.xml
+++ b/app/src/main/res/layout/dialog_checkbox.xml
@@ -8,7 +8,7 @@
android:paddingEnd="?android:listPreferredItemPaddingEnd">
diff --git a/app/src/main/res/layout/fading_snackbar_layout.xml b/app/src/main/res/layout/fading_snackbar_layout.xml
new file mode 100644
index 000000000..4c75ce8b9
--- /dev/null
+++ b/app/src/main/res/layout/fading_snackbar_layout.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml
index 88c3d4950..341e70b33 100644
--- a/app/src/main/res/layout/fragment_details.xml
+++ b/app/src/main/res/layout/fragment_details.xml
@@ -287,6 +287,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/desc_header"
+ app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="UnusedAttribute"
tools:text="@tools:sample/lorem/random[25]" />
diff --git a/app/src/main/res/layout/item_chapter.xml b/app/src/main/res/layout/item_chapter.xml
index f43d53428..873ac5830 100644
--- a/app/src/main/res/layout/item_chapter.xml
+++ b/app/src/main/res/layout/item_chapter.xml
@@ -1,5 +1,5 @@
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_empty_state.xml b/app/src/main/res/layout/item_empty_state.xml
index 12407626e..a71d5796d 100644
--- a/app/src/main/res/layout/item_empty_state.xml
+++ b/app/src/main/res/layout/item_empty_state.xml
@@ -4,9 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
+ android:layout_marginHorizontal="32dp"
android:gravity="center"
- android:layout_marginHorizontal="32dp">
+ android:orientation="vertical">
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 7d514acd7..81ead8f9e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -233,4 +233,10 @@
Géneros
Intenta reformular la consulta.
¿Realmente quiere eliminar todas las consultas de búsqueda recientes\? Esta acción no se puede deshacer.
+ Terminado
+ En curso
+ Ocultar la barra de herramientas al desplazarse
+ Este capítulo no aparece en su dispositivo. Descárguelo o léalo en línea.
+ Autorización completa
+ Tema sobre 4PDA
\ No newline at end of file
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 0c47fbc7f..4241b137f 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -240,4 +240,6 @@
Suojaa sovellus
Väärä salasana
Syötä salasana
+ Päättynyt
+ Jatkuva
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 6c6b29751..31d12b06e 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -241,4 +241,6 @@
Жанры
Завершено
Онгоинг
+ Формат даты
+ По умолчанию
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index fdb266936..ff8094f6b 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -8,7 +8,6 @@
-
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 49c6cab54..b36f51bb0 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -4,6 +4,7 @@
#1976D2
#1565C0
@android:color/white
+ #1F1976D2
#39000000
#FFFFFF
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index d97a88bd7..6e4900beb 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -18,7 +18,7 @@
2dp
86dp
120dp
- 46dp
+ 56dp
120dp
48dp
16dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eb3f5f617..b0e16214b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -242,4 +242,6 @@
Genres
Finished
Ongoing
+ Date format
+ Default
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 399534df8..fd9c5ba8b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -28,6 +28,7 @@
- @color/tab_text
- @style/TextAppearance.Kotatsu.Tab
- elastic
+ - @color/color_primary_alpha