mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-26 00:56:34 +02:00
Image uploading and Image/URL previews on new posts.
This commit is contained in:
@@ -90,7 +90,10 @@ dependencies {
|
|||||||
|
|
||||||
// link preview
|
// link preview
|
||||||
implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1'
|
||||||
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha03'
|
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha04'
|
||||||
|
|
||||||
|
// upload pictures:
|
||||||
|
implementation "com.google.accompanist:accompanist-permissions:0.28.0"
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@@ -31,7 +31,7 @@ object NostrAccountDataSource: NostrDataSource("AccountData") {
|
|||||||
fun createAccountFilter(): JsonFilter {
|
fun createAccountFilter(): JsonFilter {
|
||||||
return JsonFilter(
|
return JsonFilter(
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 4), // 4 days
|
since = System.currentTimeMillis() / 1000 - (60 * 60 * 24 * 7), // 4 days
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,57 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.UUID
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
object ImageUploader {
|
||||||
|
private fun encodeImage(bitmap: Bitmap): ByteArray {
|
||||||
|
val byteArrayOutPutStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutPutStream)
|
||||||
|
return byteArrayOutPutStream.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadImage(bitmap: Bitmap, onSuccess: (String) -> Unit) {
|
||||||
|
val client = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
|
val body: RequestBody = MultipartBody.Builder()
|
||||||
|
.setType(MultipartBody.FORM)
|
||||||
|
.addFormDataPart(
|
||||||
|
"image",
|
||||||
|
"${UUID.randomUUID()}.png",
|
||||||
|
encodeImage(bitmap).toRequestBody("image/png".toMediaType())
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request: Request = Request.Builder()
|
||||||
|
.url("https://api.imgur.com/3/image")
|
||||||
|
.header("Authorization", "Client-ID e6aea87296f3f96")
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.use {
|
||||||
|
val tree = jacksonObjectMapper().readTree(response.body!!.string())
|
||||||
|
val url = tree.get("data").get("link").asText()
|
||||||
|
onSuccess(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -17,48 +17,73 @@ import androidx.compose.material.Surface
|
|||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextFieldDefaults
|
import androidx.compose.material.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TagLink
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.UrlPreview
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.imageExtension
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.tagIndex
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
|
|
||||||
class PostViewModel: ViewModel() {
|
|
||||||
var account: Account? = null
|
|
||||||
var message by mutableStateOf("")
|
|
||||||
var replyingTo: Note? = null
|
|
||||||
|
|
||||||
fun sendPost() {
|
|
||||||
account?.sendPost(message, replyingTo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account) {
|
fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account) {
|
||||||
val postViewModel: PostViewModel = viewModel<PostViewModel>().apply {
|
val postViewModel: NewPostViewModel = viewModel<NewPostViewModel>().apply {
|
||||||
this.replyingTo = replyingTo
|
this.replyingTo = replyingTo
|
||||||
this.account = account
|
this.account = account
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialogProperties = DialogProperties()
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// initialize focus reference to be able to request focus programmatically
|
||||||
|
val focusRequester = FocusRequester()
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(100)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { onClose() }, properties = dialogProperties
|
onDismissRequest = { onClose() },
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnClickOutside = false
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.5f)
|
.fillMaxHeight()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(10.dp)
|
modifier = Modifier.padding(10.dp)
|
||||||
@@ -69,7 +94,14 @@ fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account)
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
CloseButton(onCancel = onClose)
|
CloseButton(onCancel = {
|
||||||
|
postViewModel.cancel()
|
||||||
|
onClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
UploadFromGallery {
|
||||||
|
postViewModel.upload(it, context)
|
||||||
|
}
|
||||||
|
|
||||||
PostButton(
|
PostButton(
|
||||||
onPost = {
|
onPost = {
|
||||||
@@ -96,16 +128,26 @@ fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account)
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = postViewModel.message,
|
value = postViewModel.message,
|
||||||
onValueChange = { postViewModel.message = it },
|
onValueChange = {
|
||||||
|
postViewModel.message = it
|
||||||
|
postViewModel.urlPreview = postViewModel.findUrlInMessage()
|
||||||
|
},
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
capitalization = KeyboardCapitalization.Sentences
|
capitalization = KeyboardCapitalization.Sentences
|
||||||
),
|
),
|
||||||
modifier = Modifier.fillMaxWidth().fillMaxHeight()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.border(
|
.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = MaterialTheme.colors.surface,
|
color = MaterialTheme.colors.surface,
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
),
|
)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = "What's on your mind?",
|
text = "What's on your mind?",
|
||||||
@@ -117,8 +159,32 @@ fun NewPostView(onClose: () -> Unit, replyingTo: Note? = null, account: Account)
|
|||||||
unfocusedBorderColor = Color.Transparent,
|
unfocusedBorderColor = Color.Transparent,
|
||||||
focusedBorderColor = Color.Transparent
|
focusedBorderColor = Color.Transparent
|
||||||
)
|
)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val myUrlPreview = postViewModel.urlPreview
|
||||||
|
if (myUrlPreview != null) {
|
||||||
|
Column(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
|
val removedParamsFromUrl = myUrlPreview.split("?")[0].toLowerCase()
|
||||||
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = myUrlPreview,
|
||||||
|
contentDescription = myUrlPreview,
|
||||||
|
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 {
|
||||||
|
UrlPreview("https://$myUrlPreview", myUrlPreview, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,58 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
|
||||||
|
|
||||||
|
class NewPostViewModel: ViewModel() {
|
||||||
|
var account: Account? = null
|
||||||
|
var replyingTo: Note? = null
|
||||||
|
|
||||||
|
var message by mutableStateOf("")
|
||||||
|
var urlPreview by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
fun sendPost() {
|
||||||
|
account?.sendPost(message, replyingTo)
|
||||||
|
message = ""
|
||||||
|
urlPreview = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upload(it: Uri, context: Context) {
|
||||||
|
val img = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(context.contentResolver, it)
|
||||||
|
} else {
|
||||||
|
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, it))
|
||||||
|
}
|
||||||
|
|
||||||
|
img?.let {
|
||||||
|
ImageUploader.uploadImage(img) {
|
||||||
|
message = message + "\n\n" + it
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
message = ""
|
||||||
|
urlPreview = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findUrlInMessage(): String? {
|
||||||
|
return message.split('\n').firstNotNullOfOrNull { paragraph ->
|
||||||
|
paragraph.split(' ').firstOrNull { word: String ->
|
||||||
|
isValidURL(word) || noProtocolUrlValidator.matcher(word).matches()
|
||||||
|
}.apply { println("(${this})") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,96 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.navigation
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
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.unit.dp
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.google.accompanist.permissions.shouldShowRationale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun UploadFromGallery(onImageChosen: (Uri) -> Unit) {
|
||||||
|
val cameraPermissionState = rememberPermissionState(
|
||||||
|
android.Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
)
|
||||||
|
|
||||||
|
if (cameraPermissionState.status.isGranted) {
|
||||||
|
var showGallerySelect by remember { mutableStateOf(false) }
|
||||||
|
if (showGallerySelect) {
|
||||||
|
GallerySelect(
|
||||||
|
onImageUri = { uri ->
|
||||||
|
showGallerySelect = false
|
||||||
|
if (uri != null)
|
||||||
|
onImageChosen(uri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box() {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(4.dp),
|
||||||
|
onClick = {
|
||||||
|
showGallerySelect = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Add Image from Gallery")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column {
|
||||||
|
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
|
||||||
|
// If the user has denied the permission but the rationale can be shown,
|
||||||
|
// then gently explain why the app requires this permission
|
||||||
|
"Grant access to quickly upload pictures before posting"
|
||||||
|
} else {
|
||||||
|
// If it's the first time the user lands on this feature, or the user
|
||||||
|
// doesn't want to be asked again for this permission, explain that the
|
||||||
|
// permission is required
|
||||||
|
"Grant access to quickly upload pictures before posting"
|
||||||
|
}
|
||||||
|
Text(textToShow)
|
||||||
|
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
|
||||||
|
Text("Request permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GallerySelect(
|
||||||
|
onImageUri: (Uri?) -> Unit = { }
|
||||||
|
) {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.GetContent(),
|
||||||
|
onResult = { uri: Uri? ->
|
||||||
|
onImageUri(uri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LaunchGallery() {
|
||||||
|
SideEffect {
|
||||||
|
launcher.launch("image/*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchGallery()
|
||||||
|
}
|
@@ -34,31 +34,30 @@ import com.baha.url.preview.UrlInfoItem
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UrlPreview(url: String, urlText: String) {
|
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) }
|
||||||
|
|
||||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
|
||||||
LaunchedEffect(urlPreviewState) {
|
|
||||||
if (urlPreviewState == UrlPreviewState.Loading) {
|
|
||||||
val urlPreview = BahaUrlPreview(url, object : IUrlPreviewCallback {
|
|
||||||
override fun onComplete(urlInfo: UrlInfoItem) {
|
|
||||||
if (urlInfo.allFetchComplete() && urlInfo.url == url)
|
|
||||||
urlPreviewState = UrlPreviewState.Loaded(urlInfo)
|
|
||||||
else
|
|
||||||
urlPreviewState = UrlPreviewState.Empty
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailed(throwable: Throwable) {
|
|
||||||
urlPreviewState = UrlPreviewState.Error("Error parsing preview for ${url}: ${throwable.message}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
urlPreview.fetchUrlPreview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
|
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||||
|
LaunchedEffect(url) {
|
||||||
|
println("url preview ${url}")
|
||||||
|
BahaUrlPreview(url, object : IUrlPreviewCallback {
|
||||||
|
override fun onComplete(urlInfo: UrlInfoItem) {
|
||||||
|
println("completed ${urlInfo.title}")
|
||||||
|
if (urlInfo.allFetchComplete() && urlInfo.url == url)
|
||||||
|
urlPreviewState = UrlPreviewState.Loaded(urlInfo)
|
||||||
|
else
|
||||||
|
urlPreviewState = UrlPreviewState.Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailed(throwable: Throwable) {
|
||||||
|
println("failed")
|
||||||
|
urlPreviewState = UrlPreviewState.Error("Error parsing preview for ${url}: ${throwable.message}")
|
||||||
|
}
|
||||||
|
}).fetchUrlPreview()
|
||||||
|
}
|
||||||
|
|
||||||
Crossfade(targetState = urlPreviewState) { state ->
|
Crossfade(targetState = urlPreviewState) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is UrlPreviewState.Loaded -> {
|
is UrlPreviewState.Loaded -> {
|
||||||
@@ -97,11 +96,13 @@ fun UrlPreview(url: String, urlText: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ClickableText(
|
if (showUrlIfError) {
|
||||||
text = AnnotatedString("$urlText "),
|
ClickableText(
|
||||||
onClick = { runCatching { uri.openUri(url) } },
|
text = AnnotatedString("$urlText "),
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
onClick = { runCatching { uri.openUri(url) } },
|
||||||
)
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user