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.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Collections 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 import nostr.postr.events.Event
class Note(val idHex: String) { class Note(val idHex: String) {
@@ -97,18 +101,24 @@ class Note(val idHex: String) {
} }
class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) { class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun refresh() { fun refresh() {
postValue(NoteState(note)) postValue(NoteState(note))
} }
override fun onActive() { override fun onActive() {
super.onActive() super.onActive()
NostrSingleEventDataSource.add(note.idHex) scope.launch {
NostrSingleEventDataSource.add(note.idHex)
}
} }
override fun onInactive() { override fun onInactive() {
super.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 @Composable
private fun CloseButton(onCancel: () -> Unit) { fun CloseButton(onCancel: () -> Unit) {
Button( Button(
onClick = { onClick = {
onCancel() onCancel()
@@ -207,7 +207,7 @@ private fun CloseButton(onCancel: () -> Unit) {
} }
@Composable @Composable
private fun PostButton(onPost: () -> Unit = {}) { fun PostButton(onPost: () -> Unit = {}) {
Button( Button(
onClick = { onClick = {
onPost() 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 package com.vitorpamplona.amethyst.ui.components
import android.util.Patterns import android.util.Patterns
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
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.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.vitorpamplona.amethyst.model.LocalCache 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.MalformedURLException
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URL import java.net.URL
@@ -43,8 +39,9 @@ fun isValidURL(url: String?): Boolean {
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @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)) { Column(modifier = Modifier.padding(top = 5.dp)) {
// 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 ->
@@ -58,16 +55,7 @@ fun RichTextViewer(content: String, tags: List<List<String>>?) {
} else if (isValidURL(word)) { } else if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].toLowerCase() val removedParamsFromUrl = word.split("?")[0].toLowerCase()
if (imageExtension.matcher(removedParamsFromUrl).matches()) { if (imageExtension.matcher(removedParamsFromUrl).matches()) {
AsyncImage( ExtendedImageView(word)
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))
)
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) { } else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
VideoView(word) VideoView(word)
} else { } else {

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape 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.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -19,6 +21,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.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.graphics.Color 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.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.baha.url.preview.BahaUrlPreview import com.baha.url.preview.BahaUrlPreview
import com.baha.url.preview.IUrlPreviewCallback import com.baha.url.preview.IUrlPreviewCallback
@@ -34,6 +39,7 @@ import com.baha.url.preview.UrlInfoItem
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) { fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(UrlPreviewState.Loading) } 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 { } else {
val eventContent = note.event?.content val eventContent = note.event?.content
if (eventContent != null) if (eventContent != null)
RichTextViewer(eventContent, note.event?.tags) RichTextViewer(eventContent, note.event?.tags, note, accountViewModel)
ReactionsRowState(note, accountViewModel) ReactionsRowState(note, accountViewModel)

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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) NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, account)
Row(modifier = Modifier.padding(top = 8.dp)) { Row(modifier = Modifier.padding(top = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
IconButton( IconButton(
modifier = Modifier.then(Modifier.size(24.dp)), modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { if (account.isWriteable()) wantsToReplyTo = note } onClick = { if (account.isWriteable()) wantsToReplyTo = note }

View File

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