From d4d6fe56a949acc3b132069dc6ebcf0725df7de1 Mon Sep 17 00:00:00 2001 From: tarsin <43900799+asuka-mio@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:00:45 +0800 Subject: [PATCH] Move FavouriteScene to paging3 (#1238) * Initial move favScene to paging3 * Refine * Refine * Refine * Refine * Implement goto bottom * Refine * Implement Jumpto * Refine * Refine * Refine * Refine * Refine * Cleanup code * Cleanup code * Refine * Refine * Do load in IOContext --- app/src/main/java/com/hippo/ehviewer/EhDB.kt | 23 +- .../ehviewer/client/data/FavListUrlBuilder.kt | 4 +- .../hippo/ehviewer/dao/LocalFavoritesDao.kt | 3 + .../ehviewer/ui/legacy/AddDeleteDrawable.kt | 2 +- .../com/hippo/ehviewer/ui/legacy/FabLayout.kt | 2 +- .../ui/legacy/GalleryInfoContentHelper.kt | 3 +- .../ehviewer/ui/scene/ComposeFavScene.kt | 209 ---- .../hippo/ehviewer/ui/scene/FavoritesScene.kt | 1015 +++++++---------- .../hippo/ehviewer/ui/scene/GalleryAdapter.kt | 1 - .../ehviewer/ui/scene/GalleryListScene.kt | 1 + app/src/main/res/layout/scene_favorites.xml | 42 +- 11 files changed, 434 insertions(+), 871 deletions(-) delete mode 100644 app/src/main/java/com/hippo/ehviewer/ui/scene/ComposeFavScene.kt diff --git a/app/src/main/java/com/hippo/ehviewer/EhDB.kt b/app/src/main/java/com/hippo/ehviewer/EhDB.kt index 04862ac3d..9e2a7f256 100644 --- a/app/src/main/java/com/hippo/ehviewer/EhDB.kt +++ b/app/src/main/java/com/hippo/ehviewer/EhDB.kt @@ -96,12 +96,6 @@ object EhDB { dao.deleteByKey(gid) } - @Synchronized - fun clearDownloadDirname() { - val dao = db.downloadDirnameDao() - dao.deleteAll() - } - @get:Synchronized val allDownloadLabelList: List get() = db.downloadLabelDao().list() @@ -158,21 +152,6 @@ object EhDB { dao.delete(raw!!) } - @get:Synchronized - val allLocalFavorites: List - get() { - val dao = db.localFavoritesDao() - val list = dao.list() - return ArrayList(list) - } - - @Synchronized - fun searchLocalFavorites(query: String): List { - val dao = db.localFavoritesDao() - val list = dao.list("%$query%") - return ArrayList(list) - } - @Synchronized fun removeLocalFavorites(gid: Long) { db.localFavoritesDao().deleteByKey(gid) @@ -274,6 +253,8 @@ object EhDB { val localFavLazyList: PagingSource get() = db.localFavoritesDao().listLazy() + fun searchLocalFav(keyword: String) = db.localFavoritesDao().listLazy("%$keyword%") + @Synchronized fun putHistoryInfo(galleryInfo: GalleryInfo?) { val dao = db.historyDao() diff --git a/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.kt b/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.kt index d3b362c54..fbc4d574f 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.kt @@ -23,8 +23,8 @@ import kotlinx.parcelize.Parcelize @Parcelize class FavListUrlBuilder( - private var mPrev: String? = null, - private var mNext: String? = null, + var mPrev: String? = null, + var mNext: String? = null, var jumpTo: String? = null, var keyword: String? = null, var favCat: Int = FAV_CAT_ALL, diff --git a/app/src/main/java/com/hippo/ehviewer/dao/LocalFavoritesDao.kt b/app/src/main/java/com/hippo/ehviewer/dao/LocalFavoritesDao.kt index f6b711b81..b7433feec 100644 --- a/app/src/main/java/com/hippo/ehviewer/dao/LocalFavoritesDao.kt +++ b/app/src/main/java/com/hippo/ehviewer/dao/LocalFavoritesDao.kt @@ -17,6 +17,9 @@ interface LocalFavoritesDao : BasicDao { @Query("SELECT * FROM LOCAL_FAVORITES WHERE TITLE LIKE :title ORDER BY TIME DESC") fun list(title: String): List + @Query("SELECT * FROM LOCAL_FAVORITES WHERE TITLE LIKE :title ORDER BY TIME DESC") + fun listLazy(title: String): PagingSource + @Query("SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid") fun load(gid: Long): LocalFavoriteInfo? 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 ed4998a70..7ab0503db 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 @@ -124,7 +124,7 @@ class AddDeleteDrawable(context: Context, color: Int) : Drawable() { setShape(true, duration) } - fun setShape(delete: Boolean, duration: Long) { + private fun setShape(delete: Boolean, duration: Long) { if (!(!delete && mProgress == 0f || delete && mProgress == 1f)) { val endProgress = if (delete) 1f else 0f if (duration <= 0) { diff --git a/app/src/main/java/com/hippo/ehviewer/ui/legacy/FabLayout.kt b/app/src/main/java/com/hippo/ehviewer/ui/legacy/FabLayout.kt index a74bf2150..4174cf9fb 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/FabLayout.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/legacy/FabLayout.kt @@ -414,7 +414,7 @@ class FabLayout @JvmOverloads constructor( } } - interface OnExpandListener { + fun interface OnExpandListener { fun onExpand(expanded: Boolean) } 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 index 4917ddfd5..bb608f6ff 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/legacy/GalleryInfoContentHelper.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/legacy/GalleryInfoContentHelper.kt @@ -75,8 +75,7 @@ abstract class GalleryInfoContentHelper : ContentHelper() { } fun goTo(time: Long, isNext: Boolean) { - val formatter = DateTimeFormatter - .ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC) jumpTo = formatter.format(Instant.ofEpochMilli(time)) if (isNext) { goTo(mNext ?: "2", true) diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/ComposeFavScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/ComposeFavScene.kt deleted file mode 100644 index a3fa08140..000000000 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/ComposeFavScene.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.hippo.ehviewer.ui.scene - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SearchBar -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.currentRecomposeScope -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf -import androidx.core.view.GravityCompat -import androidx.lifecycle.lifecycleScope -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingSource -import androidx.paging.PagingState -import androidx.paging.cachedIn -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemContentType -import androidx.paging.compose.itemKey -import com.hippo.ehviewer.EhDB -import com.hippo.ehviewer.R -import com.hippo.ehviewer.Settings -import com.hippo.ehviewer.client.EhEngine -import com.hippo.ehviewer.client.data.FavListUrlBuilder -import com.hippo.ehviewer.client.data.FavListUrlBuilder.Companion.FAV_CAT_LOCAL -import com.hippo.ehviewer.client.data.GalleryInfo -import com.hippo.ehviewer.ui.main.GalleryInfoListItem -import com.hippo.ehviewer.ui.setMD3Content -import eu.kanade.tachiyomi.util.system.pxToDp - -class ComposeFavScene : BaseScene() { - private var curFav by mutableIntStateOf(Settings.recentFavCat) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ) = ComposeView(inflater.context).apply { - setMD3Content { - Scaffold( - topBar = { - var active by remember { mutableStateOf(false) } - val favCatName: String = when (curFav) { - in 0..9 -> Settings.favCat[curFav] - FAV_CAT_LOCAL -> stringResource(id = R.string.local_favorites) - else -> stringResource(id = R.string.cloud_favorites) - } - SearchBar( - query = favCatName, - onQueryChange = { }, - onSearch = { }, - active = active, - onActiveChange = { active = it }, - leadingIcon = { - IconButton(onClick = { toggleDrawer(GravityCompat.START) }) { - Icon(imageVector = Icons.Default.Menu, contentDescription = null) - } - }, - modifier = Modifier.padding(horizontal = 18.dp), - ) { - } - }, - ) { - val favBuilder = rememberSaveable(curFav) { FavListUrlBuilder(favCat = curFav) } - val localFavData = remember { - Pager(PagingConfig(25)) { - EhDB.localFavLazyList - }.flow.cachedIn(lifecycleScope) - }.collectAsLazyPagingItems() - val cloudData = remember(curFav) { - Pager(PagingConfig(25)) { - object : PagingSource() { - override fun getRefreshKey(state: PagingState): String? = null - override suspend fun load(params: LoadParams): LoadResult { - when (params) { - is LoadParams.Prepend -> { - favBuilder.setIndex(params.key, isNext = false) - } - is LoadParams.Append -> { - favBuilder.setIndex(params.key, isNext = true) - } - is LoadParams.Refresh -> { - } - } - val r = EhEngine.getFavorites(favBuilder.build()) - Settings.favCat = r.catArray - Settings.favCount = r.countArray - Settings.favCloudCount = r.countArray.sum() - return LoadResult.Page(r.galleryInfoList, r.prev, r.next) - } - } - }.flow.cachedIn(lifecycleScope) - }.collectAsLazyPagingItems() - val data = if (curFav == FAV_CAT_LOCAL) localFavData else cloudData - val height = remember { (3 * Settings.listThumbSize * 3).pxToDp.dp } - LazyColumn( - modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.gallery_list_margin_h), vertical = dimensionResource(id = R.dimen.gallery_list_margin_v)), - verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), - contentPadding = it, - ) { - if (data.itemCount == 0) { - item { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } - } - items( - count = data.itemCount, - key = data.itemKey(key = { item -> item.gid }), - contentType = data.itemContentType(), - ) { index -> - val item = data[index] - if (item != null) { - GalleryInfoListItem( - onClick = { - navAnimated( - R.id.galleryDetailScene, - bundleOf( - GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO, - GalleryDetailScene.KEY_GALLERY_INFO to item, - ), - ) - }, - onLongClick = {}, - info = item, - modifier = Modifier.height(height), - isInFavScene = true, - showPages = Settings.showGalleryPages, - ) - } - } - } - } - } - } - - override fun onCreateDrawerView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - return ComposeView(inflater.context).apply { - setMD3Content { - ElevatedCard { - val scope = currentRecomposeScope - LaunchedEffect(Unit) { - Settings.favChangesFlow.collect { - scope.invalidate() - } - } - val faves = arrayOf( - stringResource(id = R.string.local_favorites) to Settings.favLocalCount, - stringResource(id = R.string.cloud_favorites) to Settings.favCloudCount, - *Settings.favCat.zip(Settings.favCount.toTypedArray()).toTypedArray(), - ) - TopAppBar(title = { Text(text = stringResource(id = R.string.collections)) }) - LazyColumn { - itemsIndexed(faves) { index, (name, count) -> - ListItem( - headlineContent = { Text(text = name) }, - trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) }, - modifier = Modifier.clickable { - curFav = index - 2 - toggleDrawer(GravityCompat.END) - }, - ) - } - } - } - } - } - } -} 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 e23ff40fa..3eee278f4 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 @@ -15,10 +15,7 @@ */ package com.hippo.ehviewer.ui.scene -import android.annotation.SuppressLint -import android.app.Activity import android.content.DialogInterface -import android.content.res.Resources import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -36,12 +33,23 @@ import androidx.compose.runtime.currentRecomposeScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.core.util.valueIterator import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsAnimationCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView +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 @@ -57,7 +65,6 @@ import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.data.FavListUrlBuilder import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.client.ehUrl -import com.hippo.ehviewer.client.parser.FavoritesParser import com.hippo.ehviewer.databinding.SceneFavoritesBinding import com.hippo.ehviewer.ui.CommonOperations import com.hippo.ehviewer.ui.legacy.AddDeleteDrawable @@ -66,36 +73,35 @@ import com.hippo.ehviewer.ui.legacy.EasyRecyclerView import com.hippo.ehviewer.ui.legacy.EasyRecyclerView.CustomChoiceListener 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.ViewTransition import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper import com.hippo.ehviewer.ui.setMD3Content +import com.hippo.ehviewer.util.ExceptionUtils import com.hippo.ehviewer.util.getParcelableCompat import com.hippo.ehviewer.yorozuya.SimpleHandler import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.withUIContext +import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.withIOContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import moe.tarsin.coroutines.runSuspendCatching import rikka.core.res.resolveColor +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 -// TODO Get favorite, modify favorite, add favorite, what a mess! -@SuppressLint("NotifyDataSetChanged", "RtlHardcoded") -class FavoritesScene : - SearchBarScene(), - OnDragHandlerListener, - OnClickFabListener, - OnExpandListener, - CustomChoiceListener { +class FavoritesScene : SearchBarScene() { private var _binding: SceneFavoritesBinding? = null private val binding get() = _binding!! - private val mModifyGiList: MutableList = ArrayList() - private var mAdapter: FavoritesAdapter? = null - private lateinit var mHelper: FavoritesHelper - private lateinit var mActionFabDrawable: AddDeleteDrawable - private var mUrlBuilder: FavListUrlBuilder? = null + private var mAdapter: PagingDataAdapter? = null + private var mAdapterDelegate: GalleryAdapter? = null + private var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat) private val showNormalFabsRunnable = Runnable { updateJumpFab() // index: 0, 2 binding.fabLayout.run { @@ -103,63 +109,138 @@ class FavoritesScene : (3..6).forEach { setSecondaryFabVisibilityAt(it, false) } } } - private var mHasFirstRefresh = false - - // Avoid unnecessary search bar update - private var mOldFavCat: String? = null - - // Avoid unnecessary search bar update - private var mOldKeyword: String? = null - - // For modify action - private var mEnableModify = false - - // For modify action - private var mModifyFavCat = 0 - - // For modify action - private var mModifyAdd = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (savedInstanceState == null) { - onInit() - } else { + if (savedInstanceState != null) { onRestore(savedInstanceState) } } - override fun onResume() { - super.onResume() - mAdapter?.type = Settings.listMode + private var initialKey: String? = null + + private val cloudDataFlow = Pager(PagingConfig(25)) { + object : PagingSource() { + override fun getRefreshKey(state: PagingState): String? = initialKey + override suspend fun load(params: LoadParams) = withIOContext { + 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.jumpTo != null) { + urlBuilder.mNext ?: urlBuilder.setIndex("2", true) + } + } else { + urlBuilder.setIndex(key, false) + } + } + } + val r = runSuspendCatching { + EhEngine.getFavorites(urlBuilder.build()) + }.onFailure { + return@withIOContext LoadResult.Error(it) + }.getOrThrow() + Settings.favCat = r.catArray + Settings.favCount = r.countArray + Settings.favCloudCount = r.countArray.sum() + urlBuilder.jumpTo = null + LoadResult.Page(r.galleryInfoList, r.prev, r.next) + } + } + }.flow.cachedIn(lifecycleScope) + + private val localFavDataFlow = Pager(PagingConfig(20, enablePlaceholders = false, jumpThreshold = 40)) { + val keyword = urlBuilder.keyword + if (keyword.isNullOrBlank()) { + EhDB.localFavLazyList + } else { + EhDB.searchLocalFav(keyword) + } + }.flow.cachedIn(lifecycleScope) + + fun onItemClick(position: Int) { + if (isDrawerOpen(GravityCompat.END)) { + // Skip if in search mode + if (!binding.recyclerView.isInCustomChoice) { + switchFav(position - 2) + 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, + ), + ) + } + } + } } - private fun onInit() { - mUrlBuilder = FavListUrlBuilder() - mUrlBuilder!!.favCat = Settings.recentFavCat + private var collectJob: Job? = null + + private fun switchFav(newCat: Int, keyword: String? = null) { + _binding ?: return + urlBuilder.keyword = keyword + urlBuilder.favCat = newCat + urlBuilder.jumpTo = null + urlBuilder.setIndex(null, true) + initialKey = null + collectJob?.cancel() + when (newCat) { + -2 -> { + collectJob = lifecycleScope.launchIO { + localFavDataFlow.collectLatest { + @Suppress("UNCHECKED_CAST") + mAdapter?.submitData(it as PagingData) + } + } + } + + else -> { + collectJob = lifecycleScope.launchIO { + cloudDataFlow.collectLatest { + mAdapter?.submitData(it) + } + } + } + } + mAdapter?.refresh() + val favCatName: String = when (val favCat = urlBuilder.favCat) { + in 0..9 -> Settings.favCat[favCat] + FavListUrlBuilder.FAV_CAT_LOCAL -> getString(R.string.local_favorites) + else -> getString(R.string.cloud_favorites) + } + if (keyword.isNullOrEmpty()) { + setSearchBarHint(getString(R.string.favorites_title, favCatName)) + } else { + setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword)) + } + setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName)) + Settings.recentFavCat = urlBuilder.favCat + } + + override fun onResume() { + super.onResume() + mAdapterDelegate?.type = Settings.listMode } private fun onRestore(savedInstanceState: Bundle) { - mUrlBuilder = savedInstanceState.getParcelableCompat(KEY_URL_BUILDER) - if (mUrlBuilder == null) { - mUrlBuilder = FavListUrlBuilder() + savedInstanceState.getParcelableCompat(KEY_URL_BUILDER)?.let { + urlBuilder = it } - mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - val hasFirstRefresh: Boolean = if (1 == mHelper.shownViewIndex) { - false - } else { - mHasFirstRefresh - } - outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh) - outState.putParcelable(KEY_URL_BUILDER, mUrlBuilder) - } - - override fun onDestroy() { - super.onDestroy() - mUrlBuilder = null + outState.putParcelable(KEY_URL_BUILDER, urlBuilder) } override fun onCreateViewWithToolbar( @@ -168,38 +249,84 @@ class FavoritesScene : savedInstanceState: Bundle?, ): View { _binding = SceneFavoritesBinding.inflate(inflater, container, false) - setOnApplySearch { query: String? -> - onApplySearch(query) + setOnApplySearch { + if (!binding.recyclerView.isInCustomChoice) { + switchFav(urlBuilder.favCat, it) + } } + binding.fastScroller.attachToRecyclerView(binding.recyclerView) + binding.fastScroller.setHandlerDrawable(HandlerDrawable().apply { setColor(inflater.context.theme.resolveColor(androidx.appcompat.R.attr.colorPrimary)) }) binding.fabLayout.run { - addOnExpandListener(FabLayoutListener()) + addOnExpandListener { if (!it && binding.recyclerView.isInCustomChoice) binding.recyclerView.outOfCustomChoiceMode() } (parent as ViewGroup).removeView(this) container!!.addView(this) - ViewCompat.setWindowInsetsAnimationCallback( - binding.root, - WindowInsetsAnimationHelper( - WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP, - this, - ), - ) + ViewCompat.setWindowInsetsAnimationCallback(binding.root, WindowInsetsAnimationHelper(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP, this)) updateJumpFab() val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface) - mActionFabDrawable = AddDeleteDrawable(context, colorID) - primaryFab!!.setImageDrawable(mActionFabDrawable) + val addDelete = AddDeleteDrawable(context, colorID) + primaryFab!!.setImageDrawable(addDelete) setExpanded(expanded = false, animation = false) setAutoCancel(true) setHidePrimaryFab(false) - setOnClickFabListener(this@FavoritesScene) - addOnExpandListener(this@FavoritesScene) + setOnClickFabListener(object : OnClickFabListener { + override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) { + if (binding.recyclerView.isInCustomChoice) { + binding.recyclerView.outOfCustomChoiceMode() + } else { + binding.fabLayout.toggle() + } + } + + override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) { + if (!binding.recyclerView.isInCustomChoice) { + when (position) { + 0 -> showGoToDialog() + 1 -> switchFav(urlBuilder.favCat) + 2 -> { + initialKey = "1-0" + mAdapter?.refresh() + } + } + view.isExpanded = false + return + } + when (position) { + // Check all + 3 -> binding.recyclerView.checkAll() + // Download + 4 -> CommonOperations.startDownload(mainActivity!!, takeCheckedInfo(), false) + // Delete + 5 -> { + val helper = DeleteDialogHelper() + BaseDialogBuilder(context) + .setTitle(R.string.delete_favorites_dialog_title) + .setMessage(getString(R.string.delete_favorites_dialog_message, checkedSize())) + .setPositiveButton(android.R.string.ok, helper) + .show() + } + // Move + 6 -> { + val helper = MoveDialogHelper() + // First is local favorite, the other 10 is cloud favorite + val array = arrayOfNulls(11) + array[0] = getString(R.string.local_favorites) + System.arraycopy(Settings.favCat, 0, array, 1, 10) + BaseDialogBuilder(context).setTitle(R.string.move_favorites_dialog_title).setItems(array, helper).show() + } + } + } + }) + addOnExpandListener { + if (it) { + addDelete.setDelete(ANIMATE_TIME) + } else { + addDelete.setAdd(ANIMATE_TIME) + } + } addAboveSnackView(this) } - binding.contentLayout.run { - mHelper = FavoritesHelper() - mHelper.setEmptyString(getString(R.string.gallery_list_empty_hit)) - setHelper(mHelper) - val paddingTopSB = - resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar) - setFitPaddingTop(paddingTopSB) + binding.run { + val paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar) fastScroller.run { setPadding( paddingLeft, @@ -207,95 +334,122 @@ class FavoritesScene : paddingRight, paddingBottom, ) - setOnDragHandlerListener(this@FavoritesScene) + setOnDragHandlerListener(object : OnDragHandlerListener { + override fun onStartDragHandler() { + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) + } + + override fun onEndDragHandler() { + if (!binding.recyclerView.isInCustomChoice) { + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) + } + showSearchBar() + } + }) } } - binding.contentLayout.recyclerView.run { - mAdapter = FavoritesAdapter(resources, this, Settings.listMode) - clipToPadding = false - clipChildren = false + 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 + 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) + binding.tip.setOnClickListener { mAdapter?.refresh() } + val transition = ViewTransition(binding.recyclerView, binding.progress, binding.tip) + val empty = getString(R.string.gallery_list_empty_hit) + 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) { + binding.tip.text = empty + transition.showView(2) + } else { + transition.showView(0) + } + } + } + } + } + } + switchFav(Settings.recentFavCat) + mAdapterDelegate = delegateAdapter setChoiceMode(EasyRecyclerView.CHOICE_MODE_MULTIPLE_CUSTOM) - setCustomCheckedListener(this@FavoritesScene) + setCustomCheckedListener(object : CustomChoiceListener { + override fun onIntoCustomChoice(view: EasyRecyclerView) { + showSelectionFab() + binding.fabLayout.setAutoCancel(false) + // Delay expanding action to make layout work fine + SimpleHandler.post { binding.fabLayout.isExpanded = true } + binding.refreshLayout.isEnabled = false + // Lock drawer + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) + setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) + } + + override fun onOutOfCustomChoice(view: EasyRecyclerView) { + showNormalFab() + binding.fabLayout.setAutoCancel(true) + binding.fabLayout.isExpanded = false + binding.refreshLayout.isEnabled = true + // Unlock drawer + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) + } + + override fun onItemCheckedStateChanged(view: EasyRecyclerView, position: Int, id: Long, checked: Boolean) { + if (view.checkedItemCount == 0) { + view.outOfCustomChoiceMode() + } + } + }) } setAllowEmptySearch(false) - updateSearchBar() - - // Only refresh for the first time - if (!mHasFirstRefresh) { - mHasFirstRefresh = true - mHelper.firstRefresh() - } return binding.root } - // keyword of mUrlBuilder, fav cat of mUrlBuilder, mFavCatArray. - // They changed, call it - private fun updateSearchBar() { - _binding ?: return - mUrlBuilder ?: return - - // Update title - val favCatName: String = when (val favCat = mUrlBuilder!!.favCat) { - in 0..9 -> { - Settings.favCat[favCat] - } - - FavListUrlBuilder.FAV_CAT_LOCAL -> { - getString(R.string.local_favorites) - } - - else -> { - getString(R.string.cloud_favorites) - } - } - val keyword = mUrlBuilder!!.keyword - if (keyword.isNullOrEmpty()) { - if (favCatName != mOldFavCat) { - setSearchBarHint(getString(R.string.favorites_title, favCatName)) - } - } else { - if (favCatName != mOldFavCat || keyword != mOldKeyword) { - setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword)) - } - } - - // Update hint - if (favCatName != mOldFavCat) { - setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName)) - } - mOldFavCat = favCatName - mOldKeyword = keyword - - // Save recent fav cat - Settings.recentFavCat = mUrlBuilder!!.favCat - } - // Hide jump fab on local fav cat private fun updateJumpFab() { - if (mUrlBuilder != null) { - binding.fabLayout.setSecondaryFabVisibilityAt( - 0, - mUrlBuilder!!.favCat != FavListUrlBuilder.FAV_CAT_LOCAL, - ) - binding.fabLayout.setSecondaryFabVisibilityAt( - 2, - mUrlBuilder!!.favCat != FavListUrlBuilder.FAV_CAT_LOCAL, - ) - } + binding.fabLayout.setSecondaryFabVisibilityAt( + 0, + urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL, + ) + binding.fabLayout.setSecondaryFabVisibilityAt( + 2, + urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL, + ) } override fun onDestroyView() { super.onDestroyView() - mHelper.destroy() - if (1 == mHelper.shownViewIndex) { - mHasFirstRefresh = false - } - binding.contentLayout.recyclerView.stopScroll() + binding.recyclerView.stopScroll() (binding.fabLayout.parent as ViewGroup).removeView(binding.fabLayout) removeAboveSnackView(binding.fabLayout) mAdapter = null - mOldFavCat = null - mOldKeyword = null + mAdapterDelegate = null _binding = null } @@ -303,132 +457,47 @@ class FavoritesScene : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View { - return ComposeView(inflater.context).apply { - setMD3Content { - ElevatedCard { - val scope = currentRecomposeScope - LaunchedEffect(Unit) { - Settings.favChangesFlow.collect { - scope.invalidate() - } + ) = ComposeView(inflater.context).apply { + setMD3Content { + ElevatedCard { + TopAppBar(title = { Text(text = stringResource(id = R.string.collections)) }) + val scope = currentRecomposeScope + LaunchedEffect(Unit) { + Settings.favChangesFlow.collect { + scope.invalidate() } - val faves = arrayOf( - stringResource(id = R.string.local_favorites) to Settings.favLocalCount, - stringResource(id = R.string.cloud_favorites) to Settings.favCloudCount, - *Settings.favCat.zip(Settings.favCount.toTypedArray()).toTypedArray(), - ) - TopAppBar(title = { Text(text = stringResource(id = R.string.collections)) }) - LazyColumn { - itemsIndexed(faves) { index, (name, count) -> - ListItem( - headlineContent = { Text(text = name) }, - trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) }, - modifier = Modifier.clickable { onItemClick(index) }, - ) - } + } + val faves = arrayOf( + stringResource(id = R.string.local_favorites) to Settings.favLocalCount, + stringResource(id = R.string.cloud_favorites) to Settings.favCloudCount, + *Settings.favCat.zip(Settings.favCount.toTypedArray()).toTypedArray(), + ) + LazyColumn { + itemsIndexed(faves) { index, (name, count) -> + ListItem( + headlineContent = { Text(text = name) }, + trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) }, + modifier = Modifier.clickable { onItemClick(index) }, + ) } } } } } - override fun onStartDragHandler() { - // Lock right drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) - } - - override fun onEndDragHandler() { - // Restore right drawer - if (!binding.contentLayout.recyclerView.isInCustomChoice) { - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - } - showSearchBar() - } - - fun onItemClick(position: Int): Boolean { - if (isDrawerOpen(GravityCompat.END)) { - // Skip if in search mode - if (binding.contentLayout.recyclerView.isInCustomChoice) { - return true - } - if (mUrlBuilder == null) { - return true - } - - // Local favorite position is 0, All favorite position is 1, so position - 2 is OK - val newFavCat = position - 2 - - // Check is the same - if (mUrlBuilder!!.favCat == newFavCat) { - return true - } - mUrlBuilder!!.keyword = null - mUrlBuilder!!.favCat = newFavCat - updateSearchBar() - updateJumpFab() - mHelper.refresh() - closeDrawer(GravityCompat.END) - } else { - if (binding.contentLayout.recyclerView.isInCustomChoice) { - binding.contentLayout.recyclerView.toggleItemChecked(position) - } else { - val gi = mHelper.getDataAtEx(position) ?: return true - val args = Bundle() - args.putString( - GalleryDetailScene.KEY_ACTION, - GalleryDetailScene.ACTION_GALLERY_INFO, - ) - args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi) - navAnimated(R.id.galleryDetailScene, args) - } - } - return true - } - fun onItemLongClick(position: Int): Boolean { // Can not into - if (!binding.contentLayout.recyclerView.isInCustomChoice) { - binding.contentLayout.recyclerView.intoCustomChoiceMode() + if (!binding.recyclerView.isInCustomChoice) { + binding.recyclerView.intoCustomChoiceMode() } - binding.contentLayout.recyclerView.toggleItemChecked(position) + binding.recyclerView.toggleItemChecked(position) return true } - private fun onApplySearch(query: String?) { - // Skip if in search mode - if (binding.contentLayout.recyclerView.isInCustomChoice) { - return - } - if (mUrlBuilder == null) { - return - } - mUrlBuilder!!.keyword = query - updateSearchBar() - mHelper.refresh() - } - - override fun onExpand(expanded: Boolean) { - if (expanded) { - mActionFabDrawable.setDelete(ANIMATE_TIME) - } else { - mActionFabDrawable.setAdd(ANIMATE_TIME) - } - } - - override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) { - if (binding.contentLayout.recyclerView.isInCustomChoice) { - binding.contentLayout.recyclerView.outOfCustomChoiceMode() - } else { - binding.fabLayout.toggle() - } - } - private fun showGoToDialog() { context ?: return val local = LocalDateTime.of(2007, 3, 21, 0, 0) - val fromDate = - local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli() + val fromDate = local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli() val toDate = MaterialDatePicker.todayInUtcMilliseconds() val listValidators = ArrayList() listValidators.add(DateValidatorPointForward.from(fromDate)) @@ -443,90 +512,29 @@ class FavoritesScene : .setSelection(toDate) .build() datePicker.show(requireActivity().supportFragmentManager, "date-picker") - datePicker.addOnPositiveButtonClickListener { v: Long? -> - mHelper.goTo( - v!!, - true, - ) + datePicker.addOnPositiveButtonClickListener { time: Long -> + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC) + val jumpTo = formatter.format(Instant.ofEpochMilli(time)) + urlBuilder.jumpTo = jumpTo + mAdapter?.refresh() } } - override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) { - val context = context ?: return - if (!binding.contentLayout.recyclerView.isInCustomChoice) { - when (position) { - 0 -> { - if (mHelper.canGoTo()) showGoToDialog() - } + private fun takeCheckedInfo() = run { + val stateArray = binding.recyclerView.checkedItemPositions!! + stateArray.valueIterator().asSequence().withIndex().filter { it.value } + .mapNotNull { mAdapter?.peek(stateArray.keyAt(it.index)) }.toList() + }.also { binding.recyclerView.outOfCustomChoiceMode() } - 1 -> mHelper.refresh() - 2 -> mHelper.goTo("1-0", false) - } - view.isExpanded = false - return - } - mModifyGiList.clear() - val stateArray = binding.contentLayout.recyclerView.checkedItemPositions!! - for (i in 0 until stateArray.size()) { - if (stateArray.valueAt(i)) { - val gi = mHelper.getDataAtEx(stateArray.keyAt(i)) - if (gi != null) { - mModifyGiList.add(gi) - } - } - } - when (position) { - 3 -> // Check all - binding.contentLayout.recyclerView.checkAll() + private fun checkedSize() = binding.recyclerView.checkedItemCount - 4 -> { // Download - val activity: Activity? = mainActivity - if (activity != null) { - CommonOperations.startDownload(mainActivity!!, mModifyGiList, false) - } - mModifyGiList.clear() - if (binding.contentLayout.recyclerView.isInCustomChoice) { - binding.contentLayout.recyclerView.outOfCustomChoiceMode() - } - } - - 5 -> { // Delete - val helper = DeleteDialogHelper() - BaseDialogBuilder(context) - .setTitle(R.string.delete_favorites_dialog_title) - .setMessage( - getString( - R.string.delete_favorites_dialog_message, - mModifyGiList.size, - ), - ) - .setPositiveButton(android.R.string.ok, helper) - .setOnCancelListener(helper) - .show() - } - - 6 -> { // Move - val helper = MoveDialogHelper() - // First is local favorite, the other 10 is cloud favorite - val array = arrayOfNulls(11) - array[0] = getString(R.string.local_favorites) - System.arraycopy(Settings.favCat, 0, array, 1, 10) - BaseDialogBuilder(context) - .setTitle(R.string.move_favorites_dialog_title) - .setItems(array, helper) - .setOnCancelListener(helper) - .show() - } - } - } - - private fun showNormalFabs() { - // Delay showing normal fabs to avoid mutation + private fun showNormalFab() { + // Delay showing normal fab to avoid mutation SimpleHandler.removeCallbacks(showNormalFabsRunnable) SimpleHandler.postDelayed(showNormalFabsRunnable, 300) } - private fun showSelectionFabs() { + private fun showSelectionFab() { SimpleHandler.removeCallbacks(showNormalFabsRunnable) binding.fabLayout.run { (0..2).forEach { setSecondaryFabVisibilityAt(it, false) } @@ -534,303 +542,48 @@ class FavoritesScene : } } - override fun onIntoCustomChoice(view: EasyRecyclerView) { - showSelectionFabs() - binding.fabLayout.setAutoCancel(false) - // Delay expanding action to make layout work fine - SimpleHandler.post { binding.fabLayout.isExpanded = true } - mHelper.setRefreshLayoutEnable(false) - // Lock drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START) - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END) - } - - override fun onOutOfCustomChoice(view: EasyRecyclerView) { - showNormalFabs() - binding.fabLayout.setAutoCancel(true) - binding.fabLayout.isExpanded = false - mHelper.setRefreshLayoutEnable(true) - // Unlock drawer - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START) - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END) - } - - override fun onItemCheckedStateChanged( - view: EasyRecyclerView, - position: Int, - id: Long, - checked: Boolean, - ) { - if (view.checkedItemCount == 0) { - view.outOfCustomChoiceMode() - } - } - - private fun onGetFavoritesSuccess(result: FavoritesParser.Result, taskId: Int) { - if (mHelper.isCurrentTask(taskId)) { - Settings.favCat = result.catArray - Settings.favCount = result.countArray - Settings.favCloudCount = result.countArray.sum() - _binding ?: return - updateSearchBar() - mHelper.onGetPageData(taskId, 0, 0, result.prev, result.next, result.galleryInfoList) - } - } - - private fun onGetFavoritesFailure(e: Throwable, taskId: Int) { - if (mHelper.isCurrentTask(taskId)) { - mHelper.onGetException(taskId, e) - } - } - - private fun onGetFavoritesLocal(keyword: String?, taskId: Int) { - if (mHelper.isCurrentTask(taskId)) { - val list: List = if (keyword.isNullOrEmpty()) { - EhDB.allLocalFavorites - } else { - EhDB.searchLocalFavorites(keyword) - } - if (list.isEmpty()) { - mHelper.onGetPageData(taskId, 0, 0, null, null, list) - } else { - mHelper.onGetPageData(taskId, 1, 0, null, null, list) - } - if (keyword.isNullOrEmpty()) { - Settings.favLocalCount = list.size - } - } - } - - private inner class DeleteDialogHelper : - DialogInterface.OnClickListener, - DialogInterface.OnCancelListener { + private inner class DeleteDialogHelper : DialogInterface.OnClickListener { override fun onClick(dialog: DialogInterface, which: Int) { - if (which != DialogInterface.BUTTON_POSITIVE) { - return - } - if (mUrlBuilder == null) { - return - } - binding.contentLayout.recyclerView.outOfCustomChoiceMode() - if (mUrlBuilder!!.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav - val gidArray = mModifyGiList.map { it.gid }.toLongArray() - EhDB.removeLocalFavorites(gidArray) - mModifyGiList.clear() - mHelper.refresh() - } else { // Delete cloud fav - mEnableModify = true - mModifyFavCat = -1 - mModifyAdd = false - mHelper.refresh() - } - } - - override fun onCancel(dialog: DialogInterface) { - mModifyGiList.clear() - } - } - - private inner class MoveDialogHelper : - DialogInterface.OnClickListener, - DialogInterface.OnCancelListener { - override fun onClick(dialog: DialogInterface, which: Int) { - if (mUrlBuilder == null) { - return - } - val srcCat = mUrlBuilder!!.favCat - val dstCat: Int = if (which == 0) { - FavListUrlBuilder.FAV_CAT_LOCAL - } else { - which - 1 - } - if (srcCat == dstCat) { - return - } - binding.contentLayout.recyclerView.outOfCustomChoiceMode() - if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Move from local to cloud - val gidArray = mModifyGiList.map { it.gid }.toLongArray() - EhDB.removeLocalFavorites(gidArray) - mEnableModify = true - mModifyFavCat = dstCat - mModifyAdd = true - mHelper.refresh() - } else if (dstCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Move from cloud to local - EhDB.putLocalFavorites(mModifyGiList) - mEnableModify = true - mModifyFavCat = -1 - mModifyAdd = false - mHelper.refresh() - } else { - mEnableModify = true - mModifyFavCat = dstCat - mModifyAdd = false - mHelper.refresh() - } - } - - override fun onCancel(dialog: DialogInterface) { - mModifyGiList.clear() - } - } - - private inner class FavoritesAdapter( - resources: Resources, - recyclerView: RecyclerView, - type: Int, - ) : GalleryAdapter( - resources, - recyclerView, - type, - false, - { onItemClick(it) }, - { onItemLongClick(it) }, - ) { - override fun getItemCount(): Int { - return mHelper.size() - } - - override fun getDataAt(position: Int): GalleryInfo? { - return mHelper.getDataAtEx(position) - } - } - - private inner class FabLayoutListener : OnExpandListener { - override fun onExpand(expanded: Boolean) { - if (!expanded && binding.contentLayout.recyclerView.isInCustomChoice) binding.contentLayout.recyclerView.outOfCustomChoiceMode() - } - } - - private inner class FavoritesHelper : GalleryInfoContentHelper() { - override fun getPageData( - taskId: Int, - type: Int, - page: Int, - index: String?, - isNext: Boolean, - ) { - val activity = mainActivity - if (null == activity || null == mUrlBuilder) { - return - } - if (mEnableModify) { - mEnableModify = false - val local = mUrlBuilder!!.favCat == FavListUrlBuilder.FAV_CAT_LOCAL - if (mModifyAdd) { - val gidTokenArray = mModifyGiList.map { Pair(it.gid, it.token!!) }.toTypedArray() - val modifyGiListBackup: List = ArrayList(mModifyGiList) - mModifyGiList.clear() - lifecycleScope.launchIO { - runSuspendCatching { - EhEngine.addFavoritesRange(gidTokenArray, mModifyFavCat) - }.onSuccess { - withUIContext { - onGetFavoritesLocal(mUrlBuilder?.keyword, taskId) - } - }.onFailure { - // TODO It's a failure, add all of backup back to db. - // But how to known which one is failed? - EhDB.putLocalFavorites(modifyGiListBackup) - withUIContext { - onGetFavoritesLocal(mUrlBuilder?.keyword, taskId) - } - } - } + val info = takeCheckedInfo() + lifecycleScope.launchIO { + if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav + val gidArray = info.map { it.gid }.toLongArray() + EhDB.removeLocalFavorites(gidArray) } else { - val gidArray = mModifyGiList.map { it.gid }.toLongArray() - mModifyGiList.clear() - val url: String = if (local) { - // Local fav is shown now, but operation need be done for cloud fav - ehUrl { addPathSegments(EhUrl.FAV_PATH) }.toString() - } else { - mUrlBuilder!!.build() - } - mUrlBuilder!!.setIndex(index, true) - lifecycleScope.launchIO { - runSuspendCatching { - EhEngine.modifyFavorites(url, gidArray, mModifyFavCat) - }.onSuccess { result -> - // Put fav cat - Settings.favCat = result.catArray - Settings.favCount = result.countArray - withUIContext { - if (local) { - onGetFavoritesLocal(mUrlBuilder?.keyword, taskId) - } else { - onGetFavoritesSuccess(result, taskId) - } - } - }.onFailure { - it.printStackTrace() - withUIContext { - if (local) { - onGetFavoritesLocal(mUrlBuilder?.keyword, taskId) - } else { - onGetFavoritesFailure(it, taskId) - } - } - } - } + val delArray = info.map { it.gid to it.token!! }.toTypedArray() + EhEngine.addFavoritesRange(delArray, -1) } - } else if (mUrlBuilder!!.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) { - val keyword = mUrlBuilder!!.keyword - SimpleHandler.post { onGetFavoritesLocal(keyword, taskId) } - } else { - mUrlBuilder!!.setIndex(index, isNext) - mUrlBuilder!!.jumpTo = jumpTo - val url = mUrlBuilder!!.build() - lifecycleScope.launchIO { - runSuspendCatching { - EhEngine.getFavorites(url) - }.onSuccess { result -> - Settings.favCat = result.catArray - Settings.favCount = result.countArray - withUIContext { - onGetFavoritesSuccess(result, taskId) - } - }.onFailure { - withUIContext { - onGetFavoritesFailure(it, taskId) - } - } - } - } - } - - override val context - get() = this@FavoritesScene.requireContext() - - override fun notifyDataSetChanged() { - // Ensure outOfCustomChoiceMode to avoid error - binding.contentLayout.recyclerView.outOfCustomChoiceMode() - if (mAdapter != null) { - mAdapter!!.notifyDataSetChanged() - } - } - - override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) { - if (mAdapter != null) { - mAdapter!!.notifyItemRangeInserted(positionStart, itemCount) - } - } - - override fun onShowView(hiddenView: View, shownView: View) { - showSearchBar() - } - - override fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean { - return d1.gid == d2.gid - } - - override fun onScrollToPosition(position: Int) { - if (0 == position) { - showSearchBar() + mAdapter?.refresh() } } } - companion object { - private const val ANIMATE_TIME = 300L - private const val KEY_URL_BUILDER = "url_builder" - private const val KEY_HAS_FIRST_REFRESH = "has_first_refresh" + private inner class MoveDialogHelper : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val srcCat = urlBuilder.favCat + val dstCat = if (which == 0) { FavListUrlBuilder.FAV_CAT_LOCAL } else { which - 1 } + if (srcCat == dstCat) return + val info = takeCheckedInfo() + lifecycleScope.launchIO { + if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { + // Move from local to cloud + val gidArray = info.map { it.gid }.toLongArray() + EhDB.removeLocalFavorites(gidArray) + } + if (dstCat == FavListUrlBuilder.FAV_CAT_LOCAL) { + // Move from cloud to local + EhDB.putLocalFavorites(info) + } else { + // Move from cloud/local to cloud + val gidArray = info.map { it.gid }.toLongArray() + val url = ehUrl { addPathSegments(EhUrl.FAV_PATH) }.toString() + EhEngine.modifyFavorites(url, gidArray, dstCat) + } + mAdapter?.refresh() + } + } } } + +private const val ANIMATE_TIME = 300L +private const val KEY_URL_BUILDER = "url_builder" 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 4c63e7e40..0cd857893 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 @@ -130,7 +130,6 @@ abstract class GalleryAdapter( } init { - recyclerView.adapter = this recyclerView.layoutManager = layoutManager this.type = type adjustPaddings() 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 87d7f9bb4..d40894155 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 @@ -324,6 +324,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, OnClickFabList 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) diff --git a/app/src/main/res/layout/scene_favorites.xml b/app/src/main/res/layout/scene_favorites.xml index 9a470e266..e234016ad 100644 --- a/app/src/main/res/layout/scene_favorites.xml +++ b/app/src/main/res/layout/scene_favorites.xml @@ -22,11 +22,47 @@ android:paddingTop="16dp" app:layout_behavior="@string/searchbar_scrolling_view_behavior"> - + + + + + android:clipChildren="false" + app:fitsSystemWindowsInsets="top"> + + + + + +