From f6895c2163af4a521d554b9e97dcff1aaf580cf6 Mon Sep 17 00:00:00 2001 From: Novachrono-Ryhze Date: Wed, 13 Aug 2025 12:13:14 +0200 Subject: [PATCH 1/3] bug(patch)- privacy policy screen blank with no display --- .../main/kotlin/eu/peernetwork/app/ui/privacy/PrivacyScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/privacy/PrivacyScreen.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/privacy/PrivacyScreen.kt index 376676e89..c3161c064 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/privacy/PrivacyScreen.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/privacy/PrivacyScreen.kt @@ -26,7 +26,7 @@ fun PrivacyScreen( webViewClient = WebViewClient() }) } - val link = remember { mutableStateOf(null) } + val link = remember(url) { mutableStateOf(url) } Column( modifier = Modifier .fillMaxSize() From d298f50cf5dcddc07eb6ae358645cac103691fae Mon Sep 17 00:00:00 2001 From: Novachrono-Ryhze Date: Tue, 19 Aug 2025 12:07:01 +0200 Subject: [PATCH 2/3] On Double-click likes the posts with Animation --- blog/ui/build.gradle.kts | 2 +- .../peernetwork/blog/ui/compose/MediaView.kt | 93 +++++++++++++++-- .../peernetwork/blog/ui/compose/PostItem.kt | 7 ++ .../peernetwork/blog/ui/compose/PostView.kt | 95 ++++++++++++++++-- .../blog/ui/timeline/photo/PhotoListing.kt | 7 ++ .../blog/ui/timeline/video/VideoListing.kt | 15 +-- core/ui/src/main/res/raw/like.lottie | Bin 0 -> 1889 bytes 7 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 core/ui/src/main/res/raw/like.lottie diff --git a/blog/ui/build.gradle.kts b/blog/ui/build.gradle.kts index b5ca7cd8d..274f13f9b 100644 --- a/blog/ui/build.gradle.kts +++ b/blog/ui/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { implementation(project(":blog:domain")) implementation(libs.dagger) ksp(libs.dagger.compiler) - + implementation(libs.lottie) testImplementation(libs.junit) testImplementation(libs.kotlin.test) testImplementation(libs.androidx.test) diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt index 8b5c145d8..b1421a0e0 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt @@ -3,12 +3,12 @@ package eu.peernetwork.blog.ui.compose import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -17,22 +17,41 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.blog.ui.model.UiAction +import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.core.ui.R import eu.peernetwork.core.ui.design.compose.DesignTextButton import eu.peernetwork.core.ui.theme.PeerTheme +import androidx.compose.foundation.gestures.detectTapGestures +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.ui.Alignment @Composable fun MediaView( @@ -40,7 +59,9 @@ fun MediaView( description: String, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), + isLiked: Boolean = false, onClick: () -> Unit = {}, + onDoubleClick: () -> Unit = {}, onAuthorClick: () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, engagements: @Composable RowScope.() -> Unit = {}, @@ -53,6 +74,35 @@ fun MediaView( val updatedCaption by rememberUpdatedState(caption) val updatedEngagements by rememberUpdatedState(engagements) val updatedModeration by rememberUpdatedState(moderation) + val click by rememberUpdatedState(onClick) + val dblClick by rememberUpdatedState(onDoubleClick) + + val (playHeart, setPlayHeart) = remember { mutableStateOf(false) } + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.like) + ) + val heartProgress by animateLottieCompositionAsState( + composition = composition, + isPlaying = playHeart, + iterations = 1, + speed = 1.0f + ) + val (prevLiked, setPrevLiked) = remember { mutableStateOf(isLiked) } + LaunchedEffect(isLiked) { + if (!prevLiked && isLiked) { + setPlayHeart(false) + setPlayHeart(true) + } + setPrevLiked(isLiked) + } + + LaunchedEffect(heartProgress) { + if (heartProgress >= 1f) setPlayHeart(false) + } + + val scope = rememberCoroutineScope() + val (singleTapJob, setSingleTapJob) = remember { mutableStateOf(null) } + PostScaffold( modifier = modifier, header = {}, @@ -73,13 +123,39 @@ fun MediaView( footer = { } ) { Box( - modifier = Modifier.clickable( - enabled = true, - role = Role.Button, - onClick = onClick - ) + modifier = Modifier.semantics { role = Role.Button } + .pointerInput(Unit) { + detectTapGestures( + onTap = { + val job = scope.launch { + delay(120) + click() + } + setSingleTapJob(job) + }, + onDoubleTap = { + singleTapJob?.cancel() + dblClick() + } + ) } ) { updatedContent() + + AnimatedVisibility( + visible = playHeart && heartProgress < 1f, + enter = fadeIn(animationSpec = tween(160)), + exit = fadeOut(animationSpec = tween(220)), + modifier = Modifier.align(Alignment.Center) + ) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + LottieAnimation( + composition = composition, + progress = { heartProgress }, + modifier = Modifier.size(160.dp) + ) + } + } + Image( painter = painterResource(R.drawable.overlay_gradient), contentDescription = null, @@ -120,6 +196,7 @@ fun PreviewMediaPreview() { isfollowed = false ), description = "Description...", + isLiked = false, engagements = { UiAction.ENGAGEMENTS.forEach { DesignTextButton( diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostItem.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostItem.kt index a99aef5ea..65455b0f6 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostItem.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostItem.kt @@ -1,3 +1,4 @@ +@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) package eu.peernetwork.blog.ui.compose import androidx.compose.foundation.layout.RowScope @@ -16,7 +17,9 @@ import eu.peernetwork.blog.ui.model.UiPost fun LazyItemScope.PostItem( post: UiPost, position: Int, + isLiked: Boolean = false, onClick: () -> Unit = {}, + onDoubleClick: () -> Unit = {}, onAuthorClick: () -> Unit = {}, onMentionClick: (String) -> Unit = {}, onHashtagClick: (String) -> Unit = {}, @@ -37,6 +40,7 @@ fun LazyItemScope.PostItem( MediaView( author = post.author, description = post.time, + isLiked = isLiked, modifier = Modifier .padding(bottom = 16.dp), caption = { @@ -52,6 +56,7 @@ fun LazyItemScope.PostItem( engagements = engagements, moderation = moderation, onClick = onClick, + onDoubleClick = onDoubleClick, onAuthorClick = onAuthorClick, actions = actions ) { updatedContent(post) } @@ -59,11 +64,13 @@ fun LazyItemScope.PostItem( TextPreview( author = post.author, description = post.time, + isLiked = isLiked, modifier = Modifier.padding(bottom = 12.dp) .padding(horizontal = 8.dp), engagements = engagements, moderation = moderation, onClick = onClick, + onDoubleClick = onDoubleClick, onAuthorClick = onAuthorClick, actions = actions ) { diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt index 031005de7..724cb0195 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt @@ -1,8 +1,6 @@ package eu.peernetwork.blog.ui.compose -import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -18,25 +16,47 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.blog.ui.model.UiAction +import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.core.ui.design.compose.DesignTextButton import eu.peernetwork.core.ui.theme.PeerTheme +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import eu.peernetwork.core.ui.R +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut @Composable fun TextPreview( author: UiAuthor, description: String, modifier: Modifier = Modifier, + isLiked: Boolean = false, onClick: () -> Unit = {}, + onDoubleClick: () -> Unit = {}, onAuthorClick: () -> Unit = {}, contentPadding: PaddingValues = PaddingValues( top = 16.dp, @@ -53,6 +73,34 @@ fun TextPreview( val updatedContent by rememberUpdatedState(content) val updatedEngagements by rememberUpdatedState(engagements) val updatedModeration by rememberUpdatedState(moderation) + val tap by rememberUpdatedState(onClick) + val dblTap by rememberUpdatedState(onDoubleClick) + + val (playHeart, setPlayHeart) = remember { mutableStateOf(false) } + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.like) + ) + val heartProgress by animateLottieCompositionAsState( + composition = composition, + isPlaying = playHeart, + iterations = 1, + speed = 1.0f + ) + val (prevLiked, setPrevLiked) = remember { mutableStateOf(isLiked) } + LaunchedEffect(isLiked) { + if (!prevLiked && isLiked) { + setPlayHeart(false) + setPlayHeart(true) + } + setPrevLiked(isLiked) + } + LaunchedEffect(heartProgress) { + if (heartProgress >= 1f) setPlayHeart(false) + } + + val scope = rememberCoroutineScope() + val (singleTapJob, setSingleTapJob) = remember { mutableStateOf(null) } + PostScaffold( modifier = modifier, header = { @@ -73,12 +121,38 @@ fun TextPreview( .background( MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(24.dp) - ).clickable( - enabled = true, - role = Role.Button, - onClick = onClick ) - ) }, + .semantics { role = Role.Button } + .pointerInput(Unit) { + detectTapGestures( + onTap = { + val job = scope.launch { + delay(120) + tap() + } + setSingleTapJob(job) + }, + onDoubleTap = { + singleTapJob?.cancel() + dblTap() + } + ) + } + ) { + AnimatedVisibility( + visible = playHeart && heartProgress < 1f, + enter = fadeIn(animationSpec = tween(160)), + exit = fadeOut(animationSpec = tween(220)), + modifier = Modifier.align(Alignment.Center) + ) { + LottieAnimation( + composition = composition, + progress = { heartProgress }, + modifier = Modifier + .align(Alignment.Center) + .size(140.dp) + ) + } } }, contentPadding = contentPadding, footer = { Row( @@ -94,7 +168,7 @@ fun TextPreview( } @Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview fun PreviewTextPostCard() { PeerTheme { TextPreview( @@ -108,6 +182,7 @@ fun PreviewTextPostCard() { isfollowed = false ), description = "Description...", + isLiked = false, engagements = { UiAction.ENGAGEMENTS.forEach { DesignTextButton( diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/photo/PhotoListing.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/photo/PhotoListing.kt index 52b31296c..214897ad2 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/photo/PhotoListing.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/photo/PhotoListing.kt @@ -138,13 +138,20 @@ fun LazyItemScope.PhotoListing( content: @Composable (UiPost) -> Unit = {} ) { val uiContent = post.mapToContent() + val engagementModel = uiEngagementEvent.onLoad(uiContent) val clickHandler by rememberUpdatedState { onAuthorClick(post.author.id) } val updatedConnection by rememberUpdatedState(connection) val handleOnPostClick by rememberUpdatedState(onPostClick) PostItem( post = post, position = index, + isLiked = engagementModel.isLiked, onClick = { handleOnPostClick(post.id, index) }, + onDoubleClick = { + if (!engagementModel.isLiked) { + uiEngagementEvent.onLike(engagementModel) + } + }, onAuthorClick = clickHandler, onMentionClick = onMentionClick, onHashtagClick = onHashtagClick, diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/video/VideoListing.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/video/VideoListing.kt index 0eabdf5f0..3ed19cad6 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/video/VideoListing.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/timeline/video/VideoListing.kt @@ -1,7 +1,6 @@ package eu.peernetwork.blog.ui.timeline.video import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -24,7 +23,6 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState @@ -145,10 +143,12 @@ fun VideoListing( val updatedContent by rememberUpdatedState(content) val updatedConnection by rememberUpdatedState(connection) val uiContent = post.mapToContent() + val engagementModel = uiEngagementEvent.onLoad(uiContent) MediaView( author = post.author, onAuthorClick = { clickHandler(post.author.id) }, description = post.time, + isLiked = engagementModel.isLiked, caption = { TextView( uiContent.author.username, @@ -183,11 +183,14 @@ fun VideoListing( ) ) } + }, + onClick = { selectHandler() }, + onDoubleClick = { + if (!engagementModel.isLiked) { + uiEngagementEvent.onLike(engagementModel) + } } ) { - Box(modifier = Modifier.clickable( - role = Role.Button, - onClick = { selectHandler() } - )) { updatedContent(post) } + updatedContent(post) } } diff --git a/core/ui/src/main/res/raw/like.lottie b/core/ui/src/main/res/raw/like.lottie new file mode 100644 index 0000000000000000000000000000000000000000..fb08077129d16f409918711b90e3e8a085333954 GIT binary patch literal 1889 zcmbW2XH*ji62}udOVb!Y>4?&V&X|SF!TQN>4!!N3V{ItfEYkF8iaAt zIEqX;R4s=kby(rp2wZ3gkp#gLBO~(j?WKFwjyUp+2{o0~aPQ|ST85BJxa`@NFRlcZ z&d&ID#O@|<`p^EvLu2$#I2G2XV}{C4Qiqc_Gu+j4us<+dw&s-7r7^wkmG7$e=hONq zBJ&{`D^n#i??o3Mjn7&{p%1jj<8_{>plL;IUQ$nNk0p)vi<(E*NN7&hP0OSIVilKv z>~w=L0MKy^05CnYdgwG9OTtA)5Dh{DLk&Yr0*&;c!4`q~rY2A`eXK>Wkvi<+#8i42nuFq%zl*7kiK*=@*T z+QkbbeM#eVvk9c*3Nmy5bB&txJ- zmzJtPhV&8c)q#X>z_9s`&n+ru@!wta8b9|5*k#E0Gi z$5d#b6S{TE{PuO*Fhfph#feG6$d(3keo*kZ}-mA`0BAC&2n*|{a|*I+oU zqS?^URbSj6biBAJ=+5NsWVYt|9T6=_jXG_*=-Gg4_6k(3gskQ3lR$jzahhg)NQA&> zOvAdSPe5FGt)*a<!f!XX6Xp5Ry(;|f$icpSdW6d|n5|G< zl2I|G@x3h1w>m2ta!1(lUCnB)csaFF4RXEM@@CQ};akGh>0S!z!zG;qE=vA{I3Ffq z)VF6Kl(OZyZft!KI(6kSFcSsOl4bf3Ua|>54p)|dO(Or1ZXm~0QrAt`d#gGaruAW@ z=W93YUiM`Z4UNoK; zpm7`p{5$Dnx`Go9+Iu?z_n&>5uQTTeP#byf!v*!cH11`A#SDr@ z#@rGvsGK@F29z(f8q1NW-aE5ex+WLqU88Q#DqGYkAdrA}cBpqwZa7{TE5v zsy!@~XG3$=Bl4o6%dBMoOb=)ysWYhmk+`33$(OXt1BhH%IEaEiKT!fUrRF!J)Ck#* zKfM4COV1e-k`}~5?_vg02dZ5owLHO_bz%teoSc|VN3un7eEZ(U%Bycr$5elq+U>6~ z=cHK)nxmCqqPM;N;8JA#NraFXJ?I`5cjr|O4DUK!B32l0Qh@)Mt**YZb+tmCVMI9WSL=*; z->7h}&8DUgH$|`8L-fetMh`iN>3M``W~moZ=5(avrnn_=8xxmje2&tHS*WgxY2(je zvv0PLg359IHVcYM>a-Afp~U8_GfM^}5TKgzbLYg3gyxRK#WDm|Z)c^q+f_oL->gdR zT4mZ>o^t_@n<*Wy%3Q`~wb^Lp-H^P=s)(Z4IwG78wBx>+2kM*})Zia6R6wG5552uJ z+(A~q?J}yZQczpCH<3lwy-sVVT-oj(w5#xXeMlFzQWmc(0&caJ`3_<8BtB+qFNA~f z#>ZC+S32%&Rku9wAs}y`jhiql%b2S;fS3kB%B_1AQk|NWd=spyiY~OQ>5dfFFn%ym zja5rB7uNU6_$!fs9}mSRh!z2Fih*f02EWe3putrO-AehBQ@7k0VYpteq@SdBeu$uv uKmjn|fAi{)1OF9&q1WG6|IUy9?A0EQ{|`;jNMVtGUlBM=#$m7fQ~e9T=1|c9 literal 0 HcmV?d00001 From 28c14cbaa13474c36ea3730e573024b17d52a187 Mon Sep 17 00:00:00 2001 From: Novachrono-Ryhze Date: Tue, 19 Aug 2025 16:35:35 +0200 Subject: [PATCH 3/3] On Double-click like- (with composition made) --- .../peernetwork/blog/ui/compose/MediaView.kt | 113 ++++++++++-------- .../peernetwork/blog/ui/compose/PostView.kt | 58 ++------- 2 files changed, 71 insertions(+), 100 deletions(-) diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt index b1421a0e0..ede02c52d 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/MediaView.kt @@ -1,14 +1,19 @@ package eu.peernetwork.blog.ui.compose import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -23,6 +28,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.input.pointer.pointerInput @@ -33,25 +40,61 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import eu.peernetwork.blog.ui.model.UiAction import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.core.ui.R import eu.peernetwork.core.ui.design.compose.DesignTextButton import eu.peernetwork.core.ui.theme.PeerTheme -import androidx.compose.foundation.gestures.detectTapGestures -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.ui.Alignment + +@Composable +fun BoxScope.ConfirmedLikeHeartOverlay( + isLiked: Boolean, + modifier: Modifier = Modifier, + size: Dp = 160.dp, + rawRes: Int = R.raw.like +) { + val (play, setPlay) = remember { mutableStateOf(false) } + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(rawRes)) + val progress by animateLottieCompositionAsState( + composition = composition, + isPlaying = play, + iterations = 1, + speed = 1f + ) + var prev by remember { mutableStateOf(isLiked) } + LaunchedEffect(isLiked) { + if (!prev && isLiked) { + setPlay(false) + setPlay(true) + } + prev = isLiked + } + LaunchedEffect(progress) { + if (progress >= 1f) setPlay(false) + } + + AnimatedVisibility( + visible = play && progress < 1f, + enter = fadeIn(animationSpec = tween(160)), + exit = fadeOut(animationSpec = tween(220)), + modifier = modifier + ) { + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier.size(size) + ) + } +} @Composable fun MediaView( @@ -77,29 +120,6 @@ fun MediaView( val click by rememberUpdatedState(onClick) val dblClick by rememberUpdatedState(onDoubleClick) - val (playHeart, setPlayHeart) = remember { mutableStateOf(false) } - val composition by rememberLottieComposition( - LottieCompositionSpec.RawRes(R.raw.like) - ) - val heartProgress by animateLottieCompositionAsState( - composition = composition, - isPlaying = playHeart, - iterations = 1, - speed = 1.0f - ) - val (prevLiked, setPrevLiked) = remember { mutableStateOf(isLiked) } - LaunchedEffect(isLiked) { - if (!prevLiked && isLiked) { - setPlayHeart(false) - setPlayHeart(true) - } - setPrevLiked(isLiked) - } - - LaunchedEffect(heartProgress) { - if (heartProgress >= 1f) setPlayHeart(false) - } - val scope = rememberCoroutineScope() val (singleTapJob, setSingleTapJob) = remember { mutableStateOf(null) } @@ -123,7 +143,8 @@ fun MediaView( footer = { } ) { Box( - modifier = Modifier.semantics { role = Role.Button } + modifier = Modifier + .semantics { role = Role.Button } .pointerInput(Unit) { detectTapGestures( onTap = { @@ -140,21 +161,11 @@ fun MediaView( ) } ) { updatedContent() - - AnimatedVisibility( - visible = playHeart && heartProgress < 1f, - enter = fadeIn(animationSpec = tween(160)), - exit = fadeOut(animationSpec = tween(220)), - modifier = Modifier.align(Alignment.Center) - ) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - LottieAnimation( - composition = composition, - progress = { heartProgress }, - modifier = Modifier.size(160.dp) - ) - } - } + ConfirmedLikeHeartOverlay( + isLiked = isLiked, + modifier = Modifier.align(Alignment.Center), + size = 160.dp + ) Image( painter = painterResource(R.drawable.overlay_gradient), @@ -231,4 +242,4 @@ fun PreviewMediaPreview() { ) } } -} +} \ No newline at end of file diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt index 724cb0195..4b7867ebb 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/compose/PostView.kt @@ -1,6 +1,7 @@ package eu.peernetwork.blog.ui.compose import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -16,7 +17,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -24,6 +24,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics @@ -33,21 +35,9 @@ import eu.peernetwork.blog.ui.model.UiAction import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.core.ui.design.compose.DesignTextButton import eu.peernetwork.core.ui.theme.PeerTheme -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.res.painterResource -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import eu.peernetwork.core.ui.R -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut @Composable fun TextPreview( @@ -76,28 +66,6 @@ fun TextPreview( val tap by rememberUpdatedState(onClick) val dblTap by rememberUpdatedState(onDoubleClick) - val (playHeart, setPlayHeart) = remember { mutableStateOf(false) } - val composition by rememberLottieComposition( - LottieCompositionSpec.RawRes(R.raw.like) - ) - val heartProgress by animateLottieCompositionAsState( - composition = composition, - isPlaying = playHeart, - iterations = 1, - speed = 1.0f - ) - val (prevLiked, setPrevLiked) = remember { mutableStateOf(isLiked) } - LaunchedEffect(isLiked) { - if (!prevLiked && isLiked) { - setPlayHeart(false) - setPlayHeart(true) - } - setPrevLiked(isLiked) - } - LaunchedEffect(heartProgress) { - if (heartProgress >= 1f) setPlayHeart(false) - } - val scope = rememberCoroutineScope() val (singleTapJob, setSingleTapJob) = remember { mutableStateOf(null) } @@ -139,20 +107,12 @@ fun TextPreview( ) } ) { - AnimatedVisibility( - visible = playHeart && heartProgress < 1f, - enter = fadeIn(animationSpec = tween(160)), - exit = fadeOut(animationSpec = tween(220)), - modifier = Modifier.align(Alignment.Center) - ) { - LottieAnimation( - composition = composition, - progress = { heartProgress }, - modifier = Modifier - .align(Alignment.Center) - .size(140.dp) - ) - } } }, + ConfirmedLikeHeartOverlay( + isLiked = isLiked, + modifier = Modifier.align(Alignment.Center), + size = 140.dp + ) + } }, contentPadding = contentPadding, footer = { Row(