Skip to content

Commit

Permalink
Add better blockquote support (#194)
Browse files Browse the repository at this point in the history
- Surround highlighted text and auto-filled description with <blockquote> by default when saving
- Properly display bookmark description with blockquote on the main screen and when expanding the description
  • Loading branch information
fibelatti committed Aug 13, 2023
1 parent 960d692 commit 233cc8e
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.fibelatti.pinboard.core.android

import android.graphics.Canvas
import android.graphics.Paint
import android.text.Layout
import android.text.Spannable
import android.text.style.LeadingMarginSpan
import android.text.style.LineBackgroundSpan
import android.text.style.QuoteSpan
import androidx.annotation.ColorInt
import com.fibelatti.pinboard.core.android.CustomQuoteSpan.Companion.replaceQuoteSpans

/**
* [android.text.style.QuoteSpan] requires min API 28 in order to customize the stripe color and gap,
* so this is a substitute which also allows customizing the background color.
*
* @see replaceQuoteSpans
*/
class CustomQuoteSpan(
@ColorInt private val backgroundColor: Int,
@ColorInt private val stripeColor: Int,
private val stripeWidth: Float,
private val gap: Float,
) : LeadingMarginSpan, LineBackgroundSpan {

override fun getLeadingMargin(first: Boolean): Int = (stripeWidth + gap).toInt()

override fun drawLeadingMargin(
c: Canvas,
p: Paint,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
first: Boolean,
layout: Layout,
) {
val style = p.style
val paintColor = p.color
p.style = Paint.Style.FILL
p.color = stripeColor
c.drawRect(x.toFloat(), top.toFloat(), x + dir * stripeWidth, bottom.toFloat(), p)
p.style = style
p.color = paintColor
}

override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lnum: Int,
) {
val paintColor = p.color
p.color = backgroundColor
c.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), p)
p.color = paintColor
}

companion object {

fun replaceQuoteSpans(
spannable: Spannable,
@ColorInt backgroundColor: Int,
@ColorInt stripeColor: Int,
stripeWidth: Float,
gap: Float,
) {
val quoteSpans = spannable.getSpans(0, spannable.length, QuoteSpan::class.java)
for (quoteSpan in quoteSpans) {
val start = spannable.getSpanStart(quoteSpan)
val end = spannable.getSpanEnd(quoteSpan)
val flags = spannable.getSpanFlags(quoteSpan)

spannable.removeSpan(quoteSpan)
spannable.setSpan(CustomQuoteSpan(backgroundColor, stripeColor, stripeWidth, gap), start, end, flags)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.fibelatti.pinboard.core.android.composable

import android.text.TextUtils
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
import androidx.core.text.toSpannable
import com.fibelatti.core.extension.setupLinks
import com.fibelatti.pinboard.core.android.CustomQuoteSpan
import com.google.android.material.textview.MaterialTextView

@Composable
fun TextWithBlockquote(
text: String,
modifier: Modifier = Modifier,
textColor: Color = MaterialTheme.colorScheme.onSurface,
textSize: TextUnit = 14.sp,
maxLines: Int = Int.MAX_VALUE,
blockquoteBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
blockquoteStripeColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
) {
val argTextColor = textColor.toArgb()
val rgbBlockquoteBackgroundColor = blockquoteBackgroundColor.toArgb()
val rgbBlockquoteStripeColor = blockquoteStripeColor.toArgb()
val stripeWidth = with(LocalDensity.current) { 2.dp.toPx() }
val gap = with(LocalDensity.current) { 8.dp.toPx() }

val formattedText = remember(text) {
HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toSpannable().apply {
CustomQuoteSpan.replaceQuoteSpans(
spannable = this,
backgroundColor = rgbBlockquoteBackgroundColor,
stripeColor = rgbBlockquoteStripeColor,
stripeWidth = stripeWidth,
gap = gap,
)
}
}

AndroidView(
factory = { context ->
MaterialTextView(context).apply {
this.text = formattedText
this.textSize = textSize.value
this.maxLines = maxLines
this.ellipsize = TextUtils.TruncateAt.END
this.setTextColor(argTextColor)
setupLinks()
}
},
modifier = modifier,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,16 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.fibelatti.pinboard.R
import com.fibelatti.pinboard.core.AppConfig.DEFAULT_PAGE_SIZE
import com.fibelatti.pinboard.core.android.composable.EmptyListContent
import com.fibelatti.pinboard.core.android.composable.PullRefreshLayout
import com.fibelatti.pinboard.core.android.composable.TextWithBlockquote
import com.fibelatti.pinboard.core.extension.ScrollDirection
import com.fibelatti.pinboard.core.extension.rememberScrollDirection
import com.fibelatti.pinboard.features.MainViewModel
Expand Down Expand Up @@ -367,15 +368,14 @@ private fun BookmarkItem(
)

if (showDescription && post.description.isNotBlank()) {
Text(
TextWithBlockquote(
text = post.description,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
overflow = TextOverflow.Ellipsis,
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
textSize = 14.sp,
maxLines = 5,
style = MaterialTheme.typography.bodyMedium,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.fibelatti.pinboard.core.android.ComposeBottomSheetDialog
import com.fibelatti.pinboard.core.android.composable.TextWithBlockquote
import com.fibelatti.pinboard.features.posts.domain.model.Post
import com.fibelatti.ui.components.TextWithLinks
import com.fibelatti.ui.preview.ThemePreviews
Expand Down Expand Up @@ -71,12 +73,11 @@ private fun BookmarkDescriptionScreen(
color = MaterialTheme.colorScheme.onSurface,
)

TextWithLinks(
TextWithBlockquote(
text = description,
modifier = Modifier.padding(top = 16.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
linkColor = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge,
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
textSize = 16.sp,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class ShareReceiverViewModel @Inject constructor(
val newPost = Post(
url = finalUrl,
title = title,
description = description ?: "",
description = description?.let { "<blockquote>$it</blockquote>" }.orEmpty(),
private = userRepository.defaultPrivate ?: false,
readLater = userRepository.defaultReadLater ?: false,
tags = userRepository.defaultTags,
Expand All @@ -101,7 +101,7 @@ class ShareReceiverViewModel @Inject constructor(
params = Post(
url = finalUrl,
title = title,
description = description.orEmpty(),
description = description?.let { "<blockquote>$it</blockquote>" }.orEmpty(),
private = userRepository.defaultPrivate,
readLater = userRepository.defaultReadLater,
tags = userRepository.defaultTags,
Expand Down

0 comments on commit 233cc8e

Please sign in to comment.