New fast scroller used on chapter list

Scrolls by volume now
Updated to jsoup 1.13.1 since there was some errors with 1.12.2
This commit is contained in:
Jay 2020-04-06 23:52:15 -04:00
parent 21953424c0
commit 4d8340a0c3
13 changed files with 303 additions and 36 deletions

View File

@ -148,7 +148,7 @@ dependencies {
implementation("com.github.inorichi:unifile:e9ee588")
// HTML parser
implementation("org.jsoup:jsoup:1.12.2")
implementation("org.jsoup:jsoup:1.13.1")
// Job scheduling
implementation("com.evernote:android-job:1.4.2")

View File

@ -49,6 +49,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.system.sendLocalBroadcast
import kotlinx.coroutines.Dispatchers
@ -286,7 +287,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/
suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga {
return withContext(Dispatchers.IO) {
val networkManga = source.fetchMangaDetails(manga).toBlocking().single()
val networkManga = source.fetchMangaDetailsAsync(manga)!!
manga.copyFrom(networkManga)
manga.favorite = true
manga.initialized = true

View File

@ -48,11 +48,17 @@ interface Source {
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
}
suspend fun Source.fetchMangaDetails(manga: SManga): SManga? {
suspend fun Source.fetchMangaDetailsAsync(manga: SManga): SManga? {
return withContext(Dispatchers.IO) {
fetchMangaDetails(manga).toBlocking().single()
}
}
suspend fun Source.fetchChapterListAsync(manga: SManga): List<SChapter>? {
return withContext(Dispatchers.IO) {
fetchChapterList(manga).toBlocking().single()
}
}
fun Source.icon(): Drawable? =
Injekt.get<ExtensionManager>().getAppIconForSource(this)

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.library
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
@ -67,11 +68,8 @@ import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
import kotlinx.android.synthetic.main.filter_bottom_sheet.*
import kotlinx.android.synthetic.main.library_grid_recycler.*
import kotlinx.android.synthetic.main.library_grid_recycler.recycler
import kotlinx.android.synthetic.main.library_list_controller.*
import kotlinx.android.synthetic.main.library_list_controller.swipe_refresh
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.recents_controller.*
import kotlinx.coroutines.delay
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -199,9 +197,8 @@ class LibraryController(
context, R.drawable.fast_scroll_background
) else null
fast_scroller.textColor = ColorStateList.valueOf(
context.getResourceColor(
if (!alwaysShowScroller) android.R.attr.textColorPrimaryInverse else android.R.attr.textColorPrimary
)
if (!alwaysShowScroller) Color.WHITE
else context.getResourceColor(android.R.attr.textColorPrimary)
)
fast_scroller.iconColor = fast_scroller.textColor
}

View File

@ -24,6 +24,7 @@ class MangaDetailsAdapter(
private var isAnimating = false
val delegate: MangaDetailsInterface = controller
val presenter = controller.presenter
val readColor = context.getResourceColor(android.R.attr.textColorHint)
@ -67,6 +68,104 @@ class MangaDetailsAdapter(
}
}
fun getSectionText(position: Int): String? {
val chapter = getItem(position) as? ChapterItem ?: return null
if (position == itemCount - 1) return "-"
return when (presenter.scrollType) {
MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS ->
presenter.getGroupNumber(chapter)?.toString() ?: "*"
MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS ->
if (chapter.chapter_number < 0) "*"
else (chapter.chapter_number / 100).toInt().toString()
MangaDetailsPresenter.TENS_OF_CHAPTERS ->
if (chapter.chapter_number < 0) "*"
else (chapter.chapter_number / 10).toInt().toString()
else -> null
}
}
fun getFullText(position: Int): String {
val chapter =
getItem(position) as? ChapterItem ?: return recyclerView.context.getString(R.string.top)
if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom)
return when (val scrollType = presenter.scrollType) {
MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> {
val volume = presenter.getGroupNumber(chapter)
if (volume != null) recyclerView.context.getString(
if (scrollType == MangaDetailsPresenter.MULTIPLE_SEASONS) R.string.season_x
else R.string.volume_x, volume)
else recyclerView.context.getString(R.string.unknown)
}
MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS -> recyclerView.context.getString(
R.string.chapters_x, get100sRange(
chapter.chapter_number
)
)
MangaDetailsPresenter.TENS_OF_CHAPTERS -> recyclerView.context.getString(
R.string.chapters_x, get10sRange(
chapter.chapter_number
)
)
else -> recyclerView.context.getString(R.string.unknown)
}
}
private fun get100sRange(value: Float): String {
return when (value.toInt()) {
in 0..99 -> "0-99"
in 100..199 -> "100-199"
in 200..299 -> "200-299"
in 300..399 -> "300-399"
in 400..499 -> "400-499"
in 500..599 -> "500-599"
in 600..699 -> "600-699"
in 700..799 -> "700-799"
in 800..899 -> "800-899"
in 900..Int.MAX_VALUE -> "900+"
else -> "None"
}
}
private fun get10sRange(value: Float): String {
return when (value.toInt()) {
in 0..9 -> "0-9"
in 10..19 -> "10-19"
in 20..29 -> "20-29"
in 30..39 -> "30-39"
in 40..49 -> "40-49"
in 50..59 -> "50-59"
in 60..69 -> "60-69"
in 70..79 -> "70-79"
in 80..89 -> "80-89"
in 80..89 -> "80-89"
in 90..99 -> "90-99"
in 100..109 -> "100-109"
in 110..119 -> "110-119"
in 120..129 -> "120-129"
in 130..139 -> "130-139"
in 140..149 -> "140-149"
in 150..159 -> "150-159"
in 160..169 -> "160-169"
in 170..179 -> "170-179"
in 180..189 -> "180-189"
in 190..199 -> "190-199"
in 190..199 -> "190-199"
in 200..209 -> "200-209"
in 210..219 -> "210-219"
in 220..229 -> "220-229"
in 230..239 -> "230-239"
in 240..249 -> "240-249"
in 250..259 -> "250-259"
in 260..269 -> "260-269"
in 270..279 -> "270-279"
in 280..289 -> "280-289"
in 290..299 -> "290-299"
in 290..299 -> "290-299"
in 300..Int.MAX_VALUE -> "300+"
else -> recyclerView.context.getString(R.string.unknown)
}
}
interface MangaDetailsInterface : MangaHeaderInterface, DownloadInterface
interface MangaHeaderInterface {

View File

@ -25,6 +25,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.view.animation.DecelerateInterpolator
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
@ -56,6 +57,8 @@ import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.signature.ObjectKey
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
import com.reddit.indicatorfastscroll.FastScrollerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
@ -165,6 +168,8 @@ class MangaDetailsController : BaseController,
private var startingDLChapterPos: Int? = null
private var editMangaDialog: EditMangaDialog? = null
var refreshTracker: Int? = null
private var textAnim: ViewPropertyAnimator? = null
private var scrollAnim: ViewPropertyAnimator? = null
/**
* Library search query.
@ -184,6 +189,7 @@ class MangaDetailsController : BaseController,
// Hold a reference to the current animator, so that it can be canceled mid-way.
private var currentAnimator: Animator? = null
var showScroll = false
var headerHeight = 0
init {
@ -212,7 +218,6 @@ class MangaDetailsController : BaseController,
)
)
recycler.setHasFixedSize(true)
adapter?.fastScroller = fast_scroller
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
val array = view.context.obtainStyledAttributes(attrsArray)
val appbarHeight = array.getDimensionPixelSize(0, 0)
@ -228,15 +233,15 @@ class MangaDetailsController : BaseController,
swipe_refresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset)
(recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder)
?.setTopHeight(headerHeight)
fast_scroller?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
fast_scroll_layout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = headerHeight
bottomMargin = insets.systemWindowInsetBottom
}
v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom)
}
presenter.onCreate()
fast_scroller.translationX = if (showScroll) 0f else 22f.dpToPx
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
@ -259,6 +264,19 @@ class MangaDetailsController : BaseController,
getHeader()?.backdrop?.translationY = 0f
activity!!.appbar.y = 0f
}
val fPosition =
(recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
if (fPosition > 0 && !showScroll) {
showScroll = true
scrollAnim?.cancel()
scrollAnim = fast_scroller.animate().setDuration(100).translationX(0f)
scrollAnim?.start()
} else if (fPosition <= 0 && showScroll) {
showScroll = false
scrollAnim?.cancel()
scrollAnim = fast_scroller.animate().setDuration(100).translationX(22f.dpToPx)
scrollAnim?.start()
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
@ -291,6 +309,38 @@ class MangaDetailsController : BaseController,
})
setPaletteColor()
fast_scroller.setupWithRecyclerView(recycler, { position ->
val letter = adapter?.getSectionText(position)
when {
presenter.scrollType == 0 -> null
letter != null -> FastScrollItemIndicator.Text(letter)
else -> FastScrollItemIndicator.Icon(R.drawable.star)
}
})
fast_scroller.useDefaultScroller = false
fast_scroller.itemIndicatorSelectedCallbacks += object :
FastScrollerView.ItemIndicatorSelectedCallback {
override fun onItemIndicatorSelected(
indicator: FastScrollItemIndicator,
indicatorCenterY: Int,
itemPosition: Int
) {
textAnim?.cancel()
textAnim = text_view_m.animate().alpha(0f).setDuration(250L).setStartDelay(1000)
textAnim?.start()
text_view_m.translationY = indicatorCenterY.toFloat() - text_view_m.height / 2
text_view_m.alpha = 1f
text_view_m.text = adapter?.getFullText(itemPosition)
val appbar = activity?.appbar
appbar?.y = 0f
(recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
itemPosition, headerHeight
)
colorToolbar(itemPosition > 0, false)
}
}
swipe_refresh.isRefreshing = presenter.isLoading
if (manga?.initialized != true)
swipe_refresh.post { swipe_refresh.isRefreshing = true }
@ -298,7 +348,8 @@ class MangaDetailsController : BaseController,
swipe_refresh.setOnRefreshListener { presenter.refreshAll() }
}
fun colorToolbar(isColor: Boolean) {
fun colorToolbar(isColor: Boolean, animate: Boolean = true) {
if (isColor == toolbarIsColored) return
toolbarIsColored = isColor
val isCurrentController =
router?.backstack?.lastOrNull()?.controller() == this@MangaDetailsController
@ -319,6 +370,7 @@ class MangaDetailsController : BaseController,
color, if (toolbarIsColored) 175 else 0
)
colorAnimator?.cancel()
if (animate) {
colorAnimator = ValueAnimator.ofObject(
android.animation.ArgbEvaluator(), colorFrom, colorTo
)
@ -328,6 +380,10 @@ class MangaDetailsController : BaseController,
activity?.window?.statusBarColor = (animator.animatedValue as Int)
}
colorAnimator?.start()
} else {
(activity as MainActivity).toolbar.setBackgroundColor(colorTo)
activity?.window?.statusBarColor = colorTo
}
}
fun setPaletteColor() {

View File

@ -24,6 +24,8 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.fetchChapterListAsync
import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
@ -60,6 +62,9 @@ class MangaDetailsPresenter(
var isLockedFromSearch = false
var hasRequested = false
var isLoading = false
var scrollType = 0
private val volumeRegex = Regex("""(vol|volume)\.? *([0-9]+)?""", RegexOption.IGNORE_CASE)
private val seasonRegex = Regex("""(Season |S)([0-9]+)?""")
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
var tracks = emptyList<Track>()
@ -242,9 +247,71 @@ class MangaDetailsPresenter(
else -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
}
chapters = chapters.sortedWith(Comparator(sortFunction))
getScrollType(chapters)
return chapters
}
private fun getScrollType(chapters: List<ChapterItem>) {
scrollType = when {
hasMultipleVolumes(chapters) -> MULTIPLE_VOLUMES
hasMultipleSeasons(chapters) -> MULTIPLE_SEASONS
hasHundredsOfChapters(chapters) -> HUNDREDS_OF_CHAPTERS
hasTensOfChapters(chapters) -> TENS_OF_CHAPTERS
else -> 0
}
}
fun getGroupNumber(chapter: ChapterItem): Int? {
val groups = volumeRegex.find(chapter.name)?.groups
if (groups != null) return groups[2]?.value?.toIntOrNull()
val seasonGroups = seasonRegex.find(chapter.name)?.groups
if (seasonGroups != null) return seasonGroups[2]?.value?.toIntOrNull()
return null
}
private fun getVolumeNumber(chapter: ChapterItem): Int? {
val groups = volumeRegex.find(chapter.name)?.groups
if (groups != null) return groups[2]?.value?.toIntOrNull()
return null
}
private fun getSeasonNumber(chapter: ChapterItem): Int? {
val groups = seasonRegex.find(chapter.name)?.groups
if (groups != null) return groups[2]?.value?.toIntOrNull()
return null
}
private fun hasMultipleVolumes(chapters: List<ChapterItem>): Boolean {
val volumeSet = mutableSetOf<Int>()
chapters.forEach {
val volNum = getVolumeNumber(it)
if (volNum != null) {
volumeSet.add(volNum)
if (volumeSet.size >= 3) return true
}
}
return false
}
private fun hasMultipleSeasons(chapters: List<ChapterItem>): Boolean {
val volumeSet = mutableSetOf<Int>()
chapters.forEach {
val volNum = getSeasonNumber(it)
if (volNum != null) {
volumeSet.add(volNum)
if (volumeSet.size >= 3) return true
}
}
return false
}
private fun hasHundredsOfChapters(chapters: List<ChapterItem>): Boolean {
return chapters.size > 300
}
private fun hasTensOfChapters(chapters: List<ChapterItem>): Boolean {
return chapters.size in 21..300
}
/**
* Returns the next unread chapter or null if everything is read.
*/
@ -309,7 +376,7 @@ class MangaDetailsPresenter(
var chapterError: java.lang.Exception? = null
val chapters = async(Dispatchers.IO) {
try {
source.fetchChapterList(manga).toBlocking().single()
source.fetchChapterListAsync(manga)
} catch (e: Exception) {
chapterError = e
emptyList<SChapter>()
@ -318,7 +385,7 @@ class MangaDetailsPresenter(
val thumbnailUrl = manga.thumbnail_url
val nManga = async(Dispatchers.IO) {
try {
source.fetchMangaDetails(manga).toBlocking().single()
source.fetchMangaDetailsAsync(manga)
} catch (e: java.lang.Exception) {
mangaError = e
null
@ -342,6 +409,14 @@ class MangaDetailsPresenter(
}
isLoading = false
if (chapterError == null) withContext(Dispatchers.Main) { controller.updateChapters(this@MangaDetailsPresenter.chapters) }
else {
withContext(Dispatchers.Main) {
controller.showError(
trimException(mangaError!!)
)
}
return@launch
}
if (mangaError != null) withContext(Dispatchers.Main) {
controller.showError(
trimException(mangaError!!)
@ -359,7 +434,7 @@ class MangaDetailsPresenter(
scope.launch(Dispatchers.IO) {
val chapters = try {
source.fetchChapterList(manga).toBlocking().single()
source.fetchChapterListAsync(manga)
} catch (e: Exception) {
withContext(Dispatchers.Main) { controller.showError(trimException(e)) }
return@launch
@ -760,4 +835,11 @@ class MangaDetailsPresenter(
track.last_chapter_read = chapterNumber
updateRemote(track, item.service)
}
companion object {
const val MULTIPLE_VOLUMES = 1
const val TENS_OF_CHAPTERS = 2
const val HUNDREDS_OF_CHAPTERS = 3
const val MULTIPLE_SEASONS = 4
}
}

View File

@ -5,8 +5,6 @@ import android.os.Bundle
import android.widget.Toast
import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -16,7 +14,6 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineStart
@ -152,13 +149,6 @@ class SettingsAdvancedController : SettingsController() {
}
private fun clearDatabase() {
// Avoid weird behavior by going back to the library.
val newBackstack = listOf(RouterTransaction.with(
LibraryController())) +
router.backstack.drop(1)
router.setBackstack(newBackstack, FadeChangeHandler())
db.deleteMangasNotInLibrary().executeAsBlocking()
db.deleteHistoryNoLastRead().executeAsBlocking()
activity?.toast(R.string.clear_database_completed)

View File

@ -94,7 +94,7 @@
layout="@layout/download_button"
android:layout_width="50dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -32,6 +32,7 @@
android:elevation="10dp"
android:layout_gravity="end"
android:background="@drawable/fast_scroll_background"
android:backgroundTint="@color/md_grey_800"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="3dp"

View File

@ -25,13 +25,44 @@
app:layout_constraintTop_toBottomOf="@id/chapters_title"
tools:listitem="@layout/chapters_item"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<eu.davidea.fastscroller.FastScroller
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/fast_scroll_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.reddit.indicatorfastscroll.FastScrollerView
android:id="@+id/fast_scroller"
android:textColor="?android:attr/textColorPrimary"
app:iconColor="?android:attr/textColorPrimary"
android:layout_width="22dp"
android:layout_height="0dp"
android:elevation="10dp"
android:layout_gravity="end"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="1dp"
android:paddingEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_view_m"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0"
tools:alpha="1"
android:layout_marginEnd="50dp"
android:background="@drawable/round_textview_background"
android:backgroundTint="@color/md_grey_800_50"
android:padding="8dp"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@id/fast_scroller"
app:layout_constraintTop_toTopOf="@id/fast_scroller"
tools:text="sdfsdf" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/full_backdrop"

View File

@ -372,7 +372,7 @@
android:tint="?colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="9dp"
android:layout_marginEnd="20dp"
android:background="@null"
android:padding="5dp"
android:src="@drawable/ic_filter_list_white_24dp"

View File

@ -211,6 +211,10 @@
<item quantity="other">%d categories</item>
</plurals>
<string name="top">Top</string>
<string name="bottom">Bottom</string>
<string name="volume_x">Volume %1$d</string>
<string name="season_x">Season %1$d</string>
<string name="chapters_x">Chapters %1$s</string>
<string name="pref_category_library_update">Updates</string>
<string name="pref_library_update_interval">Library update frequency</string>