Move GalleryListScene to paging3 (#1245)
* Initial WIP move GalleryListScene to paging3 * Refine * Refine * Refine * Refine * Refine * Refine * Typesafe State * Refine * Refine * Refine * Refine * Cleanup code * Cleanup code
This commit is contained in:
1 parent
7fd3361608
commit
df8c70d8e6
15 files changed
+405
-1575
No files matched your search
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.yorozuya.IntIdGenerator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -25,26 +23,7 @@ import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object FavouriteStatusRouter {
|
||||
private val idGenerator = IntIdGenerator(Settings.dataMapNextId)
|
||||
private val maps = HashMap<Int, MutableMap<Long, GalleryInfo>>()
|
||||
fun saveDataMap(map: MutableMap<Long, GalleryInfo>): Int {
|
||||
val id = idGenerator.nextId()
|
||||
maps[id] = map
|
||||
Settings.dataMapNextId = idGenerator.nextId()
|
||||
return id
|
||||
}
|
||||
|
||||
fun restoreDataMap(id: Int): MutableMap<Long, GalleryInfo>? {
|
||||
return maps.remove(id)
|
||||
}
|
||||
|
||||
fun modifyFavourites(gid: Long, slot: Int) {
|
||||
for (map in maps.values) {
|
||||
val info = map[gid]
|
||||
if (info != null) {
|
||||
info.favoriteSlot = slot
|
||||
}
|
||||
}
|
||||
_globalFlow.tryEmit(gid to slot)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ data class ListUrlBuilder(
|
||||
@get:Mode @Mode
|
||||
var mode: Int = MODE_NORMAL,
|
||||
private var mPrev: String? = null,
|
||||
private var mNext: String? = null,
|
||||
private var mJumpTo: String? = null,
|
||||
var mNext: String? = null,
|
||||
var mJumpTo: String? = null,
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
var hash: String? = null,
|
||||
|
||||
@@ -38,6 +38,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
|
||||
private var mProgress = 0f
|
||||
private var mAutoUpdateMirror = false
|
||||
private var mVerticalMirror = false
|
||||
|
||||
init {
|
||||
val resources = context.resources
|
||||
mSize = resources.getDimensionPixelSize(R.dimen.add_size)
|
||||
@@ -78,7 +79,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
|
||||
}
|
||||
|
||||
override fun setColorFilter(cf: ColorFilter?) {
|
||||
mPaint.setColorFilter(cf)
|
||||
mPaint.colorFilter = cf
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
@@ -131,7 +132,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
|
||||
progress = endProgress
|
||||
} else {
|
||||
val oa = ObjectAnimator.ofFloat(this, "progress", endProgress)
|
||||
oa.setDuration(duration)
|
||||
oa.duration = duration
|
||||
oa.setAutoCancel(true)
|
||||
oa.start()
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ package com.hippo.ehviewer.ui.legacy
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class BringOutTransition(contentLayout: ContentLayout, mSearchLayout: SearchLayout) :
|
||||
class BringOutTransition(contentLayout: FrameLayout, mSearchLayout: SearchLayout) :
|
||||
ViewTransition(contentLayout, mSearchLayout) {
|
||||
override fun startAnimations(hiddenView: View, shownView: View) {
|
||||
mAnimator1 = hiddenView.animate().alpha(0f).scaleY(0.7f).scaleX(0.7f).apply {
|
||||
|
||||
@@ -1,820 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.databinding.WidgetContentLayoutBinding
|
||||
import com.hippo.ehviewer.ui.legacy.LayoutManagerUtils.OnScrollToPositionListener
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition.OnShowViewListener
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import com.hippo.ehviewer.yorozuya.IntIdGenerator
|
||||
import com.hippo.ehviewer.yorozuya.collect.IntList
|
||||
import com.hippo.ehviewer.yorozuya.dp2px
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
|
||||
private lateinit var mContentHelper: ContentHelper
|
||||
private val mRecyclerViewOriginBottom: Int
|
||||
private val mFastScrollerOriginBottom: Int
|
||||
private val binding: WidgetContentLayoutBinding
|
||||
|
||||
init {
|
||||
binding = WidgetContentLayoutBinding.inflate((context as Activity).layoutInflater, this)
|
||||
clipChildren = false
|
||||
clipToPadding = false
|
||||
binding.fastScroller.attachToRecyclerView(binding.recyclerView)
|
||||
val drawable = HandlerDrawable()
|
||||
drawable.setColor(context.getTheme().resolveColor(androidx.appcompat.R.attr.colorPrimary))
|
||||
binding.fastScroller.setHandlerDrawable(drawable)
|
||||
mRecyclerViewOriginBottom = binding.recyclerView.paddingBottom
|
||||
mFastScrollerOriginBottom = binding.fastScroller.paddingBottom
|
||||
}
|
||||
|
||||
val recyclerView
|
||||
get() = binding.recyclerView
|
||||
|
||||
val fastScroller
|
||||
get() = binding.fastScroller
|
||||
|
||||
fun setHelper(helper: ContentHelper) {
|
||||
mContentHelper = helper
|
||||
helper.init(this)
|
||||
}
|
||||
|
||||
fun hideFastScroll() {
|
||||
binding.fastScroller.detachedFromRecyclerView()
|
||||
}
|
||||
|
||||
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.setPadding(left, top, right, 0)
|
||||
setFitPaddingBottom(bottom)
|
||||
}
|
||||
|
||||
fun setFitPaddingTop(fitPaddingTop: Int) {
|
||||
// RefreshLayout
|
||||
binding.refreshLayout.setProgressViewOffset(
|
||||
true,
|
||||
0,
|
||||
fitPaddingTop + dp2px(context, 32f),
|
||||
) // TODO
|
||||
}
|
||||
|
||||
private fun setFitPaddingBottom(fitPaddingBottom: Int) {
|
||||
// RecyclerView
|
||||
binding.recyclerView.setPadding(
|
||||
binding.recyclerView.paddingLeft,
|
||||
binding.recyclerView.paddingTop,
|
||||
binding.recyclerView.paddingRight,
|
||||
mRecyclerViewOriginBottom + fitPaddingBottom,
|
||||
)
|
||||
binding.tip.setPadding(
|
||||
binding.tip.paddingLeft,
|
||||
binding.tip.paddingTop,
|
||||
binding.tip.paddingRight,
|
||||
fitPaddingBottom,
|
||||
)
|
||||
binding.progress.setPadding(
|
||||
binding.progress.paddingLeft,
|
||||
binding.progress.paddingTop,
|
||||
binding.progress.paddingRight,
|
||||
fitPaddingBottom,
|
||||
)
|
||||
binding.fastScroller.setPadding(
|
||||
binding.fastScroller.paddingLeft,
|
||||
binding.fastScroller.paddingTop,
|
||||
binding.fastScroller.paddingRight,
|
||||
mFastScrollerOriginBottom + fitPaddingBottom,
|
||||
)
|
||||
if (fitPaddingBottom > dp2px(context, 16f)) {
|
||||
binding.bottomProgress.setPadding(0, 0, 0, fitPaddingBottom)
|
||||
} else {
|
||||
binding.bottomProgress.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
return mContentHelper.saveInstanceState(super.onSaveInstanceState())
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state))
|
||||
}
|
||||
|
||||
abstract class ContentHelper : OnShowViewListener {
|
||||
/**
|
||||
* Generate task id
|
||||
*/
|
||||
private val mIdGenerator = IntIdGenerator()
|
||||
private val mOnScrollToPositionListener =
|
||||
OnScrollToPositionListener { position: Int -> onScrollToPosition(position) }
|
||||
protected var mPrev: String? = null
|
||||
protected var mNext: String? = null
|
||||
private lateinit var mViewTransition: ViewTransition
|
||||
private lateinit var binding: WidgetContentLayoutBinding
|
||||
|
||||
/**
|
||||
* Store data
|
||||
*/
|
||||
private var mData = ArrayList<GalleryInfo>()
|
||||
|
||||
/**
|
||||
* Store the page divider index
|
||||
*
|
||||
*
|
||||
* For example, the data contain page 3, page 4, page 5,
|
||||
* page 3 size is 7, page 4 size is 8, page 5 size is 9,
|
||||
* so `mPageDivider` contain 7, 15, 24.
|
||||
*/
|
||||
private var mPageDivider = IntList()
|
||||
|
||||
/**
|
||||
* The first page in `mData`
|
||||
*/
|
||||
private var mStartPage = 0
|
||||
|
||||
/**
|
||||
* The last page + 1 in `mData`
|
||||
*/
|
||||
private var mEndPage = 0
|
||||
|
||||
/**
|
||||
* The available page count.
|
||||
*/
|
||||
var pages = 0
|
||||
private set
|
||||
private var mNextPage = 0
|
||||
private var mCurrentTaskId = 0
|
||||
private var mCurrentTaskType = 0
|
||||
private var mCurrentTaskPage = 0
|
||||
private val mOnRefreshListener = OnRefreshListener {
|
||||
if (mPrev != null || mStartPage > 0) {
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_PRE_PAGE_KEEP_POS
|
||||
mCurrentTaskPage = mStartPage - 1
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, mPrev, false)
|
||||
} else {
|
||||
doRefresh()
|
||||
}
|
||||
}
|
||||
private var mNextPageScrollSize = 0
|
||||
private var mEmptyString = "No hint"
|
||||
private val mOnScrollListener: RecyclerView.OnScrollListener =
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (!binding.refreshLayout.isRefreshing && !recyclerView.canScrollVertically(1)) {
|
||||
if (mNext != null || mEndPage < pages) {
|
||||
binding.bottomProgress.show()
|
||||
// Get next page
|
||||
// Fill pages before NextPage with empty list
|
||||
while (mNextPage > mEndPage && mEndPage < pages) {
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS
|
||||
mCurrentTaskPage = mEndPage
|
||||
onGetPageData(
|
||||
mCurrentTaskId,
|
||||
pages,
|
||||
mNextPage,
|
||||
null,
|
||||
null,
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS
|
||||
mCurrentTaskPage = mEndPage
|
||||
getPageData(
|
||||
mCurrentTaskId,
|
||||
mCurrentTaskType,
|
||||
mCurrentTaskPage,
|
||||
mNext,
|
||||
true,
|
||||
)
|
||||
} else if (mStartPage > 0 && mEndPage == pages) {
|
||||
binding.bottomProgress.show()
|
||||
// Refresh last page
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_REFRESH_PAGE
|
||||
mCurrentTaskPage = mEndPage - 1
|
||||
getPageData(
|
||||
mCurrentTaskId,
|
||||
mCurrentTaskType,
|
||||
mCurrentTaskPage,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun init(contentLayout: ContentLayout) {
|
||||
binding = contentLayout.binding
|
||||
mNextPageScrollSize = dp2px(contentLayout.context, 48f)
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!!
|
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
binding.tip.setCompoundDrawables(null, drawable, null, null)
|
||||
mViewTransition = ViewTransition(
|
||||
binding.contentView,
|
||||
binding.progress,
|
||||
binding.tip,
|
||||
)
|
||||
mViewTransition.setOnShowViewListener(this)
|
||||
binding.recyclerView.addOnScrollListener(mOnScrollListener)
|
||||
binding.refreshLayout.setOnRefreshListener(mOnRefreshListener)
|
||||
binding.tip.setOnClickListener { refresh() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Call [.onGetPageData] when get data
|
||||
*
|
||||
* @param taskId task id
|
||||
* @param page the page to get
|
||||
* @param index the index to get
|
||||
* @param isNext the index is next or prev
|
||||
*/
|
||||
protected abstract fun getPageData(
|
||||
taskId: Int,
|
||||
type: Int,
|
||||
page: Int,
|
||||
index: String?,
|
||||
isNext: Boolean,
|
||||
)
|
||||
|
||||
protected abstract val context: Context
|
||||
protected open fun notifyDataSetChanged(callback: () -> Unit) {
|
||||
callback()
|
||||
}
|
||||
|
||||
protected abstract fun notifyDataSetChanged()
|
||||
protected abstract fun notifyItemRangeInserted(positionStart: Int, itemCount: Int)
|
||||
protected open fun onScrollToPosition(position: Int) {}
|
||||
override fun onShowView(hiddenView: View, shownView: View) {}
|
||||
val shownViewIndex: Int
|
||||
get() = mViewTransition.shownViewIndex
|
||||
|
||||
fun setRefreshLayoutEnable(enable: Boolean) {
|
||||
binding.refreshLayout.isEnabled = enable
|
||||
}
|
||||
|
||||
fun setEmptyString(str: String) {
|
||||
mEmptyString = str
|
||||
}
|
||||
|
||||
val data: List<GalleryInfo>
|
||||
get() = mData
|
||||
|
||||
fun getDataAtEx(location: Int): GalleryInfo? {
|
||||
return if (location >= 0 && location < mData.size) {
|
||||
mData[location]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val firstVisibleItem: GalleryInfo?
|
||||
get() = getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager!!))
|
||||
|
||||
fun size(): Int {
|
||||
return mData.size
|
||||
}
|
||||
|
||||
fun isCurrentTask(taskId: Int): Boolean {
|
||||
return mCurrentTaskId == taskId
|
||||
}
|
||||
|
||||
protected abstract fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean
|
||||
private fun removeDuplicateData(data: List<GalleryInfo>, start: Int, end: Int) {
|
||||
val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size))
|
||||
data.dropWhile { d1 ->
|
||||
slicedData.any { d2 ->
|
||||
isDuplicate(d1, d2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onAddData(data: List<GalleryInfo>) {}
|
||||
protected open fun onRemoveData(data: List<GalleryInfo>) {}
|
||||
protected open fun onClearData() {}
|
||||
fun onGetPageData(
|
||||
taskId: Int,
|
||||
pages: Int,
|
||||
nextPage: Int,
|
||||
prev: String?,
|
||||
next: String?,
|
||||
data: List<GalleryInfo>,
|
||||
) {
|
||||
if (mCurrentTaskId == taskId) {
|
||||
val dataSize: Int
|
||||
when (mCurrentTaskType) {
|
||||
TYPE_REFRESH -> {
|
||||
mStartPage = 0
|
||||
mEndPage = 1
|
||||
this.pages = pages
|
||||
mNextPage = nextPage
|
||||
mPrev = prev
|
||||
mNext = next
|
||||
mPageDivider.clear()
|
||||
mPageDivider.add(data.size)
|
||||
if (data.isEmpty()) {
|
||||
mData.clear()
|
||||
onClearData()
|
||||
notifyDataSetChanged()
|
||||
|
||||
notifyDataSetChanged {
|
||||
// Not found
|
||||
// Ui change, show empty string
|
||||
showEmptyString()
|
||||
}
|
||||
} else {
|
||||
mData.clear()
|
||||
onClearData()
|
||||
mData.addAll(data)
|
||||
onAddData(data)
|
||||
notifyDataSetChanged()
|
||||
|
||||
notifyDataSetChanged {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
|
||||
// RecyclerView scroll
|
||||
if (binding.recyclerView.isAttachedToWindow) {
|
||||
scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TYPE_PRE_PAGE, TYPE_PRE_PAGE_KEEP_POS -> {
|
||||
removeDuplicateData(data, 0, CHECK_DUPLICATE_RANGE)
|
||||
dataSize = data.size
|
||||
var i = 0
|
||||
val n = mPageDivider.size
|
||||
while (i < n) {
|
||||
mPageDivider[i] = mPageDivider[i] + dataSize
|
||||
i++
|
||||
}
|
||||
mPageDivider.add(0, dataSize)
|
||||
mStartPage--
|
||||
this.pages = pages.coerceAtLeast(mEndPage)
|
||||
mPrev = prev
|
||||
// assert mStartPage >= 0
|
||||
if (data.isEmpty()) {
|
||||
// OK, that's all
|
||||
if (mData.isEmpty()) {
|
||||
// Ui change, show empty string
|
||||
showEmptyString()
|
||||
} else {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
if (mCurrentTaskType == TYPE_PRE_PAGE && binding.recyclerView.isAttachedToWindow) {
|
||||
// RecyclerView scroll, to top
|
||||
scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mData.addAll(0, data)
|
||||
onAddData(data)
|
||||
notifyItemRangeInserted(0, data.size)
|
||||
|
||||
notifyDataSetChanged {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
if (binding.recyclerView.isAttachedToWindow) {
|
||||
// RecyclerView scroll
|
||||
if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) {
|
||||
binding.recyclerView.stopScroll()
|
||||
LayoutManagerUtils.scrollToPositionProperly(
|
||||
binding.recyclerView.layoutManager!!,
|
||||
context,
|
||||
dataSize - 1,
|
||||
mOnScrollToPositionListener,
|
||||
)
|
||||
} else {
|
||||
scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TYPE_NEXT_PAGE, TYPE_NEXT_PAGE_KEEP_POS -> {
|
||||
removeDuplicateData(data, mData.size - CHECK_DUPLICATE_RANGE, mData.size)
|
||||
dataSize = data.size
|
||||
val oldDataSize = mData.size
|
||||
mPageDivider.add(oldDataSize + dataSize)
|
||||
mEndPage++
|
||||
mNextPage = nextPage
|
||||
this.pages = pages.coerceAtLeast(mEndPage)
|
||||
mNext = next
|
||||
if (data.isEmpty()) {
|
||||
// OK, that's all
|
||||
if (mData.isEmpty()) {
|
||||
// Ui change, show empty string
|
||||
showEmptyString()
|
||||
} else {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
if (mCurrentTaskType == TYPE_NEXT_PAGE && binding.recyclerView.isAttachedToWindow) {
|
||||
// RecyclerView scroll
|
||||
scrollToPosition(oldDataSize)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mData.addAll(data)
|
||||
onAddData(data)
|
||||
notifyItemRangeInserted(oldDataSize, dataSize)
|
||||
|
||||
notifyDataSetChanged {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
if (binding.recyclerView.isAttachedToWindow) {
|
||||
if (mCurrentTaskType == TYPE_NEXT_PAGE_KEEP_POS) {
|
||||
binding.recyclerView.stopScroll()
|
||||
binding.recyclerView.smoothScrollBy(0, mNextPageScrollSize)
|
||||
} else {
|
||||
scrollToPosition(oldDataSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TYPE_SOMEWHERE -> {
|
||||
mStartPage = mCurrentTaskPage
|
||||
mEndPage = mCurrentTaskPage + 1
|
||||
mNextPage = nextPage
|
||||
this.pages = pages
|
||||
mPrev = prev
|
||||
mNext = next
|
||||
mPageDivider.clear()
|
||||
mPageDivider.add(data.size)
|
||||
if (data.isEmpty()) {
|
||||
mData.clear()
|
||||
onClearData()
|
||||
notifyDataSetChanged()
|
||||
|
||||
notifyDataSetChanged {
|
||||
// Not found
|
||||
// Ui change, show empty string
|
||||
showEmptyString()
|
||||
}
|
||||
} else {
|
||||
mData.clear()
|
||||
onClearData()
|
||||
mData.addAll(data)
|
||||
onAddData(data)
|
||||
notifyDataSetChanged()
|
||||
|
||||
notifyDataSetChanged {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
if (binding.recyclerView.isAttachedToWindow) {
|
||||
// RecyclerView scroll
|
||||
scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TYPE_REFRESH_PAGE -> {
|
||||
if (mCurrentTaskPage < mStartPage || mCurrentTaskPage >= mEndPage) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"TYPE_REFRESH_PAGE, but mCurrentTaskPage = " + mCurrentTaskPage +
|
||||
", mStartPage = " + mStartPage + ", mEndPage = " + mEndPage,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (mCurrentTaskPage == mEndPage - 1) {
|
||||
mNextPage = nextPage
|
||||
}
|
||||
this.pages = pages.coerceAtLeast(mEndPage)
|
||||
val oldIndexStart =
|
||||
if (mCurrentTaskPage == mStartPage) 0 else mPageDivider[mCurrentTaskPage - mStartPage - 1]
|
||||
val oldIndexEnd = mPageDivider[mCurrentTaskPage - mStartPage]
|
||||
val toRemove = mData.subList(oldIndexStart, oldIndexEnd)
|
||||
onRemoveData(toRemove)
|
||||
toRemove.clear()
|
||||
removeDuplicateData(
|
||||
data,
|
||||
oldIndexStart - CHECK_DUPLICATE_RANGE,
|
||||
oldIndexStart + CHECK_DUPLICATE_RANGE,
|
||||
)
|
||||
val newIndexEnd = oldIndexStart + data.size
|
||||
mData.addAll(oldIndexStart, data)
|
||||
onAddData(data)
|
||||
notifyDataSetChanged()
|
||||
notifyDataSetChanged {
|
||||
var i = mCurrentTaskPage - mStartPage
|
||||
val n = mPageDivider.size
|
||||
while (i < n) {
|
||||
mPageDivider[i] = mPageDivider[i] - oldIndexEnd + newIndexEnd
|
||||
i++
|
||||
}
|
||||
if (mData.isEmpty()) {
|
||||
// Ui change, show empty string
|
||||
showEmptyString()
|
||||
} else {
|
||||
// Ui change, show content
|
||||
showContent()
|
||||
|
||||
// RecyclerView scroll
|
||||
if (newIndexEnd > oldIndexEnd && newIndexEnd > 0 && binding.recyclerView.isAttachedToWindow) {
|
||||
scrollToPosition(newIndexEnd - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onGetException(taskId: Int, e: Throwable) {
|
||||
if (mCurrentTaskId == taskId) {
|
||||
showProgress(PROGRESS_NONE)
|
||||
val readableError = ExceptionUtils.getReadableString(e)
|
||||
if (mViewTransition.shownViewIndex == 0) {
|
||||
Toast.makeText(context, readableError, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
showText(readableError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToPosition(position: Int) {
|
||||
binding.recyclerView.stopScroll()
|
||||
LayoutManagerUtils.scrollToPositionWithOffset(
|
||||
binding.recyclerView.layoutManager!!,
|
||||
position,
|
||||
0,
|
||||
)
|
||||
onScrollToPosition(position)
|
||||
}
|
||||
|
||||
private fun showProgress(type: Int) {
|
||||
when (type) {
|
||||
PROGRESS_NONE -> {
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
binding.bottomProgress.hide()
|
||||
}
|
||||
|
||||
PROGRESS_TOP -> {
|
||||
binding.refreshLayout.isRefreshing = true
|
||||
binding.bottomProgress.hide()
|
||||
}
|
||||
|
||||
PROGRESS_BOTTOM -> {
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
binding.bottomProgress.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showContent() {
|
||||
showProgress(PROGRESS_NONE)
|
||||
mViewTransition.showView(0)
|
||||
}
|
||||
|
||||
private val isContentShowing: Boolean
|
||||
get() = mViewTransition.shownViewIndex == 0
|
||||
|
||||
fun showProgressBar(animation: Boolean = true) {
|
||||
mViewTransition.showView(1, animation)
|
||||
}
|
||||
|
||||
private fun showText(text: CharSequence?) {
|
||||
binding.tip.text = text
|
||||
mViewTransition.showView(2)
|
||||
}
|
||||
|
||||
private fun showEmptyString() {
|
||||
showProgress(PROGRESS_NONE)
|
||||
showText(mEmptyString)
|
||||
}
|
||||
|
||||
private fun doRefresh() {
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_REFRESH
|
||||
mCurrentTaskPage = 0
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [.refresh], but no animation when show progress bar
|
||||
*/
|
||||
fun firstRefresh() {
|
||||
showProgressBar(false)
|
||||
doRefresh()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show progress bar first, than do refresh
|
||||
*/
|
||||
fun refresh() {
|
||||
showProgressBar()
|
||||
doRefresh()
|
||||
}
|
||||
|
||||
private fun cancelCurrentTask() {
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
showProgress(PROGRESS_NONE)
|
||||
}
|
||||
|
||||
private fun getPageStart(page: Int): Int {
|
||||
return if (mStartPage == page) {
|
||||
0
|
||||
} else {
|
||||
mPageDivider[page - mStartPage - 1]
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPageForPosition(position: Int): Int {
|
||||
if (position < 0) {
|
||||
return -1
|
||||
}
|
||||
val pageDivider = mPageDivider
|
||||
var i = 0
|
||||
val n = pageDivider.size
|
||||
while (i < n) {
|
||||
if (position < pageDivider[i]) {
|
||||
return i + mStartPage
|
||||
}
|
||||
i++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
val pageForTop: Int
|
||||
get() = getPageForPosition(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager!!))
|
||||
val pageForBottom: Int
|
||||
get() = getPageForPosition(LayoutManagerUtils.getLastVisibleItemPosition(binding.recyclerView.layoutManager!!))
|
||||
|
||||
fun canGoTo(): Boolean {
|
||||
return isContentShowing
|
||||
}
|
||||
|
||||
/**
|
||||
* Check range first!
|
||||
*
|
||||
* @param page the target page
|
||||
* @throws IndexOutOfBoundsException if page < 0 or page >= mPages
|
||||
*/
|
||||
fun goTo(page: Int) {
|
||||
when (page) {
|
||||
!in 0 until pages -> {
|
||||
throw IndexOutOfBoundsException("Page count is $pages, page is $page")
|
||||
}
|
||||
|
||||
in mStartPage until mEndPage -> {
|
||||
cancelCurrentTask()
|
||||
val position = getPageStart(page)
|
||||
scrollToPosition(position)
|
||||
}
|
||||
|
||||
mStartPage - 1 -> {
|
||||
showProgress(PROGRESS_TOP)
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_PRE_PAGE
|
||||
mCurrentTaskPage = page
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
|
||||
}
|
||||
|
||||
mEndPage -> {
|
||||
showProgress(PROGRESS_BOTTOM)
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_NEXT_PAGE
|
||||
mCurrentTaskPage = page
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
showProgress(PROGRESS_TOP)
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_SOMEWHERE
|
||||
mCurrentTaskPage = page
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun goTo(index: String?, isNext: Boolean) {
|
||||
showProgress(PROGRESS_TOP)
|
||||
mCurrentTaskId = mIdGenerator.nextId()
|
||||
mCurrentTaskType = TYPE_SOMEWHERE
|
||||
mCurrentTaskPage = 0
|
||||
getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, index, isNext)
|
||||
}
|
||||
|
||||
open fun saveInstanceState(superState: Parcelable?): Parcelable {
|
||||
if (mData.isNotEmpty()) cancelCurrentTask()
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_SUPER, superState)
|
||||
val shownView = mViewTransition.shownViewIndex
|
||||
bundle.putInt(KEY_SHOWN_VIEW, shownView)
|
||||
bundle.putString(KEY_TIP, binding.tip.text.toString())
|
||||
synchronized(contentMap) {
|
||||
val size = contentMap.keys.takeIf { it.isNotEmpty() }?.max()?.inc() ?: 0
|
||||
contentMap[size] = mData
|
||||
bundle.putInt(KEY_DATA, size)
|
||||
}
|
||||
bundle.putInt(KEY_NEXT_ID, mIdGenerator.nextId())
|
||||
bundle.putParcelable(KEY_PAGE_DIVIDER, mPageDivider)
|
||||
bundle.putInt(KEY_START_PAGE, mStartPage)
|
||||
bundle.putInt(KEY_END_PAGE, mEndPage)
|
||||
bundle.putInt(KEY_PAGES, pages)
|
||||
bundle.putString(KEY_PREV, mPrev)
|
||||
bundle.putString(KEY_NEXT, mNext)
|
||||
return bundle
|
||||
}
|
||||
|
||||
open fun restoreInstanceState(state: Parcelable): Parcelable? {
|
||||
return if (state is Bundle) {
|
||||
mViewTransition.showView(state.getInt(KEY_SHOWN_VIEW), false)
|
||||
binding.tip.text = state.getString(KEY_TIP)
|
||||
val data = state.getInt(KEY_DATA)
|
||||
synchronized(contentMap) {
|
||||
mData = contentMap.remove(data) ?: ArrayList()
|
||||
}
|
||||
mIdGenerator.setNextId(state.getInt(KEY_NEXT_ID))
|
||||
mPageDivider = state.getParcelableCompat(KEY_PAGE_DIVIDER) ?: IntList()
|
||||
mStartPage = state.getInt(KEY_START_PAGE)
|
||||
mEndPage = state.getInt(KEY_END_PAGE)
|
||||
pages = state.getInt(KEY_PAGES)
|
||||
mPrev = state.getString(KEY_PREV)
|
||||
mNext = state.getString(KEY_NEXT)
|
||||
notifyDataSetChanged()
|
||||
notifyDataSetChanged {}
|
||||
if (mData.size == 0) {
|
||||
mPageDivider.clear()
|
||||
mStartPage = 0
|
||||
mEndPage = 0
|
||||
pages = 0
|
||||
mPrev = null
|
||||
mNext = null
|
||||
firstRefresh()
|
||||
}
|
||||
state.getParcelableCompat(KEY_SUPER)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_REFRESH = 0
|
||||
const val TYPE_PRE_PAGE = 1
|
||||
const val TYPE_PRE_PAGE_KEEP_POS = 2
|
||||
const val TYPE_NEXT_PAGE = 3
|
||||
const val TYPE_NEXT_PAGE_KEEP_POS = 4
|
||||
const val TYPE_SOMEWHERE = 5
|
||||
const val TYPE_REFRESH_PAGE = 6
|
||||
private val TAG = ContentHelper::class.java.simpleName
|
||||
private const val CHECK_DUPLICATE_RANGE = 50
|
||||
private const val PROGRESS_NONE = 0
|
||||
private const val PROGRESS_TOP = 1
|
||||
private const val PROGRESS_BOTTOM = 2
|
||||
private const val KEY_SUPER = "super"
|
||||
private const val KEY_SHOWN_VIEW = "shown_view"
|
||||
private const val KEY_TIP = "tip"
|
||||
private const val KEY_DATA = "data"
|
||||
private const val KEY_NEXT_ID = "next_id"
|
||||
private const val KEY_PAGE_DIVIDER = "page_divider"
|
||||
private const val KEY_START_PAGE = "start_page"
|
||||
private const val KEY_END_PAGE = "end_page"
|
||||
private const val KEY_PAGES = "pages"
|
||||
private const val KEY_PREV = "prev"
|
||||
private const val KEY_NEXT = "next"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// data "ArrayList<GalleryInfo>" is too large, we cannot put it as Parcelable otherwise TransactionTooLargeException would occur
|
||||
// TODO: convert to paging3 source then keep it in a proper window
|
||||
// @see PagingConfig.maxSize
|
||||
private val contentMap = hashMapOf<Int, ArrayList<GalleryInfo>>()
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 com.hippo.ehviewer.ui.legacy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import com.hippo.ehviewer.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.ui.legacy.ContentLayout.ContentHelper
|
||||
import com.hippo.ehviewer.yorozuya.IntIdGenerator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
abstract class GalleryInfoContentHelper : ContentHelper() {
|
||||
var jumpTo: String? = null
|
||||
val scope = CoroutineScope(Dispatchers.IO).apply {
|
||||
launch {
|
||||
FavouriteStatusRouter.globalFlow.collect { (gid, slot) -> map[gid]?.favoriteSlot = slot }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
private var map: MutableMap<Long, GalleryInfo> = HashMap()
|
||||
|
||||
fun destroy() = scope.cancel()
|
||||
|
||||
override fun onAddData(data: List<GalleryInfo>) {
|
||||
data.forEach { map[it.gid] = it }
|
||||
}
|
||||
|
||||
override fun onRemoveData(data: List<GalleryInfo>) {
|
||||
data.forEach { map.remove(it.gid) }
|
||||
}
|
||||
|
||||
override fun onClearData() {
|
||||
map.clear()
|
||||
}
|
||||
|
||||
override fun saveInstanceState(superState: Parcelable?): Parcelable {
|
||||
val bundle = super.saveInstanceState(superState) as Bundle
|
||||
|
||||
// TODO It's a bad design
|
||||
val id = FavouriteStatusRouter.saveDataMap(map)
|
||||
bundle.putInt(KEY_DATA_MAP, id)
|
||||
return bundle
|
||||
}
|
||||
|
||||
override fun restoreInstanceState(state: Parcelable): Parcelable? {
|
||||
val bundle = state as Bundle
|
||||
val id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID)
|
||||
if (id != IntIdGenerator.INVALID_ID) {
|
||||
FavouriteStatusRouter.restoreDataMap(id)?.let { this.map = it }
|
||||
}
|
||||
return super.restoreInstanceState(state)
|
||||
}
|
||||
|
||||
fun goTo(time: Long, isNext: Boolean) {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC)
|
||||
jumpTo = formatter.format(Instant.ofEpochMilli(time))
|
||||
if (isNext) {
|
||||
goTo(mNext ?: "2", true)
|
||||
} else {
|
||||
goTo(mPrev, false)
|
||||
}
|
||||
jumpTo = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_DATA_MAP = "data_map"
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class HandlerDrawable : Drawable() {
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
mPaint.setColorFilter(colorFilter)
|
||||
mPaint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
@@ -69,9 +69,11 @@ class HandlerDrawable : Drawable() {
|
||||
0xff -> {
|
||||
PixelFormat.OPAQUE
|
||||
}
|
||||
|
||||
0x00 -> {
|
||||
PixelFormat.TRANSPARENT
|
||||
}
|
||||
|
||||
else -> {
|
||||
PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ package com.hippo.ehviewer.ui.legacy
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.google.android.material.math.MathUtils
|
||||
import com.hippo.ehviewer.yorozuya.SimpleHandler
|
||||
@@ -42,7 +42,7 @@ object LayoutManagerUtils {
|
||||
}
|
||||
|
||||
fun scrollToPositionWithOffset(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
layoutManager: LayoutManager,
|
||||
position: Int,
|
||||
offset: Int,
|
||||
) {
|
||||
@@ -57,7 +57,7 @@ object LayoutManagerUtils {
|
||||
}
|
||||
|
||||
fun smoothScrollToPosition(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
layoutManager: LayoutManager,
|
||||
context: Context,
|
||||
position: Int,
|
||||
millisecondsPerInch: Int = -1,
|
||||
@@ -104,13 +104,13 @@ object LayoutManagerUtils {
|
||||
}
|
||||
|
||||
fun scrollToPositionProperly(
|
||||
layoutManager: RecyclerView.LayoutManager,
|
||||
layoutManager: LayoutManager,
|
||||
context: Context,
|
||||
position: Int,
|
||||
listener: OnScrollToPositionListener?,
|
||||
) {
|
||||
SimpleHandler.postDelayed({
|
||||
val first = getFirstVisibleItemPosition(layoutManager)
|
||||
val first = layoutManager.firstVisibleItemPosition
|
||||
val last = getLastVisibleItemPosition(layoutManager)
|
||||
val offset = abs(position - first)
|
||||
val max = last - first
|
||||
@@ -128,19 +128,14 @@ object LayoutManagerUtils {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
fun getFirstVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int {
|
||||
return when (layoutManager) {
|
||||
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
|
||||
is StaggeredGridLayoutManager -> {
|
||||
val positions = layoutManager.findFirstVisibleItemPositions(null)
|
||||
positions.min()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Can't do getFirstVisibleItemPosition for " + layoutManager.javaClass.name)
|
||||
val LayoutManager.firstVisibleItemPosition: Int
|
||||
get() = when (this) {
|
||||
is LinearLayoutManager -> findFirstVisibleItemPosition()
|
||||
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null).min()
|
||||
else -> error("Can't do getFirstVisibleItemPosition for " + javaClass.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLastVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int {
|
||||
fun getLastVisibleItemPosition(layoutManager: LayoutManager): Int {
|
||||
return when (layoutManager) {
|
||||
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
|
||||
is StaggeredGridLayoutManager -> {
|
||||
|
||||
@@ -48,11 +48,9 @@ import androidx.paging.LoadState
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
|
||||
import com.google.android.material.datepicker.CompositeDateValidator
|
||||
@@ -104,10 +102,9 @@ import java.util.Locale
|
||||
// Note that we do not really follow mvvm structure, just use it as ... storage
|
||||
class VMStorage : ViewModel() {
|
||||
var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat)
|
||||
var initialKey: String? = null
|
||||
private val cloudDataFlow = Pager(PagingConfig(25)) {
|
||||
object : PagingSource<String, GalleryInfo>() {
|
||||
override fun getRefreshKey(state: PagingState<String, GalleryInfo>): String? = initialKey
|
||||
override fun getRefreshKey(state: PagingState<String, GalleryInfo>): String? = null
|
||||
override suspend fun load(params: LoadParams<String>) = withIOContext {
|
||||
when (params) {
|
||||
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
|
||||
@@ -151,11 +148,9 @@ class VMStorage : ViewModel() {
|
||||
class FavoritesScene : SearchBarScene() {
|
||||
private val vm: VMStorage by viewModels()
|
||||
private var urlBuilder by lazyMut { vm::urlBuilder }
|
||||
private var initialKey by lazyMut { vm::initialKey }
|
||||
private var _binding: SceneFavoritesBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var mAdapter: PagingDataAdapter<GalleryInfo, GalleryHolder>? = null
|
||||
private var mAdapterDelegate: GalleryAdapter? = null
|
||||
private var mAdapter: GalleryAdapter? = null
|
||||
private val showNormalFabsRunnable = Runnable {
|
||||
updateJumpFab() // index: 0, 2
|
||||
binding.fabLayout.run {
|
||||
@@ -164,7 +159,7 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onItemClick(position: Int) {
|
||||
private fun onItemClick(position: Int) {
|
||||
if (isDrawerOpen(GravityCompat.END)) {
|
||||
// Skip if in search mode
|
||||
if (!binding.recyclerView.isInCustomChoice) {
|
||||
@@ -172,20 +167,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
updateJumpFab()
|
||||
closeDrawer(GravityCompat.END)
|
||||
}
|
||||
} else {
|
||||
if (binding.recyclerView.isInCustomChoice) {
|
||||
binding.recyclerView.toggleItemChecked(position)
|
||||
} else {
|
||||
mAdapter?.peek(position)?.let {
|
||||
navAnimated(
|
||||
R.id.galleryDetailScene,
|
||||
bundleOf(
|
||||
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
|
||||
GalleryDetailScene.KEY_GALLERY_INFO to it,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +178,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
urlBuilder.favCat = newCat
|
||||
urlBuilder.jumpTo = null
|
||||
urlBuilder.setIndex(null, true)
|
||||
initialKey = null
|
||||
collectJob?.cancel()
|
||||
collectJob = viewLifecycleOwner.lifecycleScope.launchIO {
|
||||
vm.dataflow().collectLatest {
|
||||
@@ -222,7 +202,7 @@ class FavoritesScene : SearchBarScene() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mAdapterDelegate?.type = Settings.listMode
|
||||
mAdapter?.type = Settings.listMode
|
||||
}
|
||||
|
||||
override fun onCreateViewWithToolbar(
|
||||
@@ -265,7 +245,7 @@ class FavoritesScene : SearchBarScene() {
|
||||
0 -> showGoToDialog()
|
||||
1 -> switchFav(urlBuilder.favCat)
|
||||
2 -> {
|
||||
initialKey = "1-0"
|
||||
urlBuilder.setIndex("1-0", false)
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
}
|
||||
@@ -331,22 +311,29 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
binding.recyclerView.run {
|
||||
val delegateAdapter = object : GalleryAdapter(resources, this@run, Settings.listMode, false, { onItemClick(it) }, { onItemLongClick(it) }) {
|
||||
override fun getItemCount() = TODO()
|
||||
override fun getDataAt(position: Int) = mAdapter?.peek(position)
|
||||
}
|
||||
val diffCallback = object : DiffUtil.ItemCallback<GalleryInfo>() {
|
||||
override fun areItemsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
|
||||
override fun areContentsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
|
||||
}
|
||||
mAdapter = object : PagingDataAdapter<GalleryInfo, GalleryHolder>(diffCallback) {
|
||||
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
|
||||
getItem(position)
|
||||
delegateAdapter.onBindViewHolder(holder, position)
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = delegateAdapter.onCreateViewHolder(parent, delegateAdapter.type)
|
||||
}.also { adapter ->
|
||||
binding.recyclerView.adapter = adapter
|
||||
mAdapter = GalleryAdapter(
|
||||
this@run,
|
||||
false,
|
||||
{ info, pos ->
|
||||
if (binding.recyclerView.isInCustomChoice) {
|
||||
binding.recyclerView.toggleItemChecked(pos)
|
||||
} else {
|
||||
navAnimated(
|
||||
R.id.galleryDetailScene,
|
||||
bundleOf(
|
||||
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
|
||||
GalleryDetailScene.KEY_GALLERY_INFO to info,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
{ _, pos ->
|
||||
if (!binding.recyclerView.isInCustomChoice) {
|
||||
binding.recyclerView.intoCustomChoiceMode()
|
||||
}
|
||||
binding.recyclerView.toggleItemChecked(pos)
|
||||
},
|
||||
).also { adapter ->
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!!
|
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
binding.tip.setCompoundDrawables(null, drawable, null, null)
|
||||
@@ -378,7 +365,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
switchFav(Settings.recentFavCat)
|
||||
mAdapterDelegate = delegateAdapter
|
||||
setChoiceMode(EasyRecyclerView.CHOICE_MODE_MULTIPLE_CUSTOM)
|
||||
setCustomCheckedListener(object : CustomChoiceListener {
|
||||
override fun onIntoCustomChoice(view: EasyRecyclerView) {
|
||||
@@ -431,7 +417,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
(binding.fabLayout.parent as ViewGroup).removeView(binding.fabLayout)
|
||||
removeAboveSnackView(binding.fabLayout)
|
||||
mAdapter = null
|
||||
mAdapterDelegate = null
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@@ -467,15 +452,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onItemLongClick(position: Int): Boolean {
|
||||
// Can not into
|
||||
if (!binding.recyclerView.isInCustomChoice) {
|
||||
binding.recyclerView.intoCustomChoiceMode()
|
||||
}
|
||||
binding.recyclerView.toggleItemChecked(position)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showGoToDialog() {
|
||||
context ?: return
|
||||
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
|
||||
|
||||
@@ -15,15 +15,17 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.Settings.detailSize
|
||||
import com.hippo.ehviewer.Settings.thumbSizeDp
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
@@ -34,14 +36,18 @@ import com.hippo.ehviewer.ui.legacy.STRATEGY_SUITABLE_SIZE
|
||||
import com.hippo.ehviewer.yorozuya.dp2px
|
||||
import splitties.init.appCtx
|
||||
|
||||
abstract class GalleryAdapter(
|
||||
private val resources: Resources,
|
||||
private val diffCallback = object : DiffUtil.ItemCallback<GalleryInfo>() {
|
||||
override fun areItemsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
|
||||
override fun areContentsTheSame(oldItem: GalleryInfo, newItem: GalleryInfo) = oldItem.gid == newItem.gid
|
||||
}
|
||||
|
||||
class GalleryAdapter(
|
||||
private val recyclerView: RecyclerView,
|
||||
type: Int,
|
||||
private val showFavorite: Boolean,
|
||||
private val onItemClick: (Int) -> Unit,
|
||||
private val onItemLongClick: (Int) -> Unit,
|
||||
) : RecyclerView.Adapter<GalleryHolder>() {
|
||||
private val onItemClick: (GalleryInfo, Int) -> Unit,
|
||||
private val onItemLongClick: (GalleryInfo, Int) -> Unit,
|
||||
) : PagingDataAdapter<GalleryInfo, GalleryHolder>(diffCallback) {
|
||||
private val resources = recyclerView.context.resources
|
||||
private val layoutManager: AutoStaggeredGridLayoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
|
||||
private val mPaddingTopSB: Int = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)
|
||||
private var mListDecoration: ItemDecoration? = null
|
||||
@@ -126,12 +132,13 @@ abstract class GalleryAdapter(
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
refresh()
|
||||
}
|
||||
|
||||
init {
|
||||
recyclerView.adapter = this
|
||||
recyclerView.layoutManager = layoutManager
|
||||
this.type = type
|
||||
type = Settings.listMode
|
||||
adjustPaddings()
|
||||
}
|
||||
|
||||
@@ -150,18 +157,14 @@ abstract class GalleryAdapter(
|
||||
else -> error("Unexpected value: $viewType")
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return mType
|
||||
}
|
||||
|
||||
abstract fun getDataAt(position: Int): GalleryInfo?
|
||||
override fun getItemViewType(position: Int) = mType
|
||||
|
||||
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
|
||||
val gi = getDataAt(position) ?: return
|
||||
val gi = getItem(position) ?: return
|
||||
holder.bind(
|
||||
gi,
|
||||
{ onItemClick(position) },
|
||||
{ onItemLongClick(position) },
|
||||
{ onItemClick(gi, position) },
|
||||
{ onItemLongClick(gi, position) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 com.hippo.ehviewer.yorozuya
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class IntIdGenerator {
|
||||
private val mId = AtomicInteger()
|
||||
|
||||
constructor()
|
||||
constructor(init: Int) {
|
||||
setNextId(init)
|
||||
}
|
||||
|
||||
fun nextId(): Int {
|
||||
var id: Int
|
||||
while (mId.getAndIncrement().also { id = it } == INVALID_ID);
|
||||
return id
|
||||
}
|
||||
|
||||
fun setNextId(id: Int) {
|
||||
checkInValidId(id)
|
||||
mId.set(id)
|
||||
}
|
||||
|
||||
private fun checkInValidId(id: Int) {
|
||||
check(INVALID_ID != id) { "Can't set INVALID_ID" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INVALID_ID = -1
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 com.hippo.ehviewer.yorozuya
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
fun View.removeFromParent() = (parent as? ViewGroup)?.removeView(this)
|
||||
@@ -22,11 +22,54 @@
|
||||
android:paddingTop="16dp"
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
|
||||
|
||||
<com.hippo.ehviewer.ui.legacy.ContentLayout
|
||||
<FrameLayout
|
||||
android:id="@+id/content_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:fitsSystemWindowsInsets="top|bottom" />
|
||||
android:clipChildren="false">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tip"
|
||||
style="@style/TextAppearance.AppCompat.Medium"
|
||||
android:layout_width="228dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_horizontal" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
style="@style/ProgressView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout
|
||||
android:id="@+id/refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
app:fitsSystemWindowsInsets="top">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
|
||||
|
||||
<com.hippo.ehviewer.ui.legacy.FastScroller
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingBottom="8dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<com.hippo.ehviewer.ui.legacy.SearchLayout
|
||||
android:id="@+id/search_layout"
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2015 Hippo Seven
|
||||
~
|
||||
~ 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
|
||||
~
|
||||
~ http://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.
|
||||
-->
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
style="@style/ProgressView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tip"
|
||||
style="@style/TextAppearance.AppCompat.Medium"
|
||||
android:layout_width="228dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_horizontal" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout
|
||||
android:id="@+id/refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false">
|
||||
|
||||
<com.hippo.ehviewer.ui.legacy.EasyRecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.ThemedSwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/bottom_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:fitsSystemWindowsInsets="end"
|
||||
app:hideAnimationBehavior="inward"
|
||||
app:showAnimationBehavior="outward" />
|
||||
|
||||
<com.hippo.ehviewer.ui.legacy.FastScroller
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</merge>
|
||||
Reference in new issue
Block a user