Implement html styling on the template tile (#2653)

* Implement styling on the template tyle

* Also process preview in phone settings

* Small cleanup

* Improve template null handling

* Update template rendering error

* Update help text to indicate new line and markup options

* Also support newline

* More extensive replacement for <br>
This commit is contained in:
leroyboerefijn 2022-07-29 23:42:17 +02:00 committed by GitHub
parent f9ceeb0d65
commit e7a926396d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 29 deletions

View File

@ -112,10 +112,12 @@ class SettingsWearViewModel @Inject constructor(
viewModelScope.launch {
try {
templateTileContentRendered.value =
integrationUseCase.renderTemplate(template, mapOf())
integrationUseCase.renderTemplate(template, mapOf()) ?: getApplication<Application>().getString(
commonR.string.template_error
)
} catch (e: Exception) {
templateTileContentRendered.value = getApplication<Application>().getString(
commonR.string.template_tile_error
commonR.string.template_render_error
)
}
}

View File

@ -1,5 +1,12 @@
package io.homeassistant.companion.android.settings.wear.views
import android.graphics.Typeface
import android.text.style.AbsoluteSizeSpan
import android.text.style.CharacterStyle
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -19,12 +26,20 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import androidx.core.text.HtmlCompat.fromHtml
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.util.IntervalToString
@ -47,8 +62,8 @@ fun SettingsWearTemplateTile(
docsLink = WEAR_DOCS_LINK
)
}
) {
Column(Modifier.padding(all = 16.dp)) {
) { padding ->
Column(Modifier.padding(padding).padding(all = 16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
asset = CommunityMaterial.Icon3.cmd_timer_cog,
@ -95,10 +110,34 @@ fun SettingsWearTemplateTile(
maxLines = 10
)
Text(
renderedTemplate,
parseHtml(renderedTemplate),
fontSize = 12.sp,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
private fun parseHtml(renderedText: String) = buildAnnotatedString {
// Replace control char \r\n, \r, \n and also \r\n, \r, \n as text literals in strings to <br>
val renderedSpanned = fromHtml(renderedText.replace("(\r\n|\r|\n)|(\\\\r\\\\n|\\\\r|\\\\n)".toRegex(), "<br>"), FROM_HTML_MODE_LEGACY)
append(renderedSpanned.toString())
renderedSpanned.getSpans(0, renderedSpanned.length, CharacterStyle::class.java).forEach { span ->
val start = renderedSpanned.getSpanStart(span)
val end = renderedSpanned.getSpanEnd(span)
when (span) {
is AbsoluteSizeSpan -> addStyle(SpanStyle(fontSize = span.size.sp), start, end)
is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
is RelativeSizeSpan -> {
val defaultSize = 12
addStyle(SpanStyle(fontSize = (span.sizeChange * defaultSize).sp), start, end)
}
is StyleSpan -> when (span.style) {
Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end)
}
is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
}
}
}

View File

@ -85,11 +85,19 @@ class TemplateWidget : BaseWidgetProvider() {
}
// Content
var renderedTemplate = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading"
var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading"
try {
renderedTemplate = integrationUseCase.renderTemplate(widget.template, mapOf())
templateWidgetDao.updateTemplateWidgetLastUpdate(appWidgetId, renderedTemplate)
setViewVisibility(R.id.widgetTemplateError, View.GONE)
if (renderedTemplate != null) {
templateWidgetDao.updateTemplateWidgetLastUpdate(
appWidgetId,
renderedTemplate
)
setViewVisibility(R.id.widgetTemplateError, View.GONE)
} else {
Log.e(TAG, "Template returned null: ${widget.template}")
setViewVisibility(R.id.widgetTemplateError, View.VISIBLE)
}
} catch (e: Exception) {
Log.e(TAG, "Unable to render template: ${widget.template}", e)
setViewVisibility(R.id.widgetTemplateError, View.VISIBLE)

View File

@ -216,10 +216,10 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() {
var enabled: Boolean
withContext(Dispatchers.IO) {
try {
templateText = integrationUseCase.renderTemplate(template, mapOf())
templateText = integrationUseCase.renderTemplate(template, mapOf()) ?: getString(commonR.string.template_error)
enabled = true
} catch (e: Exception) {
templateText = "Error in template"
templateText = getString(commonR.string.template_render_error)
enabled = false
}
}

View File

@ -14,7 +14,7 @@ interface IntegrationRepository {
suspend fun getNotificationRateLimits(): RateLimitResponse
suspend fun renderTemplate(template: String, variables: Map<String, String>): String
suspend fun renderTemplate(template: String, variables: Map<String, String>): String?
suspend fun updateLocation(updateLocation: UpdateLocation)

View File

@ -157,7 +157,7 @@ class IntegrationRepositoryImpl @Inject constructor(
return urlRepository.getApiUrls().isNotEmpty()
}
override suspend fun renderTemplate(template: String, variables: Map<String, String>): String {
override suspend fun renderTemplate(template: String, variables: Map<String, String>): String? {
var causeException: Exception? = null
for (it in urlRepository.getApiUrls()) {
try {
@ -167,7 +167,7 @@ class IntegrationRepositoryImpl @Inject constructor(
"render_template",
mapOf("template" to Template(template, variables))
)
).getValue("template")
)["template"]
} catch (e: Exception) {
if (causeException == null) causeException = e
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception

View File

@ -686,8 +686,9 @@
<string name="template_tile_content">Template tile content</string>
<string name="template_tile_desc">Renders and displays a template</string>
<string name="template_tile_empty">Set template in the phone settings</string>
<string name="template_tile_error">Error in template</string>
<string name="template_tile_help">Provide a template below that will be displayed on the Wear OS template tile.</string>
<string name="template_error">Error in template</string>
<string name="template_render_error">Error rendering template</string>
<string name="template_tile_help">Provide a template below that will be displayed on the Wear OS template tile. See help for markup options.</string>
<string name="template_widget_default">Enter Template Here</string>
<string name="template_widget_desc">Render any template with HTML formatting</string>
<string name="template_widget">Template Widget</string>

View File

@ -1,15 +1,26 @@
package io.homeassistant.companion.android.tiles
import android.graphics.Typeface
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.text.style.AbsoluteSizeSpan
import android.text.style.CharacterStyle
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import androidx.core.content.getSystemService
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import androidx.core.text.HtmlCompat.fromHtml
import androidx.wear.tiles.ActionBuilders
import androidx.wear.tiles.ColorBuilders
import androidx.wear.tiles.DimensionBuilders
import androidx.wear.tiles.DimensionBuilders.dp
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.LayoutElementBuilders.Box
import androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
import androidx.wear.tiles.LayoutElementBuilders.Layout
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
import androidx.wear.tiles.ModifiersBuilders
@ -58,9 +69,11 @@ class TemplateTile : TileService() {
val template = integrationUseCase.getTemplateTileContent()
val renderedText = try {
integrationUseCase.renderTemplate(template, mapOf())
integrationUseCase.renderTemplate(template, mapOf()) ?: getString(
commonR.string.template_error
)
} catch (e: Exception) {
getString(commonR.string.template_tile_error)
getString(commonR.string.template_render_error)
}
Tile.Builder()
@ -102,18 +115,18 @@ class TemplateTile : TileService() {
}
fun layout(renderedText: String): LayoutElement = Box.Builder().apply {
addContent(
LayoutElementBuilders.Text.Builder()
.setText(
if (renderedText.isEmpty()) {
getString(commonR.string.template_tile_empty)
} else {
renderedText
}
)
.setMaxLines(10)
.build()
)
if (renderedText.isEmpty()) {
addContent(
LayoutElementBuilders.Text.Builder()
.setText(getString(commonR.string.template_tile_empty))
.setMaxLines(10)
.build()
)
} else {
addContent(
parseHtml(renderedText)
)
}
addContent(
LayoutElementBuilders.Arc.Builder()
.setAnchorAngle(
@ -152,4 +165,58 @@ class TemplateTile : TileService() {
)
.build()
}
private fun parseHtml(renderedText: String): LayoutElementBuilders.Spannable {
// Replace control char \r\n, \r, \n and also \r\n, \r, \n as text literals in strings to <br>
val renderedSpanned = fromHtml(renderedText.replace("(\r\n|\r|\n)|(\\\\r\\\\n|\\\\r|\\\\n)".toRegex(), "<br>"), FROM_HTML_MODE_LEGACY)
return LayoutElementBuilders.Spannable.Builder().apply {
var start = 0
var end = 0
while (end < renderedSpanned.length) {
end = renderedSpanned.nextSpanTransition(end, renderedSpanned.length, CharacterStyle::class.java)
val fontStyle = LayoutElementBuilders.FontStyle.Builder().apply {
renderedSpanned.getSpans(start, end, CharacterStyle::class.java).forEach { span ->
when (span) {
is AbsoluteSizeSpan -> setSize(
DimensionBuilders.SpProp.Builder()
.setValue(span.size / applicationContext.resources.displayMetrics.scaledDensity)
.build()
)
is ForegroundColorSpan -> setColor(
ColorBuilders.ColorProp.Builder()
.setArgb(span.foregroundColor)
.build()
)
is RelativeSizeSpan -> {
val defaultSize = 16 // https://developer.android.com/training/wearables/design/typography
setSize(
DimensionBuilders.SpProp.Builder()
.setValue(span.sizeChange * defaultSize)
.build()
)
}
is StyleSpan -> when (span.style) {
Typeface.BOLD -> setWeight(FONT_WEIGHT_BOLD)
Typeface.ITALIC -> setItalic(true)
Typeface.BOLD_ITALIC -> setWeight(FONT_WEIGHT_BOLD).setItalic(true)
}
is UnderlineSpan -> setUnderline(true)
}
}
}.build()
addSpan(
LayoutElementBuilders.SpanText.Builder()
.setText(renderedSpanned.substring(start, end))
.setFontStyle(fontStyle)
.build()
)
start = end
}
}
.setMaxLines(10)
.build()
}
}