summaryrefslogtreecommitdiff
path: root/android-app/app/src
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-15 20:32:22 +0000
committerClaude <noreply@anthropic.com>2026-05-15 21:25:54 +0000
commitbaa07bc5c062d4f358e8c655260bf32810184634 (patch)
treec2698d415ec210108f3aed20fa3e3662c0859230 /android-app/app/src
parent555483b7248b0cb75bb9ed83b47880349f0c6ee0 (diff)
fix: saved tracks browser, GPX import persistence
Saved tracks browser: - TrackDetailSheet converted from BottomSheetDialogFragment to Fragment so it fills the overlay instead of floating over the map - Adapter tap pushes detail fragment onto the back stack; back button returns to the list naturally via popBackStack() - SavedTracksFragment back button calls hideOverlays() (made internal) to close the overlay and return to the map - Layout updated to full-screen: header with back button, RecyclerView fills remaining space via layout_weight instead of fixed 300dp GPX import persistence: - TrackRepository.importTrack() saves parsed points to Documents/Nav/ via TrackStorage (URI is persisted to SharedPreferences) and adds to in-memory list - MainViewModel.addImportedTrack() now calls importTrack() so files opened via the GPX handler load on future launches https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
Diffstat (limited to 'android-app/app/src')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt3
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt10
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt23
-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/ui/MainViewModel.kt11
-rw-r--r--android-app/app/src/main/res/layout/layout_track_detail_sheet.xml58
6 files changed, 78 insertions, 44 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 10eb0a8..1bd0b08 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
@@ -45,7 +45,6 @@ import org.terst.nav.settings.HardwareSource
import org.terst.nav.thermal.ThermalState
import org.terst.nav.thermal.thermalFlow
import org.terst.nav.track.SavedTracksFragment
-import org.terst.nav.track.TrackDetailSheet
import org.terst.nav.ui.*
import org.terst.nav.ui.doc.DocFragment
import org.terst.nav.ui.map.ParticleWindView
@@ -396,7 +395,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
.commit()
}
- private fun hideOverlays() {
+ internal fun hideOverlays() {
fragmentContainer.visibility = View.GONE
fabRecordTrack.visibility = View.VISIBLE
fabLayers.visibility = View.VISIBLE
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
index b73b034..b2181c9 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
+import org.terst.nav.MainActivity
import org.terst.nav.R
import org.terst.nav.ui.MainViewModel
import java.time.Instant
@@ -28,7 +29,7 @@ class SavedTracksFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<View>(R.id.btn_back).setOnClickListener {
- parentFragmentManager.popBackStack()
+ (requireActivity() as MainActivity).hideOverlays()
}
val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
@@ -38,8 +39,11 @@ class SavedTracksFragment : Fragment() {
val adapter = SavedTrackAdapter { track ->
viewModel.selectTrack(track)
- parentFragmentManager.popBackStack()
- TrackDetailSheet.from(track).show(parentFragmentManager, "track_detail")
+ parentFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragment_container, TrackDetailSheet.newInstance())
+ .addToBackStack(null)
+ .commit()
}
rv.adapter = adapter
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
index 0ec42f7..c1764a8 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
@@ -5,11 +5,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
+import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.terst.nav.R
import org.terst.nav.ui.MainViewModel
import java.time.Instant
@@ -28,24 +28,26 @@ data class LogEvent(
val detail: String
)
-class TrackDetailSheet : BottomSheetDialogFragment() {
+class TrackDetailSheet : Fragment() {
private val viewModel: MainViewModel by activityViewModels()
companion object {
- private const val ARG_TRACK_INDEX = "track_index"
-
- fun from(track: SavedTrack): TrackDetailSheet {
- // We pass the track via the shared ViewModel — the BottomSheet reads selectedTrack.
- return TrackDetailSheet()
- }
+ fun newInstance() = TrackDetailSheet()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.layout_track_detail_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val track = viewModel.selectedTrack.value ?: run { dismiss(); return }
+ val track = viewModel.selectedTrack.value ?: run {
+ parentFragmentManager.popBackStack()
+ return
+ }
+
+ view.findViewById<View>(R.id.btn_back).setOnClickListener {
+ parentFragmentManager.popBackStack()
+ }
val titleFmt = DateTimeFormatter.ofPattern("dd MMM yyyy · HH:mm", Locale.getDefault())
.withZone(ZoneId.systemDefault())
@@ -80,12 +82,10 @@ class TrackDetailSheet : BottomSheetDialogFragment() {
val points = track.points
val tacks = track.tacks
- // Start / end
points.firstOrNull()?.let { p ->
events += LogEvent(p.lat, p.lon, p.timestampMs, "🚩", "Start", formatPointDetail(p))
}
- // Tacks — detected maneuvers
for (t in tacks) {
val p = points[t.index]
val delta = TackDetector.angleDiff(t.cogBefore, t.cogAfter)
@@ -96,7 +96,6 @@ class TrackDetailSheet : BottomSheetDialogFragment() {
"%.0f° → %.0f° (Δ%.0f°)".format(t.cogBefore, t.cogAfter, abs(delta)))
}
- // Significant speed changes (> 1.5 kt shift sustained over 2 consecutive points)
for (i in 1 until points.size - 1) {
val prev = points[i - 1].sogKnots
val curr = points[i].sogKnots
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 7f765a1..20fc214 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
@@ -60,6 +60,23 @@ class TrackRepository(context: Context) {
summary
}
+ /**
+ * Saves a track parsed from an external GPX file into Documents/Nav/ and
+ * adds it to the in-memory list. Returns true if storage succeeded.
+ */
+ suspend fun importTrack(points: List<TrackPoint>): Boolean = withContext(Dispatchers.IO) {
+ if (points.isEmpty()) return@withContext false
+ val startMs = points.first().timestampMs
+ val saved = storage.saveTrack(points, startMs)
+ if (saved) {
+ _pastTracks.add(0, points)
+ NavLogger.i("track", "importTrack: saved ${points.size} pts startMs=$startMs")
+ } else {
+ NavLogger.e("track", "importTrack: save failed for ${points.size} pts")
+ }
+ saved
+ }
+
fun addPoint(point: TrackPoint): Boolean {
if (!isRecording) return false
activePoints.add(point)
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 dcbd98e..579ccdf 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
@@ -18,7 +18,6 @@ import org.terst.nav.track.SavedTrack
import org.terst.nav.track.TrackPoint
import org.terst.nav.track.TrackRepository
import org.terst.nav.track.TrackStats
-import org.terst.nav.track.toSavedTrack
import org.terst.nav.settings.FeatureFlags
import org.terst.nav.settings.HardwareSource
import org.terst.nav.vessel.CrewMember
@@ -411,11 +410,13 @@ class MainViewModel(
fun selectTrack(track: SavedTrack) { _selectedTrack.value = track }
fun clearSelectedTrack() { _selectedTrack.value = null }
- /** Adds a track parsed from an external GPX file (not persisted to storage). */
+ /** Saves a track parsed from an external GPX file to storage and updates state. */
fun addImportedTrack(points: List<TrackPoint>) {
- val updated = _pastTracks.value.toMutableList().also { it.add(0, points) }
- _pastTracks.value = updated
- _savedTracks.value = updated.filter { it.isNotEmpty() }.map { it.toSavedTrack() }
+ viewModelScope.launch {
+ trackRepository.importTrack(points)
+ _pastTracks.value = trackRepository.getPastTracks()
+ _savedTracks.value = trackRepository.getSavedTracks()
+ }
}
fun panToTrackPoint(lat: Double, lon: Double) {
diff --git a/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml b/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
index 9b2eb30..6a2ba50 100644
--- a/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
+++ b/android-app/app/src/main/res/layout/layout_track_detail_sheet.xml
@@ -2,34 +2,47 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:orientation="vertical"
- android:paddingBottom="16dp">
+ android:background="?attr/colorSurface">
- <!-- drag handle -->
- <View
- android:layout_width="40dp"
- android:layout_height="4dp"
- android:layout_gravity="center_horizontal"
- android:layout_marginTop="8dp"
- android:layout_marginBottom="8dp"
- android:background="#BDBDBD" />
-
- <TextView
- android:id="@+id/detail_title"
+ <!-- Header -->
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingHorizontal="20dp"
- android:paddingBottom="12dp"
- android:textSize="17sp"
- android:textStyle="bold" />
+ android:layout_height="56dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="16dp"
+ android:elevation="4dp"
+ android:background="?attr/colorSurface">
- <!-- stats row: distance · duration · max · avg -->
+ <ImageButton
+ android:id="@+id/btn_back"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:src="@drawable/ic_back"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="Back" />
+
+ <TextView
+ android:id="@+id/detail_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="17sp"
+ android:textStyle="bold"
+ android:layout_marginStart="4dp" />
+
+ </LinearLayout>
+
+ <!-- Stats row: distance · duration · max · avg -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
+ android:paddingTop="12dp"
android:paddingBottom="12dp">
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
@@ -69,7 +82,6 @@
<View android:layout_width="match_parent" android:layout_height="1dp"
android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
- <!-- event log -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -85,7 +97,9 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_log_entries"
android:layout_width="match_parent"
- android:layout_height="300dp"
- android:nestedScrollingEnabled="true" />
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:paddingBottom="24dp" />
</LinearLayout>