Sending dimensions to Image files.
This commit is contained in:
@@ -417,6 +417,7 @@ class Account(
|
|||||||
mimeType = headerInfo.mimeType,
|
mimeType = headerInfo.mimeType,
|
||||||
hash = headerInfo.hash,
|
hash = headerInfo.hash,
|
||||||
size = headerInfo.size.toString(),
|
size = headerInfo.size.toString(),
|
||||||
|
dimensions = headerInfo.dim,
|
||||||
blurhash = headerInfo.blurHash,
|
blurhash = headerInfo.blurHash,
|
||||||
description = headerInfo.description,
|
description = headerInfo.description,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
@@ -445,6 +446,7 @@ class Account(
|
|||||||
mimeType = headerInfo.mimeType,
|
mimeType = headerInfo.mimeType,
|
||||||
hash = headerInfo.hash,
|
hash = headerInfo.hash,
|
||||||
size = headerInfo.size.toString(),
|
size = headerInfo.size.toString(),
|
||||||
|
dimensions = headerInfo.dim,
|
||||||
blurhash = headerInfo.blurHash,
|
blurhash = headerInfo.blurHash,
|
||||||
description = headerInfo.description,
|
description = headerInfo.description,
|
||||||
privateKey = loggedIn.privKey!!
|
privateKey = loggedIn.privKey!!
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class FileHeader(
|
|||||||
val mimeType: String?,
|
val mimeType: String?,
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
|
val dim: String?,
|
||||||
val blurHash: String?,
|
val blurHash: String?,
|
||||||
val description: String? = null
|
val description: String? = null
|
||||||
) {
|
) {
|
||||||
@@ -43,7 +44,7 @@ class FileHeader(
|
|||||||
val hash = sha256.digest(data).toHexKey()
|
val hash = sha256.digest(data).toHexKey()
|
||||||
val size = data.size
|
val size = data.size
|
||||||
|
|
||||||
val blurHash = if (mimeType?.startsWith("image/") == true) {
|
val (blurHash, dim) = if (mimeType?.startsWith("image/") == true) {
|
||||||
val opt = BitmapFactory.Options()
|
val opt = BitmapFactory.Options()
|
||||||
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||||
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
|
val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt)
|
||||||
@@ -59,20 +60,22 @@ class FileHeader(
|
|||||||
mBitmap.height
|
mBitmap.height
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val dim = "${mBitmap.width}x${mBitmap.height}"
|
||||||
|
|
||||||
val aspectRatio = (mBitmap.width).toFloat() / (mBitmap.height).toFloat()
|
val aspectRatio = (mBitmap.width).toFloat() / (mBitmap.height).toFloat()
|
||||||
|
|
||||||
if (aspectRatio > 1) {
|
if (aspectRatio > 1) {
|
||||||
BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 9, (9 * (1 / aspectRatio)).roundToInt())
|
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 9, (9 * (1 / aspectRatio)).roundToInt()), dim)
|
||||||
} else if (aspectRatio < 1) {
|
} else if (aspectRatio < 1) {
|
||||||
BlurHash.encode(intArray, mBitmap.width, mBitmap.height, (9 * aspectRatio).roundToInt(), 9)
|
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, (9 * aspectRatio).roundToInt(), 9), dim)
|
||||||
} else {
|
} else {
|
||||||
BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4)
|
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
Pair(null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(FileHeader(fileUrl, mimeType, hash, size, blurHash, description))
|
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||||
onError()
|
onError()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class FileHeaderEvent(
|
|||||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||||
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
||||||
|
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||||
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
||||||
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
||||||
@@ -30,6 +31,7 @@ class FileHeaderEvent(
|
|||||||
private const val ENCRYPTION_KEY = "aes-256-gcm"
|
private const val ENCRYPTION_KEY = "aes-256-gcm"
|
||||||
private const val MIME_TYPE = "m"
|
private const val MIME_TYPE = "m"
|
||||||
private const val FILE_SIZE = "size"
|
private const val FILE_SIZE = "size"
|
||||||
|
private const val DIMENSION = "dim"
|
||||||
private const val HASH = "x"
|
private const val HASH = "x"
|
||||||
private const val MAGNET_URI = "magnet"
|
private const val MAGNET_URI = "magnet"
|
||||||
private const val TORRENT_INFOHASH = "i"
|
private const val TORRENT_INFOHASH = "i"
|
||||||
@@ -41,6 +43,7 @@ class FileHeaderEvent(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
hash: String? = null,
|
hash: String? = null,
|
||||||
size: String? = null,
|
size: String? = null,
|
||||||
|
dimensions: String? = null,
|
||||||
blurhash: String? = null,
|
blurhash: String? = null,
|
||||||
magnetURI: String? = null,
|
magnetURI: String? = null,
|
||||||
torrentInfoHash: String? = null,
|
torrentInfoHash: String? = null,
|
||||||
@@ -53,6 +56,7 @@ class FileHeaderEvent(
|
|||||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||||
hash?.let { listOf(HASH, it) },
|
hash?.let { listOf(HASH, it) },
|
||||||
size?.let { listOf(FILE_SIZE, it) },
|
size?.let { listOf(FILE_SIZE, it) },
|
||||||
|
dimensions?.let { listOf(DIMENSION, it) },
|
||||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||||
magnetURI?.let { listOf(MAGNET_URI, it) },
|
magnetURI?.let { listOf(MAGNET_URI, it) },
|
||||||
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class FileStorageHeaderEvent(
|
|||||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||||
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
||||||
|
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||||
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
||||||
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
||||||
@@ -30,6 +31,7 @@ class FileStorageHeaderEvent(
|
|||||||
private const val ENCRYPTION_KEY = "aes-256-gcm"
|
private const val ENCRYPTION_KEY = "aes-256-gcm"
|
||||||
private const val MIME_TYPE = "m"
|
private const val MIME_TYPE = "m"
|
||||||
private const val FILE_SIZE = "size"
|
private const val FILE_SIZE = "size"
|
||||||
|
private const val DIMENSION = "dim"
|
||||||
private const val HASH = "x"
|
private const val HASH = "x"
|
||||||
private const val MAGNET_URI = "magnet"
|
private const val MAGNET_URI = "magnet"
|
||||||
private const val TORRENT_INFOHASH = "i"
|
private const val TORRENT_INFOHASH = "i"
|
||||||
@@ -41,6 +43,7 @@ class FileStorageHeaderEvent(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
hash: String? = null,
|
hash: String? = null,
|
||||||
size: String? = null,
|
size: String? = null,
|
||||||
|
dimensions: String? = null,
|
||||||
blurhash: String? = null,
|
blurhash: String? = null,
|
||||||
magnetURI: String? = null,
|
magnetURI: String? = null,
|
||||||
torrentInfoHash: String? = null,
|
torrentInfoHash: String? = null,
|
||||||
@@ -53,6 +56,7 @@ class FileStorageHeaderEvent(
|
|||||||
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
mimeType?.let { listOf(MIME_TYPE, mimeType) },
|
||||||
hash?.let { listOf(HASH, it) },
|
hash?.let { listOf(HASH, it) },
|
||||||
size?.let { listOf(FILE_SIZE, it) },
|
size?.let { listOf(FILE_SIZE, it) },
|
||||||
|
dimensions?.let { listOf(DIMENSION, it) },
|
||||||
blurhash?.let { listOf(BLUR_HASH, it) },
|
blurhash?.let { listOf(BLUR_HASH, it) },
|
||||||
magnetURI?.let { listOf(MAGNET_URI, it) },
|
magnetURI?.let { listOf(MAGNET_URI, it) },
|
||||||
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) },
|
||||||
|
|||||||
+149
-197
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
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.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -78,53 +79,62 @@ import java.io.File
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
abstract class ZoomableContent(
|
abstract class ZoomableContent(
|
||||||
val description: String? = null
|
val description: String? = null,
|
||||||
|
val dim: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract class ZoomableUrlContent(
|
abstract class ZoomableUrlContent(
|
||||||
val url: String,
|
val url: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
val hash: String? = null,
|
val hash: String? = null,
|
||||||
|
dim: String? = null,
|
||||||
val uri: String? = null
|
val uri: String? = null
|
||||||
) : ZoomableContent(description)
|
) : ZoomableContent(description, dim)
|
||||||
|
|
||||||
class ZoomableUrlImage(
|
class ZoomableUrlImage(
|
||||||
url: String,
|
url: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
hash: String? = null,
|
hash: String? = null,
|
||||||
val bluehash: String? = null,
|
val bluehash: String? = null,
|
||||||
|
dim: String? = null,
|
||||||
uri: String? = null
|
uri: String? = null
|
||||||
) : ZoomableUrlContent(url, description, hash, uri)
|
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
||||||
|
|
||||||
class ZoomableUrlVideo(
|
class ZoomableUrlVideo(
|
||||||
url: String,
|
url: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
hash: String? = null,
|
hash: String? = null,
|
||||||
|
dim: String? = null,
|
||||||
uri: String? = null
|
uri: String? = null
|
||||||
) : ZoomableUrlContent(url, description, hash, uri)
|
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
||||||
|
|
||||||
abstract class ZoomablePreloadedContent(
|
abstract class ZoomablePreloadedContent(
|
||||||
|
val localFile: File,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
|
val mimeType: String? = null,
|
||||||
val isVerified: Boolean? = null,
|
val isVerified: Boolean? = null,
|
||||||
|
dim: String? = null,
|
||||||
val uri: String
|
val uri: String
|
||||||
) : ZoomableContent(description)
|
) : ZoomableContent(description, dim)
|
||||||
|
|
||||||
class ZoomableBitmapImage(
|
class ZoomableLocalImage(
|
||||||
val localFile: File,
|
localFile: File,
|
||||||
val mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
val bluehash: String? = null,
|
val blurhash: String? = null,
|
||||||
|
dim: String? = null,
|
||||||
isVerified: Boolean? = null,
|
isVerified: Boolean? = null,
|
||||||
uri: String
|
uri: String
|
||||||
) : ZoomablePreloadedContent(description, isVerified, uri)
|
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
|
||||||
|
|
||||||
class ZoomableBytesVideo(
|
class ZoomableLocalVideo(
|
||||||
val localFile: File,
|
localFile: File,
|
||||||
val mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
|
dim: String? = null,
|
||||||
isVerified: Boolean? = null,
|
isVerified: Boolean? = null,
|
||||||
uri: String
|
uri: String
|
||||||
) : ZoomablePreloadedContent(description, isVerified, uri)
|
) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri)
|
||||||
|
|
||||||
fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
||||||
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
||||||
@@ -144,45 +154,12 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent> = listOf(content)) {
|
fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent> = listOf(content)) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
// store the dialog open or close state
|
// store the dialog open or close state
|
||||||
var dialogOpen by remember {
|
var dialogOpen by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// store the dialog open or close state
|
|
||||||
var imageState by remember {
|
|
||||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
var verifiedHash by remember {
|
|
||||||
mutableStateOf<Boolean?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content is ZoomableUrlContent) {
|
|
||||||
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
verifiedHash = verifyHash(content, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableBitmapImage) {
|
|
||||||
LaunchedEffect(key1 = content.localFile, key2 = imageState) {
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
verifiedHash = content.isVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableBytesVideo) {
|
|
||||||
LaunchedEffect(key1 = content.localFile, key2 = imageState) {
|
|
||||||
verifiedHash = content.isVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mainImageModifier = Modifier
|
var mainImageModifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(shape = RoundedCornerShape(15.dp))
|
.clip(shape = RoundedCornerShape(15.dp))
|
||||||
@@ -192,17 +169,17 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
|
|||||||
RoundedCornerShape(15.dp)
|
RoundedCornerShape(15.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val ratio = aspectRatio(content.dim)
|
||||||
|
if (ratio != null) {
|
||||||
|
mainImageModifier = mainImageModifier.aspectRatio(ratio)
|
||||||
|
}
|
||||||
|
|
||||||
if (content is ZoomableUrlContent) {
|
if (content is ZoomableUrlContent) {
|
||||||
mainImageModifier = mainImageModifier.combinedClickable(
|
mainImageModifier = mainImageModifier.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri ?: content.url)) }
|
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri ?: content.url)) }
|
||||||
)
|
)
|
||||||
} else if (content is ZoomableBitmapImage) {
|
} else if (content is ZoomablePreloadedContent) {
|
||||||
mainImageModifier = mainImageModifier.combinedClickable(
|
|
||||||
onClick = { dialogOpen = true },
|
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) }
|
|
||||||
)
|
|
||||||
} else if (content is ZoomableBytesVideo) {
|
|
||||||
mainImageModifier = mainImageModifier.combinedClickable(
|
mainImageModifier = mainImageModifier.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) }
|
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) }
|
||||||
@@ -213,71 +190,127 @@ fun ZoomableContentView(content: ZoomableContent, images: List<ZoomableContent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content is ZoomableUrlImage) {
|
when (content) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
is ZoomableUrlImage -> UrlImageView(content, mainImageModifier)
|
||||||
AsyncImage(
|
is ZoomableUrlVideo -> VideoView(content.url, content.description) { dialogOpen = true }
|
||||||
model = content.url,
|
is ZoomableLocalImage -> LocalImageView(content, mainImageModifier)
|
||||||
contentDescription = content.description,
|
is ZoomableLocalVideo -> VideoView(content.localFile, content.description) { dialogOpen = true }
|
||||||
contentScale = ContentScale.FillWidth,
|
}
|
||||||
modifier = mainImageModifier,
|
|
||||||
onLoading = {
|
|
||||||
imageState = it
|
|
||||||
},
|
|
||||||
onSuccess = {
|
|
||||||
imageState = it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
if (dialogOpen) {
|
||||||
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
@Composable
|
||||||
visible = imageState !is AsyncImagePainter.State.Success,
|
private fun LocalImageView(
|
||||||
exit = fadeOut(animationSpec = tween(200))
|
content: ZoomableLocalImage,
|
||||||
) {
|
mainImageModifier: Modifier
|
||||||
if (content.bluehash != null) {
|
) {
|
||||||
DisplayBlueHash(content, mainImageModifier)
|
// store the dialog open or close state
|
||||||
} else {
|
var imageState by remember {
|
||||||
DisplayUrlWithLoadingSymbol(content)
|
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableUrlVideo) {
|
|
||||||
VideoView(content.url, content.description) { dialogOpen = true }
|
|
||||||
} else if (content is ZoomableBitmapImage) {
|
|
||||||
Box() {
|
|
||||||
AsyncImage(
|
|
||||||
model = content.localFile,
|
|
||||||
contentDescription = content.description,
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = mainImageModifier,
|
|
||||||
onLoading = {
|
|
||||||
imageState = it
|
|
||||||
},
|
|
||||||
onSuccess = {
|
|
||||||
imageState = it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
AsyncImage(
|
||||||
|
model = content.localFile,
|
||||||
|
contentDescription = content.description,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = mainImageModifier,
|
||||||
|
onLoading = {
|
||||||
|
imageState = it
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
imageState = it
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
|
HashVerificationSymbol(content.isVerified, Modifier.align(Alignment.TopEnd))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageState !is AsyncImagePainter.State.Success) {
|
AnimatedVisibility(
|
||||||
|
visible = imageState !is AsyncImagePainter.State.Success,
|
||||||
|
exit = fadeOut(animationSpec = tween(200))
|
||||||
|
) {
|
||||||
|
if (content.blurhash != null) {
|
||||||
|
DisplayBlueHash(content, mainImageModifier)
|
||||||
|
} else {
|
||||||
|
DisplayUrlWithLoadingSymbol(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UrlImageView(
|
||||||
|
content: ZoomableUrlImage,
|
||||||
|
mainImageModifier: Modifier
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// store the dialog open or close state
|
||||||
|
var imageState by remember {
|
||||||
|
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
var verifiedHash by remember {
|
||||||
|
mutableStateOf<Boolean?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
||||||
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
verifiedHash = verifyHash(content, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
AsyncImage(
|
||||||
|
model = content.url,
|
||||||
|
contentDescription = content.description,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = mainImageModifier,
|
||||||
|
onLoading = {
|
||||||
|
imageState = it
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
imageState = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (imageState is AsyncImagePainter.State.Success) {
|
||||||
|
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = imageState !is AsyncImagePainter.State.Success,
|
||||||
|
exit = fadeOut(animationSpec = tween(200))
|
||||||
|
) {
|
||||||
if (content.bluehash != null) {
|
if (content.bluehash != null) {
|
||||||
DisplayBlueHash(content, mainImageModifier)
|
DisplayBlueHash(content, mainImageModifier)
|
||||||
} else {
|
} else {
|
||||||
DisplayUrlWithLoadingSymbol(content)
|
DisplayUrlWithLoadingSymbol(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (content is ZoomableBytesVideo) {
|
|
||||||
VideoView(content.localFile, content.description) { dialogOpen = true }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (dialogOpen) {
|
private fun aspectRatio(dim: String?): Float? {
|
||||||
ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false })
|
if (dim == null) return null
|
||||||
|
|
||||||
|
val parts = dim.split("x")
|
||||||
|
if (parts.size != 2) return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val width = parts[0].toFloat()
|
||||||
|
val height = parts[1].toFloat()
|
||||||
|
width / height
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,16 +374,16 @@ private fun DisplayBlueHash(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisplayBlueHash(
|
private fun DisplayBlueHash(
|
||||||
content: ZoomableBitmapImage,
|
content: ZoomableLocalImage,
|
||||||
modifier: Modifier
|
modifier: Modifier
|
||||||
) {
|
) {
|
||||||
if (content.bluehash == null) return
|
if (content.blurhash == null) return
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = BlurHashRequester.imageRequest(
|
model = BlurHashRequester.imageRequest(
|
||||||
context,
|
context,
|
||||||
content.bluehash
|
content.blurhash
|
||||||
),
|
),
|
||||||
contentDescription = content.description,
|
contentDescription = content.description,
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
@@ -388,7 +421,7 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: List<ZoomableConte
|
|||||||
val myContent = allImages[pagerState.currentPage]
|
val myContent = allImages[pagerState.currentPage]
|
||||||
if (myContent is ZoomableUrlContent) {
|
if (myContent is ZoomableUrlContent) {
|
||||||
SaveToGallery(url = myContent.url)
|
SaveToGallery(url = myContent.url)
|
||||||
} else if (myContent is ZoomableBitmapImage && myContent.localFile != null) {
|
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
|
||||||
SaveToGallery(localFile = myContent.localFile, mimeType = myContent.mimeType)
|
SaveToGallery(localFile = myContent.localFile, mimeType = myContent.mimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,100 +444,19 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: List<ZoomableConte
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderImageOrVideo(content: ZoomableContent) {
|
fun RenderImageOrVideo(content: ZoomableContent) {
|
||||||
val scope = rememberCoroutineScope()
|
val mainModifier = Modifier
|
||||||
val context = LocalContext.current
|
.fillMaxSize()
|
||||||
|
.zoomable(rememberZoomState())
|
||||||
// store the dialog open or close state
|
|
||||||
var imageState by remember {
|
|
||||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
var verifiedHash by remember {
|
|
||||||
mutableStateOf<Boolean?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content is ZoomableUrlContent) {
|
|
||||||
LaunchedEffect(key1 = content.url, key2 = imageState) {
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
verifiedHash = verifyHash(content, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableBitmapImage) {
|
|
||||||
LaunchedEffect(key1 = content.localFile, key2 = imageState) {
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
verifiedHash = content.isVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableBytesVideo) {
|
|
||||||
LaunchedEffect(key1 = content.localFile, key2 = imageState) {
|
|
||||||
verifiedHash = content.isVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content is ZoomableUrlImage) {
|
if (content is ZoomableUrlImage) {
|
||||||
Box() {
|
UrlImageView(content = content, mainImageModifier = mainModifier)
|
||||||
AsyncImage(
|
|
||||||
model = content.url,
|
|
||||||
contentDescription = content.description,
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.zoomable(rememberZoomState()),
|
|
||||||
onLoading = {
|
|
||||||
imageState = it
|
|
||||||
},
|
|
||||||
onSuccess = {
|
|
||||||
imageState = it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (imageState is AsyncImagePainter.State.Success) {
|
|
||||||
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = imageState !is AsyncImagePainter.State.Success,
|
|
||||||
exit = fadeOut(animationSpec = tween(200))
|
|
||||||
) {
|
|
||||||
if (content.bluehash != null) {
|
|
||||||
DisplayBlueHash(content = content, modifier = Modifier.fillMaxWidth())
|
|
||||||
} else {
|
|
||||||
DisplayUrlWithLoadingSymbol(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableUrlVideo) {
|
} else if (content is ZoomableUrlVideo) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||||
VideoView(content.url, content.description)
|
VideoView(content.url, content.description)
|
||||||
}
|
}
|
||||||
} else if (content is ZoomableBitmapImage) {
|
} else if (content is ZoomableLocalImage) {
|
||||||
Box() {
|
LocalImageView(content = content, mainImageModifier = mainModifier)
|
||||||
AsyncImage(
|
} else if (content is ZoomableLocalVideo) {
|
||||||
model = content.localFile,
|
|
||||||
contentDescription = content.description,
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.zoomable(rememberZoomState()),
|
|
||||||
onLoading = {
|
|
||||||
imageState = it
|
|
||||||
},
|
|
||||||
onSuccess = {
|
|
||||||
imageState = it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (imageState !is AsyncImagePainter.State.Success) {
|
|
||||||
DisplayBlueHash(content = content, modifier = Modifier.fillMaxWidth())
|
|
||||||
} else {
|
|
||||||
HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (content is ZoomableBytesVideo) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||||
VideoView(content.localFile, content.description)
|
VideoView(content.localFile, content.description)
|
||||||
}
|
}
|
||||||
@@ -512,7 +464,7 @@ fun RenderImageOrVideo(content: ZoomableContent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
private suspend fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean? {
|
private fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean? {
|
||||||
if (content.hash == null) return null
|
if (content.hash == null) return null
|
||||||
|
|
||||||
context.imageLoader.diskCache?.get(content.url)?.use { snapshot ->
|
context.imageLoader.diskCache?.get(content.url)?.use { snapshot ->
|
||||||
|
|||||||
@@ -873,13 +873,13 @@ fun FileHeaderDisplay(note: Note) {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val blurHash = event.blurhash()
|
val blurHash = event.blurhash()
|
||||||
val hash = event.hash()
|
val hash = event.hash()
|
||||||
|
val dimensions = event.dimensions()
|
||||||
val description = event.content
|
val description = event.content
|
||||||
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
val removedParamsFromUrl = fullUrl.split("?")[0].lowercase()
|
||||||
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||||
val isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) }
|
|
||||||
val uri = "nostr:" + note.toNEvent()
|
val uri = "nostr:" + note.toNEvent()
|
||||||
content = if (isImage) {
|
content = if (isImage) {
|
||||||
ZoomableUrlImage(fullUrl, description, hash, blurHash, uri)
|
ZoomableUrlImage(fullUrl, description, hash, blurHash, dimensions, uri)
|
||||||
} else {
|
} else {
|
||||||
ZoomableUrlVideo(fullUrl, description, hash, uri)
|
ZoomableUrlVideo(fullUrl, description, hash, uri)
|
||||||
}
|
}
|
||||||
@@ -911,14 +911,30 @@ fun FileStorageHeaderDisplay(baseNote: Note) {
|
|||||||
val localDir = File(File(appContext.externalCacheDir, "NIP95"), fileNote.idHex)
|
val localDir = File(File(appContext.externalCacheDir, "NIP95"), fileNote.idHex)
|
||||||
val bytes = eventBytes?.decode()
|
val bytes = eventBytes?.decode()
|
||||||
val blurHash = eventHeader.blurhash()
|
val blurHash = eventHeader.blurhash()
|
||||||
|
val dimensions = eventHeader.dimensions()
|
||||||
val description = eventHeader.content
|
val description = eventHeader.content
|
||||||
val mimeType = eventHeader.mimeType()
|
val mimeType = eventHeader.mimeType()
|
||||||
|
|
||||||
content = if (mimeType?.startsWith("image") == true) {
|
content = if (mimeType?.startsWith("image") == true) {
|
||||||
ZoomableBitmapImage(localDir, mimeType, description, blurHash, true, uri)
|
ZoomableLocalImage(
|
||||||
|
localFile = localDir,
|
||||||
|
mimeType = mimeType,
|
||||||
|
description = description,
|
||||||
|
blurhash = blurHash,
|
||||||
|
dim = dimensions,
|
||||||
|
isVerified = true,
|
||||||
|
uri = uri
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
ZoomableBytesVideo(localDir, mimeType, description, true, uri)
|
ZoomableLocalVideo(
|
||||||
|
localFile = localDir,
|
||||||
|
mimeType = mimeType,
|
||||||
|
description = description,
|
||||||
|
dim = dimensions,
|
||||||
|
isVerified = true,
|
||||||
|
uri = uri
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user