Show image and QuotedAddressableNote.kt
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
package ac.cord.auxiliary.compose.ui
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import coil3.Bitmap
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.withSign
|
||||
|
||||
object BlurHashDecoder {
|
||||
|
||||
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? {
|
||||
if (blurHash == null || blurHash.length < 6) return null
|
||||
|
||||
val numComponents = decode83(blurHash, 0, 1)
|
||||
val nx = (numComponents % 9) + 1
|
||||
val ny = (numComponents / 9) + 1
|
||||
|
||||
if (blurHash.length != 4 + 2 * nx * ny) return null
|
||||
|
||||
val maxAc = (decode83(blurHash, 1, 2) + 1) / 166f
|
||||
val colors = Array(nx * ny) { i ->
|
||||
if (i == 0) {
|
||||
val color83 = decode83(blurHash, 2, 6)
|
||||
decodeDc(color83)
|
||||
} else {
|
||||
val color83 = decode83(blurHash, 4 + i * 2, 4 + i * 2 + 2)
|
||||
decodeAc(color83, maxAc * punch)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
// val bitmap = Bitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
// val pixels = IntArray(width * height)
|
||||
//
|
||||
// for (y in 0 until height) {
|
||||
// for (x in 0 until width) {
|
||||
// var r = 0f
|
||||
// var g = 0f
|
||||
// var b = 0f
|
||||
//
|
||||
// for (j in 0 until ny) {
|
||||
// for (i in 0 until nx) {
|
||||
// val basis = cos(PI * x * i / width) * cos(PI * y * j / height)
|
||||
// val color = colors[j * nx + i]
|
||||
// r += color[0] * basis.toFloat()
|
||||
// g += color[1] * basis.toFloat()
|
||||
// b += color[2] * basis.toFloat()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val red = linearToSrgb(r)
|
||||
// val green = linearToSrgb(g)
|
||||
// val blue = linearToSrgb(b)
|
||||
//
|
||||
// pixels[y * width + x] = Color(red, green, blue).toArgb()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
|
||||
// return bitmap
|
||||
}
|
||||
|
||||
private fun decode83(str: String, start: Int, end: Int): Int {
|
||||
var res = 0
|
||||
for (i in start until end) {
|
||||
val c = str[i]
|
||||
val digit = charMap[c] ?: 0
|
||||
res = res * 83 + digit
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private fun decodeDc(color83: Int): FloatArray {
|
||||
val r = (color83 shr 16) / 255f
|
||||
val g = ((color83 shr 8) and 255) / 255f
|
||||
val b = (color83 and 255) / 255f
|
||||
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
|
||||
}
|
||||
|
||||
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
|
||||
val r = value / (19 * 19)
|
||||
val g = (value / 19) % 19
|
||||
val b = value % 19
|
||||
return floatArrayOf(
|
||||
signedPow2((r - 9) / 9f) * maxAc,
|
||||
signedPow2((g - 9) / 9f) * maxAc,
|
||||
signedPow2((b - 9) / 9f) * maxAc
|
||||
)
|
||||
}
|
||||
|
||||
private fun srgbToLinear(v: Float): Float {
|
||||
val f = v / 255f
|
||||
return if (f <= 0.04045f) f / 12.92f else ((f + 0.055f) / 1.055f).pow(2.4f)
|
||||
}
|
||||
|
||||
private fun linearToSrgb(v: Float): Int {
|
||||
val f = v.coerceIn(0f, 1f)
|
||||
return if (f <= 0.0031308f) {
|
||||
(f * 12.92f * 255f + 0.5f).toInt()
|
||||
} else {
|
||||
((1.055f * f.pow(1f / 2.4f) - 0.055f) * 255f + 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun signedPow2(v: Float) = v.pow(2f).withSign(v)
|
||||
|
||||
private val charMap = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
|
||||
.mapIndexed { i, c -> c to i }
|
||||
.toMap()
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package ac.cord.auxiliary.compose.ui.composable.widgets.content
|
||||
|
||||
import ac.cord.auxiliary.compose.ui.BlurHashDecoder
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
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.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.asImage
|
||||
|
||||
@kotlin.OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun ImageWithContextMenu(meta: MediaMeta, onFullScreen: () -> Unit) {
|
||||
val url = meta.url
|
||||
val autoLoad = true
|
||||
var loaded by remember { mutableStateOf(autoLoad) }
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
// val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
if (!loaded) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable { loaded = true },
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Image,
|
||||
contentDescription = "Load image",
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
"Tap to load",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val ratio = remember(meta.dimension) { parseAspectRatio(meta.dimension) }
|
||||
|
||||
Box {
|
||||
LoadingAsyncImage(
|
||||
model = url,
|
||||
contentDescription = "Image",
|
||||
contentScale = ContentScale.FillWidth,
|
||||
onClick = onFullScreen,
|
||||
onLongClick = { showMenu = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.let { if (ratio != null) it.aspectRatio(ratio) else it }
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy URL") },
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(url))
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Download") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
// scope.launch { MediaDownloader.downloadMedia(context, url) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseAspectRatio(dim: String?): Float? {
|
||||
if (dim == null) return null
|
||||
val parts = dim.split('x')
|
||||
if (parts.size != 2) return null
|
||||
val w = parts[0].toFloatOrNull() ?: return null
|
||||
val h = parts[1].toFloatOrNull() ?: return null
|
||||
return if (h > 0) w / h else null
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package ac.cord.auxiliary.compose.ui.composable.widgets.content
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Color
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun LoadingAsyncImage(
|
||||
model: Any?,
|
||||
contentDescription: String?,
|
||||
contentScale: ContentScale,
|
||||
modifier: Modifier = Modifier,
|
||||
blurPainter: BitmapPainter? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
) {
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
placeholder = blurPainter,
|
||||
onLoading = { isLoading = true },
|
||||
onSuccess = { isLoading = false },
|
||||
onError = { isLoading = false },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
if (onClick != null || onLongClick != null) {
|
||||
Modifier.combinedClickable(
|
||||
onClick = onClick ?: {},
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = if (blurPainter != null) Color.White else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package ac.cord.auxiliary.compose.ui.composable.widgets.content
|
||||
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalQuotedNostrEvent
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -22,7 +23,7 @@ import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
internal fun QuotedAddressableNote(
|
||||
localNostrEvent: LocalNostrEvent? = null,
|
||||
localQuotedNostrEvent: LocalQuotedNostrEvent?,
|
||||
kind: Int,
|
||||
dTag: String,
|
||||
author: String,
|
||||
@@ -34,15 +35,10 @@ internal fun QuotedAddressableNote(
|
||||
) {
|
||||
|
||||
// TODO: Sync quoted event...
|
||||
// LaunchedEffect(kind, author, dTag) {
|
||||
// if (eventRepo.findAddressableEvent(kind, author, dTag) == null) {
|
||||
// eventRepo.requestAddressableEvent(kind, author, dTag, relayHints)
|
||||
// }
|
||||
// }
|
||||
|
||||
if (localNostrEvent?.localQuotedNostrEvent != null) {
|
||||
if (localQuotedNostrEvent != null) {
|
||||
QuotedNote(
|
||||
localQuotedNostrEvent = localNostrEvent.localQuotedNostrEvent,
|
||||
localQuotedNostrEvent = localQuotedNostrEvent,
|
||||
onNoteClick = onNoteClick,
|
||||
noteActions = noteActions
|
||||
)
|
||||
|
||||
@@ -250,12 +250,12 @@ fun RichContent(
|
||||
Logger.w("Doing the else: $group")
|
||||
val segment = group as ContentSegment
|
||||
when (segment) {
|
||||
// is ContentSegment.ImageSegment -> {
|
||||
// ImageWithContextMenu(
|
||||
// meta = segment.meta,
|
||||
// onFullScreen = { fullScreenImageUrl = segment.meta.url }
|
||||
// )
|
||||
// }
|
||||
is ContentSegment.ImageSegment -> {
|
||||
ImageWithContextMenu(
|
||||
meta = segment.meta,
|
||||
onFullScreen = { fullScreenImageUrl = segment.meta.url }
|
||||
)
|
||||
}
|
||||
// is ContentSegment.VideoSegment -> {
|
||||
// InlineVideoPlayerWithFullscreen(
|
||||
// meta = segment.meta,
|
||||
@@ -297,26 +297,27 @@ fun RichContent(
|
||||
is ContentSegment.NostrAddressableSegment -> {
|
||||
val kind = segment.kind
|
||||
when {
|
||||
// kind == 1 || kind == 0 -> {
|
||||
// if (localNostrEvent.quotedPost != null && segment.author != null) {
|
||||
// QuotedAddressableNote(
|
||||
// kind = kind,
|
||||
// dTag = segment.dTag,
|
||||
// author = segment.author,
|
||||
// relayHints = segment.relays,
|
||||
// onNoteClick = onNoteClick,
|
||||
// onProfileClick = onProfileClick,
|
||||
// noteActions = noteActions,
|
||||
// style = style
|
||||
// )
|
||||
// } else {
|
||||
// Text(
|
||||
// text = "nostr:${segment.dTag.take(12)}...",
|
||||
// style = style,
|
||||
// color = MaterialTheme.colorScheme.primary
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
kind == 1 || kind == 0 -> {
|
||||
if (localQuotedNostrEvent != null && segment.author != null) {
|
||||
QuotedAddressableNote(
|
||||
localQuotedNostrEvent = localQuotedNostrEvent,
|
||||
kind = kind,
|
||||
dTag = segment.dTag,
|
||||
author = segment.author,
|
||||
relayHints = segment.relays,
|
||||
onNoteClick = onNoteClick,
|
||||
onProfileClick = onProfileClick,
|
||||
noteActions = noteActions,
|
||||
style = style
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "nostr:${segment.dTag.take(12)}...",
|
||||
style = style,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
// kind == 30023 -> {
|
||||
// if (segment.author != null) {
|
||||
// // TODO: Add support for articles in localNostrEvents...
|
||||
|
||||
Reference in New Issue
Block a user