Add missing chapter warning (#985)

* add methods to check for missing chapters

* check for missing chapters in pager reader

* check for missing chapters in webtoon reader

* add new string to strings.xml

for missing chapter warning. can be plural or singular

* create a warning drawable

* make reader transition into separate view

adding missing chapters to current would make it messy so I just did
whatever main tachi did and made a separate view

* linting by android studio while building
This commit is contained in:
nicki 2021-09-07 22:27:06 +05:30 committed by GitHub
parent 0d0e2fc5b4
commit 81fa233dcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 243 additions and 144 deletions

View file

@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import kotlin.math.floor
private val pattern = Regex("""\d+""")
fun hasMissingChapters(higherReaderChapter: ReaderChapter?, lowerReaderChapter: ReaderChapter?): Boolean {
if (higherReaderChapter == null || lowerReaderChapter == null) return false
return hasMissingChapters(higherReaderChapter.chapter, lowerReaderChapter.chapter)
}
fun hasMissingChapters(higherChapter: Chapter?, lowerChapter: Chapter?): Boolean {
if (higherChapter == null || lowerChapter == null) return false
// Check if name contains a number that is potential chapter number
if (!pattern.containsMatchIn(higherChapter.name) || !pattern.containsMatchIn(lowerChapter.name)) return false
// Check if potential chapter number was recognized as chapter number
if (!higherChapter.isRecognizedNumber || !lowerChapter.isRecognizedNumber) return false
return hasMissingChapters(higherChapter.chapter_number, lowerChapter.chapter_number)
}
fun hasMissingChapters(higherChapterNumber: Float, lowerChapterNumber: Float): Boolean {
if (higherChapterNumber < 0f || lowerChapterNumber < 0f) return false
return calculateChapterDifference(higherChapterNumber, lowerChapterNumber) > 0f
}
fun calculateChapterDifference(higherReaderChapter: ReaderChapter?, lowerReaderChapter: ReaderChapter?): Float {
if (higherReaderChapter == null || lowerReaderChapter == null) return 0f
return calculateChapterDifference(higherReaderChapter.chapter, lowerReaderChapter.chapter)
}
fun calculateChapterDifference(higherChapter: Chapter?, lowerChapter: Chapter?): Float {
if (higherChapter == null || lowerChapter == null) return 0f
// Check if name contains a number that is potential chapter number
if (!pattern.containsMatchIn(higherChapter.name) || !pattern.containsMatchIn(lowerChapter.name)) return 0f
// Check if potential chapter number was recognized as chapter number
if (!higherChapter.isRecognizedNumber || !lowerChapter.isRecognizedNumber) return 0f
return calculateChapterDifference(higherChapter.chapter_number, lowerChapter.chapter_number)
}
fun calculateChapterDifference(higherChapterNumber: Float, lowerChapterNumber: Float): Float {
if (higherChapterNumber < 0f || lowerChapterNumber < 0f) return 0f
return floor(higherChapterNumber) - floor(lowerChapterNumber) - 1f
}

View file

@ -0,0 +1,105 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs) {
private val binding: ReaderTransitionViewBinding =
ReaderTransitionViewBinding.inflate(LayoutInflater.from(context), this, true)
init {
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
fun bind(transition: ChapterTransition) {
when (transition) {
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
is ChapterTransition.Next -> bindNextChapterTransition(transition)
}
missingChapterWarning(transition)
}
/**
* Binds a previous chapter transition on this view and subscribes to the page load status.
*/
private fun bindPrevChapterTransition(transition: ChapterTransition) {
val prevChapter = transition.to
val hasPrevChapter = prevChapter != null
binding.lowerText.isVisible = hasPrevChapter
if (hasPrevChapter) {
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
binding.upperText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_previous)) }
append("\n${prevChapter!!.chapter.name}")
}
binding.lowerText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_current)) }
append("\n${transition.from.chapter.name}")
}
} else {
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
binding.upperText.text = context.getString(R.string.transition_no_previous)
}
}
/**
* Binds a next chapter transition on this view and subscribes to the load status.
*/
private fun bindNextChapterTransition(transition: ChapterTransition) {
val nextChapter = transition.to
val hasNextChapter = nextChapter != null
binding.lowerText.isVisible = hasNextChapter
if (hasNextChapter) {
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
binding.upperText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_finished)) }
append("\n${transition.from.chapter.name}")
}
binding.lowerText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_next)) }
append("\n${nextChapter!!.chapter.name}")
}
} else {
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
binding.upperText.text = context.getString(R.string.transition_no_next)
}
}
private fun missingChapterWarning(transition: ChapterTransition) {
if (transition.to == null) {
binding.warning.isVisible = false
return
}
val hasMissingChapters = when (transition) {
is ChapterTransition.Prev -> hasMissingChapters(transition.from, transition.to)
is ChapterTransition.Next -> hasMissingChapters(transition.to, transition.from)
}
if (!hasMissingChapters) {
binding.warning.isVisible = false
return
}
val chapterDifference = when (transition) {
is ChapterTransition.Prev -> calculateChapterDifference(transition.from, transition.to)
is ChapterTransition.Next -> calculateChapterDifference(transition.to, transition.from)
}
binding.warningText.text = resources.getQuantityString(R.plurals.missing_chapters_warning, chapterDifference.toInt(), chapterDifference.toInt())
binding.warning.isVisible = true
}
}

