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 {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
templateTileContentRendered.value =
|
templateTileContentRendered.value =
|
||||||
integrationUseCase.renderTemplate(template, mapOf())
|
integrationUseCase.renderTemplate(template, mapOf()) ?: getApplication<Application>().getString(
|
||||||
|
commonR.string.template_error
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
templateTileContentRendered.value = getApplication<Application>().getString(
|
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
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -19,12 +26,20 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.compose.Image
|
||||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||||
import io.homeassistant.companion.android.util.IntervalToString
|
import io.homeassistant.companion.android.util.IntervalToString
|
||||||
|
@ -47,8 +62,8 @@ fun SettingsWearTemplateTile(
|
||||||
docsLink = WEAR_DOCS_LINK
|
docsLink = WEAR_DOCS_LINK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) { padding ->
|
||||||
Column(Modifier.padding(all = 16.dp)) {
|
Column(Modifier.padding(padding).padding(all = 16.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Image(
|
Image(
|
||||||
asset = CommunityMaterial.Icon3.cmd_timer_cog,
|
asset = CommunityMaterial.Icon3.cmd_timer_cog,
|
||||||
|
@ -95,10 +110,34 @@ fun SettingsWearTemplateTile(
|
||||||
maxLines = 10
|
maxLines = 10
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
renderedTemplate,
|
parseHtml(renderedTemplate),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
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
|
// Content
|
||||||
var renderedTemplate = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading"
|
var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading"
|
||||||
try {
|
try {
|
||||||
renderedTemplate = integrationUseCase.renderTemplate(widget.template, mapOf())
|
renderedTemplate = integrationUseCase.renderTemplate(widget.template, mapOf())
|
||||||
templateWidgetDao.updateTemplateWidgetLastUpdate(appWidgetId, renderedTemplate)
|
if (renderedTemplate != null) {
|
||||||
setViewVisibility(R.id.widgetTemplateError, View.GONE)
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to render template: ${widget.template}", e)
|
Log.e(TAG, "Unable to render template: ${widget.template}", e)
|
||||||
setViewVisibility(R.id.widgetTemplateError, View.VISIBLE)
|
setViewVisibility(R.id.widgetTemplateError, View.VISIBLE)
|
||||||
|
|
|
@ -216,10 +216,10 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() {
|
||||||
var enabled: Boolean
|
var enabled: Boolean
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
templateText = integrationUseCase.renderTemplate(template, mapOf())
|
templateText = integrationUseCase.renderTemplate(template, mapOf()) ?: getString(commonR.string.template_error)
|
||||||
enabled = true
|
enabled = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
templateText = "Error in template"
|
templateText = getString(commonR.string.template_render_error)
|
||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface IntegrationRepository {
|
||||||
|
|
||||||
suspend fun getNotificationRateLimits(): RateLimitResponse
|
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)
|
suspend fun updateLocation(updateLocation: UpdateLocation)
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ class IntegrationRepositoryImpl @Inject constructor(
|
||||||
return urlRepository.getApiUrls().isNotEmpty()
|
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
|
var causeException: Exception? = null
|
||||||
for (it in urlRepository.getApiUrls()) {
|
for (it in urlRepository.getApiUrls()) {
|
||||||
try {
|
try {
|
||||||
|
@ -167,7 +167,7 @@ class IntegrationRepositoryImpl @Inject constructor(
|
||||||
"render_template",
|
"render_template",
|
||||||
mapOf("template" to Template(template, variables))
|
mapOf("template" to Template(template, variables))
|
||||||
)
|
)
|
||||||
).getValue("template")
|
)["template"]
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (causeException == null) causeException = e
|
if (causeException == null) causeException = e
|
||||||
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
|
// 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_content">Template tile content</string>
|
||||||
<string name="template_tile_desc">Renders and displays a template</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_empty">Set template in the phone settings</string>
|
||||||
<string name="template_tile_error">Error in template</string>
|
<string name="template_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_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_default">Enter Template Here</string>
|
||||||
<string name="template_widget_desc">Render any template with HTML formatting</string>
|
<string name="template_widget_desc">Render any template with HTML formatting</string>
|
||||||
<string name="template_widget">Template Widget</string>
|
<string name="template_widget">Template Widget</string>
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
package io.homeassistant.companion.android.tiles
|
package io.homeassistant.companion.android.tiles
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.VibrationEffect
|
import android.os.VibrationEffect
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.os.VibratorManager
|
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.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.ActionBuilders
|
||||||
|
import androidx.wear.tiles.ColorBuilders
|
||||||
import androidx.wear.tiles.DimensionBuilders
|
import androidx.wear.tiles.DimensionBuilders
|
||||||
import androidx.wear.tiles.DimensionBuilders.dp
|
import androidx.wear.tiles.DimensionBuilders.dp
|
||||||
import androidx.wear.tiles.LayoutElementBuilders
|
import androidx.wear.tiles.LayoutElementBuilders
|
||||||
import androidx.wear.tiles.LayoutElementBuilders.Box
|
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.Layout
|
||||||
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
|
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
|
||||||
import androidx.wear.tiles.ModifiersBuilders
|
import androidx.wear.tiles.ModifiersBuilders
|
||||||
|
@ -58,9 +69,11 @@ class TemplateTile : TileService() {
|
||||||
|
|
||||||
val template = integrationUseCase.getTemplateTileContent()
|
val template = integrationUseCase.getTemplateTileContent()
|
||||||
val renderedText = try {
|
val renderedText = try {
|
||||||
integrationUseCase.renderTemplate(template, mapOf())
|
integrationUseCase.renderTemplate(template, mapOf()) ?: getString(
|
||||||
|
commonR.string.template_error
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
getString(commonR.string.template_tile_error)
|
getString(commonR.string.template_render_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
Tile.Builder()
|
Tile.Builder()
|
||||||
|
@ -102,18 +115,18 @@ class TemplateTile : TileService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun layout(renderedText: String): LayoutElement = Box.Builder().apply {
|
fun layout(renderedText: String): LayoutElement = Box.Builder().apply {
|
||||||
addContent(
|
if (renderedText.isEmpty()) {
|
||||||
LayoutElementBuilders.Text.Builder()
|
addContent(
|
||||||
.setText(
|
LayoutElementBuilders.Text.Builder()
|
||||||
if (renderedText.isEmpty()) {
|
.setText(getString(commonR.string.template_tile_empty))
|
||||||
getString(commonR.string.template_tile_empty)
|
.setMaxLines(10)
|
||||||
} else {
|
.build()
|
||||||
renderedText
|
)
|
||||||
}
|
} else {
|
||||||
)
|
addContent(
|
||||||
.setMaxLines(10)
|
parseHtml(renderedText)
|
||||||
.build()
|
)
|
||||||
)
|
}
|
||||||
addContent(
|
addContent(
|
||||||
LayoutElementBuilders.Arc.Builder()
|
LayoutElementBuilders.Arc.Builder()
|
||||||
.setAnchorAngle(
|
.setAnchorAngle(
|
||||||
|
@ -152,4 +165,58 @@ class TemplateTile : TileService() {
|
||||||
)
|
)
|
||||||
.build()
|
.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