From df8c70d8e638d608a47c31eb2646fffa35d54147 Mon Sep 17 00:00:00 2001 From: tarsin <43900799+asuka-mio@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:40:03 +0800 Subject: [PATCH] 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 --- .../hippo/ehviewer/FavouriteStatusRouter.kt | 21 - .../ehviewer/client/data/ListUrlBuilder.kt | 4 +- .../ehviewer/ui/legacy/AddDeleteDrawable.kt | 5 +- .../ehviewer/ui/legacy/BringOutTransition.kt | 3 +- .../hippo/ehviewer/ui/legacy/ContentLayout.kt | 820 ------------------ .../ui/legacy/GalleryInfoContentHelper.kt | 91 -- .../ehviewer/ui/legacy/HandlerDrawable.kt | 4 +- .../ehviewer/ui/legacy/LayoutManagerUtils.kt | 27 +- .../hippo/ehviewer/ui/scene/FavoritesScene.kt | 80 +- .../hippo/ehviewer/ui/scene/GalleryAdapter.kt | 37 +- .../ehviewer/ui/scene/GalleryListScene.kt | 695 +++++++-------- .../hippo/ehviewer/yorozuya/IntIdGenerator.kt | 46 - .../com/hippo/ehviewer/yorozuya/ViewUtils.kt | 21 - .../main/res/layout/scene_gallery_list.xml | 47 +- .../main/res/layout/widget_content_layout.xml | 79 -- 15 files changed, 405 insertions(+), 1575 deletions(-) delete mode 100644 app/src/main/java/com/hippo/ehviewer/ui/legacy/ContentLayout.kt delete mode 100644 app/src/main/java/com/hippo/ehviewer/ui/legacy/GalleryInfoContentHelper.kt delete mode 100644 app/src/main/java/com/hippo/ehviewer/yorozuya/IntIdGenerator.kt delete mode 100644 app/src/main/java/com/hippo/ehviewer/yorozuya/ViewUtils.kt delete mode 100644 app/src/main/res/layout/widget_content_layout.xml diff --git a/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.kt b/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.kt index a05382a5c..b634edc67 100644 --- a/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.kt +++ b/app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.kt @@ -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>() - fun saveDataMap(map: MutableMap): Int { - val id = idGenerator.nextId() - maps[id] = map - Settings.dataMapNextId = idGenerator.nextId() - return id - } - - fun restoreDataMap(id: Int): MutableMap? { - 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) } diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.kt b/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.kt index 1b736c50e..6b83b68cd 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.kt @@ -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, diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/AddDeleteDrawable.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/AddDeleteDrawable.kt index 7ab0503db..4f21c99de 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/AddDeleteDrawable.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/legacy/AddDeleteDrawable.kt @@ -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() } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/BringOutTransition.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/BringOutTransition.kt index 6536087fc..2551c70c7 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/BringOutTransition.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/legacy/BringOutTransition.kt @@ -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 { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/ContentLayout.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/ContentLayout.kt deleted file mode 100644 index e7b5d7d5f..000000000 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/ContentLayout.kt +++ /dev/null @@ -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() - - /** - * 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 - 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, 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) {} - protected open fun onRemoveData(data: List) {} - protected open fun onClearData() {} - fun onGetPageData( - taskId: Int, - pages: Int, - nextPage: Int, - prev: String?, - next: String?, - data: List, - ) { - 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" 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>() diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/GalleryInfoContentHelper.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/GalleryInfoContentHelper.kt deleted file mode 100644 index bb608f6ff..000000000 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/GalleryInfoContentHelper.kt +++ /dev/null @@ -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 = HashMap() - - fun destroy() = scope.cancel() - - override fun onAddData(data: List) { - data.forEach { map[it.gid] = it } - } - - override fun onRemoveData(data: List) { - 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" - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt index 8f0f39c05..8459348d2 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/legacy/HandlerDrawable.kt @@ -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 } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/LayoutManagerUtils.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/LayoutManagerUtils.kt index 133d84ba1..c1537d82d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/LayoutManagerUtils.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/legacy/LayoutManagerUtils.kt @@ -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 -> { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt index 6770b28c7..9da1fca09 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt @@ -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() { - override fun getRefreshKey(state: PagingState): String? = initialKey + override fun getRefreshKey(state: PagingState): String? = null override suspend fun load(params: LoadParams) = 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? = 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() { - 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(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) diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt index 0cd857893..a0b8e44fa 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt @@ -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() { + 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() { + private val onItemClick: (GalleryInfo, Int) -> Unit, + private val onItemLongClick: (GalleryInfo, Int) -> Unit, +) : PagingDataAdapter(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) }, ) } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt index 569ac7144..e2985d5c8 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt @@ -16,11 +16,10 @@ package com.hippo.ehviewer.ui.scene import android.animation.Animator -import android.annotation.SuppressLint import android.content.Context import android.content.DialogInterface -import android.content.res.Resources import android.os.Bundle +import android.os.Parcelable import android.text.InputType import android.text.Spannable import android.text.SpannableStringBuilder @@ -33,7 +32,6 @@ import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.widget.TextView import androidx.activity.OnBackPressedCallback -import androidx.annotation.IntDef import androidx.compose.ui.platform.ComposeView import androidx.core.content.ContextCompat import androidx.core.os.bundleOf @@ -41,11 +39,19 @@ import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsAnimationCompat import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import androidx.paging.LoadState +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.cachedIn import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import coil.imageLoader import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints.DateValidator import com.google.android.material.datepicker.CompositeDateValidator @@ -68,10 +74,7 @@ import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT import com.hippo.ehviewer.client.exception.EhException import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser -import com.hippo.ehviewer.client.parser.GalleryListParser import com.hippo.ehviewer.client.parser.GalleryPageUrlParser -import com.hippo.ehviewer.coil.imageRequest -import com.hippo.ehviewer.coil.justDownload import com.hippo.ehviewer.dao.QuickSearch import com.hippo.ehviewer.databinding.DrawerListRvBinding import com.hippo.ehviewer.databinding.ItemDrawerListBinding @@ -84,65 +87,128 @@ import com.hippo.ehviewer.ui.legacy.EasyRecyclerView import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder import com.hippo.ehviewer.ui.legacy.FabLayout import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener -import com.hippo.ehviewer.ui.legacy.FabLayout.OnExpandListener import com.hippo.ehviewer.ui.legacy.FastScroller.OnDragHandlerListener -import com.hippo.ehviewer.ui.legacy.GalleryInfoContentHelper +import com.hippo.ehviewer.ui.legacy.HandlerDrawable +import com.hippo.ehviewer.ui.legacy.LayoutManagerUtils.firstVisibleItemPosition import com.hippo.ehviewer.ui.legacy.ViewTransition import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper import com.hippo.ehviewer.ui.setMD3Content import com.hippo.ehviewer.ui.settings.showNewVersion import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.updater.AppUpdater +import com.hippo.ehviewer.util.ExceptionUtils import com.hippo.ehviewer.util.getParcelableCompat +import com.hippo.ehviewer.util.getValue +import com.hippo.ehviewer.util.lazyMut +import com.hippo.ehviewer.util.setValue import com.hippo.ehviewer.yorozuya.AnimationUtils import com.hippo.ehviewer.yorozuya.SimpleAnimatorListener import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withUIContext import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize import moe.tarsin.coroutines.runSuspendCatching import rikka.core.res.resolveColor import java.io.File +import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.Locale -class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListener, OnExpandListener { - private val mCallback = object : OnBackPressedCallback(false) { +class VMStorage1 : ViewModel() { + var urlBuilder = ListUrlBuilder() + val dataFlow = Pager(PagingConfig(25)) { + object : PagingSource() { + override fun getRefreshKey(state: PagingState): String? = null + override suspend fun load(params: LoadParams) = withIOContext { + if (urlBuilder.mode == MODE_TOPLIST) { + // TODO: Since we know total pages, let pager support jump + val key = (params.key ?: urlBuilder.mJumpTo ?: "0").toInt() + val prev = if (key != 0) key - 1 else null + val next = if (key != 199) key + 1 else null + runSuspendCatching { + urlBuilder.setJumpTo(key.toString()) + EhEngine.getGalleryList(urlBuilder.build()) + }.onFailure { + return@withIOContext LoadResult.Error(it) + }.onSuccess { + return@withIOContext LoadResult.Page(it.galleryInfoList, prev?.toString(), next?.toString()) + } + } + when (params) { + is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false) + is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true) + is LoadParams.Refresh -> { + val key = params.key + if (key.isNullOrBlank()) { + if (urlBuilder.mJumpTo != null) { + urlBuilder.mNext ?: urlBuilder.setIndex("2", true) + } + } else { + urlBuilder.setIndex(key, false) + } + } + } + val r = runSuspendCatching { + if (ListUrlBuilder.MODE_IMAGE_SEARCH == urlBuilder.mode) { + EhEngine.imageSearch( + File(urlBuilder.imagePath!!), + urlBuilder.isUseSimilarityScan, + urlBuilder.isOnlySearchCovers, + ) + } else { + val url = urlBuilder.build() + EhEngine.getGalleryList(url) + } + }.onFailure { + return@withIOContext LoadResult.Error(it) + }.getOrThrow() + urlBuilder.mJumpTo = null + LoadResult.Page(r.galleryInfoList, r.prev, r.next) + } + } + }.flow.cachedIn(viewModelScope) +} + +class GalleryListScene : SearchBarScene() { + private val vm: VMStorage1 by viewModels() + private var mUrlBuilder by lazyMut { vm::urlBuilder } + private var mIsTopList = false + + private val stateBackPressedCallback = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() { when (mState) { - STATE_NORMAL -> throw IllegalStateException("SearchStateOnBackPressedCallback should not be enabled on STATE_NORMAL") - STATE_SIMPLE_SEARCH, STATE_SEARCH -> setState(STATE_NORMAL) - STATE_SEARCH_SHOW_LIST -> setState(STATE_SEARCH) + State.NORMAL -> error("SearchStateOnBackPressedCallback should not be enabled on $mState") + State.SIMPLE_SEARCH, State.SEARCH -> setState(State.NORMAL) + State.SEARCH_SHOW_LIST -> setState(State.SEARCH) } } } - private lateinit var mUrlBuilder: ListUrlBuilder private var _binding: SceneGalleryListBinding? = null private val binding get() = _binding!! - private val mSearchFabAnimatorListener: Animator.AnimatorListener = - object : SimpleAnimatorListener() { - override fun onAnimationEnd(animation: Animator) { - mSearchFab.visibility = View.INVISIBLE - } + private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() { + override fun onAnimationEnd(animation: Animator) { + mSearchFab.visibility = View.INVISIBLE } - private val mActionFabAnimatorListener: Animator.AnimatorListener = - object : SimpleAnimatorListener() { - override fun onAnimationEnd(animation: Animator) { - binding.fabLayout.primaryFab?.visibility = View.INVISIBLE - } + } + private val mActionFabAnimatorListener = object : SimpleAnimatorListener() { + override fun onAnimationEnd(animation: Animator) { + binding.fabLayout.primaryFab?.visibility = View.INVISIBLE } + } private var fabAnimator: ViewPropertyAnimator? = null private var mViewTransition: ViewTransition? = null - private var mAdapter: GalleryListAdapter? = null - private var mHelper: GalleryListHelper? = null - private var mActionFabDrawable: AddDeleteDrawable? = null + private var mAdapter: GalleryAdapter? = null lateinit var mQuickSearchList: MutableList private var mHideActionFabSlop = 0 private var mShowActionFab = true - - @State - private var mState = STATE_NORMAL + private var mState = State.NORMAL private val mOnScrollListener: RecyclerView.OnScrollListener = object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {} @@ -154,9 +220,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } } } - private var mHasFirstRefresh = false - - private var mIsTopList = false override fun getMenuResId(): Int { return R.menu.scene_gallery_list_searchbar_menu } @@ -168,19 +231,10 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION) ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT) ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = "11") - ACTION_LIST_URL_BUILDER -> - args?.getParcelableCompat(KEY_LIST_URL_BUILDER)?.copy() - ?: ListUrlBuilder() - + ACTION_LIST_URL_BUILDER -> args?.getParcelableCompat(KEY_LIST_URL_BUILDER)?.copy() ?: ListUrlBuilder() else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!") } - val goto = args?.getString(KEY_GOTO) - goto?.let { - lifecycleScope.launchUI { - delay(100) - mHelper?.goTo(it, true) - } - } + args?.getString(KEY_GOTO)?.let { mUrlBuilder.mNext = it } } override fun onCreate(savedInstanceState: Bundle?) { @@ -202,21 +256,14 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } private fun onRestore(savedInstanceState: Bundle) { - mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH) mUrlBuilder = savedInstanceState.getParcelableCompat(KEY_LIST_URL_BUILDER)!! - mState = savedInstanceState.getInt(KEY_STATE) + mState = savedInstanceState.getParcelableCompat(KEY_STATE)!! } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - val hasFirstRefresh: Boolean = if (mHelper != null && 1 == mHelper!!.shownViewIndex) { - false - } else { - mHasFirstRefresh - } - outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh) outState.putParcelable(KEY_LIST_URL_BUILDER, mUrlBuilder) - outState.putInt(KEY_STATE, mState) + outState.putParcelable(KEY_STATE, mState) } private fun setSearchBarHint() { @@ -282,7 +329,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } // Update title - var title = getSuitableTitleForUrlBuilder(requireContext(), mUrlBuilder, true) + var title = requireContext().getSuitableTitleForUrlBuilder(mUrlBuilder, true) if (null == title) { title = resources.getString(R.string.search) } @@ -310,7 +357,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList _binding = SceneGalleryListBinding.inflate(inflater, container, false) binding.root.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Handler() } }) checkForUpdates() - requireActivity().onBackPressedDispatcher.addCallback(mCallback) + requireActivity().onBackPressedDispatcher.addCallback(stateBackPressedCallback) mHideActionFabSlop = ViewConfiguration.get(requireContext()).scaledTouchSlop mShowActionFab = true ViewCompat.setWindowInsetsAnimationCallback( @@ -329,20 +376,77 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList binding.contentLayout, binding.searchLayout, ) - mHelper = GalleryListHelper() - binding.contentLayout.setHelper(mHelper!!) - binding.contentLayout.fastScroller.setOnDragHandlerListener(this) - binding.contentLayout.setFitPaddingTop(paddingTopSB) - mAdapter = GalleryListAdapter( - resources, - binding.contentLayout.recyclerView, - Settings.listMode, - ) - binding.contentLayout.recyclerView.adapter = mAdapter - binding.contentLayout.recyclerView.clipToPadding = false - binding.contentLayout.recyclerView.clipChildren = false - binding.contentLayout.recyclerView.addOnScrollListener(mOnScrollListener) - binding.contentLayout.fastScroller.run { + binding.fastScroller.setOnDragHandlerListener(object : OnDragHandlerListener { + override fun onStartDragHandler() { + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) + } + override fun onEndDragHandler() { + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) + showSearchBar() + } + }) + mAdapter = GalleryAdapter( + binding.recyclerView, + true, + { info, _ -> + navAnimated( + R.id.galleryDetailScene, + bundleOf( + GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO, + GalleryDetailScene.KEY_GALLERY_INFO to info, + ), + ) + }, + { info, _ -> + lifecycleScope.launchIO { + dialogState.doGalleryInfoAction(info, requireContext()) + } + }, + ).also { adapter -> + viewLifecycleOwner.lifecycleScope.launch { + val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_sad_pandroid)!! + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + binding.tip.setCompoundDrawables(null, drawable, null, null) + binding.tip.setOnClickListener { mAdapter?.refresh() } + val transition = ViewTransition(binding.recyclerView, binding.progress, binding.tip) + val empty = getString(R.string.gallery_list_empty_hit) + val noWatch = getString(R.string.gallery_list_empty_hit_subscription) + adapter.addLoadStateListener { + lifecycleScope.launchUI { + when (val state = it.refresh) { + is LoadState.Loading -> { + showSearchBar() + transition.showView(1) + } + is LoadState.Error -> { + binding.tip.text = ExceptionUtils.getReadableString(state.error) + transition.showView(2) + } + is LoadState.NotLoading -> { + delay(500) + if (mAdapter?.itemCount == 0) { + if (mUrlBuilder.mode == MODE_SUBSCRIPTION) { + binding.tip.text = noWatch + } else { + binding.tip.text = empty + } + transition.showView(2) + } else { + transition.showView(0) + } + } + } + } + } + vm.dataFlow.collectLatest { + adapter.submitData(it) + } + } + } + binding.recyclerView.addOnScrollListener(mOnScrollListener) + binding.fastScroller.attachToRecyclerView(binding.recyclerView) + binding.fastScroller.setHandlerDrawable(HandlerDrawable().apply { setColor(inflater.context.theme.resolveColor(androidx.appcompat.R.attr.colorPrimary)) }) + binding.fastScroller.apply { setPadding(paddingLeft, paddingTopSB + paddingTop, paddingRight, paddingBottom) } setOnApplySearch { query: String? -> @@ -362,12 +466,46 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList binding.fabLayout.setAutoCancel(true) binding.fabLayout.isExpanded = false binding.fabLayout.setHidePrimaryFab(false) - binding.fabLayout.setOnClickFabListener(this) - binding.fabLayout.addOnExpandListener(this) - addAboveSnackView(binding.fabLayout) + binding.fabLayout.setOnClickFabListener(object : OnClickFabListener { + override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) { + if (State.NORMAL == mState) { + view.toggle() + } + } + override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) { + when (position) { + 0 -> showGoToDialog() + 1 -> { + mUrlBuilder.setIndex(null, true) + mAdapter?.refresh() + } + 2 -> { + if (mIsTopList) { + mAdapter?.refresh() + } else { + mUrlBuilder.setIndex("1", false) + mAdapter?.refresh() + } + } + } + view.isExpanded = false + } + }) val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface) - mActionFabDrawable = AddDeleteDrawable(requireContext(), colorID) - binding.fabLayout.primaryFab!!.setImageDrawable(mActionFabDrawable) + val actionFabDrawable = AddDeleteDrawable(requireContext(), colorID) + binding.fabLayout.addOnExpandListener { + if (it) { + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) + actionFabDrawable.setDelete(ANIMATE_TIME) + } else { + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) + actionFabDrawable.setAdd(ANIMATE_TIME) + } + } + binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable) + addAboveSnackView(binding.fabLayout) mSearchFab.setOnClickListener { onApplySearch() } // Update list url builder @@ -375,33 +513,20 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList // Restore state val newState = mState - mState = STATE_NORMAL + mState = State.NORMAL setState(newState, false) - - // Only refresh for the first time - if (!mHasFirstRefresh) { - mHasFirstRefresh = true - mHelper!!.firstRefresh() - } return binding.root } override fun onDestroyView() { super.onDestroyView() - mCallback.remove() - if (null != mHelper) { - mHelper!!.destroy() - if (1 == mHelper!!.shownViewIndex) { - mHasFirstRefresh = false - } - } - binding.contentLayout.recyclerView.stopScroll() + stateBackPressedCallback.remove() + binding.recyclerView.stopScroll() (binding.fabLayout.parent as ViewGroup).removeView(binding.fabLayout) removeAboveSnackView(binding.fabLayout) _binding = null mAdapter = null mViewTransition = null - mActionFabDrawable = null } private fun showQuickSearchTipDialog() { @@ -417,17 +542,14 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList recyclerView: EasyRecyclerView, tip: TextView, ) { - val context = context - if (null == context || null == mHelper) { - return - } + val context = context ?: return // Can't add image search as quick search if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.mode) { showTip(R.string.image_search_not_quick_search, LENGTH_LONG) return } - val gi = mHelper!!.firstVisibleItem + val gi = mAdapter?.peek(binding.recyclerView.layoutManager!!.firstVisibleItemPosition) val next = if (gi != null) "@" + (gi.gid + 1) else null // Check duplicate @@ -442,7 +564,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } val builder = EditTextDialogBuilder( context, - getSuitableTitleForUrlBuilder(context, mUrlBuilder, false), + context.getSuitableTitleForUrlBuilder(mUrlBuilder, false), getString(R.string.quick_search), ) builder.setTitle(R.string.add_quick_search_dialog_title) @@ -450,10 +572,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList // TODO: It's ugly val checked = booleanArrayOf(Settings.qSSaveProgress) val hint = arrayOf(getString(R.string.save_progress)) - builder.setMultiChoiceItems( - hint, - checked, - ) { _: DialogInterface?, which: Int, isChecked: Boolean -> checked[which] = isChecked } + builder.setMultiChoiceItems(hint, checked) { _, which, isChecked -> checked[which] = isChecked } val dialog = builder.show() dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { lifecycleScope.launchIO { @@ -551,42 +670,17 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList return drawerBinding.root } - fun onItemClick(position: Int) { - _binding ?: return - mHelper ?: return - val gi = mHelper!!.getDataAtEx(position) ?: return - val args = Bundle() - args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO) - args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi) - navAnimated(R.id.galleryDetailScene, args) - } - - override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) { - if (STATE_NORMAL == mState) { - view.toggle() - } - } - private fun showGoToDialog() { - val context = context - if (null == context || null == mHelper) { - return - } + val context = context ?: return + mAdapter ?: return if (mIsTopList) { - val page = mHelper!!.pageForTop + 1 - val pages = mHelper!!.pages + val page = 1 + val pages = 200 val hint = getString(R.string.go_to_hint, page, pages) val builder = EditTextDialogBuilder(context, null, hint) - builder.editText.inputType = - InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL - val dialog = builder.setTitle(R.string.go_to) - .setPositiveButton(android.R.string.ok, null) - .show() + builder.editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + val dialog = builder.setTitle(R.string.go_to).setPositiveButton(android.R.string.ok, null).show() dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { - if (null == mHelper) { - dialog.dismiss() - return@setOnClickListener - } val text = builder.text.trim { it <= ' ' } val goTo: Int = try { text.toInt() - 1 @@ -599,7 +693,8 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList return@setOnClickListener } builder.setError(null) - mHelper!!.goTo(goTo) + mUrlBuilder.setJumpTo(goTo.toString()) + mAdapter?.refresh() dialog.dismiss() } } else { @@ -620,73 +715,28 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList .setSelection(toDate) .build() datePicker.show(requireActivity().supportFragmentManager, "date-picker") - datePicker.addOnPositiveButtonClickListener { v: Long? -> - mHelper!!.goTo( - v!!, - true, - ) + datePicker.addOnPositiveButtonClickListener { time -> + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC) + val jumpTo = formatter.format(Instant.ofEpochMilli(time)) + mUrlBuilder.mJumpTo = jumpTo + mAdapter?.refresh() } } } - override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) { - if (null == mHelper) { - return - } - when (position) { - 0 -> { - if (mHelper!!.canGoTo()) showGoToDialog() - } - - 1 -> mHelper!!.refresh() - 2 -> { - if (mIsTopList) { - mHelper!!.goTo(mHelper!!.pages - 1) - } else { - mHelper!!.goTo("1", false) - } - } - } - view.isExpanded = false - } - - override fun onExpand(expanded: Boolean) { - if (null == mActionFabDrawable) { - return - } - if (expanded) { - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) - mActionFabDrawable!!.setDelete(ANIMATE_TIME) - } else { - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - mActionFabDrawable!!.setAdd(ANIMATE_TIME) - } - } - override fun onSearchViewExpanded() { super.onSearchViewExpanded() - if (mState == STATE_NORMAL) selectSearchFab(true) + if (mState == State.NORMAL) selectSearchFab(true) } override fun onSearchViewHidden() { super.onSearchViewHidden() - if (mState == STATE_NORMAL) selectActionFab(true) - } - - fun onItemLongClick(position: Int): Boolean { - val context = context ?: return true - val info = mHelper?.getDataAtEx(position) ?: return true - lifecycleScope.launchIO { - dialogState.doGalleryInfoAction(info, context) - } - return true + if (mState == State.NORMAL) selectActionFab(true) } private fun showActionFab() { _binding ?: return - if (STATE_NORMAL == mState && !mShowActionFab) { + if (State.NORMAL == mState && !mShowActionFab) { mShowActionFab = true val fab: View? = binding.fabLayout.primaryFab if (fabAnimator != null) { @@ -703,7 +753,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList private fun hideActionFab() { _binding ?: return - if (STATE_NORMAL == mState && mShowActionFab) { + if (State.NORMAL == mState && mShowActionFab) { mShowActionFab = false val fab: View? = binding.fabLayout.primaryFab if (fabAnimator != null) { @@ -781,11 +831,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } } - private fun setState(@State state: Int) { + private fun setState(state: State) { setState(state, true) } - private fun setState(@State state: Int, animation: Boolean) { + private fun setState(state: State, animation: Boolean) { _binding ?: return if (null == mViewTransition) { return @@ -796,36 +846,30 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList showSearchBar() onStateChange(state) when (oldState) { - STATE_NORMAL -> when (state) { - STATE_SIMPLE_SEARCH -> { + State.NORMAL -> when (state) { + State.SIMPLE_SEARCH -> { selectSearchFab(animation) } - - STATE_SEARCH -> { + State.SEARCH -> { mViewTransition!!.showView(1, animation) selectSearchFab(animation) } - - STATE_SEARCH_SHOW_LIST -> { + State.SEARCH_SHOW_LIST -> { mViewTransition!!.showView(1, animation) selectSearchFab(animation) } + else -> error("Unreachable!!!") } - - STATE_SIMPLE_SEARCH -> when (state) { - STATE_NORMAL -> { - selectActionFab(animation) - } - - STATE_SEARCH -> mViewTransition!!.showView(1, animation) - - STATE_SEARCH_SHOW_LIST -> mViewTransition!!.showView(1, animation) + State.SIMPLE_SEARCH -> when (state) { + State.NORMAL -> selectActionFab(animation) + State.SEARCH -> mViewTransition!!.showView(1, animation) + State.SEARCH_SHOW_LIST -> mViewTransition!!.showView(1, animation) + else -> error("Unreachable!!!") } - - STATE_SEARCH, STATE_SEARCH_SHOW_LIST -> if (state == STATE_NORMAL) { + State.SEARCH, State.SEARCH_SHOW_LIST -> if (state == State.NORMAL) { mViewTransition!!.showView(0, animation) selectActionFab(animation) - } else if (state == STATE_SIMPLE_SEARCH) { + } else if (state == State.SIMPLE_SEARCH) { mViewTransition!!.showView(0, animation) } } @@ -833,19 +877,18 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } override fun onMenuItemClick(item: MenuItem): Boolean { - if (mState == STATE_NORMAL) { - setState(STATE_SEARCH) + if (mState == State.NORMAL) { + setState(State.SEARCH) } else { - setState(STATE_NORMAL) + setState(State.NORMAL) } return true } private fun onApplySearch(query: String?) { _binding ?: return - mHelper ?: return lifecycleScope.launchIO { - if (mState == STATE_SEARCH || mState == STATE_SEARCH_SHOW_LIST) { + if (mState == State.SEARCH || mState == State.SEARCH_SHOW_LIST) { try { binding.searchLayout.formatListUrlBuilder(mUrlBuilder, query) } catch (e: EhException) { @@ -862,26 +905,15 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } withUIContext { onUpdateUrlBuilder() - mHelper!!.refresh() - setState(STATE_NORMAL) + mAdapter?.refresh() + setState(State.NORMAL) } } } - override fun onStartDragHandler() { - // Lock right drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) - } - - override fun onEndDragHandler() { - // Restore right drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - showSearchBar() - } - - private fun onStateChange(newState: Int) { - mCallback.isEnabled = newState != STATE_NORMAL - if (newState == STATE_NORMAL || newState == STATE_SIMPLE_SEARCH) { + private fun onStateChange(newState: State) { + stateBackPressedCallback.isEnabled = newState != State.NORMAL + if (newState == State.NORMAL || newState == State.SIMPLE_SEARCH) { setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) } else { @@ -890,42 +922,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } } - private fun onGetGalleryListSuccess(result: GalleryListParser.Result, taskId: Int) { - if (Settings.preloadThumbAggressively) { - lifecycleScope.launchIO { - result.galleryInfoList.forEach { context?.run { imageLoader.enqueue(imageRequest(it) { justDownload() }) } } - } - } - if (mHelper != null && mHelper!!.isCurrentTask(taskId)) { - val emptyString = - getString(if (mUrlBuilder.mode == MODE_SUBSCRIPTION && result.noWatchedTags) R.string.gallery_list_empty_hit_subscription else R.string.gallery_list_empty_hit) - mHelper!!.setEmptyString(emptyString) - if (mIsTopList) { - mHelper!!.onGetPageData( - taskId, - result.pages, - result.nextPage, - null, - null, - result.galleryInfoList, - ) - } else { - mHelper!!.onGetPageData( - taskId, - 0, - 0, - result.prev, - result.next, - result.galleryInfoList, - ) - } - } - } - - @IntDef(STATE_NORMAL, STATE_SIMPLE_SEARCH, STATE_SEARCH, STATE_SEARCH_SHOW_LIST) - @Retention(AnnotationRetention.SOURCE) - private annotation class State - private class QsDrawerHolder(val binding: ItemDrawerListBinding) : RecyclerView.ViewHolder(binding.root) private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) : @@ -934,29 +930,21 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false)) if (!mIsTopList) { holder.itemView.setOnClickListener { - if (null == mHelper) { - return@setOnClickListener - } val q = mQuickSearchList[holder.bindingAdapterPosition] val i = q.name!!.lastIndexOf("@") val goto = if (i != -1) q.name!!.substring(i + 1) else null - val args = ListUrlBuilder().apply { - set(q) - }.toStartArgs().apply { putString(KEY_GOTO, goto) } + val args = ListUrlBuilder().apply { set(q) }.toStartArgs().apply { putString(KEY_GOTO, goto) } navAnimated(R.id.galleryListScene, args, true) - setState(STATE_NORMAL) + setState(State.NORMAL) closeDrawer(GravityCompat.END) } } else { val keywords = intArrayOf(11, 12, 13, 15) holder.itemView.setOnClickListener { - if (null == mHelper) { - return@setOnClickListener - } mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString() onUpdateUrlBuilder() - mHelper!!.refresh() - setState(STATE_NORMAL) + mAdapter?.refresh() + setState(State.NORMAL) closeDrawer(GravityCompat.END) } } @@ -1012,10 +1000,10 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList override fun onClick() { navAnimated(getDestination(), getArgs()) - if (mState == STATE_SIMPLE_SEARCH) { - setState(STATE_NORMAL) - } else if (mState == STATE_SEARCH_SHOW_LIST) { - setState(STATE_SEARCH) + if (mState == State.SIMPLE_SEARCH) { + setState(State.NORMAL) + } else if (mState == State.SEARCH_SHOW_LIST) { + setState(State.SEARCH) } } @@ -1032,13 +1020,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList return R.id.galleryDetailScene } - override fun getArgs(): Bundle { - val args = Bundle() - args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN) - args.putLong(GalleryDetailScene.KEY_GID, mGid) - args.putString(GalleryDetailScene.KEY_TOKEN, mToken) - return args - } + override fun getArgs() = bundleOf( + GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GID_TOKEN, + GalleryDetailScene.KEY_GID to mGid, + GalleryDetailScene.KEY_TOKEN to mToken, + ) } private inner class GalleryPageUrlSuggestion( @@ -1058,113 +1044,15 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList ) } - private inner class GalleryListAdapter( - resources: Resources, - recyclerView: RecyclerView, - type: Int, - ) : GalleryAdapter( - resources, - recyclerView, - type, - true, - { onItemClick(it) }, - { onItemLongClick(it) }, - ) { - override fun getItemCount(): Int { - return if (null != mHelper) mHelper!!.size() else 0 - } - - override fun getDataAt(position: Int): GalleryInfo? { - return if (null != mHelper) mHelper!!.getDataAtEx(position) else null - } - } - - private inner class GalleryListHelper : GalleryInfoContentHelper() { - override fun getPageData( - taskId: Int, - type: Int, - page: Int, - index: String?, - isNext: Boolean, - ) { - val activity = mainActivity - if (null == activity || null == mHelper) { - return - } - if (mIsTopList) { - mUrlBuilder.setJumpTo(page.toString()) - } else { - mUrlBuilder.setIndex(index, isNext) - mUrlBuilder.setJumpTo(jumpTo) - } - lifecycleScope.launchIO { - runSuspendCatching { - if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.mode) { - EhEngine.imageSearch( - File(mUrlBuilder.imagePath!!), - mUrlBuilder.isUseSimilarityScan, - mUrlBuilder.isOnlySearchCovers, - ) - } else { - val url = mUrlBuilder.build() - EhEngine.getGalleryList(url) - } - }.onSuccess { - withUIContext { - onGetGalleryListSuccess(it, taskId) - } - }.onFailure { throwable -> - withUIContext { - mHelper?.takeIf { it.isCurrentTask(taskId) }?.onGetException(taskId, throwable) - } - } - } - } - - override val context - get() = requireContext() - - @SuppressLint("NotifyDataSetChanged") - override fun notifyDataSetChanged() { - if (null != mAdapter) { - mAdapter!!.notifyDataSetChanged() - } - } - - override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) { - if (null != mAdapter) { - mAdapter!!.notifyItemRangeInserted(positionStart, itemCount) - } - } - - override fun onShowView(hiddenView: View, shownView: View) { - showSearchBar() - showActionFab() - } - - override fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean { - return d1.gid == d2.gid - } - - override fun onScrollToPosition(position: Int) { - if (0 == position) { - showSearchBar() - showActionFab() - } - } - } - private inner class GalleryListQSItemTouchHelperCallback(private val mAdapter: QsDrawerAdapter) : ItemTouchHelper.Callback() { override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, - ): Int { - return makeMovementFlags( - ItemTouchHelper.UP or ItemTouchHelper.DOWN, - ItemTouchHelper.LEFT, - ) - } + ) = makeMovementFlags( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.LEFT, + ) override fun onMove( recyclerView: RecyclerView, @@ -1217,12 +1105,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList const val ACTION_TOP_LIST = "action_top_list" const val ACTION_LIST_URL_BUILDER = "action_list_url_builder" const val KEY_LIST_URL_BUILDER = "list_url_builder" - const val KEY_HAS_FIRST_REFRESH = "has_first_refresh" const val KEY_STATE = "state" - private const val STATE_NORMAL = 0 - private const val STATE_SIMPLE_SEARCH = 1 - private const val STATE_SEARCH = 2 - private const val STATE_SEARCH_SHOW_LIST = 3 private const val ANIMATE_TIME = 300L fun ListUrlBuilder.toStartArgs() = bundleOf( KEY_ACTION to ACTION_LIST_URL_BUILDER, @@ -1231,8 +1114,12 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList } } -private fun getSuitableTitleForUrlBuilder( - context: Context, +@Parcelize +enum class State : Parcelable { + NORMAL, SIMPLE_SEARCH, SEARCH, SEARCH_SHOW_LIST +} + +private fun Context.getSuitableTitleForUrlBuilder( urlBuilder: ListUrlBuilder, appName: Boolean, ): String? { @@ -1240,29 +1127,29 @@ private fun getSuitableTitleForUrlBuilder( val category = urlBuilder.category val mode = urlBuilder.mode return if (mode == MODE_WHATS_HOT) { - context.getString(R.string.whats_hot) + getString(R.string.whats_hot) } else if (!keyword.isNullOrEmpty()) { when (mode) { MODE_TOPLIST -> { when (keyword) { - "11" -> context.getString(R.string.toplist_alltime) - "12" -> context.getString(R.string.toplist_pastyear) - "13" -> context.getString(R.string.toplist_pastmonth) - "15" -> context.getString(R.string.toplist_yesterday) + "11" -> getString(R.string.toplist_alltime) + "12" -> getString(R.string.toplist_pastyear) + "13" -> getString(R.string.toplist_pastmonth) + "15" -> getString(R.string.toplist_yesterday) else -> null } } MODE_TAG -> { - val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(context) && EhTagDatabase.initialized + val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(this) && EhTagDatabase.initialized wrapTagKeyword(keyword, canTranslate) } else -> keyword } } else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) { when (mode) { - MODE_NORMAL -> context.getString(if (appName) R.string.app_name else R.string.homepage) - MODE_SUBSCRIPTION -> context.getString(R.string.subscription) + MODE_NORMAL -> getString(if (appName) R.string.app_name else R.string.homepage) + MODE_SUBSCRIPTION -> getString(R.string.subscription) else -> null } } else if (category.countOneBits() == 1) { diff --git a/app/src/main/java/com/hippo/ehviewer/yorozuya/IntIdGenerator.kt b/app/src/main/java/com/hippo/ehviewer/yorozuya/IntIdGenerator.kt deleted file mode 100644 index 5aebe83dc..000000000 --- a/app/src/main/java/com/hippo/ehviewer/yorozuya/IntIdGenerator.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/yorozuya/ViewUtils.kt b/app/src/main/java/com/hippo/ehviewer/yorozuya/ViewUtils.kt deleted file mode 100644 index 45f4b5f59..000000000 --- a/app/src/main/java/com/hippo/ehviewer/yorozuya/ViewUtils.kt +++ /dev/null @@ -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) diff --git a/app/src/main/res/layout/scene_gallery_list.xml b/app/src/main/res/layout/scene_gallery_list.xml index 6bcc7a5eb..b155b0220 100644 --- a/app/src/main/res/layout/scene_gallery_list.xml +++ b/app/src/main/res/layout/scene_gallery_list.xml @@ -22,11 +22,54 @@ android:paddingTop="16dp" app:layout_behavior="@string/searchbar_scrolling_view_behavior"> - + android:clipChildren="false"> + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - -