summaryrefslogtreecommitdiff
path: root/android-app/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt7
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt17
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt95
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt13
-rw-r--r--android-app/app/src/main/res/layout/layout_track_summary_sheet.xml199
5 files changed, 325 insertions, 6 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
index 022b748..d64ce8d 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt
@@ -109,6 +109,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
fabRecordTrack.contentDescription = if (recording) "Stop Recording" else "Record Track"
}
}
+ lifecycleScope.launch {
+ viewModel.trackSummary.collect { summary ->
+ org.terst.nav.track.TrackSummarySheet
+ .from(summary, viewModel.trackStartMs)
+ .show(supportFragmentManager, "track_summary")
+ }
+ }
}
private fun setupBottomSheet() {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
index ed32497..228a484 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt
@@ -12,7 +12,10 @@ class TrackRepository(context: Context) {
private set
private val activePoints = mutableListOf<TrackPoint>()
- private var trackStartMs = 0L
+
+ /** Epoch-ms when the current (or last) track started. Exposed for the summary sheet title. */
+ var trackStartMs: Long = 0L
+ private set
// Loaded lazily from Documents/Nav/ on first access
private val _pastTracks = mutableListOf<List<TrackPoint>>()
@@ -26,18 +29,22 @@ class TrackRepository(context: Context) {
/**
* Stops the active track, computes a [TrackSummary], persists the track
- * to shared storage, and returns the summary. Returns null if no points
- * were recorded.
+ * to shared storage, and returns the summary. Returns null if the track
+ * was too short (< 2 minutes) or had no points.
*/
+
suspend fun stopTrack(): TrackSummary? = withContext(Dispatchers.IO) {
if (!isRecording) return@withContext null
isRecording = false
val points = activePoints.toList()
activePoints.clear()
- if (points.isEmpty()) return@withContext null
+ if (points.size < 2) return@withContext null
val summary = summarise(points)
- _pastTracks.add(0, points) // prepend so most recent is first
+ // Discard tracks shorter than 2 minutes — likely accidental taps
+ if (summary.durationMs < 2 * 60_000L) return@withContext null
+
+ _pastTracks.add(0, points)
storage.saveTrack(points, trackStartMs)
summary
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt
new file mode 100644
index 0000000..8d9d7c7
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackSummarySheet.kt
@@ -0,0 +1,95 @@
+package org.terst.nav.track
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import org.terst.nav.R
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.util.Locale
+
+class TrackSummarySheet : BottomSheetDialogFragment() {
+
+ companion object {
+ private const val ARG_DISTANCE = "distance_nm"
+ private const val ARG_DURATION = "duration_ms"
+ private const val ARG_MAX_SOG = "max_sog"
+ private const val ARG_AVG_SOG = "avg_sog"
+ private const val ARG_AVG_WIND = "avg_wind" // -1 if absent
+ private const val ARG_AVG_WAVE = "avg_wave_m" // -1 if absent
+ private const val ARG_START_MS = "start_ms"
+
+ fun from(summary: TrackSummary, startMs: Long) = TrackSummarySheet().apply {
+ arguments = Bundle().apply {
+ putDouble(ARG_DISTANCE, summary.distanceNm)
+ putLong(ARG_DURATION, summary.durationMs)
+ putDouble(ARG_MAX_SOG, summary.maxSogKt)
+ putDouble(ARG_AVG_SOG, summary.avgSogKt)
+ putDouble(ARG_AVG_WIND, summary.avgWindKt ?: -1.0)
+ putDouble(ARG_AVG_WAVE, summary.avgWaveHeightM ?: -1.0)
+ putLong(ARG_START_MS, startMs)
+ }
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
+ inflater.inflate(R.layout.layout_track_summary_sheet, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val args = requireArguments()
+ val distanceNm = args.getDouble(ARG_DISTANCE)
+ val durationMs = args.getLong(ARG_DURATION)
+ val maxSog = args.getDouble(ARG_MAX_SOG)
+ val avgSog = args.getDouble(ARG_AVG_SOG)
+ val avgWind = args.getDouble(ARG_AVG_WIND).takeIf { it >= 0 }
+ val avgWaveM = args.getDouble(ARG_AVG_WAVE).takeIf { it >= 0 }
+ val startMs = args.getLong(ARG_START_MS)
+
+ val titleFmt = DateTimeFormatter.ofPattern("dd MMM HH:mm", Locale.getDefault())
+ .withZone(ZoneId.systemDefault())
+ view.findViewById<TextView>(R.id.summary_title).text =
+ "Track · ${titleFmt.format(Instant.ofEpochMilli(startMs))}"
+
+ view.findViewById<TextView>(R.id.summary_distance).text =
+ "%.1f".format(Locale.getDefault(), distanceNm)
+
+ view.findViewById<TextView>(R.id.summary_duration).text = formatDuration(durationMs)
+
+ view.findViewById<TextView>(R.id.summary_max_sog).text =
+ "%.1f".format(Locale.getDefault(), maxSog)
+
+ view.findViewById<TextView>(R.id.summary_avg_sog).text =
+ "%.1f".format(Locale.getDefault(), avgSog)
+
+ val conditionsRow = view.findViewById<LinearLayout>(R.id.summary_conditions_row)
+ val windCell = view.findViewById<LinearLayout>(R.id.summary_wind_cell)
+ val waveCell = view.findViewById<LinearLayout>(R.id.summary_wave_cell)
+
+ if (avgWind != null) {
+ windCell.visibility = View.VISIBLE
+ view.findViewById<TextView>(R.id.summary_avg_wind).text =
+ "%.0f".format(Locale.getDefault(), avgWind)
+ }
+ if (avgWaveM != null) {
+ waveCell.visibility = View.VISIBLE
+ val waveFt = avgWaveM * 3.28084
+ view.findViewById<TextView>(R.id.summary_avg_wave).text =
+ "%.1f".format(Locale.getDefault(), waveFt)
+ }
+ if (avgWind != null || avgWaveM != null) {
+ conditionsRow.visibility = View.VISIBLE
+ }
+ }
+
+ private fun formatDuration(ms: Long): String {
+ val totalMinutes = ms / 60_000
+ val hours = totalMinutes / 60
+ val minutes = totalMinutes % 60
+ return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
+ }
+}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
index c1707ab..5797138 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
@@ -15,10 +15,14 @@ import org.terst.nav.data.repository.WeatherRepository
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
+import org.terst.nav.track.TrackSummary
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
@@ -60,18 +64,25 @@ class MainViewModel(
private val _pastTracks = MutableStateFlow<List<List<TrackPoint>>>(emptyList())
val pastTracks: StateFlow<List<List<TrackPoint>>> = _pastTracks.asStateFlow()
+ private val _trackSummary = MutableSharedFlow<TrackSummary>(replay = 0)
+ val trackSummary: SharedFlow<TrackSummary> = _trackSummary.asSharedFlow()
+
fun startTrack() {
trackRepository.startTrack()
_trackPoints.value = emptyList()
_isRecording.value = true
}
+ /** Epoch-ms when the current track started — for the summary sheet title. */
+ val trackStartMs: Long get() = trackRepository.trackStartMs
+
fun stopTrack() {
viewModelScope.launch {
- trackRepository.stopTrack()
+ val summary = trackRepository.stopTrack()
_pastTracks.value = trackRepository.getPastTracks()
_trackPoints.value = emptyList()
_isRecording.value = false
+ summary?.let { _trackSummary.emit(it) }
}
}
diff --git a/android-app/app/src/main/res/layout/layout_track_summary_sheet.xml b/android-app/app/src/main/res/layout/layout_track_summary_sheet.xml
new file mode 100644
index 0000000..a26b76e
--- /dev/null
+++ b/android-app/app/src/main/res/layout/layout_track_summary_sheet.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:paddingBottom="32dp"
+ android:background="?attr/colorSurface">
+
+ <!-- Drag handle -->
+ <View
+ android:layout_width="36dp"
+ android:layout_height="4dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="20dp"
+ android:background="@color/md_theme_outline" />
+
+ <!-- Title -->
+ <TextView
+ android:id="@+id/summary_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:textSize="13sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.12"
+ android:fontFamily="sans-serif-light"
+ android:textColor="@color/instrument_text_secondary"
+ tools:text="Track · 06 Apr 14:32" />
+
+ <!-- Distance + Duration row -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="16dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ style="@style/InstrumentLabel"
+ android:text="Distance" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/summary_distance"
+ style="@style/InstrumentPrimaryValue"
+ tools:text="12.3" />
+ <TextView
+ style="@style/InstrumentUnit"
+ android:text="nm" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ style="@style/InstrumentLabel"
+ android:text="Duration" />
+ <TextView
+ android:id="@+id/summary_duration"
+ style="@style/InstrumentPrimaryValue"
+ tools:text="2h 14m" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <!-- Speed row -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="16dp">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ style="@style/InstrumentLabel"
+ android:text="Max Speed" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/summary_max_sog"
+ style="@style/InstrumentPrimaryValue"
+ tools:text="8.1" />
+ <TextView
+ style="@style/InstrumentUnit"
+ android:text="kt" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+ <TextView
+ style="@style/InstrumentLabel"
+ android:text="Avg Speed" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/summary_avg_sog"
+ style="@style/InstrumentPrimaryValue"
+ tools:text="5.5" />
+ <TextView
+ style="@style/InstrumentUnit"
+ android:text="kt" />
+ </LinearLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <!-- Wind + Waves row (conditionally visible) -->
+ <LinearLayout
+ android:id="@+id/summary_conditions_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <LinearLayout
+ android:id="@+id/summary_wind_cell"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+ <TextView
+ style="@style/InstrumentLabel"
+ android:text="Avg Wind" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/summary_avg_wind"
+ style="@style/InstrumentPrimaryValue"
+ tools:text="14" />
+ <TextView
+ style="@style/InstrumentUnit"
+ android:text="kt" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/summary_wave_cell"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+ <TextView
+ style="@style/InstrumentLabel"
+ android:text="Avg Waves" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <TextView
+ android:id="@+id/summary_avg_wave"
+ style="@style/InstrumentPrimaryValue"
+ tools:text="2.1" />
+ <TextView
+ style="@style/InstrumentUnit"
+ android:text="ft" />
+ </LinearLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout>