mirror of
https://github.com/home-assistant/android
synced 2024-10-04 15:19:30 +00:00
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:
parent
f9ceeb0d65
commit
e7a926396d
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
if (renderedText.isEmpty()) {
|
||||
addContent(
|
||||
LayoutElementBuilders.Text.Builder()
|
||||
.setText(
|
||||
if (renderedText.isEmpty()) {
|
||||
getString(commonR.string.template_tile_empty)
|
||||
} else {
|
||||
renderedText
|
||||
}
|
||||
)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue