feat: auto-load OpenGraph data when adding web bookmarks

When creating a new web bookmark, automatically fetches OpenGraph
metadata (title, description) after the user leaves the URL field.
Only populates fields that are still empty, preserving any user input.

https://claude.ai/code/session_01RteQotL6WHqJtDr38fUHan
This commit is contained in:
Claude
2026-03-27 13:28:41 +00:00
parent 511fcb4c2a
commit c0a8da0e0c
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
@@ -39,6 +40,7 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
@@ -52,9 +54,11 @@ 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.focus.onFocusChanged
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -65,6 +69,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
@@ -80,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
import kotlinx.coroutines.launch
@Composable
fun WebBookmarksScreen(
@@ -102,6 +108,7 @@ private fun RenderWebBookmarksScreen(
if (showAddDialog) {
WebBookmarkEditDialog(
accountViewModel = accountViewModel,
onDismiss = { showAddDialog = false },
onSave = { url, title, description, tags ->
accountViewModel.launchSigner {
@@ -193,6 +200,7 @@ private fun WebBookmarkCard(
if (showEditDialog) {
WebBookmarkEditDialog(
accountViewModel = accountViewModel,
initialUrl = event.url(),
initialTitle = event.title() ?: "",
initialDescription = event.description(),
@@ -312,6 +320,7 @@ private fun WebBookmarkCard(
@Composable
fun WebBookmarkEditDialog(
accountViewModel: AccountViewModel,
initialUrl: String = "",
initialTitle: String = "",
initialDescription: String = "",
@@ -323,8 +332,38 @@ fun WebBookmarkEditDialog(
var title by remember { mutableStateOf(initialTitle) }
var description by remember { mutableStateOf(initialDescription) }
var tags by remember { mutableStateOf(initialTags) }
var isLoadingPreview by remember { mutableStateOf(false) }
var lastFetchedUrl by remember { mutableStateOf(initialUrl) }
val isEditing = initialUrl.isNotEmpty()
val scope = rememberCoroutineScope()
fun fetchOpenGraphData(urlToFetch: String) {
if (urlToFetch.isBlank() || urlToFetch == lastFetchedUrl) return
val normalizedUrl = if (!urlToFetch.startsWith("http")) "https://$urlToFetch" else urlToFetch
lastFetchedUrl = urlToFetch
isLoadingPreview = true
accountViewModel.urlPreview(normalizedUrl) { state ->
scope.launch {
when (state) {
is UrlPreviewState.Loaded -> {
val info = state.previewInfo
if (title.isBlank() && info.title.isNotBlank()) {
title = info.title
}
if (description.isBlank() && info.description.isNotBlank()) {
description = info.description
}
}
else -> {}
}
isLoadingPreview = false
}
}
}
AlertDialog(
onDismissRequest = onDismiss,
@@ -343,7 +382,22 @@ fun WebBookmarkEditDialog(
label = { Text(stringResource(R.string.web_bookmark_url_label)) },
placeholder = { Text(stringResource(R.string.web_bookmark_url_placeholder)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
modifier =
Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
if (!focusState.isFocused && url.isNotBlank()) {
fetchOpenGraphData(url)
}
},
trailingIcon = {
if (isLoadingPreview) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
)
}
},
)
Spacer(modifier = Modifier.height(8.dp))