182 lines
7.0 KiB
Kotlin
182 lines
7.0 KiB
Kotlin
package chats.presentation.components
|
|
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.border
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.isSystemInDarkTheme
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import coil.compose.AsyncImage
|
|
import core.utils.LinkMetadata
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.withContext
|
|
|
|
private val linkMetadataCache = mutableMapOf<String, LinkMetadata>()
|
|
|
|
@Composable
|
|
fun LinkPreview(
|
|
url: String,
|
|
modifier: Modifier = Modifier,
|
|
contentColor: Color = Color.White
|
|
) {
|
|
var metadata by remember(url) { mutableStateOf<LinkMetadata?>(linkMetadataCache[url]) }
|
|
var loading by remember(url) { mutableStateOf(metadata == null) }
|
|
val context = LocalContext.current
|
|
|
|
LaunchedEffect(url) {
|
|
if (metadata != null) {
|
|
loading = false
|
|
return@LaunchedEffect
|
|
}
|
|
|
|
loading = true
|
|
try {
|
|
val client = okhttp3.OkHttpClient.Builder()
|
|
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
|
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
|
.build()
|
|
|
|
val request = okhttp3.Request.Builder()
|
|
.url("https://api.microlink.io?url=${java.net.URLEncoder.encode(url, "UTF-8")}")
|
|
.header("User-Agent", "KnotMessenger/1.0 (Android)")
|
|
.build()
|
|
|
|
withContext(Dispatchers.IO) {
|
|
client.newCall(request).execute().use { response ->
|
|
if (response.isSuccessful) {
|
|
val body = response.body?.string()
|
|
if (body != null) {
|
|
val json = com.google.gson.JsonParser.parseString(body).asJsonObject
|
|
if (json.has("status") && json.get("status").asString == "success") {
|
|
val data = json.getAsJsonObject("data")
|
|
|
|
val title = data.get("title")?.takeIf { !it.isJsonNull }?.asString
|
|
val description = data.get("description")?.takeIf { !it.isJsonNull }?.asString
|
|
val imageUrl = data.getAsJsonObject("image")?.get("url")?.takeIf { !it.isJsonNull }?.asString
|
|
|
|
if (title != null || description != null || imageUrl != null) {
|
|
metadata = LinkMetadata(
|
|
url = url,
|
|
title = title,
|
|
description = description,
|
|
imageUrl = imageUrl
|
|
)
|
|
metadata?.let { linkMetadataCache[url] = it }
|
|
}
|
|
Unit
|
|
}
|
|
Unit
|
|
}
|
|
Unit
|
|
} else {
|
|
android.util.Log.e("LinkPreview", "API error: ${response.code} ${response.message}")
|
|
}
|
|
}
|
|
Unit
|
|
}
|
|
} catch (e: Exception) {
|
|
android.util.Log.e("LinkPreview", "Failed to fetch metadata for $url", e)
|
|
} finally {
|
|
loading = false
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
Text(
|
|
text = "Загрузка предпросмотра...",
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = contentColor.copy(alpha = 0.5f),
|
|
modifier = modifier.padding(vertical = 4.dp),
|
|
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
|
)
|
|
return
|
|
}
|
|
|
|
val currentMetadata = metadata ?: LinkMetadata(url = url)
|
|
|
|
Column(
|
|
modifier = modifier
|
|
.fillMaxWidth()
|
|
.clip(RoundedCornerShape(8.dp))
|
|
.background(Color.Black.copy(alpha = 0.2f))
|
|
.clickable {
|
|
try {
|
|
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url))
|
|
context.startActivity(intent)
|
|
} catch (e: Exception) {}
|
|
}
|
|
.border(
|
|
width = if (isSystemInDarkTheme()) 0.5.dp else 0.dp,
|
|
color = Color.White.copy(alpha = 0.1f),
|
|
shape = RoundedCornerShape(8.dp)
|
|
)
|
|
) {
|
|
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxHeight()
|
|
.width(3.dp)
|
|
.background(Color(0xFF3096E5))
|
|
)
|
|
|
|
Column(modifier = Modifier.padding(10.dp)) {
|
|
val domain = remember(url) {
|
|
try { java.net.URL(url).host.replace("www.", "") } catch (e: Exception) { "" }
|
|
}
|
|
|
|
Text(
|
|
text = domain,
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = Color(0xFF3096E5),
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.padding(bottom = 2.dp)
|
|
)
|
|
|
|
Text(
|
|
text = currentMetadata.title ?: url,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
fontWeight = FontWeight.Bold,
|
|
color = contentColor,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
|
|
currentMetadata.description?.let {
|
|
Text(
|
|
text = it,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = contentColor.copy(alpha = 0.7f),
|
|
maxLines = 3,
|
|
overflow = TextOverflow.Ellipsis,
|
|
lineHeight = 16.sp
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
currentMetadata.imageUrl?.let {
|
|
AsyncImage(
|
|
model = it,
|
|
contentDescription = null,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(max = 200.dp)
|
|
.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)),
|
|
contentScale = ContentScale.Crop
|
|
)
|
|
}
|
|
}
|
|
}
|