Compare commits

..
10 Commits
16 changed files with 463 additions and 111 deletions
-1
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Meghdad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -2
View File
@@ -33,8 +33,8 @@ android {
applicationId = "net.rodakot.posthub"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
versionCode = 103000
versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -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 ->
@@ -5,25 +5,20 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/posthub_primary"
android:fillColor="#0E1513"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="@color/posthub_primary_dark"
android:fillAlpha="0.34"
android:pathData="M0,0h108v38h-108z" />
android:fillColor="#123C35"
android:pathData="M0,0h108v44h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M18,0v108M36,0v108M54,0v108M72,0v108M90,0v108"
android:strokeColor="#24FFFFFF"
android:strokeWidth="1" />
android:pathData="M18,8C31,1 47,-1 62,3M92,22C103,36 108,55 104,74M15,88C30,103 55,110 78,101"
android:strokeColor="#2600D8B8"
android:strokeLineCap="round"
android:strokeWidth="3" />
<path
android:fillColor="#00000000"
android:pathData="M0,27h108M0,54h108M0,81h108"
android:strokeColor="#18FFFFFF"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:pathData="M-6,96L96,-6M18,114L114,18"
android:pathData="M-8,92L88,-4M22,116L116,22"
android:strokeColor="#14000000"
android:strokeWidth="6" />
android:strokeWidth="7" />
</vector>
@@ -5,52 +5,55 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#22000000"
android:pathData="M31,25h52c5,0 9,4 9,9v46c0,5 -4,9 -9,9H31c-5,0 -9,-4 -9,-9V34c0,-5 4,-9 9,-9z" />
android:fillColor="#26000000"
android:pathData="M56,15C32.8,15 14,33.8 14,57s18.8,42 42,42s42,-18.8 42,-42S79.2,15 56,15z" />
<path
android:fillColor="@color/posthub_surface"
android:pathData="M28,22h52c5,0 9,4 9,9v46c0,5 -4,9 -9,9H28c-5,0 -9,-4 -9,-9V31c0,-5 4,-9 9,-9z" />
android:fillColor="#F7FAF7"
android:pathData="M54,11C30.3,11 11,30.3 11,54s19.3,43 43,43s43,-19.3 43,-43S77.7,11 54,11z" />
<path
android:fillColor="#E3F4EF"
android:pathData="M28,22h52c5,0 9,4 9,9v9H19v-9c0,-5 4,-9 9,-9z" />
android:pathData="M54,18C34.1,18 18,34.1 18,54s16.1,36 36,36s36,-16.1 36,-36S73.9,18 54,18z" />
<path
android:fillColor="#00000000"
android:pathData="M28,22h52c5,0 9,4 9,9v46c0,5 -4,9 -9,9H28c-5,0 -9,-4 -9,-9V31c0,-5 4,-9 9,-9z"
android:strokeColor="@color/posthub_border"
android:strokeWidth="1.5" />
<path
android:fillColor="@color/posthub_coral"
android:pathData="M30,31m-2.7,0a2.7,2.7 0,1 0,5.4 0a2.7,2.7 0,1 0,-5.4 0" />
<path
android:fillColor="@color/posthub_amber"
android:pathData="M39,31m-2.7,0a2.7,2.7 0,1 0,5.4 0a2.7,2.7 0,1 0,-5.4 0" />
<path
android:fillColor="@color/posthub_primary"
android:pathData="M48,31m-2.7,0a2.7,2.7 0,1 0,5.4 0a2.7,2.7 0,1 0,-5.4 0" />
android:pathData="M54,13C31.4,13 13,31.4 13,54s18.4,41 41,41s41,-18.4 41,-41S76.6,13 54,13z"
android:strokeColor="#D6E8E2"
android:strokeWidth="2" />
<path
android:fillColor="#00000000"
android:pathData="M34,51H54C62,51 62,44 69,44H77M73,40l4,4l-4,4"
android:strokeColor="@color/posthub_primary"
android:pathData="M54,54L73,38M54,54L77,70M54,54L34,64M54,54L43,37"
android:strokeColor="#006B5B"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4" />
<path
android:fillColor="#00000000"
android:pathData="M34,65H49C57,65 57,73 65,73H77M73,69l4,4l-4,4"
android:strokeColor="@color/posthub_coral"
android:pathData="M37,73C45,82 61,84 72,77"
android:strokeColor="#C47A12"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4" />
<path
android:fillColor="#00000000"
android:pathData="M34,78H60"
android:strokeColor="@color/posthub_amber"
android:pathData="M36,35C45,27 61,26 72,34"
android:strokeColor="#D84B35"
android:strokeLineCap="round"
android:strokeWidth="4" />
<path
android:fillColor="@color/posthub_primary"
android:pathData="M34,51m-4,0a4,4 0,1 0,8 0a4,4 0,1 0,-8 0" />
android:fillColor="#006B5B"
android:pathData="M54,45m-9,0a9,9 0,1 0,18 0a9,9 0,1 0,-18 0"
android:fillAlpha="0.12" />
<path
android:fillColor="@color/posthub_coral"
android:pathData="M34,65m-4,0a4,4 0,1 0,8 0a4,4 0,1 0,-8 0" />
android:fillColor="#006B5B"
android:pathData="M54,54m-8,0a8,8 0,1 0,16 0a8,8 0,1 0,-16 0" />
<path
android:fillColor="#F7FAF7"
android:pathData="M54,54m-3,0a3,3 0,1 0,6 0a3,3 0,1 0,-6 0" />
<path
android:fillColor="#006B5B"
android:pathData="M73,38m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0M34,64m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
<path
android:fillColor="#D84B35"
android:pathData="M43,37m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
<path
android:fillColor="#C47A12"
android:pathData="M77,70m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
</vector>
@@ -6,17 +6,17 @@
android:viewportHeight="108">
<path
android:fillColor="#00000000"
android:pathData="M28,22h52c5,0 9,4 9,9v46c0,5 -4,9 -9,9H28c-5,0 -9,-4 -9,-9V31c0,-5 4,-9 9,-9z"
android:pathData="M54,13C31.4,13 13,31.4 13,54s18.4,41 41,41s41,-18.4 41,-41S76.6,13 54,13z"
android:strokeColor="#FF000000"
android:strokeWidth="6" />
<path
android:fillColor="#FF000000"
android:pathData="M30,31m-3,0a3,3 0,1 0,6 0a3,3 0,1 0,-6 0M40,31m-3,0a3,3 0,1 0,6 0a3,3 0,1 0,-6 0M50,31m-3,0a3,3 0,1 0,6 0a3,3 0,1 0,-6 0" />
<path
android:fillColor="#00000000"
android:pathData="M34,51H54C62,51 62,44 69,44H77M73,40l4,4l-4,4M34,65H49C57,65 57,73 65,73H77M73,69l4,4l-4,4M34,78H60"
android:pathData="M54,54L73,38M54,54L77,70M54,54L34,64M54,54L43,37M37,73C45,82 61,84 72,77M36,35C45,27 61,26 72,34"
android:strokeColor="#FF000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="6" />
<path
android:fillColor="#FF000000"
android:pathData="M54,54m-9,0a9,9 0,1 0,18 0a9,9 0,1 0,-18 0M73,38m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0M34,64m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0M43,37m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0M77,70m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
</vector>
+36 -16
View File
@@ -5,31 +5,51 @@
android:viewportWidth="120"
android:viewportHeight="120">
<path
android:fillColor="@color/posthub_primary"
android:pathData="M32,18h56c7.7,0 14,6.3 14,14v56c0,7.7 -6.3,14 -14,14H32c-7.7,0 -14,-6.3 -14,-14V32c0,-7.7 6.3,-14 14,-14z" />
android:fillColor="#26000000"
android:pathData="M62,14C36.6,14 16,34.6 16,60s20.6,46 46,46s46,-20.6 46,-46S87.4,14 62,14z" />
<path
android:fillColor="#1A000000"
android:pathData="M38,28h50c4.4,0 8,3.6 8,8v45c0,4.4 -3.6,8 -8,8H38c-4.4,0 -8,-3.6 -8,-8V36c0,-4.4 3.6,-8 8,-8z" />
<path
android:fillColor="@color/posthub_surface"
android:pathData="M34,24h50c4.4,0 8,3.6 8,8v45c0,4.4 -3.6,8 -8,8H34c-4.4,0 -8,-3.6 -8,-8V32c0,-4.4 3.6,-8 8,-8z" />
android:fillColor="#F7FAF7"
android:pathData="M60,10C32.4,10 10,32.4 10,60s22.4,50 50,50s50,-22.4 50,-50S87.6,10 60,10z" />
<path
android:fillColor="#E3F4EF"
android:pathData="M34,24h50c4.4,0 8,3.6 8,8v9H26v-9c0,-4.4 3.6,-8 8,-8z" />
android:pathData="M60,19C37.4,19 19,37.4 19,60s18.4,41 41,41s41,-18.4 41,-41S82.6,19 60,19z" />
<path
android:fillColor="#00000000"
android:pathData="M40,57H58C66,57 66,49 74,49H82M78,45l4,4l-4,4M40,70H54C62,70 62,78 70,78H82M78,74l4,4l-4,4"
android:strokeColor="@color/posthub_primary"
android:pathData="M60,12C33.5,12 12,33.5 12,60s21.5,48 48,48s48,-21.5 48,-48S86.5,12 60,12z"
android:strokeColor="#D6E8E2"
android:strokeWidth="2.5" />
<path
android:fillColor="#00000000"
android:pathData="M60,60L82,41M60,60L86,79M60,60L37,72M60,60L47,40"
android:strokeColor="#006B5B"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4.5" />
android:strokeWidth="5" />
<path
android:fillColor="#00000000"
android:pathData="M40,82H62"
android:strokeColor="@color/posthub_coral"
android:pathData="M40,82C50,92 69,94 82,86"
android:strokeColor="#C47A12"
android:strokeLineCap="round"
android:strokeWidth="4.5" />
android:strokeWidth="5" />
<path
android:fillColor="@color/posthub_primary"
android:pathData="M40,57m-4.5,0a4.5,4.5 0,1 0,9 0a4.5,4.5 0,1 0,-9 0M40,70m-4.5,0a4.5,4.5 0,1 0,9 0a4.5,4.5 0,1 0,-9 0" />
android:fillColor="#00000000"
android:pathData="M39,38C50,28 70,27 83,36"
android:strokeColor="#D84B35"
android:strokeLineCap="round"
android:strokeWidth="5" />
<path
android:fillColor="#006B5B"
android:pathData="M60,60m-9,0a9,9 0,1 0,18 0a9,9 0,1 0,-18 0" />
<path
android:fillColor="#F7FAF7"
android:pathData="M60,60m-3.5,0a3.5,3.5 0,1 0,7 0a3.5,3.5 0,1 0,-7 0" />
<path
android:fillColor="#006B5B"
android:pathData="M82,41m-7,0a7,7 0,1 0,14 0a7,7 0,1 0,-14 0M37,72m-7,0a7,7 0,1 0,14 0a7,7 0,1 0,-14 0" />
<path
android:fillColor="#D84B35"
android:pathData="M47,40m-7,0a7,7 0,1 0,14 0a7,7 0,1 0,-14 0" />
<path
android:fillColor="#C47A12"
android:pathData="M86,79m-7,0a7,7 0,1 0,14 0a7,7 0,1 0,-14 0" />
</vector>
+20 -18
View File
@@ -5,35 +5,37 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/posthub_primary"
android:pathData="M18,8h72c5.5,0 10,4.5 10,10v72c0,5.5 -4.5,10 -10,10H18c-5.5,0 -10,-4.5 -10,-10V18C8,12.5 12.5,8 18,8z" />
android:fillColor="#0E1513"
android:pathData="M54,4C26.4,4 4,26.4 4,54s22.4,50 50,50s50,-22.4 50,-50S81.6,4 54,4z" />
<path
android:fillColor="#0F7D6E"
android:pathData="M18,8h72c5.5,0 10,4.5 10,10v25H8V18C8,12.5 12.5,8 18,8z" />
android:fillColor="#123C35"
android:pathData="M14,22C23.1,11 37.2,4 54,4c27.6,0 50,22.4 50,50c0,2.3 -0.2,4.6 -0.5,6.8H14z" />
<path
android:fillColor="@color/posthub_surface"
android:pathData="M27,24h54c4.4,0 8,3.6 8,8v45c0,4.4 -3.6,8 -8,8H27c-4.4,0 -8,-3.6 -8,-8V32c0,-4.4 3.6,-8 8,-8z" />
android:fillColor="#F7FAF7"
android:pathData="M54,13C31.4,13 13,31.4 13,54s18.4,41 41,41s41,-18.4 41,-41S76.6,13 54,13z" />
<path
android:fillColor="#E3F4EF"
android:pathData="M27,24h54c4.4,0 8,3.6 8,8v9H19v-9c0,-4.4 3.6,-8 8,-8z" />
android:pathData="M54,20C35.2,20 20,35.2 20,54s15.2,34 34,34s34,-15.2 34,-34S72.8,20 54,20z" />
<path
android:fillColor="#00000000"
android:pathData="M34,51H54C62,51 62,44 69,44H77M73,40l4,4l-4,4"
android:strokeColor="@color/posthub_primary"
android:pathData="M54,54L73,38M54,54L77,70M54,54L34,64M54,54L43,37"
android:strokeColor="#006B5B"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4" />
<path
android:fillColor="#00000000"
android:pathData="M34,65H49C57,65 57,73 65,73H77M73,69l4,4l-4,4"
android:strokeColor="@color/posthub_coral"
android:pathData="M37,73C45,82 61,84 72,77M36,35C45,27 61,26 72,34"
android:strokeColor="#D84B35"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4" />
android:strokeWidth="3.5" />
<path
android:fillColor="#00000000"
android:pathData="M34,78H60"
android:strokeColor="@color/posthub_amber"
android:strokeLineCap="round"
android:strokeWidth="4" />
android:fillColor="#006B5B"
android:pathData="M54,54m-8,0a8,8 0,1 0,16 0a8,8 0,1 0,-16 0M73,38m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0M34,64m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
<path
android:fillColor="#D84B35"
android:pathData="M43,37m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
<path
android:fillColor="#C47A12"
android:pathData="M77,70m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
</vector>
@@ -5,29 +5,37 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/posthub_primary"
android:fillColor="#0E1513"
android:pathData="M54,4C26.4,4 4,26.4 4,54s22.4,50 50,50s50,-22.4 50,-50S81.6,4 54,4z" />
<path
android:fillColor="#0F7D6E"
android:pathData="M16,22C25.1,11 38.8,4 54,4c27.6,0 50,22.4 50,50c0,1.7 -0.1,3.3 -0.2,5H16z" />
android:fillColor="#123C35"
android:pathData="M14,22C23.1,11 37.2,4 54,4c27.6,0 50,22.4 50,50c0,2.3 -0.2,4.6 -0.5,6.8H14z" />
<path
android:fillColor="@color/posthub_surface"
android:pathData="M27,24h54c4.4,0 8,3.6 8,8v45c0,4.4 -3.6,8 -8,8H27c-4.4,0 -8,-3.6 -8,-8V32c0,-4.4 3.6,-8 8,-8z" />
android:fillColor="#F7FAF7"
android:pathData="M54,13C31.4,13 13,31.4 13,54s18.4,41 41,41s41,-18.4 41,-41S76.6,13 54,13z" />
<path
android:fillColor="#E3F4EF"
android:pathData="M27,24h54c4.4,0 8,3.6 8,8v9H19v-9c0,-4.4 3.6,-8 8,-8z" />
android:pathData="M54,20C35.2,20 20,35.2 20,54s15.2,34 34,34s34,-15.2 34,-34S72.8,20 54,20z" />
<path
android:fillColor="#00000000"
android:pathData="M34,51H54C62,51 62,44 69,44H77M73,40l4,4l-4,4"
android:strokeColor="@color/posthub_primary"
android:pathData="M54,54L73,38M54,54L77,70M54,54L34,64M54,54L43,37"
android:strokeColor="#006B5B"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4" />
<path
android:fillColor="#00000000"
android:pathData="M34,65H49C57,65 57,73 65,73H77M73,69l4,4l-4,4"
android:strokeColor="@color/posthub_coral"
android:pathData="M37,73C45,82 61,84 72,77M36,35C45,27 61,26 72,34"
android:strokeColor="#D84B35"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="4" />
android:strokeWidth="3.5" />
<path
android:fillColor="#006B5B"
android:pathData="M54,54m-8,0a8,8 0,1 0,16 0a8,8 0,1 0,-16 0M73,38m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0M34,64m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
<path
android:fillColor="#D84B35"
android:pathData="M43,37m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
<path
android:fillColor="#C47A12"
android:pathData="M77,70m-6,0a6,6 0,1 0,12 0a6,6 0,1 0,-12 0" />
</vector>
@@ -4,7 +4,6 @@
<style name="Theme.PostHUB.Starting" parent="android:style/Theme.Material.NoActionBar">
<item name="android:windowSplashScreenBackground">@color/posthub_canvas</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_splash_brand</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/posthub_primary_dark</item>
<item name="android:windowBackground">@drawable/splash_window_background</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
-1
View File
@@ -4,7 +4,6 @@
<style name="Theme.PostHUB.Starting" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:windowSplashScreenBackground">@color/posthub_canvas</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_splash_brand</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/posthub_primary</item>
<item name="android:windowBackground">@drawable/splash_window_background</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
+1
View File
@@ -33,6 +33,7 @@ Run these commands from the project root:
```powershell
$env:GRADLE_USER_HOME='C:\Path\To\PostHUB\.gradle'
.\gradlew.bat :app:testDebugUnitTest :app:lintRelease :app:bundleRelease --console=plain
.\gradlew assembleRelease # or thid for building APK
```
Upload the generated bundle:
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "9.1.1"
agp = "9.2.1"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
+2 -2
View File
@@ -1,8 +1,8 @@
#Mon Apr 20 02:30:20 IRST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME