[Don't use] Initial move GalleryPreviewsScene to compose

This commit is contained in:
tarsin committed 2023-05-10 18:29:52 +08:00
1 parent 5f9bdabf7e
commit a3bdb70ef1
4 files changed
+160 -264

No files matched your search

@@ -1,69 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import arrow.core.partially1
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.ui.widget.CrystalCard
import com.hippo.ehviewer.ui.widget.EhAsyncPreview
import com.hippo.ehviewer.ui.widget.setMD3Content
class GalleryPreviewsAdapter(private val onClick: (GalleryPreview) -> Unit) : ListAdapter<GalleryPreview, GalleryPreviewsAdapter.ComposeHolder>(PreviewDiffCallback) {
class ComposeHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ComposeHolder {
return ComposeHolder(ComposeView(parent.context))
}
override fun onBindViewHolder(holder: ComposeHolder, position: Int) {
val it = getItem(position)
holder.composeView.setMD3Content {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(contentAlignment = Alignment.Center) {
CrystalCard(
onClick = onClick.partially1(it),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.6666667F),
) {
EhAsyncPreview(
model = it,
modifier = Modifier.fillMaxSize(),
)
}
}
Text((it.position + 1).toString())
}
}
}
override fun getItemId(position: Int): Long {
return getItem(position).position.toLong()
}
}
object PreviewDiffCallback : DiffUtil.ItemCallback<GalleryPreview>() {
override fun areItemsTheSame(oldItem: GalleryPreview, newItem: GalleryPreview): Boolean {
return oldItem.position == newItem.position
}
override fun areContentsTheSame(oldItem: GalleryPreview, newItem: GalleryPreview): Boolean {
return oldItem == newItem
}
}
@@ -18,205 +18,147 @@ package com.hippo.ehviewer.ui.scene
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CallMissedOutgoing
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.platform.LocalConfiguration
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import arrow.core.partially1
import coil.imageLoader
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.databinding.DialogGoToBinding
import com.hippo.ehviewer.databinding.SceneGalleryPreviewsBinding
import com.hippo.ehviewer.ui.widget.CrystalCard
import com.hippo.ehviewer.ui.widget.EhAsyncPreview
import com.hippo.ehviewer.ui.widget.setMD3Content
import com.hippo.util.getParcelableCompat
import com.hippo.widget.ContentLayout.ContentHelper
import com.hippo.widget.recyclerview.AutoGridLayoutManager
import com.hippo.widget.recyclerview.STRATEGY_SUITABLE_SIZE
import com.hippo.widget.ContentLayout
import com.hippo.widget.recyclerview.getSpanCountForSuitableSize
import com.hippo.widget.rememberContentState
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import moe.tarsin.coroutines.runSuspendCatching
import java.util.Locale
class GalleryPreviewsScene : BaseToolbarScene() {
private var _binding: SceneGalleryPreviewsBinding? = null
private val binding
get() = _binding!!
private var mGalleryDetail: GalleryDetail? = null
private var mAdapter: GalleryPreviewsAdapter? = null
private var mHelper: GalleryPreviewHelper? = null
private var mHasFirstRefresh = false
private var mNextPage: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun onInit() {
val args = arguments ?: return
mGalleryDetail = args.getParcelableCompat(KEY_GALLERY_DETAIL)
mNextPage = args.getBoolean(KEY_NEXT_PAGE)
}
private fun onRestore(savedInstanceState: Bundle) {
mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)
}
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_GALLERY_DETAIL, mGalleryDetail)
}
override fun onCreateViewWithToolbar(
class GalleryPreviewsScene : BaseScene() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = SceneGalleryPreviewsBinding.inflate(inflater, container, false)
binding.contentLayout.hideFastScroll()
setLiftOnScrollTargetView(binding.contentLayout.recyclerView)
mAdapter = GalleryPreviewsAdapter {
mainActivity!!.startReaderActivity(mGalleryDetail!!, it.position)
}
binding.contentLayout.recyclerView.adapter = mAdapter
val columnWidth = Settings.thumbSize
val layoutManager = AutoGridLayoutManager(context, columnWidth)
layoutManager.setStrategy(STRATEGY_SUITABLE_SIZE)
binding.contentLayout.recyclerView.layoutManager = layoutManager
binding.contentLayout.recyclerView.clipToPadding = false
val padding = resources.getDimensionPixelSize(R.dimen.gallery_grid_margin_h)
val decoration = MarginItemDecoration(padding, padding, padding, padding, padding)
binding.contentLayout.recyclerView.addItemDecoration(decoration)
mHelper = GalleryPreviewHelper()
binding.contentLayout.setHelper(mHelper!!)
return ComposeView(inflater.context).apply {
setMD3Content {
val galleryDetail = rememberSaveable { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
fun onPreviewCLick(index: Int) = mainActivity!!.startReaderActivity(galleryDetail, index)
val toNextPage = rememberSaveable { requireArguments().getBoolean(KEY_NEXT_PAGE) }
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
val showJumpMenuButton by rememberSaveable { mutableStateOf(false) }
// Padding is not subtracted here to have the same column count as gallery list and preview
val totalSpace = LocalConfiguration.current.screenWidthDp
val columnCount = getSpanCountForSuitableSize(totalSpace, Settings.thumbSizeDp)
// Only refresh for the first time
if (!mHasFirstRefresh) {
mHasFirstRefresh = true
mHelper!!.onGetPageData(
0,
mGalleryDetail!!.previewPages,
0,
null,
null,
mGalleryDetail!!.previewList,
)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
if (null != mHelper) {
if (1 == mHelper!!.shownViewIndex) {
mHasFirstRefresh = false
}
}
binding.contentLayout.recyclerView.stopScroll()
_binding = null
mAdapter = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.gallery_previews)
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
if (mGalleryDetail != null) {
if (mGalleryDetail!!.previewPages > 2) showMenu(R.menu.scene_gallery_previews)
}
runCatching {
if (mNextPage && mHelper!!.pages > 1) mHelper!!.goTo(1)
}.onFailure {
it.printStackTrace()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val context = context ?: return false
val id = item.itemId
if (id == R.id.action_go_to) {
if (mHelper == null) {
return true
}
val pages = mHelper!!.pages
if (pages > 1 && mHelper!!.canGoTo()) {
showGoToDialog(context, pages, mHelper!!.pageForBottom)
}
return true
}
return false
}
private inner class GalleryPreviewHelper : ContentHelper<GalleryPreview>() {
override fun getPageData(
taskId: Int,
type: Int,
page: Int,
index: String?,
isNext: Boolean,
) {
val detail = mGalleryDetail ?: run {
onGetException(taskId, EhException(getString(R.string.error_cannot_find_gallery)))
return
}
detail.run {
val url = EhUrl.getGalleryDetailUrl(gid, token, page, false)
lifecycleScope.launchIO {
runSuspendCatching {
EhEngine.getPreviewList(url)
}.onSuccess { result ->
if (Settings.preloadThumbAggressively) {
lifecycleScope.launchIO {
val previewList = result.first.first
previewList.forEach {
context.run {
imageLoader.enqueue(imageRequest(it))
val state = rememberContentState(galleryDetail.previewList)
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.gallery_previews)) },
navigationIcon = {
IconButton(onClick = { findNavController().popBackStack() }) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
actions = {
if (showJumpMenuButton) {
IconButton(onClick = ::showGoto) {
Icon(imageVector = Icons.Default.CallMissedOutgoing, contentDescription = null)
}
}
},
scrollBehavior = scrollBehaviour,
)
},
) {
ContentLayout(state, getData = { page -> getPreviewListByPage(galleryDetail, page) }, modifier = Modifier.padding(top = it.calculateTopPadding())) { data: Array<GalleryPreview> ->
LazyVerticalGrid(
columns = GridCells.Fixed(columnCount),
modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection),
contentPadding = it,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.gallery_grid_margin_h)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.gallery_grid_margin_v)),
) {
items(data) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(contentAlignment = Alignment.Center) {
CrystalCard(
onClick = ::onPreviewCLick.partially1(it.position),
modifier = Modifier.fillMaxWidth().aspectRatio(0.6666667F),
) {
EhAsyncPreview(
model = it,
modifier = Modifier.fillMaxSize(),
)
}
}
Text((it.position + 1).toString())
}
}
}
withUIContext {
mHelper?.takeIf { it.isCurrentTask(taskId) }?.onGetPageData(taskId, result.second, 0, null, null, result.first.first)
}
}.onFailure { throwable ->
withUIContext {
mHelper?.takeIf { it.isCurrentTask(taskId) }?.onGetException(taskId, throwable)
}
}
}
}
}
}
override val context
get() = this@GalleryPreviewsScene.requireContext()
override fun notifyDataSetChanged(callback: () -> Unit) {
mAdapter?.submitList(data.toMutableList(), callback)
private fun showGoto() {
/*
val pages = mHelper!!.pages
if (pages > 1 && mHelper!!.canGoTo()) {
showGoToDialog(requireContext(), pages, mHelper!!.pageForBottom)
}
override fun notifyDataSetChanged() {}
*/
}
override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) {}
override fun isDuplicate(d1: GalleryPreview, d2: GalleryPreview): Boolean {
return false
private suspend fun getPreviewListByPage(galleryDetail: GalleryDetail, page: Int): List<GalleryPreview> {
galleryDetail.run {
val url = EhUrl.getGalleryDetailUrl(gid, token, page, false)
val result = EhEngine.getPreviewList(url)
if (Settings.preloadThumbAggressively) {
lifecycleScope.launchIO { context?.run { result.first.first.forEach { imageLoader.enqueue(imageRequest(it)) } } }
}
return result.first.first
}
}
@@ -231,7 +173,7 @@ class GalleryPreviewsScene : BaseToolbarScene() {
.setView(dialogBinding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
val page = (dialogBinding.slider.value - 1).toInt()
mHelper!!.goTo(page)
// mHelper!!.goTo(page)
}
.create()
dialog.show()
@@ -240,6 +182,5 @@ class GalleryPreviewsScene : BaseToolbarScene() {
companion object {
const val KEY_GALLERY_DETAIL = "gallery_detail"
const val KEY_NEXT_PAGE = "next_page"
private const val KEY_HAS_FIRST_REFRESH = "has_first_refresh"
}
}
@@ -3,28 +3,75 @@ package com.hippo.widget
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.hippo.widget.pullrefresh.PullRefreshIndicator
import com.hippo.widget.pullrefresh.pullRefresh
import com.hippo.widget.pullrefresh.rememberPullRefreshState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
@Composable
inline fun <reified T : Parcelable> ContentLayout(
content: @Composable (Array<T>) -> Unit,
state: ContentState<T>,
noinline getData: suspend (Int) -> List<T>,
modifier: Modifier = Modifier,
content: @Composable (Array<T>) -> Unit,
) {
var refreshing by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = {
refreshing = true
if (state.startPage == 0) {
coroutineScope.launch {
val result = runSuspendCatching { getData(0) }.getOrNull()
result?.let {
state.data.clear()
state.data.addAll(it)
}
}.invokeOnCompletion { refreshing = false }
} else {
TODO()
}
})
val data = rememberSaveable { mutableStateListOf<T>() }
Box(modifier = modifier.pullRefresh(refreshState)) {
content(data.toTypedArray())
PullRefreshIndicator(refreshing = refreshing, state = refreshState)
Box(modifier = Modifier.pullRefresh(refreshState)) {
content(state.data.toTypedArray())
PullRefreshIndicator(refreshing = refreshing, state = refreshState, modifier = modifier.align(Alignment.TopCenter))
}
}
class ContentState<T>(
val data: SnapshotStateList<T>,
_startPage: MutableState<Int>,
_endPage: MutableState<Int>,
) {
var startPage by _startPage
var endPage by _endPage
}
@Composable
fun <T> rememberContentState(
initialData: List<T>,
): ContentState<T> {
val snapshotStateListSaver = remember {
listSaver(
save = { it.toList() },
restore = { mutableStateListOf<T>().apply { addAll(it) } },
)
}
val data = rememberSaveable(saver = snapshotStateListSaver) { mutableStateListOf<T>().apply { addAll(initialData) } }
val startPage = rememberSaveable { mutableStateOf(0) }
val endPage = rememberSaveable { mutableStateOf(0) }
return remember { ContentState(data, startPage, endPage) }
}
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 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.
-->
<com.hippo.widget.ContentLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />