mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-20 22:01:24 +02:00
Adds an Image Carousel when there are more than 1 image in the post.
This commit is contained in:
@@ -104,6 +104,8 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
||||||
|
|
||||||
|
implementation "net.engawapg.lib:zoomable:1.4.0"
|
||||||
|
|
||||||
// Biometrics
|
// Biometrics
|
||||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
|
|
||||||
|
@@ -131,6 +131,20 @@ fun RichTextViewer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val imagesForPager = mutableListOf<String>()
|
||||||
|
|
||||||
|
content.split('\n').forEach { paragraph ->
|
||||||
|
paragraph.split(' ').forEach { word: String ->
|
||||||
|
// sequence of images will render in a slideview
|
||||||
|
if (isValidURL(word)) {
|
||||||
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
imagesForPager.add(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
FlowRow() {
|
FlowRow() {
|
||||||
@@ -140,17 +154,18 @@ fun RichTextViewer(
|
|||||||
// Explicit URL
|
// Explicit URL
|
||||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||||
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
|
||||||
if (lnInvoice != null) {
|
|
||||||
InvoicePreview(lnInvoice)
|
if (isValidURL(word)) {
|
||||||
} else if (isValidURL(word)) {
|
|
||||||
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
val removedParamsFromUrl = word.split("?")[0].lowercase()
|
||||||
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
ZoomableImageView(word)
|
ZoomableImageView(word, imagesForPager)
|
||||||
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
VideoView(word)
|
VideoView(word)
|
||||||
} else {
|
} else {
|
||||||
UrlPreview(word, "$word ")
|
UrlPreview(word, "$word ")
|
||||||
}
|
}
|
||||||
|
} else if (lnInvoice != null) {
|
||||||
|
InvoicePreview(lnInvoice)
|
||||||
} else if (lnWithdrawal != null) {
|
} else if (lnWithdrawal != null) {
|
||||||
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
ClickableWithdrawal(withdrawalString = lnWithdrawal)
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
|
@@ -0,0 +1,104 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SlidingCarousel(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
pagerState: PagerState = remember { PagerState() },
|
||||||
|
itemsCount: Int,
|
||||||
|
itemContent: @Composable (index: Int) -> Unit
|
||||||
|
) {
|
||||||
|
val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
HorizontalPager(count = itemsCount, state = pagerState) { page ->
|
||||||
|
itemContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// you can remove the surface in case you don't want
|
||||||
|
// the transparant bacground
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
.align(Alignment.BottomCenter),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = Color.Black.copy(alpha = 0.5f)
|
||||||
|
) {
|
||||||
|
DotsIndicator(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
|
totalDots = itemsCount,
|
||||||
|
selectedIndex = if (isDragged) pagerState.currentPage else pagerState.targetPage,
|
||||||
|
dotSize = 8.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DotsIndicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
totalDots: Int,
|
||||||
|
selectedIndex: Int,
|
||||||
|
selectedColor: Color = MaterialTheme.colors.primary /* Color.Yellow */,
|
||||||
|
unSelectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) /* Color.Gray */,
|
||||||
|
dotSize: Dp
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = modifier
|
||||||
|
.wrapContentWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
items(totalDots) { index ->
|
||||||
|
IndicatorDot(
|
||||||
|
color = if (index == selectedIndex) selectedColor else unSelectedColor,
|
||||||
|
size = dotSize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index != totalDots - 1) {
|
||||||
|
Spacer(modifier = Modifier.padding(horizontal = 2.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IndicatorDot(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp,
|
||||||
|
color: Color
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color)
|
||||||
|
)
|
||||||
|
}
|
@@ -1,61 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
|
||||||
import androidx.compose.foundation.gestures.calculatePan
|
|
||||||
import androidx.compose.foundation.gestures.calculateZoom
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import com.vitorpamplona.amethyst.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ZoomableAsyncImage(imageUrl: String) {
|
|
||||||
var scale by remember { mutableStateOf(1f) }
|
|
||||||
var offsetX by remember { mutableStateOf(0f) }
|
|
||||||
var offsetY by remember { mutableStateOf(0f) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
awaitEachGesture {
|
|
||||||
awaitFirstDown()
|
|
||||||
do {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
scale *= event.calculateZoom()
|
|
||||||
val offset = event.calculatePan()
|
|
||||||
offsetX += offset.x
|
|
||||||
offsetY += offset.y
|
|
||||||
} while (event.changes.any { it.pressed })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = imageUrl,
|
|
||||||
contentDescription = stringResource(id = R.string.profile_image),
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.graphicsLayer(
|
|
||||||
scaleX = scale,
|
|
||||||
scaleY = scale,
|
|
||||||
translationX = offsetX,
|
|
||||||
translationY = offsetY
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -18,22 +18,27 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.PagerState
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
||||||
|
import net.engawapg.lib.zoomable.rememberZoomState
|
||||||
|
import net.engawapg.lib.zoomable.zoomable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
fun ZoomableImageView(word: String) {
|
fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
// store the dialog open or close state
|
// store the dialog open or close state
|
||||||
@@ -49,7 +54,11 @@ fun ZoomableImageView(word: String) {
|
|||||||
.padding(top = 4.dp)
|
.padding(top = 4.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||||
|
RoundedCornerShape(15.dp)
|
||||||
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(word)) }
|
onLongClick = { clipboardManager.setText(AnnotatedString(word)) }
|
||||||
@@ -57,12 +66,13 @@ fun ZoomableImageView(word: String) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
ZoomableImageDialog(word, onDismiss = { dialogOpen = false })
|
ZoomableImageDialog(word, images, onDismiss = { dialogOpen = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ZoomableImageDialog(imageUrl: String, onDismiss: () -> Unit) {
|
fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(imageUrl), onDismiss: () -> Unit) {
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
@@ -71,6 +81,8 @@ fun ZoomableImageDialog(imageUrl: String, onDismiss: () -> Unit) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
) {
|
) {
|
||||||
|
var pagerState: PagerState = remember { PagerState() }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
@@ -79,10 +91,34 @@ fun ZoomableImageDialog(imageUrl: String, onDismiss: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
CloseButton(onCancel = onDismiss)
|
CloseButton(onCancel = onDismiss)
|
||||||
|
|
||||||
SaveToGallery(url = imageUrl)
|
SaveToGallery(url = allImages[pagerState.currentPage])
|
||||||
}
|
}
|
||||||
|
|
||||||
ZoomableAsyncImage(imageUrl)
|
if (allImages.size > 1) {
|
||||||
|
SlidingCarousel(
|
||||||
|
pagerState = pagerState,
|
||||||
|
itemsCount = allImages.size,
|
||||||
|
itemContent = { index ->
|
||||||
|
AsyncImage(
|
||||||
|
model = allImages[index],
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zoomable(rememberZoomState())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = stringResource(id = R.string.profile_image),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zoomable(rememberZoomState())
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
|
||||||
object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
||||||
|
Reference in New Issue
Block a user