Refactor tile layout to show recent cases

The tile layout has been refactored to display a list of recent cases
instead of the currently active case. This change includes:

- Removing the "current case" view and its associated logic.
- Implementing a new `caseListContent` function to render a list of up
  to 8 recent cases.
- Modifying the main `buildTileLayout` to integrate the new case list
  and a prominent "Neu" (New) button.
- Updating the styling and click handling to reflect the new layout and
  functionality.
This commit is contained in:
2026-05-03 21:41:13 +02:00
parent d5f234e159
commit 8ce2e54847
3 changed files with 64 additions and 72 deletions
@@ -14,8 +14,8 @@ private const val RESOURCES_VERSION = "1"
/** /**
* Serves the single Doctate Tile. Reads a non-blocking snapshot from * Serves the single Doctate Tile. Reads a non-blocking snapshot from
* [DoctateApp.caseStore] and renders the "glance at current case" view with * [DoctateApp.caseStore] and renders the recent-cases list with an
* three tap regions. Refreshes are triggered externally via * edge-style "Neu" button. Refreshes are triggered externally via
* `TileService.getUpdater(ctx).requestUpdate(...)` — the App wires that up * `TileService.getUpdater(ctx).requestUpdate(...)` — the App wires that up
* to the case-store flow. * to the case-store flow.
*/ */
@@ -24,9 +24,9 @@ class DoctateTileService : TileService() {
requestParams: RequestBuilders.TileRequest, requestParams: RequestBuilders.TileRequest,
): ListenableFuture<TileBuilders.Tile> { ): ListenableFuture<TileBuilders.Tile> {
val app = applicationContext as DoctateApp val app = applicationContext as DoctateApp
val current = app.caseStore.currentCase val cases = app.caseStore.casesFlow.value
val layout = LayoutElementBuilders.Layout.Builder() val layout = LayoutElementBuilders.Layout.Builder()
.setRoot(buildTileLayout(packageName, current)) .setRoot(buildTileLayout(packageName, cases))
.build() .build()
val timeline = TimelineBuilders.Timeline.Builder() val timeline = TimelineBuilders.Timeline.Builder()
@@ -4,15 +4,11 @@ import androidx.wear.protolayout.ActionBuilders
import androidx.wear.protolayout.ColorBuilders.argb import androidx.wear.protolayout.ColorBuilders.argb
import androidx.wear.protolayout.DimensionBuilders.dp import androidx.wear.protolayout.DimensionBuilders.dp
import androidx.wear.protolayout.DimensionBuilders.expand import androidx.wear.protolayout.DimensionBuilders.expand
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Box import androidx.wear.protolayout.LayoutElementBuilders.Box
import androidx.wear.protolayout.LayoutElementBuilders.Column import androidx.wear.protolayout.LayoutElementBuilders.Column
import androidx.wear.protolayout.LayoutElementBuilders.FontStyle import androidx.wear.protolayout.LayoutElementBuilders.FontStyle
import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_END
import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
import androidx.wear.protolayout.LayoutElementBuilders.Row
import androidx.wear.protolayout.LayoutElementBuilders.Spacer import androidx.wear.protolayout.LayoutElementBuilders.Spacer
import androidx.wear.protolayout.LayoutElementBuilders.Text import androidx.wear.protolayout.LayoutElementBuilders.Text
import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
@@ -22,7 +18,6 @@ import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.Corner import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.Modifiers import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding import androidx.wear.protolayout.ModifiersBuilders.Padding
import androidx.wear.protolayout.TypeBuilders.StringProp
import com.doctate.watch.domain.CaseEntry import com.doctate.watch.domain.CaseEntry
import com.doctate.watch.domain.displayText import com.doctate.watch.domain.displayText
import com.doctate.watch.domain.isManual import com.doctate.watch.domain.isManual
@@ -30,101 +25,98 @@ import com.doctate.watch.presentation.NavCommand
private const val CLICK_ID_LIST = "list" private const val CLICK_ID_LIST = "list"
private const val CLICK_ID_NEW = "new" private const val CLICK_ID_NEW = "new"
private const val CLICK_ID_CONTINUE = "continue"
/** Build the root layout element for the Tile. Returns the "current case" view or an private const val MAX_VISIBLE_CASES = 8
* empty-state view depending on whether any case exists in [CaseStoreStub]. */
internal fun buildTileLayout(packageName: String, current: CaseEntry?): LayoutElement { /**
val column = Column.Builder() * Build the root Tile layout: a vertical stack with the most recent cases
* pinned just above an edge-style "Neu" button. The whole list region is a
* single tap target — every line routes to the case list, regardless of
* which case was tapped. Up to [MAX_VISIBLE_CASES] cases are shown; the
* newest sits at the bottom (next to the edge button).
*/
internal fun buildTileLayout(packageName: String, cases: List<CaseEntry>): LayoutElement {
return Column.Builder()
.setWidth(expand()) .setWidth(expand())
.setHeight(expand()) .setHeight(expand())
.setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
.addContent(topRow(packageName)) .addContent(Spacer.Builder().setHeight(dp(10f)).build())
.addContent(Spacer.Builder().setHeight(dp(8f)).build()) .addContent(caseListContent(packageName, cases))
.addContent(middleContent(packageName, current)) .addContent(Spacer.Builder().setHeight(dp(4f)).build())
.addContent(Spacer.Builder().setHeight(dp(8f)).build())
.addContent(bottomNewButton(packageName)) .addContent(bottomNewButton(packageName))
.build() .build()
}
private fun caseListContent(packageName: String, cases: List<CaseEntry>): LayoutElement {
val column = Column.Builder()
.setWidth(expand())
.setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
if (cases.isEmpty()) {
column.addContent(
Text.Builder()
.setText("Noch kein Fall heute")
.setFontStyle(FontStyle.Builder().setSize(sp(14f)).build())
.build(),
)
} else {
val visible = cases.take(MAX_VISIBLE_CASES).asReversed()
visible.forEachIndexed { index, case ->
if (index > 0) {
column.addContent(Spacer.Builder().setHeight(dp(2f)).build())
}
column.addContent(
Text.Builder()
.setText(formatLine(case))
.setMaxLines(1)
.setFontStyle(FontStyle.Builder().setSize(sp(13f)).build())
.build(),
)
}
}
return Box.Builder() return Box.Builder()
.setWidth(expand()) .setWidth(expand())
.setHeight(expand()) .setHeight(expand())
.setVerticalAlignment(VERTICAL_ALIGN_CENTER) .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
.addContent(column) .setVerticalAlignment(VERTICAL_ALIGN_BOTTOM)
.build()
}
private fun topRow(packageName: String): LayoutElement {
val faelle = Text.Builder()
.setText("☰ Fälle")
.setFontStyle(FontStyle.Builder().setSize(sp(14f)).build())
.setModifiers( .setModifiers(
Modifiers.Builder() Modifiers.Builder()
.setClickable(launchClickable(CLICK_ID_LIST, packageName, NavCommand.OPEN_LIST)) .setClickable(launchClickable(CLICK_ID_LIST, packageName, NavCommand.OPEN_LIST))
.setPadding(
Padding.Builder().setStart(dp(8f)).setEnd(dp(8f))
.setTop(dp(4f)).setBottom(dp(4f)).build(),
)
.build(), .build(),
) )
.build() .addContent(column.build())
// Row has no horizontal-alignment prop; use a Box with END alignment instead.
return Box.Builder()
.setWidth(expand())
.setHorizontalAlignment(HORIZONTAL_ALIGN_END)
.setVerticalAlignment(VERTICAL_ALIGN_CENTER)
.addContent(faelle)
.build() .build()
} }
private fun middleContent(packageName: String, current: CaseEntry?): LayoutElement { /** "👤 text" if doctor-authored, plain text if auto-generated, "…" placeholder otherwise. */
val label = when { private fun formatLine(case: CaseEntry): String {
current == null -> "Noch kein Fall heute" val text = case.oneliner.displayText() ?: return ""
else -> current.oneliner.displayText() return if (case.oneliner.isManual()) "👤 $text" else text
?.let { if (current.oneliner.isManual()) "👤 $it" else it }
?: ""
}
val textBuilder = Text.Builder()
.setText(label)
.setMaxLines(3)
.setFontStyle(FontStyle.Builder().setSize(sp(16f)).build())
val modifiersBuilder = Modifiers.Builder()
.setPadding(
Padding.Builder().setStart(dp(12f)).setEnd(dp(12f))
.setTop(dp(6f)).setBottom(dp(6f)).build(),
)
if (current != null) {
modifiersBuilder.setClickable(
launchClickable(
CLICK_ID_CONTINUE,
packageName,
NavCommand.OPEN_CONTINUE,
caseId = current.caseId,
),
)
}
textBuilder.setModifiers(modifiersBuilder.build())
return textBuilder.build()
} }
private fun bottomNewButton(packageName: String): LayoutElement { private fun bottomNewButton(packageName: String): LayoutElement {
val text = Text.Builder() val text = Text.Builder()
.setText("● Neu") .setText("● Neu")
.setFontStyle(FontStyle.Builder().setSize(sp(14f)).build()) .setFontStyle(
FontStyle.Builder()
.setSize(sp(14f))
.setColor(argb(0xFF0A305F.toInt()))
.build(),
)
.build() .build()
return Box.Builder() return Box.Builder()
.setWidth(expand()) .setWidth(expand())
.setHeight(dp(36f)) .setHeight(dp(48f))
.setVerticalAlignment(VERTICAL_ALIGN_BOTTOM) .setVerticalAlignment(VERTICAL_ALIGN_CENTER)
.setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
.setModifiers( .setModifiers(
Modifiers.Builder() Modifiers.Builder()
.setClickable(launchClickable(CLICK_ID_NEW, packageName, NavCommand.OPEN_NEW)) .setClickable(launchClickable(CLICK_ID_NEW, packageName, NavCommand.OPEN_NEW))
.setBackground( .setBackground(
Background.Builder() Background.Builder()
.setColor(argb(0xFF1F1F1F.toInt())) .setColor(argb(0xFFAAC7FF.toInt()))
.setCorner(Corner.Builder().setRadius(dp(18f)).build()) .setCorner(Corner.Builder().setRadius(dp(24f)).build())
.build(), .build(),
) )
.setPadding( .setPadding(