-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
60b03e9
commit deb06e3
Showing
4 changed files
with
295 additions
and
42 deletions.
There are no files selected for viewing
151 changes: 151 additions & 0 deletions
151
app/src/main/java/com/kongjak/koreatechboard/ui/components/text/AutoLinkText.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package com.kongjak.koreatechboard.ui.components.text | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.text.SpanStyle | ||
import androidx.compose.ui.text.buildAnnotatedString | ||
import androidx.compose.ui.text.style.TextDecoration | ||
import com.kongjak.koreatechboard.constraint.REGEX_EMAIL | ||
import com.kongjak.koreatechboard.constraint.REGEX_HTTP_HTTPS | ||
import com.kongjak.koreatechboard.constraint.REGEX_PHONE_NUMBER | ||
|
||
@Composable | ||
fun AutoLinkText( | ||
text: String, | ||
modifier: Modifier = Modifier, | ||
vararg autoLinkType: AutoLinkType = arrayOf(AutoLinkType.ALL), | ||
openWeb: ((webUrl: String) -> Unit?)? = null, | ||
openPhone: ((phoneNumber: String) -> Unit?)? = null, | ||
openEmail: ((email: String) -> Unit?)? = null | ||
) { | ||
if (autoLinkType.isEmpty()) throw IllegalArgumentException("AutoLinkType must not be empty") | ||
|
||
var web = false | ||
var phone = false | ||
var email = false | ||
|
||
var webOffsets = emptyList<IntRange>() | ||
var phoneOffsets = emptyList<IntRange>() | ||
var emailOffsets = emptyList<IntRange>() | ||
|
||
if (autoLinkType.contains(AutoLinkType.ALL)) { | ||
web = true | ||
phone = true | ||
email = true | ||
} else if (autoLinkType.contains(AutoLinkType.WEB)) { | ||
web = true | ||
webOffsets = extractAllURLOffsets(text) | ||
} else if (autoLinkType.contains(AutoLinkType.PHONE)) { | ||
phone = true | ||
phoneOffsets = extractAllPhoneNumberOffsets(text) | ||
} else if (autoLinkType.contains(AutoLinkType.EMAIL)) { | ||
email = true | ||
emailOffsets = extractAllEmailOffsets(text) | ||
} | ||
|
||
val annotatedString = buildAnnotatedString { | ||
append(text) | ||
if (web) { | ||
for (offset in webOffsets) { | ||
val urlTag = "${ANNOTATION_URL_PREFIX}${System.currentTimeMillis()}" | ||
addStringAnnotation( | ||
tag = urlTag, | ||
annotation = text.substring(offset.first, offset.last), | ||
start = offset.first, | ||
end = offset.last | ||
) | ||
addStyle( | ||
SpanStyle( | ||
textDecoration = TextDecoration.Underline | ||
), | ||
offset.first, | ||
offset.last | ||
) | ||
} | ||
} | ||
|
||
if (phone) { | ||
for (offset in phoneOffsets) { | ||
val phoneNumberTag = | ||
"${ANNOTATION_PHONE_NUMBER_PREFIX}${System.currentTimeMillis()}" | ||
addStringAnnotation( | ||
tag = phoneNumberTag, | ||
annotation = text.substring(offset.first, offset.last), | ||
start = offset.first, | ||
end = offset.last | ||
) | ||
addStyle( | ||
SpanStyle( | ||
textDecoration = TextDecoration.Underline | ||
), | ||
offset.first, | ||
offset.last | ||
) | ||
} | ||
} | ||
|
||
if (email) { | ||
for (offset in emailOffsets) { | ||
val emailTag = "${ANNOTATION_EMAIL_PREFIX}${System.currentTimeMillis()}" | ||
addStringAnnotation( | ||
tag = emailTag, | ||
annotation = text.substring(offset.first, offset.last), | ||
start = offset.first, | ||
end = offset.last | ||
) | ||
addStyle( | ||
SpanStyle( | ||
textDecoration = TextDecoration.Underline | ||
), | ||
offset.first, | ||
offset.last | ||
) | ||
} | ||
} | ||
} | ||
|
||
CustomClickableText( | ||
modifier = modifier, | ||
text = annotatedString | ||
) { offset -> | ||
annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { url -> | ||
if (url.tag.startsWith(ANNOTATION_URL_PREFIX)) { | ||
openWeb?.invoke(url.item) | ||
} else if (url.tag.startsWith(ANNOTATION_PHONE_NUMBER_PREFIX)) { | ||
openPhone?.invoke(url.item) | ||
} else if (url.tag.startsWith(ANNOTATION_EMAIL_PREFIX)) { | ||
openEmail?.invoke(url.item) | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
sealed class AutoLinkType { | ||
data object WEB : AutoLinkType() | ||
data object PHONE : AutoLinkType() | ||
data object EMAIL : AutoLinkType() | ||
data object ALL : AutoLinkType() | ||
} | ||
|
||
private fun extractAllURLOffsets(text: String): List<IntRange> { | ||
val urlRegex = Regex(REGEX_HTTP_HTTPS) | ||
val matches = urlRegex.findAll(text) | ||
return matches.map { it.range.first..it.range.last + 1 }.toList() | ||
} | ||
|
||
private fun extractAllPhoneNumberOffsets(text: String): List<IntRange> { | ||
val phoneNumberRegex = Regex(REGEX_PHONE_NUMBER) | ||
val matches = phoneNumberRegex.findAll(text) | ||
return matches.map { it.range.first..it.range.last + 1 }.toList() | ||
} | ||
|
||
private fun extractAllEmailOffsets(text: String): List<IntRange> { | ||
val emailRegex = Regex(REGEX_EMAIL) | ||
val matches = emailRegex.findAll(text) | ||
return matches.map { it.range.first..it.range.last + 1 }.toList() | ||
} | ||
|
||
const val ANNOTATION_URL_PREFIX = "url_" | ||
const val ANNOTATION_PHONE_NUMBER_PREFIX = "phone_number_" | ||
const val ANNOTATION_EMAIL_PREFIX = "email_" |
90 changes: 90 additions & 0 deletions
90
app/src/main/java/com/kongjak/koreatechboard/ui/components/text/CustomClickableText.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package com.kongjak.koreatechboard.ui.components.text | ||
|
||
import androidx.compose.foundation.gestures.detectTapGestures | ||
import androidx.compose.foundation.text.BasicText | ||
import androidx.compose.foundation.text.InlineTextContent | ||
import androidx.compose.foundation.text.selection.SelectionContainer | ||
import androidx.compose.material3.LocalContentColor | ||
import androidx.compose.material3.LocalTextStyle | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.takeOrElse | ||
import androidx.compose.ui.input.pointer.pointerInput | ||
import androidx.compose.ui.text.AnnotatedString | ||
import androidx.compose.ui.text.TextLayoutResult | ||
import androidx.compose.ui.text.TextStyle | ||
import androidx.compose.ui.text.font.FontFamily | ||
import androidx.compose.ui.text.font.FontStyle | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.text.style.TextAlign | ||
import androidx.compose.ui.text.style.TextDecoration | ||
import androidx.compose.ui.text.style.TextOverflow | ||
import androidx.compose.ui.unit.TextUnit | ||
|
||
@Composable | ||
fun CustomClickableText( | ||
text: AnnotatedString, | ||
modifier: Modifier = Modifier, | ||
color: Color = Color.Unspecified, | ||
fontSize: TextUnit = TextUnit.Unspecified, | ||
fontStyle: FontStyle? = null, | ||
fontWeight: FontWeight? = null, | ||
fontFamily: FontFamily? = null, | ||
letterSpacing: TextUnit = TextUnit.Unspecified, | ||
textDecoration: TextDecoration? = null, | ||
textAlign: TextAlign? = null, | ||
lineHeight: TextUnit = TextUnit.Unspecified, | ||
overflow: TextOverflow = TextOverflow.Clip, | ||
softWrap: Boolean = true, | ||
maxLines: Int = Int.MAX_VALUE, | ||
minLines: Int = 1, | ||
inlineContent: Map<String, InlineTextContent> = mapOf(), | ||
onTextLayout: (TextLayoutResult) -> Unit = {}, | ||
style: TextStyle = LocalTextStyle.current, | ||
onClick: (Int) -> Unit | ||
) { | ||
val textColor = color.takeOrElse { | ||
style.color.takeOrElse { | ||
LocalContentColor.current | ||
} | ||
} | ||
|
||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) } | ||
val pressIndicator = Modifier.pointerInput(onClick) { | ||
detectTapGestures { pos -> | ||
layoutResult.value?.let { layoutResult -> | ||
onClick(layoutResult.getOffsetForPosition(pos)) | ||
} | ||
} | ||
} | ||
|
||
SelectionContainer { | ||
BasicText( | ||
text = text, | ||
modifier = modifier.then(pressIndicator), | ||
style = style.merge( | ||
color = textColor, | ||
fontSize = fontSize, | ||
fontWeight = fontWeight, | ||
textAlign = textAlign ?: TextAlign.Unspecified, | ||
lineHeight = lineHeight, | ||
fontFamily = fontFamily, | ||
textDecoration = textDecoration, | ||
fontStyle = fontStyle, | ||
letterSpacing = letterSpacing | ||
), | ||
onTextLayout = { | ||
layoutResult.value = it | ||
onTextLayout(it) | ||
}, | ||
overflow = overflow, | ||
softWrap = softWrap, | ||
maxLines = maxLines, | ||
minLines = minLines, | ||
inlineContent = inlineContent | ||
) | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
app/src/main/java/com/kongjak/koreatechboard/ui/components/text/FileText.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.kongjak.koreatechboard.ui.components.text | ||
|
||
import android.net.Uri | ||
import androidx.browser.customtabs.CustomTabsIntent | ||
import androidx.compose.foundation.text.ClickableText | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.text.SpanStyle | ||
import androidx.compose.ui.text.buildAnnotatedString | ||
import androidx.compose.ui.text.withStyle | ||
import com.kongjak.koreatechboard.domain.model.Files | ||
|
||
@Composable | ||
fun FileText(modifier: Modifier = Modifier, files: List<Files>) { | ||
val context = LocalContext.current | ||
|
||
val annotatedString = buildAnnotatedString { | ||
for (file in files) { | ||
pushStringAnnotation(file.fileName, file.fileUrl) | ||
withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { | ||
append(file.fileName) | ||
} | ||
pop() | ||
append("\n") | ||
} | ||
} | ||
|
||
ClickableText(modifier = modifier, text = annotatedString) { offset -> | ||
annotatedString.getStringAnnotations(offset, offset) | ||
.firstOrNull()?.let { url -> | ||
val builder = CustomTabsIntent.Builder() | ||
val customTabsIntent = builder.build() | ||
customTabsIntent.launchUrl( | ||
context, | ||
if (url.item.startsWith("http")) { | ||
Uri.parse(url.item) | ||
} else { | ||
Uri.parse("http://www.koreatech.ac.kr/${url.item}") | ||
} | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters