mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-27 18:56:49 +02:00
Adds a click to zoom image.
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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 {
|
||||||
|
@@ -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) }
|
||||||
|
@@ -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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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 }
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user