diff options
| author | Claude <noreply@anthropic.com> | 2026-05-15 20:32:22 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-15 21:25:54 +0000 |
| commit | baa07bc5c062d4f358e8c655260bf32810184634 (patch) | |
| tree | c2698d415ec210108f3aed20fa3e3662c0859230 /android-app/app | |
| parent | 555483b7248b0cb75bb9ed83b47880349f0c6ee0 (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')
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> |
