Adds a click to zoom image.

This commit is contained in:
Vitor Pamplona 2023-01-13 20:16:57 -05:00
parent 0001ae441f
commit 380c2e67cc
9 changed files with 171 additions and 25 deletions

View File

@ -9,6 +9,10 @@ import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Collections
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import nostr.postr.events.Event
class Note(val idHex: String) {
@ -97,18 +101,24 @@ class Note(val idHex: String) {
}
class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun refresh() {
postValue(NoteState(note))
}
override fun onActive() {
super.onActive()
NostrSingleEventDataSource.add(note.idHex)
scope.launch {
NostrSingleEventDataSource.add(note.idHex)
}
}
override fun onInactive() {
super.onInactive()
NostrSingleEventDataSource.remove(note.idHex)
scope.launch {
NostrSingleEventDataSource.remove(note.idHex)
}
}
}

View File

@ -191,7 +191,7 @@ fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account)
}
@Composable
private fun CloseButton(onCancel: () -> Unit) {
fun CloseButton(onCancel: () -> Unit) {
Button(
onClick = {
onCancel()
@ -207,7 +207,7 @@ private fun CloseButton(onCancel: () -> Unit) {
}
@Composable
private fun PostButton(onPost: () -> Unit = {}) {
fun PostButton(onPost: () -> Unit = {}) {
Button(
onClick = {
onPost()

View File

@ -0,0 +1,81 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.note.ReactionsRowState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun ExtendedImageView(word: String) {
// store the dialog open or close state
var dialogOpen by remember {
mutableStateOf(false)
}
AsyncImage(
model = word,
contentDescription = word,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
.clickable(
onClick = { dialogOpen = true }
)
)
if (dialogOpen) {
Dialog(
onDismissRequest = { dialogOpen = false },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column(
modifier = Modifier.padding(10.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
dialogOpen = false
})
}
ZoomableAsyncImage(word)
}
}
}
}
}

View File

@ -1,23 +1,19 @@
package com.vitorpamplona.amethyst.ui.components
import android.util.Patterns
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
@ -43,8 +39,9 @@ fun isValidURL(url: String?): Boolean {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun RichTextViewer(content: String, tags: List<List<String>>?) {
fun RichTextViewer(content: String, tags: List<List<String>>?, note: Note, accountViewModel: AccountViewModel) {
Column(modifier = Modifier.padding(top = 5.dp)) {
// FlowRow doesn't work well with paragraphs. So we need to split them
content.split('\n').forEach { paragraph ->
@ -58,16 +55,7 @@ fun RichTextViewer(content: String, tags: List<List<String>>?) {
} else if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
AsyncImage(
model = word,
contentDescription = word,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
)
ExtendedImageView(word)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
VideoView(word)
} else {

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
@ -12,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -19,6 +21,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@ -27,6 +30,8 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import com.baha.url.preview.BahaUrlPreview
import com.baha.url.preview.IUrlPreviewCallback
@ -34,6 +39,7 @@ import com.baha.url.preview.UrlInfoItem
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(UrlPreviewState.Loading) }

View File

@ -0,0 +1,60 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculatePan
import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.gestures.forEachGesture
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 coil.compose.AsyncImage
@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) {
forEachGesture {
awaitPointerEventScope {
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 = "Profile Image",
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxSize().graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = offsetX,
translationY = offsetY
),
)
}
}

View File

@ -145,7 +145,7 @@ fun NoteCompose(baseNote: Note, modifier: Modifier = Modifier, isInnerNote: Bool
} else {
val eventContent = note.event?.content
if (eventContent != null)
RichTextViewer(eventContent, note.event?.tags)
RichTextViewer(eventContent, note.event?.tags, note, accountViewModel)
ReactionsRowState(note, accountViewModel)

View File

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -35,7 +36,7 @@ fun ReactionsRow(note: Note, account: Account, boost: (Note) -> Unit, reactTo: (
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, account)
Row(modifier = Modifier.padding(top = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { if (account.isWriteable()) wantsToReplyTo = note }

View File

@ -213,7 +213,7 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
Column() {
val eventContent = note.event?.content
if (eventContent != null)
RichTextViewer(eventContent, note.event?.tags)
RichTextViewer(eventContent, note.event?.tags, note, accountViewModel)
ReactionsRowState(note, accountViewModel)