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 @@ + + + + + + + + + +