Compare commits
19 Commits
83cb35fe6e
...
4db61d3c04
| Author | SHA1 | Date | |
|---|---|---|---|
|
4db61d3c04
|
|||
|
cd0575a524
|
|||
|
6eb2608f88
|
|||
|
39e21ff93c
|
|||
|
5ec2eab6b8
|
|||
|
850f6c2f3e
|
|||
|
ec53eb9c70
|
|||
|
cdd76f723f
|
|||
|
e7c9d1943d
|
|||
|
b1240e7efa
|
|||
|
a0a72b1192
|
|||
|
5d9a59d577
|
|||
|
a37e8825b0
|
|||
|
2450544454
|
|||
|
f6a510653e
|
|||
|
5990da587c
|
|||
|
91e3d2f5db
|
|||
|
971c683746
|
|||
|
15e9aaab26
|
@@ -17,7 +17,7 @@ android {
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 615
|
||||
versionName = '6.6.5'
|
||||
versionName = '6.6.6'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:8852d1e22e') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:a8f9423307') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@JvmName("mangaIds")
|
||||
@@ -113,3 +115,16 @@ val Manga.appUrl: Uri
|
||||
.appendQueryParameter("name", title)
|
||||
.appendQueryParameter("url", url)
|
||||
.build()
|
||||
|
||||
private val chaptersNumberFormat = DecimalFormat("#.#").also { f ->
|
||||
f.decimalFormatSymbols = DecimalFormatSymbols.getInstance().also {
|
||||
it.decimalSeparator = '.'
|
||||
}
|
||||
}
|
||||
|
||||
fun MangaChapter.formatNumber(): String? {
|
||||
if (number <= 0f) {
|
||||
return null
|
||||
}
|
||||
return chaptersNumberFormat.format(number.toDouble())
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ data class ParcelableChapter(
|
||||
MangaChapter(
|
||||
id = parcel.readLong(),
|
||||
name = parcel.readString().orEmpty(),
|
||||
number = parcel.readInt(),
|
||||
number = parcel.readFloat(),
|
||||
volume = parcel.readInt(),
|
||||
url = parcel.readString().orEmpty(),
|
||||
scanlator = parcel.readString(),
|
||||
uploadDate = parcel.readLong(),
|
||||
@@ -31,7 +32,8 @@ data class ParcelableChapter(
|
||||
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
|
||||
parcel.writeLong(id)
|
||||
parcel.writeString(name)
|
||||
parcel.writeInt(number)
|
||||
parcel.writeFloat(number)
|
||||
parcel.writeInt(volume)
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(scanlator)
|
||||
parcel.writeLong(uploadDate)
|
||||
|
||||
@@ -347,7 +347,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
||||
|
||||
var historySortOrder: ListSortOrder
|
||||
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, ListSortOrder.UPDATED)
|
||||
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, ListSortOrder.LAST_READ)
|
||||
set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) }
|
||||
|
||||
var allFavoritesSortOrder: ListSortOrder
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.core.model.formatNumber
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
|
||||
@@ -49,8 +50,8 @@ data class ChapterListItem(
|
||||
|
||||
private fun buildDescription(): String {
|
||||
val joiner = StringJoiner(" • ")
|
||||
if (chapter.number != 0) {
|
||||
joiner.add("#").append(chapter.number.toString())
|
||||
chapter.formatNumber()?.let {
|
||||
joiner.add("#").append(it)
|
||||
}
|
||||
uploadDate?.let { date ->
|
||||
joiner.add(date.toString())
|
||||
|
||||
@@ -21,6 +21,7 @@ import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.formatNumber
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
@@ -306,7 +307,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
return chapters.mapNotNullTo(ArrayList(size)) {
|
||||
if (chapterIds == null || it.id in chapterIds) {
|
||||
DownloadChapter(
|
||||
number = it.number,
|
||||
number = it.formatNumber(),
|
||||
name = it.name,
|
||||
isDownloaded = it.id in localChapters,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class DownloadChapter(
|
||||
val number: Int,
|
||||
val number: String?,
|
||||
val name: String,
|
||||
val isDownloaded: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
@@ -193,8 +193,8 @@ abstract class FavouritesDao {
|
||||
ListSortOrder.ALPHABETIC -> "manga.title ASC"
|
||||
ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC"
|
||||
ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC"
|
||||
ListSortOrder.UPDATED, // for legacy support
|
||||
ListSortOrder.PROGRESS -> "IFNULL((SELECT percent FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
|
||||
ListSortOrder.LAST_READ -> "IFNULL((SELECT updated_at FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
|
||||
|
||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
||||
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class AllCategoriesListModel(
|
||||
@@ -12,4 +13,12 @@ data class AllCategoriesListModel(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is AllCategoriesListModel
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is AllCategoriesListModel && previousState.isVisible != isVisible) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ abstract class HistoryDao {
|
||||
|
||||
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
|
||||
val orderBy = when (order) {
|
||||
ListSortOrder.UPDATED -> "history.updated_at DESC"
|
||||
ListSortOrder.LAST_READ -> "history.updated_at DESC"
|
||||
ListSortOrder.NEWEST -> "history.created_at DESC"
|
||||
ListSortOrder.PROGRESS -> "history.percent DESC"
|
||||
ListSortOrder.ALPHABETIC -> "manga.title"
|
||||
|
||||
@@ -172,7 +172,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
|
||||
ListSortOrder.UPDATED -> ListHeader(calculateTimeAgo(updatedAt))
|
||||
ListSortOrder.LAST_READ -> ListHeader(calculateTimeAgo(updatedAt))
|
||||
ListSortOrder.NEWEST -> ListHeader(calculateTimeAgo(createdAt))
|
||||
ListSortOrder.PROGRESS -> ListHeader(
|
||||
when (percent) {
|
||||
|
||||
@@ -9,7 +9,6 @@ enum class ListSortOrder(
|
||||
@StringRes val titleResId: Int,
|
||||
) {
|
||||
|
||||
UPDATED(R.string.updated),
|
||||
NEWEST(R.string.order_added),
|
||||
PROGRESS(R.string.progress),
|
||||
ALPHABETIC(R.string.by_name),
|
||||
@@ -17,14 +16,15 @@ enum class ListSortOrder(
|
||||
RATING(R.string.by_rating),
|
||||
RELEVANCE(R.string.by_relevance),
|
||||
NEW_CHAPTERS(R.string.new_chapters),
|
||||
LAST_READ(R.string.last_read),
|
||||
;
|
||||
|
||||
fun isGroupingSupported() = this == UPDATED || this == NEWEST || this == PROGRESS
|
||||
fun isGroupingSupported() = this == LAST_READ || this == NEWEST || this == PROGRESS
|
||||
|
||||
companion object {
|
||||
|
||||
val HISTORY: Set<ListSortOrder> = EnumSet.of(UPDATED, NEWEST, PROGRESS, ALPHABETIC, ALPHABETIC_REVERSE, NEW_CHAPTERS)
|
||||
val FAVORITES: Set<ListSortOrder> = EnumSet.of(ALPHABETIC, ALPHABETIC_REVERSE, NEWEST, RATING, NEW_CHAPTERS, PROGRESS)
|
||||
val HISTORY: Set<ListSortOrder> = EnumSet.of(LAST_READ, NEWEST, PROGRESS, ALPHABETIC, ALPHABETIC_REVERSE, NEW_CHAPTERS)
|
||||
val FAVORITES: Set<ListSortOrder> = EnumSet.of(ALPHABETIC, ALPHABETIC_REVERSE, NEWEST, RATING, NEW_CHAPTERS, PROGRESS, LAST_READ)
|
||||
val SUGGESTIONS: Set<ListSortOrder> = EnumSet.of(RELEVANCE)
|
||||
|
||||
operator fun invoke(value: String, fallback: ListSortOrder) = entries.find(value) ?: fallback
|
||||
|
||||
@@ -12,6 +12,8 @@ import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.find
|
||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
|
||||
@@ -91,6 +93,7 @@ class MangaIndex(source: String?) {
|
||||
if (!chapters.has(chapter.id.toString())) {
|
||||
val jo = JSONObject()
|
||||
jo.put("number", chapter.number)
|
||||
jo.put("volume", chapter.volume)
|
||||
jo.put("url", chapter.url)
|
||||
jo.put("name", chapter.name)
|
||||
jo.put("uploadDate", chapter.uploadDate)
|
||||
@@ -162,7 +165,8 @@ class MangaIndex(source: String?) {
|
||||
id = k.toLong(),
|
||||
name = v.getString("name"),
|
||||
url = v.getString("url"),
|
||||
number = v.getInt("number"),
|
||||
number = v.getFloatOrDefault("number", 0f),
|
||||
volume = v.getIntOrDefault("volume", 0),
|
||||
uploadDate = v.getLongOrDefault("uploadDate", 0L),
|
||||
scanlator = v.getStringOrNull("scanlator"),
|
||||
branch = v.getStringOrNull("branch"),
|
||||
|
||||
@@ -71,7 +71,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
MangaChapter(
|
||||
id = "$i${f.name}".longHashCode(),
|
||||
name = f.nameWithoutExtension.toHumanReadable(),
|
||||
number = i + 1,
|
||||
number = 0f,
|
||||
volume = 0,
|
||||
source = MangaSource.LOCAL,
|
||||
uploadDate = f.creationTime,
|
||||
url = f.toUri().toString(),
|
||||
|
||||
@@ -100,6 +100,7 @@ sealed class LocalMangaInput(
|
||||
id = id,
|
||||
name = name,
|
||||
number = number,
|
||||
volume = volume,
|
||||
url = url,
|
||||
scanlator = scanlator,
|
||||
uploadDate = uploadDate,
|
||||
|
||||
@@ -76,7 +76,8 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
||||
MangaChapter(
|
||||
id = "$i$s".longHashCode(),
|
||||
name = s.ifEmpty { title },
|
||||
number = i + 1,
|
||||
number = 0f,
|
||||
volume = 0,
|
||||
source = MangaSource.LOCAL,
|
||||
uploadDate = 0L,
|
||||
url = uriBuilder.fragment(s).build().toString(),
|
||||
|
||||
@@ -51,6 +51,7 @@ import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.assertNotNull
|
||||
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
@@ -262,7 +263,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun onCurrentPageChanged(position: Int) {
|
||||
fun onCurrentPageChanged(lowerPos: Int, upperPos: Int) {
|
||||
val prevJob = stateChangeJob
|
||||
val pages = content.value.pages // capture immediately
|
||||
stateChangeJob = launchJob(Dispatchers.Default) {
|
||||
@@ -271,7 +272,8 @@ class ReaderViewModel @Inject constructor(
|
||||
if (pages.size != content.value.pages.size) {
|
||||
return@launchJob // TODO
|
||||
}
|
||||
pages.getOrNull(position)?.let { page ->
|
||||
val centerPos = (lowerPos + upperPos) / 2
|
||||
pages.getOrNull(centerPos)?.let { page ->
|
||||
currentState.update { cs ->
|
||||
cs?.copy(chapterId = page.chapterId, page = page.index)
|
||||
}
|
||||
@@ -281,14 +283,14 @@ class ReaderViewModel @Inject constructor(
|
||||
return@launchJob
|
||||
}
|
||||
ensureActive()
|
||||
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||
}
|
||||
if (position <= BOUNDS_PAGE_OFFSET) {
|
||||
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
}
|
||||
if (pageLoader.isPrefetchApplicable()) {
|
||||
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
||||
pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,18 +383,20 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
@WorkerThread
|
||||
private fun notifyStateChanged() {
|
||||
val state = getCurrentState()
|
||||
val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) }
|
||||
val state = getCurrentState().assertNotNull("state") ?: return
|
||||
val chapter = chaptersLoader.peekChapter(state.chapterId).assertNotNull("chapter") ?: return
|
||||
val m = manga.assertNotNull("manga") ?: return
|
||||
val chapterIndex = m.chapters[chapter.branch]?.indexOf(chapter) ?: -1
|
||||
val newState = ReaderUiState(
|
||||
mangaName = manga?.toManga()?.title,
|
||||
branch = chapter?.branch,
|
||||
chapterName = chapter?.name,
|
||||
chapterNumber = chapter?.number ?: 0,
|
||||
chaptersTotal = manga?.chapters?.get(chapter?.branch)?.size ?: 0,
|
||||
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
|
||||
currentPage = state?.page ?: 0,
|
||||
mangaName = m.toManga().title,
|
||||
branch = chapter.branch,
|
||||
chapterName = chapter.name,
|
||||
chapterNumber = chapterIndex + 1,
|
||||
chaptersTotal = m.chapters[chapter.branch]?.size ?: 0,
|
||||
totalPages = chaptersLoader.getPagesCount(chapter.id),
|
||||
currentPage = state.page,
|
||||
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||
percent = if (state != null) computePercent(state.chapterId, state.page) else PROGRESS_NONE,
|
||||
percent = computePercent(state.chapterId, state.page),
|
||||
)
|
||||
uiState.value = newState
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ abstract class BasePagerReaderFragment : BaseReaderFragment<FragmentReaderPagerB
|
||||
}
|
||||
|
||||
protected open fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(page)
|
||||
viewModel.onCurrentPageChanged(page, page)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -32,7 +32,8 @@ class ReversedReaderFragment : BasePagerReaderFragment() {
|
||||
}
|
||||
|
||||
override fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(reversed(page))
|
||||
val pos = reversed(page)
|
||||
viewModel.onCurrentPageChanged(pos, pos)
|
||||
}
|
||||
|
||||
private fun reversed(position: Int): Int {
|
||||
|
||||
@@ -24,7 +24,8 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>() {
|
||||
class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>(),
|
||||
WebtoonRecyclerView.OnWebtoonScrollListener {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
@@ -46,7 +47,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = readerAdapter
|
||||
addOnPageScrollListener(PageScrollListener())
|
||||
addOnPageScrollListener(this@WebtoonReaderFragment)
|
||||
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
|
||||
addOnScrollListener(it)
|
||||
}
|
||||
@@ -70,6 +71,15 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
override fun onScrollChanged(
|
||||
recyclerView: WebtoonRecyclerView,
|
||||
dy: Int,
|
||||
firstVisiblePosition: Int,
|
||||
lastVisiblePosition: Int,
|
||||
) {
|
||||
viewModel.onCurrentPageChanged(firstVisiblePosition, lastVisiblePosition)
|
||||
}
|
||||
|
||||
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
|
||||
val setItems = launch {
|
||||
requireAdapter().setItems(pages)
|
||||
@@ -91,7 +101,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
?.restoreScroll(pendingState.scroll)
|
||||
}
|
||||
}
|
||||
notifyPageChanged(position)
|
||||
viewModel.onCurrentPageChanged(position, position)
|
||||
} else {
|
||||
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
@@ -121,10 +131,6 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
viewBinding?.frame?.onZoomOut()
|
||||
}
|
||||
|
||||
private fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(page)
|
||||
}
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
with(requireViewBinding().recyclerView) {
|
||||
if (isAnimationEnabled()) {
|
||||
@@ -147,12 +153,4 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class PageScrollListener : WebtoonRecyclerView.OnPageScrollListener() {
|
||||
|
||||
override fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) {
|
||||
super.onPageChanged(recyclerView, index)
|
||||
notifyPageChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.view.View
|
||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.iterator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||
import java.util.LinkedList
|
||||
import java.util.WeakHashMap
|
||||
|
||||
@@ -15,7 +15,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : RecyclerView(context, attrs, defStyleAttr) {
|
||||
|
||||
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
|
||||
private var onPageScrollListeners = LinkedList<OnWebtoonScrollListener>()
|
||||
private val scrollDispatcher = WebtoonScrollDispatcher()
|
||||
private val detachedViews = WeakHashMap<View, Unit>()
|
||||
private var isFixingScroll: Boolean = false
|
||||
|
||||
@@ -103,22 +104,20 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
return 0
|
||||
}
|
||||
|
||||
fun addOnPageScrollListener(listener: OnPageScrollListener) {
|
||||
val list = onPageScrollListeners ?: LinkedList<OnPageScrollListener>().also { onPageScrollListeners = it }
|
||||
list.add(listener)
|
||||
fun addOnPageScrollListener(listener: OnWebtoonScrollListener) {
|
||||
onPageScrollListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeOnPageScrollListener(listener: OnPageScrollListener) {
|
||||
onPageScrollListeners?.remove(listener)
|
||||
fun removeOnPageScrollListener(listener: OnWebtoonScrollListener) {
|
||||
onPageScrollListeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun notifyScrollChanged(dy: Int) {
|
||||
val listeners = onPageScrollListeners
|
||||
if (listeners.isNullOrEmpty()) {
|
||||
if (listeners.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val centerPosition = findCenterViewPosition()
|
||||
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
|
||||
scrollDispatcher.dispatchScroll(this, dy)
|
||||
}
|
||||
|
||||
fun relayoutChildren() {
|
||||
@@ -162,20 +161,30 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
else -> false
|
||||
}
|
||||
|
||||
abstract class OnPageScrollListener {
|
||||
private class WebtoonScrollDispatcher {
|
||||
|
||||
private var lastPosition = NO_POSITION
|
||||
private var firstPos = NO_POSITION
|
||||
private var lastPos = NO_POSITION
|
||||
|
||||
fun dispatchScroll(recyclerView: WebtoonRecyclerView, dy: Int, centerPosition: Int) {
|
||||
onScroll(recyclerView, dy)
|
||||
if (centerPosition != NO_POSITION && centerPosition != lastPosition) {
|
||||
lastPosition = centerPosition
|
||||
onPageChanged(recyclerView, centerPosition)
|
||||
fun dispatchScroll(rv: WebtoonRecyclerView, dy: Int) {
|
||||
val lm = rv.layoutManager as? LinearLayoutManager
|
||||
if (lm == null) {
|
||||
firstPos = NO_POSITION
|
||||
lastPos = NO_POSITION
|
||||
return
|
||||
}
|
||||
val newFirstPos = lm.findFirstVisibleItemPosition()
|
||||
val newLastPos = lm.findLastVisibleItemPosition()
|
||||
if (newFirstPos != firstPos || newLastPos != lastPos) {
|
||||
firstPos = newFirstPos
|
||||
lastPos = newLastPos
|
||||
rv.onPageScrollListeners.forEach { it.onScrollChanged(rv, dy, newFirstPos, newLastPos) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onScroll(recyclerView: WebtoonRecyclerView, dy: Int) = Unit
|
||||
interface OnWebtoonScrollListener {
|
||||
|
||||
open fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) = Unit
|
||||
fun onScrollChanged(recyclerView: WebtoonRecyclerView, dy: Int, firstVisiblePosition: Int, lastVisiblePosition: Int)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="24"
|
||||
android:singleLine="true"
|
||||
@@ -80,4 +81,4 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textPassword"
|
||||
android:maxLength="24"
|
||||
android:singleLine="true"
|
||||
@@ -94,4 +95,4 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -566,4 +566,5 @@
|
||||
<string name="volume_unknown">Unknown volume</string>
|
||||
<string name="incognito_mode_hint">Your reading progress will not be saved</string>
|
||||
<string name="vertical">Vertical</string>
|
||||
<string name="last_read">Last read</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user