Compare commits

..

10 Commits

Author SHA1 Message Date
4d728f6c57 increment version 2026-05-11 17:55:18 +03:30
5245ade2cf Merge remote-tracking branch 'origin/main' 2026-05-11 17:51:30 +03:30
90392c93d7 Updated the app logo and splash artwork to a cleaner circular hub-style logo 2026-05-11 17:51:13 +03:30
Meghdad
488f15fca1 Add MIT License to the project 2026-05-11 17:22:57 +03:30
2397d27c07 update gradle 2026-05-08 04:31:04 +03:30
27bc0e466c increment version 2026-05-06 20:27:41 +03:30
c613ca972f implement widget text template 2026-05-06 20:27:11 +03:30
81bb6fc156 cleanup misc.xml 2026-05-06 20:08:53 +03:30
4255fac131 increment version 2026-05-06 07:48:19 +03:30
15c70cc6f0 update release.md 2026-05-06 07:47:26 +03:30
16 changed files with 463 additions and 111 deletions

1
.idea/misc.xml generated
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
LICENSE Normal file
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.

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"
}

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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>

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>

View File

@@ -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>

View File

@@ -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>

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>

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:

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"

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