diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt index b9adaa7a1..4ad4a26d0 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt @@ -20,184 +20,166 @@ import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.material.icons.Icons +import androidx.compose.material.icons.filled.ClearAll +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissValue +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.core.view.GravityCompat import androidx.lifecycle.lifecycleScope import androidx.paging.Pager import androidx.paging.PagingConfig -import androidx.paging.PagingDataAdapter import androidx.paging.cachedIn -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.StaggeredGridLayoutManager +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.items import arrow.core.partially1 -import com.google.accompanist.themeadapter.material3.Mdc3Theme import com.hippo.app.BaseDialogBuilder -import com.hippo.easyrecyclerview.HandlerDrawable -import com.hippo.ehviewer.EhApplication import com.hippo.ehviewer.EhDB -import com.hippo.ehviewer.FavouriteStatusRouter import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.dao.DownloadInfo -import com.hippo.ehviewer.dao.HistoryInfo -import com.hippo.ehviewer.databinding.SceneHistoryBinding import com.hippo.ehviewer.download.DownloadManager -import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener import com.hippo.ehviewer.ui.CommonOperations import com.hippo.ehviewer.ui.addToFavorites import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter import com.hippo.ehviewer.ui.removeFromFavorites import com.hippo.ehviewer.ui.widget.ListInfoCard -import com.hippo.view.ViewTransition -import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager -import com.hippo.widget.recyclerview.STRATEGY_MIN_SIZE +import com.hippo.ehviewer.ui.widget.setMD3Content import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.pxToDp -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import moe.tarsin.coroutines.runSuspendCatching -import rikka.core.res.resolveColor -@SuppressLint("NotifyDataSetChanged") -class HistoryScene : BaseToolbarScene() { - private var _binding: SceneHistoryBinding? = null - private val binding get() = _binding!! - private val mAdapter: HistoryAdapter by lazy { - HistoryAdapter(object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean { - return oldItem.gid == newItem.gid - } - - override fun areContentsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean { - return oldItem.gid == newItem.gid - } - }) - } - private val mDownloadManager = DownloadManager - private val mDownloadInfoListener: DownloadInfoListener by lazy { - object : DownloadInfoListener { - override fun onAdd(info: DownloadInfo, list: List, position: Int) { - mAdapter.notifyDataSetChanged() - } - - override fun onUpdate(info: DownloadInfo, list: List) {} - override fun onUpdateAll() {} - override fun onReload() { - mAdapter.notifyDataSetChanged() - } - - override fun onChange() { - mAdapter.notifyDataSetChanged() - } - - override fun onRenameLabel(from: String, to: String) {} - override fun onRemove(info: DownloadInfo, list: List, position: Int) { - mAdapter.notifyDataSetChanged() - } - - override fun onUpdateLabels() {} - } - } - private val mFavouriteStatusRouter = EhApplication.favouriteStatusRouter - private val mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener by lazy { - FavouriteStatusRouter.Listener { _: Long, _: Int -> - mAdapter.notifyDataSetChanged() - } - } - - override fun onDestroy() { - super.onDestroy() - mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener) - mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener) - } - - @SuppressLint("NotifyDataSetChanged") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - mDownloadManager.addDownloadInfoListener(mDownloadInfoListener) - mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener) - } - - override fun onCreateViewWithToolbar( +class HistoryScene : BaseScene() { + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - _binding = SceneHistoryBinding.inflate(inflater, container, false) - setLiftOnScrollTargetView(binding.recyclerView) - val mViewTransition = ViewTransition(binding.content, binding.tip) - val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_history) - drawable?.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) - binding.tip.setCompoundDrawables(null, drawable, null, null) - val historyData = Pager( - PagingConfig(20), - ) { - EhDB.historyLazyList - }.flow.cachedIn(viewLifecycleOwner.lifecycleScope) - binding.recyclerView.adapter = mAdapter - val layoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL) - layoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.detailSizeResId)) - layoutManager.setStrategy(STRATEGY_MIN_SIZE) - layoutManager.supportsPredictiveItemAnimations = false - binding.recyclerView.layoutManager = layoutManager - val itemTouchHelper = ItemTouchHelper(HistoryItemTouchHelperCallback()) - itemTouchHelper.attachToRecyclerView(binding.recyclerView) - binding.fastScroller.attachToRecyclerView(binding.recyclerView) - val handlerDrawable = HandlerDrawable() - handlerDrawable.setColor(theme.resolveColor(com.google.android.material.R.attr.colorPrimary)) - binding.fastScroller.setHandlerDrawable(handlerDrawable) - lifecycleScope.launchIO { - historyData.collectLatest { value -> - mAdapter.submitData( - value, - ) - } - } - viewLifecycleOwner.lifecycleScope.launch { - mAdapter.onPagesUpdatedFlow.collectLatest { - if (mAdapter.itemCount == 0) { - mViewTransition.showView(1, true) - } else { - mViewTransition.showView(0, true) + return ComposeView(inflater.context).apply { + setMD3Content { + val coroutineScope = rememberCoroutineScope() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val historyData = remember { Pager(PagingConfig(20)) { EhDB.historyLazyList }.flow.cachedIn(viewLifecycleOwner.lifecycleScope) }.collectAsLazyPagingItems() + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.history)) }, + navigationIcon = { + IconButton(onClick = ::onNavigationClick) { + Icon(imageVector = Icons.Default.Menu, contentDescription = null) + } + }, + actions = { + IconButton(onClick = ::showClearAllDialog) { + Icon(imageVector = Icons.Default.ClearAll, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { paddingValues -> + LazyColumn( + contentPadding = paddingValues, + ) { + items( + items = historyData, + key = { item -> item.gid }, + ) { info -> + info?.let { + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToStart) { + coroutineScope.launchIO { + EhDB.deleteHistoryInfo(info) + } + } + true + }, + ) + + SwipeToDismiss( + state = dismissState, + background = {}, + dismissContent = { + // TODO: item delete & add animation + // Bug tracker: https://issuetracker.google.com/issues/150812265 + ListInfoCard( + onClick = ::onItemClick.partially1(it), + onLongClick = ::onItemLongClick.partially1(it), + info = it, + modifier = Modifier.height(cardHeight), + ) + }, + directions = setOf(DismissDirection.EndToStart), + ) + } + } + } + if (historyData.itemCount == 0) { + NoHistory(modifier = Modifier.padding(paddingValues)) + } } } } - return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setTitle(R.string.history) - setNavigationIcon(R.drawable.ic_baseline_menu_24) + @Composable + private fun NoHistory( + modifier: Modifier = Modifier, + ) { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.big_history), + contentDescription = null, + Modifier.padding(16.dp), + ) + Text( + text = stringResource(id = R.string.no_history), + style = MaterialTheme.typography.headlineMedium, + ) + } } - override fun onDestroyView() { - super.onDestroyView() - binding.recyclerView.stopScroll() - _binding = null - } - - override fun onNavigationClick() { + private fun onNavigationClick() { toggleDrawer(GravityCompat.START) } - override fun getMenuResId(): Int { - return R.menu.scene_history - } - @SuppressLint("NotifyDataSetChanged") private fun showClearAllDialog() { BaseDialogBuilder(requireContext()) @@ -208,36 +190,23 @@ class HistoryScene : BaseToolbarScene() { } lifecycleScope.launchIO { EhDB.clearHistoryInfo() - withUIContext { - mAdapter.refresh() - mAdapter.notifyDataSetChanged() - } } } .setNegativeButton(android.R.string.cancel, null) .show() } - override fun onMenuItemClick(item: MenuItem): Boolean { - val id = item.itemId - if (id == R.id.action_clear_all) { - showClearAllDialog() - return true - } - return false - } - - fun onItemClick(gi: GalleryInfo) { + private fun onItemClick(gi: GalleryInfo) { val args = Bundle() args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO) args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi) navigate(R.id.galleryDetailScene, args) } - fun onItemLongClick(gi: GalleryInfo) { + private fun onItemLongClick(gi: GalleryInfo) { val context = requireContext() val activity = mainActivity ?: return - val downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID + val downloaded = DownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID val favourite = gi.favoriteSlot != -2 val items = if (downloaded) { arrayOf( @@ -294,9 +263,7 @@ class HistoryScene : BaseToolbarScene() { ), ) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - mDownloadManager.deleteDownload( - gi.gid, - ) + DownloadManager.deleteDownload(gi.gid) } .show() } else { @@ -344,8 +311,6 @@ class HistoryScene : BaseToolbarScene() { }.show() } - private class ComposeHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView) - private inner class MoveDialogHelper( private val mLabels: Array, private val mGi: GalleryInfo, @@ -358,56 +323,5 @@ class HistoryScene : BaseToolbarScene() { } } - private val height = (Settings.listThumbSize * 3).pxToDp.dp - - private inner class HistoryAdapter(diffCallback: DiffUtil.ItemCallback) : - PagingDataAdapter(diffCallback) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ComposeHolder { - val view = ComposeView(requireContext()) - return ComposeHolder(view) - } - - override fun onBindViewHolder(holder: ComposeHolder, position: Int) { - val gi: GalleryInfo = getItem(position) ?: return - holder.composeView.setContent { - Mdc3Theme { - ListInfoCard( - onClick = ::onItemClick.partially1(gi), - onLongClick = ::onItemLongClick.partially1(gi), - info = gi, - modifier = Modifier.height(height), - ) - } - } - } - } - - private inner class HistoryItemTouchHelperCallback : ItemTouchHelper.Callback() { - override fun getMovementFlags( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - ): Int { - return makeMovementFlags(0, ItemTouchHelper.LEFT) - } - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder, - ): Boolean { - return false - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - val mPosition = viewHolder.bindingAdapterPosition - lifecycleScope.launchIO { - val info: HistoryInfo? = mAdapter.peek(mPosition) - info?.let { EhDB.deleteHistoryInfo(info) } - withUIContext { - mAdapter.refresh() - } - } - } - } + private val cardHeight = (Settings.listThumbSize * 3).pxToDp.dp } diff --git a/app/src/main/java/com/hippo/ehviewer/ui/widget/Theme.kt b/app/src/main/java/com/hippo/ehviewer/ui/widget/Theme.kt new file mode 100644 index 000000000..930fcf545 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/widget/Theme.kt @@ -0,0 +1,7 @@ +package com.hippo.ehviewer.ui.widget + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import com.google.accompanist.themeadapter.material3.Mdc3Theme + +fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) } diff --git a/app/src/main/res/layout/scene_history.xml b/app/src/main/res/layout/scene_history.xml deleted file mode 100644 index 405d8335e..000000000 --- a/app/src/main/res/layout/scene_history.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/scene_history.xml b/app/src/main/res/menu/scene_history.xml deleted file mode 100644 index 4674d81f6..000000000 --- a/app/src/main/res/menu/scene_history.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - -