From c5a464cf4507d8eab5dfda6756c914419bcfa7e8 Mon Sep 17 00:00:00 2001
From: rozPierog <rozpierog@gmail.com>
Date: Fri, 26 Apr 2024 10:32:24 +0200
Subject: [PATCH] Fix Annotated link clicks

---
 .../com/omelan/cofi/components/Description.kt | 12 ----
 .../pages/settings/licenses/DependencyItem.kt | 61 +++++++++----------
 .../omelan/cofi/utils/AnnotatedStringUtils.kt | 59 ++++--------------
 3 files changed, 40 insertions(+), 92 deletions(-)

diff --git a/app/src/main/java/com/omelan/cofi/components/Description.kt b/app/src/main/java/com/omelan/cofi/components/Description.kt
index 3dd08c2b..348ca5bc 100644
--- a/app/src/main/java/com/omelan/cofi/components/Description.kt
+++ b/app/src/main/java/com/omelan/cofi/components/Description.kt
@@ -16,7 +16,6 @@ import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.text.style.TextMotion
 import androidx.compose.ui.tooling.preview.Preview
@@ -28,7 +27,6 @@ import com.omelan.cofi.utils.buildAnnotatedStringWithUrls
 
 @Composable
 fun Description(modifier: Modifier = Modifier, descriptionText: String) {
-    val uriHandler = LocalUriHandler.current
     var isExpanded by remember { mutableStateOf(false) }
     var showExpandButton by remember { mutableStateOf(false) }
     val rotationDegree by animateFloatAsState(
@@ -73,16 +71,6 @@ fun Description(modifier: Modifier = Modifier, descriptionText: String) {
                     textMotion = TextMotion.Animated,
                 ),
                 modifier = Modifier.animateContentSize(),
-//                onClick = {
-//                    descriptionWithLinks
-//                        .getStringAnnotations(URL_ANNOTATION, it, it)
-//                        .firstOrNull()?.let { stringAnnotation ->
-//                        uriHandler.openUri(stringAnnotation.item)
-//                    }
-//                    if (showExpandButton) {
-//                        isExpanded = !isExpanded
-//                    }
-//                },
                 onTextLayout = { textLayoutResult ->
                     if (!isExpanded) {
                         showExpandButton = textLayoutResult.didOverflowHeight
diff --git a/app/src/main/java/com/omelan/cofi/pages/settings/licenses/DependencyItem.kt b/app/src/main/java/com/omelan/cofi/pages/settings/licenses/DependencyItem.kt
index 71575ec2..3adb65f9 100644
--- a/app/src/main/java/com/omelan/cofi/pages/settings/licenses/DependencyItem.kt
+++ b/app/src/main/java/com/omelan/cofi/pages/settings/licenses/DependencyItem.kt
@@ -3,7 +3,6 @@ package com.omelan.cofi.pages.settings.licenses
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.text.ClickableText
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
@@ -11,15 +10,16 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.text.LinkAnnotation
 import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.withLink
 import androidx.compose.ui.tooling.preview.Preview
 import com.omelan.cofi.pages.settings.settingsItemModifier
 import com.omelan.cofi.share.model.Dependency
 import com.omelan.cofi.share.model.License
 import com.omelan.cofi.ui.Spacing
-import com.omelan.cofi.utils.URL_ANNOTATION
-import com.omelan.cofi.utils.appendLink
+import com.omelan.cofi.utils.linkSpanStyle
 
 @Composable
 fun DependencyItem(dependency: Dependency) {
@@ -31,11 +31,14 @@ fun DependencyItem(dependency: Dependency) {
     }
     val licenses = buildAnnotatedString {
         dependency.licenses.forEachIndexed { index, license ->
-            appendLink(
-                text = license.license,
-                url = license.license_url,
-                color = MaterialTheme.colorScheme.secondary,
-            )
+            withLink(
+                LinkAnnotation.Url(
+                    license.license_url,
+                    style = linkSpanStyle(MaterialTheme.colorScheme.secondary),
+                ),
+            ) {
+                append(license.license)
+            }
             if (index != dependency.licenses.size - 1) {
                 append(", ")
             }
@@ -71,16 +74,8 @@ fun DependencyItem(dependency: Dependency) {
             fontWeight = FontWeight.Medium,
             color = MaterialTheme.colorScheme.onBackground,
         )
-        ClickableText(
+        Text(
             text = licenses,
-            onClick = {
-                licenses
-                    .getStringAnnotations(URL_ANNOTATION, it, it)
-                    .firstOrNull()?.let { stringAnnotation ->
-                    uriHandler.openUri(stringAnnotation.item)
-                    return@ClickableText
-                }
-            },
             modifier = Modifier.padding(bottom = Spacing.normal),
         )
         HorizontalDivider()
@@ -94,22 +89,22 @@ fun DependencyPreview() {
         dependency = Dependency(
             project = "Nice package",
             description = "Contains Guava\\u0027s " +
-                "com.google.common.util.concurrent.ListenableFuture" +
-                " class,\\n    without any of its other classes -- but is also available in " +
-                "a second\\n    \\\"version\\\" that omits the class to avoid conflicts with " +
-                "the copy in Guava\\n    itself. The idea is:\\n\\n    - If users want only " +
-                "ListenableFuture, they depend on listenablefuture-1.0.\\n\\n    " +
-                "- If users want all of Guava, they depend on guava, which, as of Guava\\n   " +
-                " 27.0, depends on\\n   " +
-                " listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. " +
-                "The 9999.0-...\\n    version number is enough for some build systems" +
-                " (notably, Gradle) to select\\n    that empty artifact over the " +
-                "\\\"real\\\" listenablefuture-1.0 -- avoiding a\\n    " +
-                "conflict with the copy of ListenableFuture in guava itself. If users are\\n " +
-                "   using an older version of Guava or a build system other than Gradle," +
-                " they\\n    may see class conflicts. If so, they can solve them by manually" +
-                " excluding\\n    the listenablefuture artifact or manually forcing their " +
-                "build systems to\\n    use 9999.0-....\",\n",
+                    "com.google.common.util.concurrent.ListenableFuture" +
+                    " class,\\n    without any of its other classes -- but is also available in " +
+                    "a second\\n    \\\"version\\\" that omits the class to avoid conflicts with " +
+                    "the copy in Guava\\n    itself. The idea is:\\n\\n    - If users want only " +
+                    "ListenableFuture, they depend on listenablefuture-1.0.\\n\\n    " +
+                    "- If users want all of Guava, they depend on guava, which, as of Guava\\n   " +
+                    " 27.0, depends on\\n   " +
+                    " listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. " +
+                    "The 9999.0-...\\n    version number is enough for some build systems" +
+                    " (notably, Gradle) to select\\n    that empty artifact over the " +
+                    "\\\"real\\\" listenablefuture-1.0 -- avoiding a\\n    " +
+                    "conflict with the copy of ListenableFuture in guava itself. If users are\\n " +
+                    "   using an older version of Guava or a build system other than Gradle," +
+                    " they\\n    may see class conflicts. If so, they can solve them by manually" +
+                    " excluding\\n    the listenablefuture artifact or manually forcing their " +
+                    "build systems to\\n    use 9999.0-....\",\n",
             version = "3.2.1",
             developers = listOf("Leon Omelan"),
             url = "jsonObject.getString( url )",
diff --git a/app/src/main/java/com/omelan/cofi/utils/AnnotatedStringUtils.kt b/app/src/main/java/com/omelan/cofi/utils/AnnotatedStringUtils.kt
index 361dbb2d..a5dc616a 100644
--- a/app/src/main/java/com/omelan/cofi/utils/AnnotatedStringUtils.kt
+++ b/app/src/main/java/com/omelan/cofi/utils/AnnotatedStringUtils.kt
@@ -4,7 +4,9 @@ import android.annotation.SuppressLint
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.*
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextDecoration
 import java.util.regex.Matcher
 import java.util.regex.Pattern
@@ -16,37 +18,6 @@ fun linkSpanStyle(color: Color) = SpanStyle(
     textDecoration = TextDecoration.Underline,
 )
 
-fun AnnotatedString.Builder.addLinkAnnotation(
-    start: Int,
-    text: String,
-    url: String = text,
-    color: Color,
-) {
-    addStringAnnotation(
-        tag = URL_ANNOTATION,
-        annotation = url,
-        start = start,
-        end = start + text.length,
-    )
-    addStyle(
-        linkSpanStyle(color),
-        start = start,
-        end = start + text.length,
-    )
-}
-
-@OptIn(ExperimentalTextApi::class)
-fun AnnotatedString.Builder.appendLink(text: String, url: String = text, color: Color) {
-    withStyle(linkSpanStyle(color)) {
-        withAnnotation(
-            tag = URL_ANNOTATION,
-            annotation = url,
-        ) {
-            append(text)
-        }
-    }
-}
-
 private fun extractUrls(text: String): List<String> {
     val containedUrls: MutableList<String> = arrayListOf()
     val urlRegex =
@@ -67,20 +38,7 @@ private fun extractUrls(text: String): List<String> {
 @SuppressLint("ComposableNaming")
 @Composable
 fun buildAnnotatedStringWithUrls(baseText: String) =
-    buildAnnotatedString {
-        val urlsInDescription = extractUrls(baseText)
-        append(baseText)
-        var lastPosition = 0
-        urlsInDescription.forEach {
-            val positionOfUrl = baseText.indexOf(it, startIndex = lastPosition)
-            lastPosition = positionOfUrl
-            addLinkAnnotation(
-                start = positionOfUrl,
-                text = it,
-                color = MaterialTheme.colorScheme.secondary,
-            )
-        }
-    }
+    buildAnnotatedStringWithUrls(baseText, MaterialTheme.colorScheme.secondary)
 
 fun buildAnnotatedStringWithUrls(baseText: String, color: Color) =
     buildAnnotatedString {
@@ -90,6 +48,13 @@ fun buildAnnotatedStringWithUrls(baseText: String, color: Color) =
         urlsInDescription.forEach {
             val positionOfUrl = baseText.indexOf(it, startIndex = lastPosition)
             lastPosition = positionOfUrl
-            addLinkAnnotation(start = positionOfUrl, text = it, color = color)
+            addLink(
+                LinkAnnotation.Url(
+                    it,
+                    style = linkSpanStyle(color),
+                ),
+                start = positionOfUrl,
+                end = positionOfUrl + it.length,
+            )
         }
     }