summaryrefslogtreecommitdiff
path: root/android-app/app/src
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-30 08:19:46 +0000
committerClaude <noreply@anthropic.com>2026-05-30 08:19:46 +0000
commit994ddaf2ec8d8aace4fef72a214df31e6ef134ef (patch)
tree8b2c228ec341d1017bd11595cd10d5627429a183 /android-app/app/src
parent2b6a7599b3a9b0159ac3f45917e6e5d77841328c (diff)
Trip report: map, photos, structured layout, preferred units
Replaces monospace text block with: - Full-bleed MapLibre track map (260dp) at the top - Structured stats row (distance / avg speed / max speed) - Conditions row (wave height, temp) in user-preferred units - Log entries with full-width photo thumbnails - Pirate mode Easter egg preserved via long-press title - Speed, temp, and depth formatted with UnitPrefs https://claude.ai/code/session_01KXYBuAHZkvv63DeUG6XaZD
Diffstat (limited to 'android-app/app/src')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt241
-rw-r--r--android-app/app/src/main/res/layout/fragment_trip_report.xml326
2 files changed, 387 insertions, 180 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt
index aa86c25..d9d2263 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt
@@ -1,9 +1,14 @@
package org.terst.nav.tripreport
+import android.graphics.BitmapFactory
+import android.graphics.Outline
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
@@ -12,11 +17,28 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.maplibre.android.camera.CameraUpdateFactory
+import org.maplibre.android.geometry.LatLng
+import org.maplibre.android.geometry.LatLngBounds
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.maps.MapView
+import org.maplibre.android.maps.Style
+import org.maplibre.android.style.layers.LineLayer
+import org.maplibre.android.style.layers.PropertyFactory
+import org.maplibre.android.style.sources.GeoJsonSource
+import org.maplibre.geojson.Feature
+import org.maplibre.geojson.LineString
+import org.maplibre.geojson.Point
import org.terst.nav.NavApplication
import org.terst.nav.R
+import org.terst.nav.UnitPrefs
import org.terst.nav.logbook.LogEntry
+import org.terst.nav.track.TrackPoint
import org.terst.nav.ui.MainViewModel
+import java.io.File
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -43,6 +65,9 @@ class TripReportFragment : Fragment() {
}
}
+ private lateinit var reportMapView: MapView
+ private var reportMap: MapLibreMap? = null
+
private lateinit var tvTitle: TextView
private lateinit var layoutStructured: LinearLayout
private lateinit var tvPassageDate: TextView
@@ -50,6 +75,8 @@ class TripReportFragment : Fragment() {
private lateinit var tvStatDistance: TextView
private lateinit var tvStatAvg: TextView
private lateinit var tvStatMax: TextView
+ private lateinit var tvLabelAvg: TextView
+ private lateinit var tvLabelMax: TextView
private lateinit var layoutConditions: LinearLayout
private lateinit var tvConditions: TextView
private lateinit var dividerLog: View
@@ -74,23 +101,28 @@ class TripReportFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- tvTitle = view.findViewById(R.id.tv_report_title)
+ reportMapView = view.findViewById(R.id.report_map_view)
+ reportMapView.onCreate(savedInstanceState)
+
+ tvTitle = view.findViewById(R.id.tv_report_title)
layoutStructured = view.findViewById(R.id.layout_structured_content)
- tvPassageDate = view.findViewById(R.id.tv_passage_date)
- tvPassageTime = view.findViewById(R.id.tv_passage_time)
- tvStatDistance = view.findViewById(R.id.tv_stat_distance)
- tvStatAvg = view.findViewById(R.id.tv_stat_avg)
- tvStatMax = view.findViewById(R.id.tv_stat_max)
+ tvPassageDate = view.findViewById(R.id.tv_passage_date)
+ tvPassageTime = view.findViewById(R.id.tv_passage_time)
+ tvStatDistance = view.findViewById(R.id.tv_stat_distance)
+ tvStatAvg = view.findViewById(R.id.tv_stat_avg)
+ tvStatMax = view.findViewById(R.id.tv_stat_max)
+ tvLabelAvg = view.findViewById(R.id.tv_label_avg)
+ tvLabelMax = view.findViewById(R.id.tv_label_max)
layoutConditions = view.findViewById(R.id.layout_conditions)
- tvConditions = view.findViewById(R.id.tv_conditions)
- dividerLog = view.findViewById(R.id.divider_log)
+ tvConditions = view.findViewById(R.id.tv_conditions)
+ dividerLog = view.findViewById(R.id.divider_log)
layoutLogSection = view.findViewById(R.id.layout_log_section)
- llLogEntries = view.findViewById(R.id.ll_log_entries)
- cvPirate = view.findViewById(R.id.cv_pirate)
- tvNarrative = view.findViewById(R.id.tv_narrative_content)
- tvError = view.findViewById(R.id.tv_error)
- btnRetry = view.findViewById(R.id.btn_retry)
- progress = view.findViewById(R.id.progress_report)
+ llLogEntries = view.findViewById(R.id.ll_log_entries)
+ cvPirate = view.findViewById(R.id.cv_pirate)
+ tvNarrative = view.findViewById(R.id.tv_narrative_content)
+ tvError = view.findViewById(R.id.tv_error)
+ btnRetry = view.findViewById(R.id.btn_retry)
+ progress = view.findViewById(R.id.progress_report)
val forSavedTrack = arguments?.getBoolean(ARG_FOR_SAVED_TRACK, false) == true
val savedTrack = if (forSavedTrack) mainViewModel.selectedTrack.value else null
@@ -117,6 +149,46 @@ class TripReportFragment : Fragment() {
}
}
+ // ── MapView lifecycle delegation ──────────────────────────────────────────
+
+ override fun onStart() {
+ super.onStart()
+ if (::reportMapView.isInitialized) reportMapView.onStart()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (::reportMapView.isInitialized) reportMapView.onResume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ if (::reportMapView.isInitialized) reportMapView.onPause()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (::reportMapView.isInitialized) reportMapView.onStop()
+ }
+
+ override fun onLowMemory() {
+ super.onLowMemory()
+ if (::reportMapView.isInitialized) reportMapView.onLowMemory()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (::reportMapView.isInitialized) reportMapView.onSaveInstanceState(outState)
+ }
+
+ override fun onDestroyView() {
+ if (::reportMapView.isInitialized) reportMapView.onDestroy()
+ reportMap = null
+ super.onDestroyView()
+ }
+
+ // ── State rendering ───────────────────────────────────────────────────────
+
private fun renderState(state: TripReportState) {
when (state) {
is TripReportState.Idle -> {
@@ -170,15 +242,21 @@ class TripReportFragment : Fragment() {
}
tvPassageTime.text = "${timeFmt.format(startInst)} – ${timeFmt.format(endInst)} · $durationStr"
+ val units = UnitPrefs(requireContext())
+
tvStatDistance.text = "%.1f".format(s.distanceNm)
- tvStatAvg.text = "%.1f".format(s.avgSogKts)
- tvStatMax.text = "%.1f".format(s.maxSogKts)
+ tvStatAvg.text = units.formatSpeed(s.avgSogKts)
+ tvStatMax.text = units.formatSpeed(s.maxSogKts)
+ tvLabelAvg.text = "avg ${units.speedUnitLabel()}"
+ tvLabelMax.text = "max ${units.speedUnitLabel()}"
val condParts = buildList<String> {
- s.maxWaveHeightM?.let { add("Seas %.1f m".format(it)) }
+ s.maxWaveHeightM?.let { add("Seas ${units.formatDepth(it)} ${units.depthUnitLabel()}") }
val minT = s.minAirTempC; val maxT = s.maxAirTempC
if (minT != null && maxT != null) {
- add(if (minT == maxT) "%.0f°C".format(minT) else "%.0f–%.0f°C".format(minT, maxT))
+ val label = units.tempUnitLabel()
+ add(if (minT == maxT) "${units.formatTemp(minT)}$label"
+ else "${units.formatTemp(minT)}–${units.formatTemp(maxT)}$label")
}
}
if (condParts.isNotEmpty()) {
@@ -198,36 +276,147 @@ class TripReportFragment : Fragment() {
dividerLog.visibility = View.GONE
layoutLogSection.visibility = View.GONE
}
+
+ // Draw the track on the map
+ reportMapView.getMapAsync { map ->
+ reportMap = map
+ map.setStyle(Style.Builder().fromUri("https://tiles.openfreemap.org/styles/bright")) { style ->
+ if (s.points.size >= 2) {
+ drawTrack(style, s.points)
+ zoomToTrack(map, s.points)
+ }
+ }
+ }
}
+ private fun drawTrack(style: Style, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val source = GeoJsonSource(
+ "report-track-source",
+ Feature.fromGeometry(LineString.fromLngLats(points.map { Point.fromLngLat(it.lon, it.lat) }))
+ )
+ style.addSource(source)
+ style.addLayer(LineLayer("report-track-layer", "report-track-source").apply {
+ setProperties(
+ PropertyFactory.lineColor("#2B8FC4"),
+ PropertyFactory.lineWidth(4f),
+ PropertyFactory.lineCap("round"),
+ PropertyFactory.lineJoin("round")
+ )
+ })
+ }
+
+ private fun zoomToTrack(map: MapLibreMap, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val bounds = LatLngBounds.Builder().apply {
+ points.forEach { include(LatLng(it.lat, it.lon)) }
+ }.build()
+ map.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, 80), 600)
+ }
+
+ // ── Log row builder ───────────────────────────────────────────────────────
+
private fun buildLogRow(entry: LogEntry): View {
val ctx = requireContext()
- val row = LinearLayout(ctx).apply {
+ val density = resources.displayMetrics.density
+
+ // Outer container — vertical
+ val container = LinearLayout(ctx).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(0, (10 * density).toInt(), 0, (10 * density).toInt())
+ }
+
+ // Header row: time + text
+ val headerRow = LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
- setPadding(0, 10, 0, 10)
}
val timeView = TextView(ctx).apply {
text = logTimeFmt.format(Instant.ofEpochMilli(entry.timestampMs))
textSize = 13f
alpha = 0.5f
- minWidth = (56 * resources.displayMetrics.density).toInt()
+ minWidth = (56 * density).toInt()
}
val isPhotoOnly = entry.text == "(photo only)"
val displayText = when {
- isPhotoOnly && entry.photoPath != null -> "📷"
- entry.photoPath != null -> "${entry.text} 📷"
+ isPhotoOnly -> ""
else -> entry.text
}
+
val bodyView = TextView(ctx).apply {
text = displayText
textSize = 14f
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
- row.addView(timeView)
- row.addView(bodyView)
- return row
+ headerRow.addView(timeView)
+ headerRow.addView(bodyView)
+ container.addView(headerRow)
+
+ // Photo (if present) — full-width, 220dp tall, rounded corners
+ val photoPath = entry.photoPath
+ if (photoPath != null) {
+ val cornerRadiusPx = (12 * density).toInt()
+ val imageView = ImageView(ctx).apply {
+ scaleType = ImageView.ScaleType.CENTER_CROP
+ visibility = View.INVISIBLE // revealed once bitmap loads
+ outlineProvider = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(0, 0, view.width, view.height, cornerRadiusPx.toFloat())
+ }
+ }
+ clipToOutline = true
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ (220 * density).toInt()
+ ).apply {
+ topMargin = (8 * density).toInt()
+ }
+ }
+ container.addView(imageView)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ val bm = withContext(Dispatchers.IO) {
+ decodeThumbnail(ctx, photoPath, 800, 600)
+ }
+ if (bm != null) {
+ imageView.setImageBitmap(bm)
+ imageView.visibility = View.VISIBLE
+ } else {
+ imageView.visibility = View.GONE
+ }
+ }
+ }
+
+ return container
+ }
+}
+
+// ── Bitmap decode utility (mirrors TrackDetailSheet.decodeThumbnail) ──────────
+
+private fun decodeThumbnail(
+ context: android.content.Context,
+ pathOrUri: String,
+ targetW: Int,
+ targetH: Int
+): android.graphics.Bitmap? {
+ if (pathOrUri.startsWith("content://")) {
+ return runCatching {
+ val uri = Uri.parse(pathOrUri)
+ val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ context.contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, opts) }
+ if (opts.outWidth <= 0) return null
+ val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1)
+ context.contentResolver.openInputStream(uri)?.use { stream ->
+ BitmapFactory.decodeStream(stream, null, BitmapFactory.Options().apply { inSampleSize = scale })
+ }
+ }.getOrNull()
}
+ if (!File(pathOrUri).exists()) return null
+ val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ BitmapFactory.decodeFile(pathOrUri, opts)
+ if (opts.outWidth <= 0) return null
+ val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1)
+ return BitmapFactory.decodeFile(pathOrUri, BitmapFactory.Options().apply { inSampleSize = scale })
}
diff --git a/android-app/app/src/main/res/layout/fragment_trip_report.xml b/android-app/app/src/main/res/layout/fragment_trip_report.xml
index ec3f58e..5df8938 100644
--- a/android-app/app/src/main/res/layout/fragment_trip_report.xml
+++ b/android-app/app/src/main/res/layout/fragment_trip_report.xml
@@ -3,220 +3,238 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="?attr/colorSurface">
+ android:background="?attr/colorSurface"
+ android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
- android:padding="24dp">
+ android:orientation="vertical">
- <TextView
- android:id="@+id/tv_report_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Trip Report"
- android:textSize="24sp"
- android:textStyle="bold"
- android:layout_marginBottom="20dp"
- android:longClickable="true" />
+ <!-- Full-bleed track map at the top, no margin/padding -->
+ <org.maplibre.android.maps.MapView
+ android:id="@+id/report_map_view"
+ android:layout_width="match_parent"
+ android:layout_height="260dp" />
- <!-- Structured content (hidden in pirate mode) -->
+ <!-- Padded scrollable content below the map -->
<LinearLayout
- android:id="@+id/layout_structured_content"
+ android:id="@+id/layout_scrollable_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
- android:visibility="gone">
-
- <!-- Passage header: date + time range -->
- <TextView
- android:id="@+id/tv_passage_date"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="13sp"
- android:textAllCaps="true"
- android:letterSpacing="0.1"
- android:alpha="0.6"
- android:layout_marginBottom="2dp" />
+ android:padding="20dp">
<TextView
- android:id="@+id/tv_passage_time"
+ android:id="@+id/tv_report_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="17sp"
- android:layout_marginBottom="20dp" />
+ android:text="Trip Report"
+ android:textSize="24sp"
+ android:textStyle="bold"
+ android:longClickable="true"
+ android:layout_marginBottom="16dp" />
- <!-- Stats row: distance · avg · max -->
+ <!-- Structured content (hidden in pirate mode) -->
<LinearLayout
+ android:id="@+id/layout_structured_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:layout_marginBottom="16dp">
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <!-- Passage header: date + time range -->
+ <TextView
+ android:id="@+id/tv_passage_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="13sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.1"
+ android:alpha="0.6"
+ android:layout_marginBottom="2dp" />
+
+ <TextView
+ android:id="@+id/tv_passage_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="17sp"
+ android:layout_marginBottom="20dp" />
+ <!-- Stats row: distance · avg · max -->
<LinearLayout
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical"
- android:gravity="center_horizontal">
- <TextView
- android:id="@+id/tv_stat_distance"
- android:layout_width="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="16dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:textSize="34sp"
- android:textStyle="bold" />
- <TextView
- android:layout_width="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
+ <TextView
+ android:id="@+id/tv_stat_distance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="34sp"
+ android:textStyle="bold" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="nm"
+ android:textSize="11sp"
+ android:alpha="0.55" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:text="nm"
- android:textSize="11sp"
- android:alpha="0.55" />
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
+ <TextView
+ android:id="@+id/tv_stat_avg"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="34sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/tv_label_avg"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="avg kt"
+ android:textSize="11sp"
+ android:alpha="0.55" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
+ <TextView
+ android:id="@+id/tv_stat_max"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="34sp"
+ android:textStyle="bold" />
+ <TextView
+ android:id="@+id/tv_label_max"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="max kt"
+ android:textSize="11sp"
+ android:alpha="0.55" />
+ </LinearLayout>
+
</LinearLayout>
+ <!-- Conditions row (seas / temp) — hidden if no data -->
<LinearLayout
- android:layout_width="0dp"
+ android:id="@+id/layout_conditions"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical"
- android:gravity="center_horizontal">
- <TextView
- android:id="@+id/tv_stat_avg"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="34sp"
- android:textStyle="bold" />
+ android:orientation="horizontal"
+ android:layout_marginBottom="20dp"
+ android:visibility="gone">
+
<TextView
+ android:id="@+id/tv_conditions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="avg kt"
- android:textSize="11sp"
- android:alpha="0.55" />
+ android:textSize="14sp"
+ android:alpha="0.7" />
+
</LinearLayout>
+ <!-- Divider before log -->
+ <View
+ android:id="@+id/divider_log"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?attr/colorOutlineVariant"
+ android:visibility="gone" />
+
+ <!-- Log entries — hidden if none -->
<LinearLayout
- android:layout_width="0dp"
+ android:id="@+id/layout_log_section"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
android:orientation="vertical"
- android:gravity="center_horizontal">
- <TextView
- android:id="@+id/tv_stat_max"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="34sp"
- android:textStyle="bold" />
+ android:layout_marginTop="16dp"
+ android:visibility="gone">
+
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="max kt"
+ android:text="LOG"
android:textSize="11sp"
- android:alpha="0.55" />
+ android:textAllCaps="true"
+ android:letterSpacing="0.12"
+ android:alpha="0.5"
+ android:layout_marginBottom="8dp" />
+
+ <LinearLayout
+ android:id="@+id/ll_log_entries"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
</LinearLayout>
</LinearLayout>
- <!-- Conditions row (seas / temp) — hidden if no data -->
- <LinearLayout
- android:id="@+id/layout_conditions"
+ <!-- Pirate mode: monospace narrative card -->
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/cv_pirate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:layout_marginBottom="20dp"
+ app:cardCornerRadius="16dp"
+ app:cardElevation="0dp"
+ app:strokeWidth="1dp"
+ app:strokeColor="?attr/colorOutlineVariant"
android:visibility="gone">
<TextView
- android:id="@+id/tv_conditions"
- android:layout_width="wrap_content"
+ android:id="@+id/tv_narrative_content"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:padding="16dp"
android:textSize="14sp"
- android:alpha="0.7" />
+ android:fontFamily="monospace"
+ android:lineSpacingExtra="4dp" />
- </LinearLayout>
+ </com.google.android.material.card.MaterialCardView>
- <!-- Divider before log -->
- <View
- android:id="@+id/divider_log"
+ <!-- Error state -->
+ <TextView
+ android:id="@+id/tv_error"
android:layout_width="match_parent"
- android:layout_height="1dp"
- android:background="?attr/colorOutlineVariant"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:alpha="0.7"
+ android:layout_marginBottom="16dp"
android:visibility="gone" />
- <!-- Log entries — hidden if none -->
- <LinearLayout
- android:id="@+id/layout_log_section"
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
- android:layout_marginTop="16dp"
- android:visibility="gone">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="LOG"
- android:textSize="11sp"
- android:textAllCaps="true"
- android:letterSpacing="0.12"
- android:alpha="0.5"
- android:layout_marginBottom="8dp" />
-
- <LinearLayout
- android:id="@+id/ll_log_entries"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical" />
-
- </LinearLayout>
-
- </LinearLayout>
-
- <!-- Pirate mode: monospace narrative card -->
- <com.google.android.material.card.MaterialCardView
- android:id="@+id/cv_pirate"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:cardCornerRadius="16dp"
- app:cardElevation="0dp"
- app:strokeWidth="1dp"
- app:strokeColor="?attr/colorOutlineVariant"
- android:visibility="gone">
+ android:text="RETRY"
+ android:visibility="gone" />
- <TextView
- android:id="@+id/tv_narrative_content"
- android:layout_width="match_parent"
+ <ProgressBar
+ android:id="@+id/progress_report"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:padding="16dp"
- android:textSize="14sp"
- android:fontFamily="monospace"
- android:lineSpacingExtra="4dp" />
-
- </com.google.android.material.card.MaterialCardView>
-
- <!-- Error state -->
- <TextView
- android:id="@+id/tv_error"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="14sp"
- android:alpha="0.7"
- android:layout_marginBottom="16dp"
- android:visibility="gone" />
-
- <com.google.android.material.button.MaterialButton
- android:id="@+id/btn_retry"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="RETRY"
- android:visibility="gone" />
+ android:layout_gravity="center"
+ android:layout_marginTop="48dp"
+ android:visibility="gone" />
- <ProgressBar
- android:id="@+id/progress_report"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:layout_marginTop="48dp"
- android:visibility="gone" />
+ </LinearLayout>
</LinearLayout>
</ScrollView>