implement widget text template
This commit is contained in:
@@ -43,6 +43,8 @@ class FlowWidgetProvider : AppWidgetProvider() {
|
||||
val appContext = context.applicationContext
|
||||
val manager = AppWidgetManager.getInstance(appContext)
|
||||
val ids = widgetIds(appContext, manager, intent)
|
||||
val widgetConfig = loadWidgetConfig(appContext)
|
||||
val widgetVariables = loadEnvironmentVariables(appContext).toEnvironmentMap()
|
||||
|
||||
ids.forEach { widgetId ->
|
||||
manager.updateAppWidget(
|
||||
@@ -51,6 +53,12 @@ class FlowWidgetProvider : AppWidgetProvider() {
|
||||
context = appContext,
|
||||
widgetId = widgetId,
|
||||
state = WidgetState(
|
||||
title = renderWidgetTemplate(
|
||||
template = widgetConfig.titleTemplate,
|
||||
values = widgetVariables,
|
||||
fallback = WidgetDisplayConfig().titleTemplate,
|
||||
maxLength = 48
|
||||
),
|
||||
subtitle = "Running saved flow",
|
||||
status = "Running",
|
||||
meta = "Starting",
|
||||
@@ -99,7 +107,7 @@ class FlowWidgetProvider : AppWidgetProvider() {
|
||||
private const val ENVIRONMENT_KEY = "environment_variables"
|
||||
private const val FLOW_KEY = "request_flow"
|
||||
private const val WIDGET_STATE_KEY = "widget_state"
|
||||
private const val COLOR_PRIMARY = 0xFF006B5B.toInt()
|
||||
private const val WIDGET_CONFIG_KEY = "widget_config"
|
||||
private const val COLOR_AMBER = 0xFFC47A12.toInt()
|
||||
private const val COLOR_GREEN = 0xFF147A56.toInt()
|
||||
private const val COLOR_RED = 0xFFC7383D.toInt()
|
||||
@@ -118,6 +126,14 @@ private data class WidgetState(
|
||||
val statusColor: Int = 0xFF006B5B.toInt()
|
||||
)
|
||||
|
||||
private data class WidgetDisplayConfig(
|
||||
val titleTemplate: String = "PostHUB Flow",
|
||||
val subtitleTemplate: String = "{{flow_steps}} step flow",
|
||||
val statusTemplate: String = "{{status_code}}",
|
||||
val metaTemplate: String = "{{duration_ms}} ms {{bytes}}",
|
||||
val bodyTemplate: String = "{{summary}}"
|
||||
)
|
||||
|
||||
private data class WidgetRequestField(
|
||||
val id: Int,
|
||||
val key: String,
|
||||
@@ -236,12 +252,27 @@ private fun runSavedFlow(
|
||||
onProgress: (WidgetState) -> Unit
|
||||
): WidgetState {
|
||||
val steps = loadFlowSteps(context).ifEmpty { defaultWidgetFlow() }
|
||||
val widgetConfig = loadWidgetConfig(context)
|
||||
var environmentRows = loadEnvironmentVariables(context)
|
||||
var lastResult: WidgetRequestResult? = null
|
||||
|
||||
steps.forEachIndexed { index, step ->
|
||||
val progressValues = environmentRows.toEnvironmentMap() + mapOf(
|
||||
"flow_steps" to steps.size.toString(),
|
||||
"step_index" to (index + 1).toString(),
|
||||
"step_name" to step.request.name,
|
||||
"status" to "Running",
|
||||
"status_code" to "Running",
|
||||
"summary" to step.request.url
|
||||
)
|
||||
onProgress(
|
||||
WidgetState(
|
||||
title = renderWidgetTemplate(
|
||||
template = widgetConfig.titleTemplate,
|
||||
values = progressValues,
|
||||
fallback = WidgetDisplayConfig().titleTemplate,
|
||||
maxLength = 48
|
||||
),
|
||||
subtitle = "Step ${index + 1} of ${steps.size}",
|
||||
status = "Running",
|
||||
meta = step.request.name,
|
||||
@@ -257,11 +288,27 @@ private fun runSavedFlow(
|
||||
lastResult = result
|
||||
|
||||
if (result.error != null) {
|
||||
val errorValues = environmentRows.toEnvironmentMap() + mapOf(
|
||||
"flow_steps" to steps.size.toString(),
|
||||
"step_index" to (index + 1).toString(),
|
||||
"step_name" to step.request.name,
|
||||
"status" to "Error",
|
||||
"status_code" to "Error",
|
||||
"status_text" to result.statusText,
|
||||
"duration_ms" to result.durationMs.toString(),
|
||||
"bytes" to formatBytes(result.byteCount),
|
||||
"byte_count" to result.byteCount.toString(),
|
||||
"final_url" to result.finalUrl,
|
||||
"summary" to result.error,
|
||||
"body" to result.error,
|
||||
"raw_body" to result.rawBody
|
||||
)
|
||||
return WidgetState(
|
||||
subtitle = "Failed at ${step.request.name}",
|
||||
status = "Error",
|
||||
meta = result.statusText,
|
||||
body = result.error,
|
||||
title = renderWidgetTemplate(widgetConfig.titleTemplate, errorValues, WidgetDisplayConfig().titleTemplate, 48),
|
||||
subtitle = renderWidgetTemplate(widgetConfig.subtitleTemplate, errorValues, "Failed at ${step.request.name}", 80),
|
||||
status = renderWidgetTemplate(widgetConfig.statusTemplate, errorValues, "Error", 18),
|
||||
meta = renderWidgetTemplate(widgetConfig.metaTemplate, errorValues, result.statusText, 80),
|
||||
body = renderWidgetTemplate(widgetConfig.bodyTemplate, errorValues, result.error, 420),
|
||||
statusColor = 0xFFC7383D.toInt()
|
||||
)
|
||||
}
|
||||
@@ -274,6 +321,17 @@ private fun runSavedFlow(
|
||||
}
|
||||
|
||||
val result = lastResult ?: return WidgetState(
|
||||
title = renderWidgetTemplate(
|
||||
template = widgetConfig.titleTemplate,
|
||||
values = mapOf(
|
||||
"flow_steps" to "0",
|
||||
"status" to "Empty",
|
||||
"status_code" to "Empty",
|
||||
"summary" to "Create or add requests to a flow, then refresh this widget."
|
||||
),
|
||||
fallback = WidgetDisplayConfig().titleTemplate,
|
||||
maxLength = 48
|
||||
),
|
||||
subtitle = "No flow steps",
|
||||
status = "Empty",
|
||||
meta = "Open PostHUB",
|
||||
@@ -281,11 +339,13 @@ private fun runSavedFlow(
|
||||
statusColor = 0xFF66736D.toInt()
|
||||
)
|
||||
|
||||
val finalValues = widgetTemplateValues(result, environmentRows, steps.size)
|
||||
return WidgetState(
|
||||
subtitle = "${steps.size} step flow",
|
||||
status = result.statusCode?.toString() ?: "Done",
|
||||
meta = "${result.durationMs} ms ${formatBytes(result.byteCount)}",
|
||||
body = summarizeWidgetResponse(result),
|
||||
title = renderWidgetTemplate(widgetConfig.titleTemplate, finalValues, WidgetDisplayConfig().titleTemplate, 48),
|
||||
subtitle = renderWidgetTemplate(widgetConfig.subtitleTemplate, finalValues, "${steps.size} step flow", 80),
|
||||
status = renderWidgetTemplate(widgetConfig.statusTemplate, finalValues, result.statusCode?.toString() ?: "Done", 18),
|
||||
meta = renderWidgetTemplate(widgetConfig.metaTemplate, finalValues, "${result.durationMs} ms ${formatBytes(result.byteCount)}", 80),
|
||||
body = renderWidgetTemplate(widgetConfig.bodyTemplate, finalValues, summarizeWidgetResponse(result), 420),
|
||||
statusColor = statusColor(result.statusCode)
|
||||
)
|
||||
}
|
||||
@@ -422,7 +482,7 @@ private fun loadLastWidgetState(context: Context): WidgetState {
|
||||
.getSharedPreferences("posthub_state", Context.MODE_PRIVATE)
|
||||
.getString("widget_state", null)
|
||||
|
||||
if (raw.isNullOrBlank()) return WidgetState()
|
||||
if (raw.isNullOrBlank()) return defaultRenderedWidgetState(context)
|
||||
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
@@ -437,6 +497,91 @@ private fun loadLastWidgetState(context: Context): WidgetState {
|
||||
}.getOrDefault(WidgetState())
|
||||
}
|
||||
|
||||
private fun defaultRenderedWidgetState(context: Context): WidgetState {
|
||||
val config = loadWidgetConfig(context)
|
||||
val values = loadEnvironmentVariables(context).toEnvironmentMap() + mapOf(
|
||||
"flow_steps" to "0",
|
||||
"status" to "Ready",
|
||||
"status_code" to "Ready",
|
||||
"status_text" to "Ready",
|
||||
"duration_ms" to "",
|
||||
"bytes" to "",
|
||||
"byte_count" to "",
|
||||
"final_url" to "",
|
||||
"summary" to "Run your saved flow from the home screen.",
|
||||
"body" to "Run your saved flow from the home screen.",
|
||||
"raw_body" to ""
|
||||
)
|
||||
|
||||
return WidgetState(
|
||||
title = renderWidgetTemplate(config.titleTemplate, values, WidgetDisplayConfig().titleTemplate, 48),
|
||||
subtitle = renderWidgetTemplate(config.subtitleTemplate, values, "Home runner", 80),
|
||||
status = renderWidgetTemplate(config.statusTemplate, values, "Ready", 18),
|
||||
meta = renderWidgetTemplate(config.metaTemplate, values, "Tap refresh", 80),
|
||||
body = renderWidgetTemplate(config.bodyTemplate, values, "Run your saved flow from the home screen.", 420),
|
||||
statusColor = 0xFF006B5B.toInt()
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadWidgetConfig(context: Context): WidgetDisplayConfig {
|
||||
val raw = context
|
||||
.getSharedPreferences("posthub_state", Context.MODE_PRIVATE)
|
||||
.getString("widget_config", null)
|
||||
|
||||
if (raw.isNullOrBlank()) return WidgetDisplayConfig()
|
||||
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
WidgetDisplayConfig(
|
||||
titleTemplate = json.optString("titleTemplate", WidgetDisplayConfig().titleTemplate),
|
||||
subtitleTemplate = json.optString("subtitleTemplate", WidgetDisplayConfig().subtitleTemplate),
|
||||
statusTemplate = json.optString("statusTemplate", WidgetDisplayConfig().statusTemplate),
|
||||
metaTemplate = json.optString("metaTemplate", WidgetDisplayConfig().metaTemplate),
|
||||
bodyTemplate = json.optString("bodyTemplate", WidgetDisplayConfig().bodyTemplate)
|
||||
)
|
||||
}.getOrDefault(WidgetDisplayConfig())
|
||||
}
|
||||
|
||||
private fun widgetTemplateValues(
|
||||
result: WidgetRequestResult,
|
||||
environmentRows: List<WidgetRequestField>,
|
||||
flowSteps: Int
|
||||
): Map<String, String> {
|
||||
val statusValue = result.statusCode?.toString() ?: "Done"
|
||||
return environmentRows.toEnvironmentMap() + mapOf(
|
||||
"flow_steps" to flowSteps.toString(),
|
||||
"status" to statusValue,
|
||||
"status_code" to statusValue,
|
||||
"status_text" to result.statusText,
|
||||
"duration_ms" to result.durationMs.toString(),
|
||||
"bytes" to formatBytes(result.byteCount),
|
||||
"byte_count" to result.byteCount.toString(),
|
||||
"final_url" to result.finalUrl,
|
||||
"summary" to summarizeWidgetResponse(result),
|
||||
"body" to result.body,
|
||||
"raw_body" to result.rawBody
|
||||
)
|
||||
}
|
||||
|
||||
private fun renderWidgetTemplate(
|
||||
template: String,
|
||||
values: Map<String, String>,
|
||||
fallback: String,
|
||||
maxLength: Int
|
||||
): String {
|
||||
val source = template.ifBlank { fallback }
|
||||
val rendered = source.applyVariables(values).trim().ifBlank {
|
||||
fallback.applyVariables(values).trim()
|
||||
}
|
||||
return rendered.limitWidgetText(maxLength)
|
||||
}
|
||||
|
||||
private fun String.limitWidgetText(maxLength: Int): String {
|
||||
if (length <= maxLength) return this
|
||||
if (maxLength <= 3) return take(maxLength)
|
||||
return take(maxLength - 3).trimEnd() + "..."
|
||||
}
|
||||
|
||||
private fun saveLastWidgetState(
|
||||
context: Context,
|
||||
state: WidgetState
|
||||
|
||||
@@ -105,6 +105,7 @@ private val VariablePattern = Regex("""\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}""")
|
||||
private const val PrefName = "posthub_state"
|
||||
private const val EnvironmentKey = "environment_variables"
|
||||
private const val FlowKey = "request_flow"
|
||||
private const val WidgetConfigKey = "widget_config"
|
||||
|
||||
private object AppColors {
|
||||
private val darkMode = mutableStateOf(false)
|
||||
@@ -230,6 +231,14 @@ private data class FlowRunEntry(
|
||||
val savedVariables: Int = 0
|
||||
)
|
||||
|
||||
private data class WidgetConfig(
|
||||
val titleTemplate: String = "PostHUB Flow",
|
||||
val subtitleTemplate: String = "{{flow_steps}} step flow",
|
||||
val statusTemplate: String = "{{status_code}}",
|
||||
val metaTemplate: String = "{{duration_ms}} ms {{bytes}}",
|
||||
val bodyTemplate: String = "{{summary}}"
|
||||
)
|
||||
|
||||
private fun appSections() = listOf(
|
||||
AppSection("request", "Request", "Composer", AppColors.Primary),
|
||||
AppSection("clients", "Clients", "MCI TCI", AppColors.Coral),
|
||||
@@ -285,6 +294,7 @@ private fun ApiClientApp() {
|
||||
loadFlowSteps(appContext).ifEmpty { defaultFlowSteps(builtInRequests) }
|
||||
)
|
||||
}
|
||||
var widgetConfig by remember { mutableStateOf(loadWidgetConfig(appContext)) }
|
||||
var flowRun by remember { mutableStateOf<List<FlowRunEntry>>(emptyList()) }
|
||||
var response by remember { mutableStateOf<RequestResult?>(null) }
|
||||
var history by remember { mutableStateOf<List<HistoryItem>>(emptyList()) }
|
||||
@@ -302,6 +312,11 @@ private fun ApiClientApp() {
|
||||
saveFlowSteps(appContext, next)
|
||||
}
|
||||
|
||||
fun updateWidgetConfig(next: WidgetConfig) {
|
||||
widgetConfig = next
|
||||
saveWidgetConfig(appContext, next)
|
||||
}
|
||||
|
||||
fun currentRequest(): SavedRequest {
|
||||
return SavedRequest(
|
||||
name = requestName(method, url),
|
||||
@@ -540,8 +555,10 @@ private fun ApiClientApp() {
|
||||
steps = flowSteps,
|
||||
runEntries = flowRun,
|
||||
isRunning = isFlowRunning,
|
||||
widgetConfig = widgetConfig,
|
||||
onAddCurrent = ::addCurrentRequestToFlow,
|
||||
onRun = ::runFlow,
|
||||
onWidgetConfigChange = ::updateWidgetConfig,
|
||||
onLoad = { loadSavedRequest(it.request) },
|
||||
onRemove = { step ->
|
||||
updateFlowSteps(flowSteps.filterNot { it.id == step.id })
|
||||
@@ -1164,8 +1181,10 @@ private fun FlowPanel(
|
||||
steps: List<FlowStep>,
|
||||
runEntries: List<FlowRunEntry>,
|
||||
isRunning: Boolean,
|
||||
widgetConfig: WidgetConfig,
|
||||
onAddCurrent: () -> Unit,
|
||||
onRun: () -> Unit,
|
||||
onWidgetConfigChange: (WidgetConfig) -> Unit,
|
||||
onLoad: (FlowStep) -> Unit,
|
||||
onRemove: (FlowStep) -> Unit
|
||||
) {
|
||||
@@ -1234,10 +1253,114 @@ private fun FlowPanel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WidgetTextEditor(
|
||||
config = widgetConfig,
|
||||
onConfigChange = onWidgetConfigChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WidgetTextEditor(
|
||||
config: WidgetConfig,
|
||||
onConfigChange: (WidgetConfig) -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = AppColors.PanelSoft,
|
||||
border = BorderStroke(1.dp, AppColors.Border)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SectionTitle(title = "Widget text", meta = "Templates")
|
||||
TextButton(onClick = { onConfigChange(WidgetConfig()) }) {
|
||||
Text("Reset", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
StatusPill("{{summary}}", AppColors.Primary)
|
||||
StatusPill("{{status_code}}", AppColors.Green)
|
||||
StatusPill("{{duration_ms}}", AppColors.Amber)
|
||||
StatusPill("{{public_ip}}", AppColors.Coral)
|
||||
}
|
||||
|
||||
WidgetTemplateField(
|
||||
label = "Title",
|
||||
value = config.titleTemplate,
|
||||
placeholder = "PostHUB Flow",
|
||||
onValueChange = { onConfigChange(config.copy(titleTemplate = it)) }
|
||||
)
|
||||
WidgetTemplateField(
|
||||
label = "Subtitle",
|
||||
value = config.subtitleTemplate,
|
||||
placeholder = "{{flow_steps}} step flow",
|
||||
onValueChange = { onConfigChange(config.copy(subtitleTemplate = it)) }
|
||||
)
|
||||
WidgetTemplateField(
|
||||
label = "Status",
|
||||
value = config.statusTemplate,
|
||||
placeholder = "{{status_code}}",
|
||||
onValueChange = { onConfigChange(config.copy(statusTemplate = it)) }
|
||||
)
|
||||
WidgetTemplateField(
|
||||
label = "Meta",
|
||||
value = config.metaTemplate,
|
||||
placeholder = "{{duration_ms}} ms {{bytes}}",
|
||||
onValueChange = { onConfigChange(config.copy(metaTemplate = it)) }
|
||||
)
|
||||
WidgetTemplateField(
|
||||
label = "Body",
|
||||
value = config.bodyTemplate,
|
||||
placeholder = "{{summary}}",
|
||||
minLines = 2,
|
||||
onValueChange = { onConfigChange(config.copy(bodyTemplate = it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WidgetTemplateField(
|
||||
label: String,
|
||||
value: String,
|
||||
placeholder: String,
|
||||
minLines: Int = 1,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = minLines,
|
||||
maxLines = if (minLines > 1) 3 else 1,
|
||||
singleLine = minLines == 1,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
|
||||
label = { Text(label) },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeholder,
|
||||
color = AppColors.Muted,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FlowStepRow(
|
||||
index: Int,
|
||||
@@ -2762,6 +2885,43 @@ private fun saveFlowSteps(
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun loadWidgetConfig(context: Context): WidgetConfig {
|
||||
val raw = context
|
||||
.getSharedPreferences(PrefName, Context.MODE_PRIVATE)
|
||||
.getString(WidgetConfigKey, null)
|
||||
|
||||
if (raw.isNullOrBlank()) return WidgetConfig()
|
||||
|
||||
return runCatching {
|
||||
val json = JSONObject(raw)
|
||||
WidgetConfig(
|
||||
titleTemplate = json.optString("titleTemplate", WidgetConfig().titleTemplate),
|
||||
subtitleTemplate = json.optString("subtitleTemplate", WidgetConfig().subtitleTemplate),
|
||||
statusTemplate = json.optString("statusTemplate", WidgetConfig().statusTemplate),
|
||||
metaTemplate = json.optString("metaTemplate", WidgetConfig().metaTemplate),
|
||||
bodyTemplate = json.optString("bodyTemplate", WidgetConfig().bodyTemplate)
|
||||
)
|
||||
}.getOrDefault(WidgetConfig())
|
||||
}
|
||||
|
||||
private fun saveWidgetConfig(
|
||||
context: Context,
|
||||
config: WidgetConfig
|
||||
) {
|
||||
val json = JSONObject()
|
||||
.put("titleTemplate", config.titleTemplate)
|
||||
.put("subtitleTemplate", config.subtitleTemplate)
|
||||
.put("statusTemplate", config.statusTemplate)
|
||||
.put("metaTemplate", config.metaTemplate)
|
||||
.put("bodyTemplate", config.bodyTemplate)
|
||||
|
||||
context
|
||||
.getSharedPreferences(PrefName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(WidgetConfigKey, json.toString())
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun requestFieldsToJson(rows: List<RequestField>): JSONArray {
|
||||
val array = JSONArray()
|
||||
rows.forEach { row ->
|
||||
|
||||
Reference in New Issue
Block a user