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:
+55
-1
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
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.Edit
|
||||||
import androidx.compose.material.icons.filled.OpenInBrowser
|
import androidx.compose.material.icons.filled.OpenInBrowser
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -52,9 +54,11 @@ import androidx.compose.runtime.Composable
|
|||||||
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.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.FeedContentState
|
||||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
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.RefresheableBox
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
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.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
|
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebBookmarksScreen(
|
fun WebBookmarksScreen(
|
||||||
@@ -102,6 +108,7 @@ private fun RenderWebBookmarksScreen(
|
|||||||
|
|
||||||
if (showAddDialog) {
|
if (showAddDialog) {
|
||||||
WebBookmarkEditDialog(
|
WebBookmarkEditDialog(
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
onDismiss = { showAddDialog = false },
|
onDismiss = { showAddDialog = false },
|
||||||
onSave = { url, title, description, tags ->
|
onSave = { url, title, description, tags ->
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
@@ -193,6 +200,7 @@ private fun WebBookmarkCard(
|
|||||||
|
|
||||||
if (showEditDialog) {
|
if (showEditDialog) {
|
||||||
WebBookmarkEditDialog(
|
WebBookmarkEditDialog(
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
initialUrl = event.url(),
|
initialUrl = event.url(),
|
||||||
initialTitle = event.title() ?: "",
|
initialTitle = event.title() ?: "",
|
||||||
initialDescription = event.description(),
|
initialDescription = event.description(),
|
||||||
@@ -312,6 +320,7 @@ private fun WebBookmarkCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebBookmarkEditDialog(
|
fun WebBookmarkEditDialog(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
initialUrl: String = "",
|
initialUrl: String = "",
|
||||||
initialTitle: String = "",
|
initialTitle: String = "",
|
||||||
initialDescription: String = "",
|
initialDescription: String = "",
|
||||||
@@ -323,8 +332,38 @@ fun WebBookmarkEditDialog(
|
|||||||
var title by remember { mutableStateOf(initialTitle) }
|
var title by remember { mutableStateOf(initialTitle) }
|
||||||
var description by remember { mutableStateOf(initialDescription) }
|
var description by remember { mutableStateOf(initialDescription) }
|
||||||
var tags by remember { mutableStateOf(initialTags) }
|
var tags by remember { mutableStateOf(initialTags) }
|
||||||
|
var isLoadingPreview by remember { mutableStateOf(false) }
|
||||||
|
var lastFetchedUrl by remember { mutableStateOf(initialUrl) }
|
||||||
|
|
||||||
val isEditing = initialUrl.isNotEmpty()
|
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(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -343,7 +382,22 @@ fun WebBookmarkEditDialog(
|
|||||||
label = { Text(stringResource(R.string.web_bookmark_url_label)) },
|
label = { Text(stringResource(R.string.web_bookmark_url_label)) },
|
||||||
placeholder = { Text(stringResource(R.string.web_bookmark_url_placeholder)) },
|
placeholder = { Text(stringResource(R.string.web_bookmark_url_placeholder)) },
|
||||||
singleLine = true,
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
Reference in New Issue
Block a user