View file

@ -1,10 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.graphics.Typeface
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StyleSpan
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
@ -12,13 +8,13 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -42,16 +38,6 @@ class PagerTransitionHolder(
*/
private var statusSubscription: Subscription? = null
/**
* Text view used to display the text of the current and next/prev chapters.
*/
private var textView = TextView(context).apply {
// if (Build.VERSION.SDK_INT >= 23)
// setTextColor(context.getResourceColor(R.attr.))
textSize = 17.5F
wrapContent()
}
/**
* View container of the current status of the transition page. Child views will be added
* dynamically.
@ -67,13 +53,12 @@ class PagerTransitionHolder(
gravity = Gravity.CENTER
val sidePadding = 64.dpToPx
setPadding(sidePadding, 0, sidePadding, 0)
addView(textView)
val transitionView = ReaderTransitionView(context)
addView(transitionView)
addView(pagesContainer)
when (transition) {
is ChapterTransition.Prev -> bindPrevChapterTransition()
is ChapterTransition.Next -> bindNextChapterTransition()
}
transitionView.bind(transition)
transition.to?.let { observeStatus(it) }
}
/**
@ -85,56 +70,6 @@ class PagerTransitionHolder(
statusSubscription = null
}
/**
* Binds a next chapter transition on this view and subscribes to the load status.
*/
private fun bindNextChapterTransition() {
val nextChapter = transition.to
textView.text = if (nextChapter != null) {
SpannableStringBuilder().apply {
append(context.getString(R.string.finished_chapter))
setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${transition.from.chapter.name}\n\n")
val currSize = length
append(context.getString(R.string.next_title))
setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${nextChapter.chapter.name}\n\n")
}
} else {
context.getString(R.string.theres_no_next_chapter)
}
if (nextChapter != null) {
observeStatus(nextChapter)
}
}
/**
* Binds a previous chapter transition on this view and subscribes to the page load status.
*/
private fun bindPrevChapterTransition() {
val prevChapter = transition.to
textView.text = if (prevChapter != null) {
SpannableStringBuilder().apply {
append(context.getString(R.string.current_chapter))
setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${transition.from.chapter.name}\n\n")
val currSize = length
append(context.getString(R.string.previous_title))
setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${prevChapter.chapter.name}\n\n")
}
} else {
context.getString(R.string.theres_no_previous_chapter)
}
if (prevChapter != null) {
observeStatus(prevChapter)
}
}
/**
* Observes the status of the page list of the next/previous chapter. Whenever there's a new
* state, the pages container is cleaned up before setting the new state.

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import timber.log.Timber
import kotlin.math.max
@ -46,6 +47,10 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
val newItems = mutableListOf<Any>()
// Force chapter transition page if there are missing chapters
val prevHasMissingChapters = hasMissingChapters(chapters.currChapter, chapters.prevChapter)
val nextHasMissingChapters = hasMissingChapters(chapters.nextChapter, chapters.currChapter)
this.forceTransition = forceTransition
// Add previous chapter pages and transition.
if (chapters.prevChapter != null) {
@ -65,7 +70,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
}
// Skip transition page if the chapter is loaded & current page is not a transition page
if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
}
@ -80,7 +85,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
// Add next chapter transition and pages.
nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)
.also {
if (forceTransition ||
if (nextHasMissingChapters || forceTransition ||
chapters.nextChapter?.state !is ReaderChapter.State.Loaded
) {
newItems.add(it)

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
/**
* RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted.
@ -30,6 +31,10 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
val newItems = mutableListOf<Any>()
// Force chapter transition page if there are missing chapters
val prevHasMissingChapters = hasMissingChapters(chapters.currChapter, chapters.prevChapter)
val nextHasMissingChapters = hasMissingChapters(chapters.nextChapter, chapters.currChapter)
// Add previous chapter pages and transition.
if (chapters.prevChapter != null) {
// We only need to add the last few pages of the previous chapter, because it'll be
@ -41,7 +46,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
}
// Skip transition page if the chapter is loaded & current page is not a transition page
if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
}
@ -54,7 +59,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
currentChapter = chapters.currChapter
// Add next chapter transition and pages.
if (forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded) {
if (nextHasMissingChapters || forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded) {
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
}

View file

@ -1,22 +1,17 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StyleSpan
import android.view.Gravity
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView
import eu.kanade.tachiyomi.util.system.dpToPx
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -34,14 +29,7 @@ class WebtoonTransitionHolder(
*/
private var statusSubscription: Subscription? = null
/**
* Text view used to display the text of the current and next/prev chapters.
*/
private var textView = TextView(context).apply {
textSize = 17.5F
setTextColor(Color.WHITE)
wrapContent()
}
private val transitionView = ReaderTransitionView(context)
/**
* View container of the current status of the transition page. Child views will be added
@ -66,7 +54,7 @@ class WebtoonTransitionHolder(
setMargins(0, childMargins, 0, childMargins)
}
layout.addView(textView, childParams)
layout.addView(transitionView)
layout.addView(pagesContainer, childParams)
}
@ -74,10 +62,8 @@ class WebtoonTransitionHolder(
* Binds the given [transition] with this view holder, subscribing to its state.
*/
fun bind(transition: ChapterTransition) {
when (transition) {
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
is ChapterTransition.Next -> bindNextChapterTransition(transition)
}
transitionView.bind(transition)
transition.to?.let { observeStatus(it, transition) }
}
/**
@ -87,56 +73,6 @@ class WebtoonTransitionHolder(
unsubscribeStatus()
}
/**
* Binds a next chapter transition on this view and subscribes to the load status.
*/
private fun bindNextChapterTransition(transition: ChapterTransition.Next) {
val nextChapter = transition.to
textView.text = if (nextChapter != null) {
SpannableStringBuilder().apply {
append(context.getString(R.string.finished_chapter))
setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${transition.from.chapter.name}\n\n")
val currSize = length
append(context.getString(R.string.next_title))
setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${nextChapter.chapter.name}\n\n")
}
} else {
context.getString(R.string.theres_no_next_chapter)
}
if (nextChapter != null) {
observeStatus(nextChapter, transition)
}
}
/**
* Binds a previous chapter transition on this view and subscribes to the page load status.
*/
private fun bindPrevChapterTransition(transition: ChapterTransition.Prev) {
val prevChapter = transition.to
textView.text = if (prevChapter != null) {
SpannableStringBuilder().apply {
append(context.getString(R.string.current_chapter))
setSpan(StyleSpan(Typeface.BOLD), 0, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${transition.from.chapter.name}\n\n")
val currSize = length
append(context.getString(R.string.previous_title))
setSpan(StyleSpan(Typeface.BOLD), currSize, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
append("\n${prevChapter.chapter.name}\n\n")
}
} else {
context.getString(R.string.theres_no_previous_chapter)
}
if (prevChapter != null) {
observeStatus(prevChapter, transition)
}
}
/**
* Observes the status of the page list of the next/previous chapter. Whenever there's a new
* state, the pages container is cleaned up before setting the new state.

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />
</vector>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/upper_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="?attr/textAppearanceSubtitle1"
tools:text="Top" />
<LinearLayout
android:id="@+id/warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_warning_white_24dp"
app:tint="?attr/colorError"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/warning_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
tools:text="Warning" />
</LinearLayout>
<TextView
android:id="@+id/lower_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
tools:text="Bottom" />
</LinearLayout>

View file

@ -479,7 +479,16 @@
<item quantity="one">%1$d page</item>
<item quantity="other">%1$d pages</item>
</plurals>
<plurals name="missing_chapters_warning">
<item quantity="one">There is 1 missing chapter</item>
<item quantity="other">There are %d missing chapters</item>
</plurals>
<string name="transition_finished">Finished:</string>
<string name="transition_current">Current:</string>
<string name="transition_next">Next:</string>
<string name="transition_previous">Previous:</string>
<string name="transition_no_next">There\'s no next chapter</string>
<string name="transition_no_previous">There\'s no previous chapter</string>
<!-- Manga details -->
<string name="about_this_">About this %1$s</string>