diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index 2e8131ae7..3dfbf25a9 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -55,6 +55,7 @@ import com.hippo.ehviewer.legacy.cleanObsoleteCache import com.hippo.ehviewer.ui.keepNoMediaFileStatus import com.hippo.ehviewer.ui.lockObserver import com.hippo.ehviewer.ui.tools.dataStateFlow +import com.hippo.ehviewer.ui.tools.initSETConnection import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.Crash import com.hippo.ehviewer.util.FavouriteStatusRouter @@ -83,6 +84,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory { override fun onCreate() { + initSETConnection() // Initialize Settings on first access lifecycleScope.launchIO { val mode = Settings.theme diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt index b6b7fb77e..6e11863a3 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt @@ -45,6 +45,7 @@ import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.ui.tools.CrystalCard import com.hippo.ehviewer.ui.tools.GalleryListCardRating import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope +import com.hippo.ehviewer.ui.tools.listThumbGenerator import com.hippo.ehviewer.util.FileUtils private val ids = Tuple9(1, 2, 3, 4, 5, 6, 7, 8, 9) @@ -108,10 +109,12 @@ fun DownloadCard( Row { val thumb = remember { movableContentOf { - EhAsyncCropThumb( - key = it, - modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(), - ) + with(listThumbGenerator) { + EhAsyncCropThumb( + key = it, + modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(), + ) + } } } if (selectMode) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt index 71e5be48f..f61c35f7b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt @@ -17,7 +17,9 @@ import coil3.compose.AsyncImage import coil3.request.ImageRequest import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.ktbuilder.imageRequest +import com.hippo.ehviewer.ui.tools.SETNodeGenerator import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope +import com.hippo.ehviewer.ui.tools.sharedBounds import com.hippo.ehviewer.ui.tools.shouldCrop @Composable @@ -27,7 +29,7 @@ fun requestOf(model: GalleryInfo): ImageRequest { return remember(model) { context.imageRequest(model) } } -context(SharedTransitionScope, TransitionsVisibilityScope) +context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator) @Composable @NonRestartableComposable fun EhAsyncThumb( @@ -38,17 +40,14 @@ fun EhAsyncThumb( ) = AsyncImage( model = requestOf(model), contentDescription = null, - modifier = modifier - // https://github.com/FooIbar/EhViewer/issues/1527 - // .sharedBounds(key = "${model.gid}") - .clip(ShapeDefaults.Medium), + modifier = modifier.sharedBounds(key = "${model.gid}").clip(ShapeDefaults.Medium), onSuccess = onSuccess?.let { callback -> { callback(it.result.image) } }, contentScale = contentScale, ) -context(SharedTransitionScope, TransitionsVisibilityScope) +context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator) @Composable fun EhAsyncCropThumb( key: GalleryInfo, @@ -58,10 +57,7 @@ fun EhAsyncCropThumb( AsyncImage( model = requestOf(key), contentDescription = null, - modifier = modifier - // https://github.com/FooIbar/EhViewer/issues/1527 - // .sharedBounds(key = "${key.gid}") - .clip(ShapeDefaults.Medium), + modifier = modifier.sharedBounds(key = "${key.gid}").clip(ShapeDefaults.Medium), onSuccess = { if (it.result.image.shouldCrop) { contentScale = ContentScale.Crop diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt index 3f573616f..f7ccaf4e0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryDetail.kt @@ -37,6 +37,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.icons.EhIcons import com.hippo.ehviewer.icons.big.SadAndroid import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope +import com.hippo.ehviewer.ui.tools.detailThumbGenerator @Composable fun GalleryDetailHeaderInfoCard( @@ -109,13 +110,15 @@ fun GalleryDetailHeaderCard( ) = ElevatedCard(modifier = modifier) { Row { Card { - EhAsyncCropThumb( - key = remember(info.gid) { info }, - modifier = Modifier.size( - dimensionResource(id = R.dimen.gallery_detail_thumb_width), - dimensionResource(id = R.dimen.gallery_detail_thumb_height), - ), - ) + with(detailThumbGenerator) { + EhAsyncCropThumb( + key = remember(info.gid) { info }, + modifier = Modifier.size( + dimensionResource(id = R.dimen.gallery_detail_thumb_width), + dimensionResource(id = R.dimen.gallery_detail_thumb_height), + ), + ) + } } Spacer(modifier = Modifier.weight(0.5F)) Column( diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt index efe31edf3..0ff149e7c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt @@ -53,6 +53,7 @@ import com.hippo.ehviewer.ui.tools.CrystalCard import com.hippo.ehviewer.ui.tools.ElevatedCard import com.hippo.ehviewer.ui.tools.GalleryListCardRating import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope +import com.hippo.ehviewer.ui.tools.listThumbGenerator import com.hippo.ehviewer.util.FavouriteStatusRouter private val ids = Tuple7(1, 2, 3, 4, 5, 6, 7) @@ -108,10 +109,12 @@ fun GalleryInfoListItem( ) { Row { Card { - EhAsyncCropThumb( - key = info, - modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(), - ) + with(listThumbGenerator) { + EhAsyncCropThumb( + key = info, + modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(), + ) + } } ConstraintLayout( modifier = Modifier.padding(start = 8.dp, top = 2.dp, end = 4.dp, bottom = 4.dp).fillMaxSize(), @@ -227,13 +230,15 @@ fun GalleryInfoGridItem( } mutableFloatStateOf(ratio) } - EhAsyncThumb( - model = info, - modifier = Modifier.aspectRatio(ratio), - onSuccess = { - ratio = (it.width.toFloat() / it.height).coerceIn(MIN_RATIO, MAX_RATIO) - }, - ) + with(listThumbGenerator) { + EhAsyncThumb( + model = info, + modifier = Modifier.aspectRatio(ratio), + onSuccess = { + ratio = (it.width.toFloat() / it.height).coerceIn(MIN_RATIO, MAX_RATIO) + }, + ) + } val categoryColor = EhUtils.getCategoryColor(info.category) Badge( modifier = Modifier.align(Alignment.TopEnd).widthIn(min = 32.dp).height(24.dp), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt index b1280aa3f..0fa4133e2 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt @@ -148,7 +148,6 @@ import com.hippo.ehviewer.util.addTextToClipboard import com.hippo.ehviewer.util.awaitActivityResult import com.hippo.ehviewer.util.bgWork import com.hippo.ehviewer.util.displayString -import com.hippo.ehviewer.util.findActivity import com.hippo.ehviewer.util.isAtLeastQ import com.hippo.ehviewer.util.requestPermission import com.hippo.files.delete @@ -220,12 +219,11 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n } val (gid, token) = remember { when (args) { - is GalleryInfoArgs -> args.galleryInfo.run { gid to token } + is GalleryInfoArgs -> with(args.galleryInfo) { gid to token } is TokenArgs -> args.gid to args.token } } val galleryDetailUrl = remember { EhUrl.getGalleryDetailUrl(gid, token, 0, false) } - val activity = remember { findActivity() } var showReadAction by rememberSaveable { mutableStateOf(true) } LaunchedEffect(args, galleryInfo) { if (showReadAction) { @@ -244,9 +242,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n } } } - with(activity) { - ProvideAssistContent(galleryDetailUrl) - } + ProvideAssistContent(galleryDetailUrl) var getDetailError by rememberSaveable { mutableStateOf("") } val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -330,7 +326,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n } val name = "$gid-${EhUtils.getSuitableTitle(galleryDetail)}.zip" try { - activity.startActivity(intent) + startActivity(intent) withUIContext { addTextToClipboard(name, true) } } catch (_: ActivityNotFoundException) { val r = DownloadManager.Request(uri) @@ -712,28 +708,26 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n } launchIO { awaitSelectAction { - with(activity) { - onSelect(copy) { - addTextToClipboard(tag) - } - if (temp != translated) { - onSelect(copyTrans) { - addTextToClipboard(translated) - } - } - onSelect(showDefine) { - openBrowser(EhUrl.getTagDefinitionUrl(temp)) - } - onSelect(addFilter) { - awaitPermissionOrCancel { Text(text = stringResource(R.string.filter_the_tag, tag)) } - Filter(FilterMode.TAG, tag).remember() - showSnackbar(filterAdded) - } - if (galleryDetail.apiUid >= 0) { - onSelect(upTag) { galleryDetail.voteTag(tag, 1) } - onSelect(downTag) { galleryDetail.voteTag(tag, -1) } + onSelect(copy) { + addTextToClipboard(tag) + } + if (temp != translated) { + onSelect(copyTrans) { + addTextToClipboard(translated) } } + onSelect(showDefine) { + openBrowser(EhUrl.getTagDefinitionUrl(temp)) + } + onSelect(addFilter) { + awaitPermissionOrCancel { Text(text = stringResource(R.string.filter_the_tag, tag)) } + Filter(FilterMode.TAG, tag).remember() + showSnackbar(filterAdded) + } + if (galleryDetail.apiUid >= 0) { + onSelect(upTag) { galleryDetail.voteTag(tag, 1) } + onSelect(downTag) { galleryDetail.voteTag(tag, -1) } + } }() } }, @@ -832,7 +826,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n val onDownloadButtonClick = rememberLambda(galleryInfo) { galleryDetail ?: return@rememberLambda if (EhDownloadManager.getDownloadState(galleryDetail.gid) == DownloadInfo.STATE_INVALID) { - launchUI { startDownload(activity, false, galleryDetail.galleryInfo) } + launchUI { startDownload(implicit(), false, galleryDetail.galleryInfo) } } else { launch { confirmRemoveDownload(galleryDetail) } } @@ -1000,7 +994,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n } IconButton( onClick = { - AppHelper.share(activity, galleryDetailUrl) + AppHelper.share(implicit(), galleryDetailUrl) // In case the link is copied to the clipboard Settings.clipboardTextHashCode = galleryDetailUrl.hashCode() }, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Modifier.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Modifier.kt index 11eb04a52..087ca3d16 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Modifier.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Modifier.kt @@ -1,41 +1,6 @@ package com.hippo.ehviewer.ui.tools -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier inline fun Modifier.thenIf(condition: Boolean, crossinline block: Modifier.() -> Modifier) = if (condition) block() else this - -val NoopTransitionsVisibilityScope = TransitionsVisibilityScope(emptySet()) - -@JvmInline -@Stable -value class TransitionsVisibilityScope(val scopes: Set) - -context(TransitionsVisibilityScope) -@Composable -inline fun togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) = - block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) }) - -context(SharedTransitionScope, TransitionsVisibilityScope) -@Composable -fun Modifier.sharedBounds( - key: String, - enter: EnterTransition = fadeIn(), - exit: ExitTransition = fadeOut(), -) = scopes.fold(this) { modifier, scope -> - modifier.sharedBounds( - rememberSharedContentState("$key + ${scope.transition.label}"), - scope, - enter, - exit, - ) -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt new file mode 100644 index 000000000..a02dc30d7 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SharedElementTransition.kt @@ -0,0 +1,82 @@ +package com.hippo.ehviewer.ui.tools + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import arrow.atomic.AtomicInt + +val NoopTransitionsVisibilityScope = TransitionsVisibilityScope(emptySet()) + +@JvmInline +@Stable +value class TransitionsVisibilityScope(val scopes: Set) + +context(TransitionsVisibilityScope) +@Composable +inline fun togetherWith(scope: AnimatedVisibilityScope, block: @Composable TransitionsVisibilityScope.() -> T) = + block(remember(scopes, scope) { TransitionsVisibilityScope(scopes + scope) }) + +context(SharedTransitionScope, TransitionsVisibilityScope, SETNodeGenerator) +@Composable +fun Modifier.sharedBounds( + key: String, + enter: EnterTransition = fadeIn(), + exit: ExitTransition = fadeOut(), +) = remember(key) { summon(key) }.let { node -> + DisposableEffect(key) { + onDispose { + dispose(node) + } + } + scopes.fold(this) { modifier, scope -> + modifier.sharedBounds( + rememberSharedContentState("${node.syntheticKey} + ${scope.transition.label}"), + scope, + enter, + exit, + ) + } +} + +data class SETNode( + val contentKey: String, + val uniqueID: String, +) + +val SETNode.syntheticKey: String + get() = contentKey + uniqueID + +val listThumbGenerator = SETNodeGenerator() +val detailThumbGenerator = SETNodeGenerator() + +fun initSETConnection() { + listThumbGenerator connectTo detailThumbGenerator +} + +private val atomicIncId = AtomicInt() + +class SETNodeGenerator { + private val tracker = hashMapOf() + private val opposites = mutableListOf() + + fun summon(contentKey: String): SETNode { + val node = opposites.firstNotNullOfOrNull { it.tracker[contentKey] } ?: SETNode(contentKey = contentKey, uniqueID = "${atomicIncId.getAndIncrement()}") + tracker[contentKey] = node + return node + } + + fun dispose(node: SETNode) = tracker.remove(node.contentKey, node) + + infix fun connectTo(other: SETNodeGenerator) { + opposites.add(other) + other.opposites.add(this) + } +}