feat(android): align native preview with measured PWA design
Android validation / emulator (37.0) (push) Failing after 3s
Android validation / emulator (35) (push) Failing after 4s
iOS validation / simulator (push) Successful in 59s
Android validation / build (push) Successful in 5m30s

This commit is contained in:
Yuvomi Mobile Contributors committed 2026-10-21 10:45:16 +02:00
1 parent bd0e4cc4fb
commit 7ac9882307
30 files changed
+2793 -750

No files matched your search

@@ -0,0 +1,107 @@
package community.yuvomi.mobile
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import org.json.JSONArray
import org.json.JSONObject
val pwaSupportedWidgets = listOf("tasks", "calendar", "shopping", "notes")
/** Presentation only. Household business rules and permissions remain server-owned. */
fun pwaGreeting(displayName: String, hour: Int): String {
val first = displayName.trim().split(Regex("\\s+")).firstOrNull().orEmpty()
val greeting = when (hour) {
in 5..11 -> "Guten Morgen"
in 12..17 -> "Guten Tag"
else -> "Guten Abend"
}
return if (first.isBlank()) greeting else "$greeting, $first"
}
fun pwaDueLabel(value: String, today: LocalDate = LocalDate.now()): String {
if (value.isBlank()) return ""
val date = runCatching { LocalDate.parse(value.take(10)) }.getOrNull() ?: return value
return when (date) {
today -> "Heute fällig"
today.plusDays(1) -> "Morgen fällig"
today.minusDays(1) -> "Gestern fällig"
else -> date.format(DateTimeFormatter.ofPattern("d. MMM", Locale.GERMAN))
}
}
fun pwaPriority(value: String) = when (value) {
"low" -> "Niedrig"
"medium" -> "Mittel"
"high" -> "Hoch"
"urgent" -> "Dringend"
else -> ""
}
fun pwaGroup(value: String) = value.takeUnless { it.isBlank() || it in setOf("general", "other", "misc") } ?: "Sonstiges"
fun NativeState.householdNow(instant: java.time.Instant = java.time.Instant.now()): ZonedDateTime {
val zone = runCatching { ZoneId.of(preferences.text("timezone_effective").ifBlank { preferences.text("timezone") }) }
.getOrDefault(ZoneId.systemDefault())
return instant.atZone(zone)
}
fun NativeState.moduleVisible(module: String): Boolean = access(module) != "none" &&
listOf("disabled_modules", "hidden_modules").none { key ->
val values = preferences.optJSONArray(key) ?: JSONArray()
(0 until values.length()).any { values.optString(it) == module }
}
fun NativeState.dashboardTiles(): List<String> {
val config = preferences.rows("dashboard_widgets")
return (
if (preferences.isNull("dashboard_widgets")) {
pwaSupportedWidgets
} else {
config.filter { it.optBoolean("visible", true) }.map { it.text("id") }
}
)
.filter { it in pwaSupportedWidgets && moduleVisible(it) && widget(it) }.distinct()
}
fun pwaWidgetVisibility(preferences: JSONObject, visible: Set<String>, editable: Set<String> = pwaSupportedWidgets.toSet()): JSONArray {
val existing = preferences.rows("dashboard_widgets")
val rows = if (preferences.isNull("dashboard_widgets")) {
pwaSupportedWidgets.mapIndexed { index, id ->
JSONObject().put("id", id).put("visible", true).put("order", index).put("size", "1x1")
}
} else {
existing
}
val result = JSONArray(
rows.map { row ->
JSONObject(row.toString()).apply {
if (text("id") in editable && text("id") in pwaSupportedWidgets) put("visible", text("id") in visible)
}
},
)
val configured = rows.map { it.text("id") }.toSet()
pwaSupportedWidgets.filter { it in visible && it in editable && it !in configured }.forEach { id ->
result.put(JSONObject().put("id", id).put("visible", true).put("order", result.length()).put("size", "1x1"))
}
return result
}
fun NativeState.nativeTabs(): List<NativePage> {
val order = preferences.optJSONArray("mobile_nav_order")?.let { array ->
(0 until array.length()).map { array.optString(it) }
}.orEmpty()
val pages = (order + listOf("tasks", "kitchen", "notes")).mapNotNull {
when (it) {
"tasks" -> NativePage.TASKS
"shopping", "kitchen" -> NativePage.SHOPPING
"notes" -> NativePage.NOTES
else -> null // Unimplemented modules are never fake navigation targets.
}
}.distinct().filter { moduleVisible(it.name.lowercase()) }
return listOf(NativePage.HOME) + pages + NativePage.MORE
}
fun NativePage.navigationLabel() = if (this == NativePage.SHOPPING) "Küche" else title