Compare commits

...
Author SHA1 Message Date
tarsin fc24a18ac5 Bump to 1.8.8.0-beta
Releases / Build (push) Has been cancelled
2023-05-19 19:19:43 +08:00
tarsin a9a8c68b94 Fix code style, fuck Android Studio 2023-05-19 18:55:18 +08:00
tarsin 193d2f8d9e Fix rememberMemorized implementation 2023-05-19 18:50:17 +08:00
tarsin 538da9b98f Cleanup rememberSaveable usage 2023-05-19 17:25:29 +08:00
tarsin 071b597552 Introduce rememberMemorized 2023-05-19 17:06:32 +08:00
tarsin 8926d74378 Introduce rememberGlobally 2023-05-19 13:10:15 +08:00
tarsin 39a4b03bc3 Avoid TransactionTooLargeException 2023-05-19 12:16:07 +08:00
tarsin 9202bbc7dc Workaround #1092 2023-05-19 11:43:21 +08:00
tarsin f5601ebca1 Don't saveInstanceState when data too large, Workaround #1092 2023-05-19 10:50:18 +08:00
tarsin 2f978b554e Adjust kotlin compiler options 2023-05-18 13:55:06 +08:00
tarsin f48a90d941 Upgrade r8 2023-05-18 13:29:41 +08:00
tarsin 4f0d9fa83c Allow downsampling on LargePreviews 2023-05-18 11:52:25 +08:00
tarsin 57a747ed9c Fix cycled preview when PreviewPages == 1 2023-05-17 23:39:19 +08:00
tarsin 746a045328 Bump to 1.8.8.0-alpha
Releases / Build (push) Has been cancelled
2023-05-17 22:49:34 +08:00
tarsin 2a4c7823c2 Bind downloadHolder with coroutine 2023-05-17 22:42:55 +08:00
tarsin b995510df1 Resolve #1082, #1081 again 2023-05-17 22:21:17 +08:00
tarsin a2852d6df9 Resolve #1082, #1081 again 2023-05-17 19:08:07 +08:00
tarsin 47863580a0 Resolve #1082, #1081 again 2023-05-17 19:01:39 +08:00
tarsin 9af6532331 Resolve #1082, #1081 2023-05-17 16:28:50 +08:00
9 changed files with 151 additions and 72 deletions

No files matched your search

+3 -1
View File
@@ -58,7 +58,7 @@ android {
minSdk = 28
targetSdk = 33
versionCode = 180038
versionName = "1.8.8.0-preview"
versionName = "1.8.8.0-beta"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -95,6 +95,8 @@ android {
freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-Xjvm-default=all",
"-Xlambdas=indy",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
@@ -11,6 +11,7 @@ import coil.request.ImageRequest
import coil.size.Size
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.NormalGalleryPreview
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.client.url
@@ -19,14 +20,13 @@ fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
data(info.thumbUrl)
memoryCacheKey(key)
diskCacheKey(key)
size(Size.ORIGINAL)
}
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
data(preview.url)
memoryCacheKey(preview.imageKey)
diskCacheKey(preview.imageKey)
size(Size.ORIGINAL)
if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
}
private val stubResult = DecodeResult(ColorDrawable(Color.BLACK), false)
@@ -0,0 +1,56 @@
package com.hippo.ehviewer.ui.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
private val remembered = hashMapOf<String, Any>()
// This works mostly like [rememberSaveable], but saves data in global hashmap, to avoid [TransactionTooLargeException]
@Composable
fun <T : Any> rememberMemorized(
vararg inputs: Any?,
key: String? = null,
init: () -> T,
): T {
// key is the one provided by the user or the one generated by the compose runtime
val finalKey = if (!key.isNullOrEmpty()) {
key
} else {
// currentCompositeKeyHash is unreliable here
// Is this Composable inlined or its groups elided?
// Or a compose compiler bug?
init.javaClass.toString()
}
val registry = LocalSaveableStateRegistry.current
// value is restored using the registry or created via [init] lambda
val value = remember(*inputs) {
@Suppress("UNCHECKED_CAST")
val restored = registry?.consumeRestored(finalKey)?.let {
remembered.remove(it as String)
} as T?
restored ?: init()
}
// re-register if the registry or key has been changed
if (registry != null) {
// we want to use the latest instances of saver and value in the valueProvider lambda
// without restarting DisposableEffect as it would cause re-registering the provider in
// the different order. so we use rememberUpdatedState.
val valueState = rememberUpdatedState(value)
DisposableEffect(registry, finalKey) {
val valueProvider = {
finalKey.also { remembered[it] = valueState.value }
}
val entry = registry.registerProvider(finalKey, valueProvider)
onDispose {
entry.unregister()
}
}
}
return value
}
@@ -28,6 +28,7 @@ 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.ViewTransition.OnShowViewListener
import com.hippo.ehviewer.ui.legacy.easyrecyclerview.HandlerDrawable
@@ -41,7 +42,7 @@ import com.hippo.ehviewer.yorozuya.collect.IntList
import rikka.core.res.resolveColor
class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
private lateinit var mContentHelper: ContentHelper<*>
private lateinit var mContentHelper: ContentHelper
private val mRecyclerViewOriginBottom: Int
private val mFastScrollerOriginBottom: Int
private val binding: WidgetContentLayoutBinding
@@ -64,7 +65,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
val fastScroller
get() = binding.fastScroller
fun setHelper(helper: ContentHelper<*>) {
fun setHelper(helper: ContentHelper) {
mContentHelper = helper
helper.init(this)
}
@@ -128,7 +129,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state))
}
abstract class ContentHelper<E : Parcelable> : OnShowViewListener {
abstract class ContentHelper : OnShowViewListener {
/**
* Generate task id
*/
@@ -143,7 +144,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
/**
* Store data
*/
private var mData = ArrayList<E>()
private var mData = ArrayList<GalleryInfo>()
/**
* Store the page divider index
@@ -287,10 +288,10 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
mEmptyString = str
}
val data: List<E>
val data: List<GalleryInfo>
get() = mData
fun getDataAtEx(location: Int): E? {
fun getDataAtEx(location: Int): GalleryInfo? {
return if (location >= 0 && location < mData.size) {
mData[location]
} else {
@@ -298,7 +299,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
}
}
val firstVisibleItem: E?
val firstVisibleItem: GalleryInfo?
get() = getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager))
fun size(): Int {
@@ -309,8 +310,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
return mCurrentTaskId == taskId
}
protected abstract fun isDuplicate(d1: E, d2: E): Boolean
private fun removeDuplicateData(data: List<E>, start: Int, end: Int) {
protected abstract fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean
private fun removeDuplicateData(data: List<GalleryInfo>, start: Int, end: Int) {
val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size))
data.dropWhile { d1 ->
slicedData.any { d2 ->
@@ -319,8 +320,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
}
}
protected open fun onAddData(data: List<E>) {}
protected open fun onRemoveData(data: List<E>) {}
protected open fun onAddData(data: List<GalleryInfo>) {}
protected open fun onRemoveData(data: List<GalleryInfo>) {}
protected open fun onClearData() {}
fun onGetPageData(
taskId: Int,
@@ -328,7 +329,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
nextPage: Int,
prev: String?,
next: String?,
data: List<E>,
data: List<GalleryInfo>,
) {
if (mCurrentTaskId == taskId) {
val dataSize: Int
@@ -734,7 +735,11 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
val shownView = mViewTransition.shownViewIndex
bundle.putInt(KEY_SHOWN_VIEW, shownView)
bundle.putString(KEY_TIP, binding.tip.text.toString())
bundle.putParcelableArrayList(KEY_DATA, mData)
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)
@@ -749,11 +754,12 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
return if (state is Bundle) {
mViewTransition.showView(state.getInt(KEY_SHOWN_VIEW), false)
binding.tip.text = state.getString(KEY_TIP)
@Suppress("DEPRECATION")
val newData = state.getParcelableArrayList<E>(KEY_DATA)
newData?.let { mData = it }
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)!!
mPageDivider = state.getParcelableCompat(KEY_PAGE_DIVIDER) ?: IntList()
mStartPage = state.getInt(KEY_START_PAGE)
mEndPage = state.getInt(KEY_END_PAGE)
pages = state.getInt(KEY_PAGES)
@@ -761,7 +767,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
mNext = state.getString(KEY_NEXT)
notifyDataSetChanged()
notifyDataSetChanged {}
if (newData == null) {
if (mData.size == 0) {
mPageDivider.clear()
mStartPage = 0
mEndPage = 0
@@ -803,3 +809,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
}
}
}
// data "ArrayList<GalleryInfo>" is too large, we cannot put it as Parcelable otherwise TransactionTooLargeException would occur
// TODO: convert to paging3 source then keep it in a proper window
// @see PagingConfig.maxSize
private val contentMap = hashMapOf<Int, ArrayList<GalleryInfo>>()
@@ -28,7 +28,7 @@ import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo>() {
abstract class GalleryInfoContentHelper : ContentHelper() {
@JvmField
var jumpTo: String? = null
private val listener: FavouriteStatusRouter.Listener
@@ -19,7 +19,6 @@ import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable
import coil.imageLoader
import coil.request.ImageRequest
import coil.size.Size
import com.hippo.ehviewer.coil.imageRequest
class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) :
@@ -31,7 +30,6 @@ class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) :
mTextView.setOnWindowAttachListener(this)
mImageRequest = mTextView.context.imageRequest {
target(onSuccess = { onGetValue(it) })
size(Size.ORIGINAL)
data(url)
}
load()
@@ -876,57 +876,68 @@ class DownloadsScene :
private val height = (Settings.listThumbSize * 3).pxToDp.dp
fun bind(info: DownloadInfo) {
val downloadDir = SpiderDen.getGalleryDownloadDir(info.gid)?.takeIf { it.isDirectory }
check(downloadDir != null)
val thumbLocation = downloadDir.subFile(".thumb")!!
val localReq = thumbLocation.takeIf { it.exists() }?.uri?.let {
context?.imageRequest {
data(it.toString())
memoryCacheKey(info.thumbKey)
lifecycleScope.launchIO {
val downloadDir = SpiderDen.getGalleryDownloadDir(info.gid) ?: run {
val title = EhUtils.getSuitableTitle(info)
val dirname = FileUtils.sanitizeFilename("${info.gid}-$title")
EhDB.putDownloadDirname(info.gid, dirname)
SpiderDen.getGalleryDownloadDir(info.gid)!!
}
}
binding.thumb.setMD3Content {
Card(onClick = ::onClick.partially1(binding.thumb)) {
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
AsyncImage(
model = localReq ?: requestOf(info),
contentDescription = null,
modifier = Modifier.height(height).aspectRatio(0.6666667F),
onState = { state ->
if (state is AsyncImagePainter.State.Success) {
state.result.drawable.run {
if (CropDefaults.shouldCrop(intrinsicWidth, intrinsicHeight)) {
contentScale = ContentScale.Crop
}
}
coroutineScope.launch {
if (!thumbLocation.exists()) {
thumbLocation.ensureFile()
val key = info.thumbKey!!
imageCache[key]?.use {
UniFile.fromFile(it.data.toFile())!!.openFileDescriptor("r").use { src ->
thumbLocation.openFileDescriptor("w").use { dst ->
src sendTo dst
check(downloadDir.ensureDir())
val thumbLocation = downloadDir.subFile(".thumb")!!
val localReq = thumbLocation.takeIf { it.exists() }?.uri?.let {
context?.imageRequest {
data(it.toString())
memoryCacheKey(info.thumbKey)
}
}
withUIContext {
binding.thumb.setMD3Content {
Card(onClick = ::onClick.partially1(binding.thumb)) {
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
AsyncImage(
model = localReq ?: requestOf(info),
contentDescription = null,
modifier = Modifier.height(height).aspectRatio(0.6666667F),
onState = { state ->
if (state is AsyncImagePainter.State.Success) {
state.result.drawable.run {
if (CropDefaults.shouldCrop(intrinsicWidth, intrinsicHeight)) {
contentScale = ContentScale.Crop
}
}
coroutineScope.launch {
runCatching {
if (!thumbLocation.exists()) {
thumbLocation.ensureFile()
val key = info.thumbKey!!
imageCache[key]?.use {
UniFile.fromFile(it.data.toFile())!!.openFileDescriptor("r").use { src ->
thumbLocation.openFileDescriptor("w").use { dst ->
src sendTo dst
}
}
}
}
}
}
}
}
}
if (state is AsyncImagePainter.State.Error) {
if (thumbLocation.exists()) {
coroutineScope.launch {
thumbLocation.delete()
withUIContext {
bind(info)
if (state is AsyncImagePainter.State.Error) {
if (thumbLocation.exists()) {
coroutineScope.launch {
thumbLocation.delete()
withUIContext {
bind(info)
}
}
}
}
}
}
},
contentScale = contentScale,
)
},
contentScale = contentScale,
)
}
}
}
}
binding.title.text = EhUtils.getSuitableTitle(info)
@@ -57,6 +57,7 @@ import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.ui.compose.data.EhPreviewItem
import com.hippo.ehviewer.ui.compose.rememberDialogState
import com.hippo.ehviewer.ui.compose.rememberMemorized
import com.hippo.ehviewer.ui.compose.setMD3Content
import com.hippo.ehviewer.ui.legacy.recyclerview.calculateSuitableSpanCount
import com.hippo.ehviewer.ui.navToReader
@@ -72,7 +73,7 @@ class GalleryPreviewScreen : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply {
setMD3Content {
val galleryDetail = rememberSaveable { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
val galleryDetail = remember { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
val context = LocalContext.current
fun onPreviewCLick(index: Int) = context.navToReader(galleryDetail, index)
val toNextPage = rememberSaveable { requireArguments().getBoolean(KEY_NEXT_PAGE) }
@@ -118,14 +119,14 @@ class GalleryPreviewScreen : Fragment() {
initialKey = goto
}
val previewPagesMap = rememberSaveable { mutableMapOf<Int, PreviewPage>().apply { put(1, galleryDetail.previewList) } }
val previewPagesMap = rememberMemorized { mutableMapOf<Int, PreviewPage>().apply { put(1, galleryDetail.previewList) } }
val data = remember(initialKey) {
Pager(PagingConfig(1, prefetchDistance = 4, enablePlaceholders = false, initialLoadSize = 1), initialKey) {
object : PagingSource<Int, PreviewPage>() {
override fun getRefreshKey(state: PagingState<Int, PreviewPage>) = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PreviewPage> {
val up = params.key ?: 1
val end = up + params.loadSize - 1
val end = (up + params.loadSize - 1).coerceAtMost(galleryDetail.previewPages)
runSuspendCatching {
(up..end).mapNotNull { it.takeUnless { previewPagesMap.contains(it) } }
.parMap(Dispatchers.IO) { getPreviewListByPage(it - 1).apply { previewPagesMap[it] = this } }
+1 -1
View File
@@ -15,6 +15,6 @@ tasks.register("Delete", Delete::class) {
buildscript {
dependencies {
classpath("com.android.tools:r8:8.2.6-dev")
classpath("com.android.tools:r8:8.2.7-dev")
}
}