summaryrefslogtreecommitdiff
path: root/android-app/app/src
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-20 19:43:25 +0000
committerClaude <noreply@anthropic.com>2026-05-20 19:43:25 +0000
commit48ad5dca0452689de86b7edf0b6638fe7c5c7025 (patch)
treedd91fe7e466e5e3c54e99fa96a4ff7b385b32f52 /android-app/app/src
parent48ae3b0225cb1a79539711be3a70cbd630c53638 (diff)
Fix tack detection and convert track detail to embedded-map fragment
TackDetector: revert STABILITY_MAX to 30° (was tightened too far to 20°), add 120s start-skip to filter GPS cold-start false positives, keep 45s minimum gap between tacks. TrackDetailSheet: convert from BottomSheetDialogFragment to a regular Fragment with an embedded MapLibreMap in the top half. This fixes the map centering issue (the embedded map is always sized to its own frame), eliminates accidental dismissal when dragging the map, and makes the view self-contained — back navigation restores the track list via the fragment back stack. Tack markers rendered on the embedded map; tapping a log event pans the embedded map directly. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
Diffstat (limited to 'android-app/app/src')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt46
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt18
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt85
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt127
-rw-r--r--android-app/app/src/main/res/layout/layout_track_detail_sheet.xml179
5 files changed, 321 insertions, 134 deletions
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 b2181c9..b4a0a91 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
@@ -1,5 +1,6 @@
package org.terst.nav.track
+import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -11,6 +12,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.launch
import org.terst.nav.MainActivity
import org.terst.nav.R
@@ -32,17 +34,19 @@ class SavedTracksFragment : Fragment() {
(requireActivity() as MainActivity).hideOverlays()
}
- val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
- val empty = view.findViewById<TextView>(R.id.tv_empty)
+ val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
+ val layoutEmpty = view.findViewById<View>(R.id.layout_empty)
+ val tvEmpty = view.findViewById<TextView>(R.id.tv_empty)
+ val btnSetup = view.findViewById<MaterialButton>(R.id.btn_setup_storage)
+
rv.layoutManager = LinearLayoutManager(requireContext())
rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
val adapter = SavedTrackAdapter { track ->
viewModel.selectTrack(track)
- parentFragmentManager
- .beginTransaction()
+ parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, TrackDetailSheet.newInstance())
- .addToBackStack(null)
+ .addToBackStack("track_detail")
.commit()
}
rv.adapter = adapter
@@ -50,18 +54,42 @@ class SavedTracksFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.savedTracks.collect { tracks ->
adapter.submitList(tracks)
- empty.visibility = if (tracks.isEmpty()) View.VISIBLE else View.GONE
- rv.visibility = if (tracks.isEmpty()) View.GONE else View.VISIBLE
+ val empty = tracks.isEmpty()
+ layoutEmpty.visibility = if (empty) View.VISIBLE else View.GONE
+ rv.visibility = if (empty) View.GONE else View.VISIBLE
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.safState.collect { state ->
+ when (state) {
+ SafState.NOT_CONFIGURED -> {
+ tvEmpty.text = "No saved tracks yet.\nRecord a trip to see it here."
+ btnSetup.text = "Set up track storage"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.PERMISSION_LOST -> {
+ tvEmpty.text = "Track storage needs re-authorization."
+ btnSetup.text = "Restore track access"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.CONFIGURED -> btnSetup.visibility = View.GONE
+ }
+ }
+ }
+ btnSetup.setOnClickListener {
+ (requireActivity() as MainActivity).launchSafPicker()
}
}
}
}
-private val DATE_FMT: DateTimeFormatter =
+internal val DATE_FMT: DateTimeFormatter =
DateTimeFormatter.ofPattern("EEE dd MMM yyyy · HH:mm", Locale.getDefault())
.withZone(ZoneId.systemDefault())
-class SavedTrackAdapter(
+internal class SavedTrackAdapter(
private val onSelect: (SavedTrack) -> Unit
) : RecyclerView.Adapter<SavedTrackAdapter.VH>() {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
index 6616acf..9668fd1 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TackDetector.kt
@@ -3,20 +3,22 @@ package org.terst.nav.track
import kotlin.math.abs
object TackDetector {
- private const val HALF_WIN = 2
- private const val MIN_DELTA = 60.0
- private const val MAX_DELTA = 140.0
- private const val SKIP_AFTER = 5
- private const val STABILITY_MAX = 30.0
+ private const val HALF_WIN = 2
+ private const val MIN_DELTA = 60.0
+ private const val MAX_DELTA = 140.0
+ private const val STABILITY_MAX = 30.0
+ private const val MIN_GAP_MS = 45_000L // 45 s minimum between tacks
+ private const val START_SKIP_MS = 120_000L // skip first 2 min (GPS cold-start noise)
fun detectTacks(points: List<TrackPoint>): List<TackEvent> {
if (points.size < 2 * HALF_WIN + 1) return emptyList()
val results = mutableListOf<TackEvent>()
- var nextCandidate = 0
+ var lastTackMs = Long.MIN_VALUE
for (i in HALF_WIN until points.size - HALF_WIN) {
- if (i < nextCandidate) continue
+ if (points[i].timestampMs - points.first().timestampMs < START_SKIP_MS) continue
+ if (points[i].timestampMs - lastTackMs < MIN_GAP_MS) continue
val spreadBefore = abs(angleDiff(points[i - 2].cogDeg, points[i - 1].cogDeg))
val spreadAfter = abs(angleDiff(points[i + 1].cogDeg, points[i + 2].cogDeg))
@@ -28,7 +30,7 @@ object TackDetector {
if (delta in MIN_DELTA..MAX_DELTA) {
results += TackEvent(i, points[i].lat, points[i].lon, cogBefore, cogAfter)
- nextCandidate = i + SKIP_AFTER
+ lastTackMs = points[i].timestampMs
}
}
return results
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 c1764a8..158e513 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
@@ -10,6 +10,23 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+<<<<<<< Updated upstream
+=======
+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.CircleLayer
+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.FeatureCollection
+import org.maplibre.geojson.LineString
+import org.maplibre.geojson.Point
+>>>>>>> Stashed changes
import org.terst.nav.R
import org.terst.nav.ui.MainViewModel
import java.time.Instant
@@ -31,6 +48,8 @@ data class LogEvent(
class TrackDetailSheet : Fragment() {
private val viewModel: MainViewModel by activityViewModels()
+ private lateinit var trackMapView: MapView
+ private var trackMap: MapLibreMap? = null
companion object {
fun newInstance() = TrackDetailSheet()
@@ -68,11 +87,75 @@ class TrackDetailSheet : Fragment() {
rv.layoutManager = LinearLayoutManager(requireContext())
rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
rv.adapter = LogEventAdapter(events) { event ->
- viewModel.panToTrackPoint(event.lat, event.lon)
+ trackMap?.easeCamera(CameraUpdateFactory.newLatLng(LatLng(event.lat, event.lon)), 300)
}
+<<<<<<< Updated upstream
+=======
+
+ trackMapView = view.findViewById(R.id.track_map_view)
+ trackMapView.onCreate(savedInstanceState)
+ trackMapView.getMapAsync { map ->
+ trackMap = map
+ map.setStyle(Style.Builder().fromUri("https://tiles.openfreemap.org/styles/bright")) { style ->
+ drawTrack(style, track.points)
+ drawTacks(style, track.tacks)
+ zoomToTrack(map, track.points)
+ }
+ }
+ }
+
+ private fun drawTrack(style: Style, points: List<TrackPoint>) {
+ if (points.size < 2) return
+ val source = GeoJsonSource("track-detail-source",
+ Feature.fromGeometry(LineString.fromLngLats(points.map { Point.fromLngLat(it.lon, it.lat) })))
+ style.addSource(source)
+ style.addLayer(LineLayer("track-detail-layer", "track-detail-source").apply {
+ setProperties(
+ PropertyFactory.lineColor("#2B8FC4"),
+ PropertyFactory.lineWidth(4f),
+ PropertyFactory.lineCap("round"),
+ PropertyFactory.lineJoin("round")
+ )
+ })
+ }
+
+ private fun drawTacks(style: Style, tacks: List<TackEvent>) {
+ if (tacks.isEmpty()) return
+ val features = tacks.map { Feature.fromGeometry(Point.fromLngLat(it.lon, it.lat)) }
+ val source = GeoJsonSource("tack-detail-source", FeatureCollection.fromFeatures(features))
+ style.addSource(source)
+ style.addLayer(CircleLayer("tack-detail-layer", "tack-detail-source").apply {
+ setProperties(
+ PropertyFactory.circleColor("#FDD835"),
+ PropertyFactory.circleRadius(7f),
+ PropertyFactory.circleStrokeColor("#F57F17"),
+ PropertyFactory.circleStrokeWidth(2f)
+ )
+ })
+ }
+
+ 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)
+ }
+
+ override fun onStart() { super.onStart(); if (::trackMapView.isInitialized) trackMapView.onStart() }
+ override fun onResume() { super.onResume(); if (::trackMapView.isInitialized) trackMapView.onResume() }
+ override fun onPause() { super.onPause(); if (::trackMapView.isInitialized) trackMapView.onPause() }
+ override fun onStop() { super.onStop(); if (::trackMapView.isInitialized) trackMapView.onStop() }
+ override fun onLowMemory() { super.onLowMemory(); if (::trackMapView.isInitialized) trackMapView.onLowMemory() }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (::trackMapView.isInitialized) trackMapView.onSaveInstanceState(outState)
+>>>>>>> Stashed changes
}
override fun onDestroyView() {
+ if (::trackMapView.isInitialized) trackMapView.onDestroy()
super.onDestroyView()
viewModel.clearSelectedTrack()
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
index ab30d96..116a90c 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
@@ -5,6 +5,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
@@ -18,24 +19,35 @@ import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import kotlinx.coroutines.launch
+import org.terst.nav.MainActivity
import org.terst.nav.R
import org.terst.nav.logbook.VoiceLogState
import org.terst.nav.logbook.VoiceLogViewModel
+import org.terst.nav.track.SafState
+import org.terst.nav.track.SavedTrackAdapter
+import org.terst.nav.track.TrackDetailSheet
+import org.terst.nav.ui.MainViewModel
import java.io.File
import java.io.FileOutputStream
import java.util.Locale
class VoiceLogFragment : Fragment() {
- private val viewModel by lazy {
+ private val logViewModel by lazy {
VoiceLogViewModel(repository = org.terst.nav.NavApplication.logbookRepository)
}
+ private val mainViewModel: MainViewModel by activityViewModels()
+ private lateinit var layoutLogEntry: View
private lateinit var speechRecognizer: SpeechRecognizer
private lateinit var etNote: TextInputEditText
private lateinit var fabMic: FloatingActionButton
@@ -48,13 +60,9 @@ class VoiceLogFragment : Fragment() {
private lateinit var btnSave: MaterialButton
private lateinit var btnClear: MaterialButton
private lateinit var tvSavedConfirmation: TextView
- private lateinit var btnGenerateReport: MaterialButton
- /** Path or URI string for the currently attached photo, null if none. */
private var pendingPhotoPath: String? = null
- // ── Camera (TakePicturePreview returns a thumbnail bitmap — no FileProvider needed) ──
-
private val cameraLauncher = registerForActivityResult(
ActivityResultContracts.TakePicturePreview()
) { bitmap: Bitmap? ->
@@ -69,18 +77,12 @@ class VoiceLogFragment : Fragment() {
}
}
- // ── Gallery (GetContent — no storage permission needed; URI valid for this session) ──
-
private val galleryLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
- if (uri != null) {
- setPhoto(uri.toString()) { ivPhoto.setImageURI(uri) }
- }
+ if (uri != null) setPhoto(uri.toString()) { ivPhoto.setImageURI(uri) }
}
- // ── Lifecycle ─────────────────────────────────────────────────────────────
-
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -90,18 +92,18 @@ class VoiceLogFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- etNote = view.findViewById(R.id.et_note)
- fabMic = view.findViewById(R.id.fab_mic)
- btnCamera = view.findViewById(R.id.btn_camera)
- btnGallery = view.findViewById(R.id.btn_gallery)
- tvStatus = view.findViewById(R.id.tv_status)
- framePhoto = view.findViewById(R.id.frame_photo)
- ivPhoto = view.findViewById(R.id.iv_photo)
- btnRemovePhoto = view.findViewById(R.id.btn_remove_photo)
- btnSave = view.findViewById(R.id.btn_save)
- btnClear = view.findViewById(R.id.btn_clear)
+ layoutLogEntry = view.findViewById(R.id.layout_log_entry)
+ etNote = view.findViewById(R.id.et_note)
+ fabMic = view.findViewById(R.id.fab_mic)
+ btnCamera = view.findViewById(R.id.btn_camera)
+ btnGallery = view.findViewById(R.id.btn_gallery)
+ tvStatus = view.findViewById(R.id.tv_status)
+ framePhoto = view.findViewById(R.id.frame_photo)
+ ivPhoto = view.findViewById(R.id.iv_photo)
+ btnRemovePhoto = view.findViewById(R.id.btn_remove_photo)
+ btnSave = view.findViewById(R.id.btn_save)
+ btnClear = view.findViewById(R.id.btn_clear)
tvSavedConfirmation = view.findViewById(R.id.tv_saved_confirmation)
- btnGenerateReport = view.findViewById(R.id.btn_generate_report)
setupSpeechRecognizer()
@@ -109,23 +111,77 @@ class VoiceLogFragment : Fragment() {
btnCamera.setOnClickListener { cameraLauncher.launch(null) }
btnGallery.setOnClickListener { galleryLauncher.launch("image/*") }
btnRemovePhoto.setOnClickListener { clearPhoto() }
-
btnSave.setOnClickListener {
- val text = etNote.text?.toString()?.trim() ?: ""
- viewModel.save(text, pendingPhotoPath)
+ logViewModel.save(etNote.text?.toString()?.trim() ?: "", pendingPhotoPath)
}
-
btnClear.setOnClickListener { clearEntry() }
- btnGenerateReport.setOnClickListener {
+ view.findViewById<MaterialButton>(R.id.btn_generate_report).setOnClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, org.terst.nav.tripreport.TripReportFragment())
.addToBackStack(null)
.commit()
}
+ // Show log-entry section only while recording
viewLifecycleOwner.lifecycleScope.launch {
- viewModel.state.collect { state -> renderState(state) }
+ mainViewModel.isRecording.collect { recording ->
+ layoutLogEntry.visibility = if (recording) View.VISIBLE else View.GONE
+ }
+ }
+
+ // Inline track list
+ val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks)
+ val layoutEmpty = view.findViewById<View>(R.id.layout_empty)
+ val tvEmpty = view.findViewById<TextView>(R.id.tv_empty)
+ val btnSetup = view.findViewById<MaterialButton>(R.id.btn_setup_storage)
+
+ rv.layoutManager = LinearLayoutManager(requireContext())
+ rv.isNestedScrollingEnabled = false
+ rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
+
+ val tracksAdapter = SavedTrackAdapter { track ->
+ mainViewModel.selectTrack(track)
+ parentFragmentManager.beginTransaction()
+ .replace(R.id.fragment_container, TrackDetailSheet.newInstance())
+ .addToBackStack("track_detail")
+ .commit()
+ }
+ rv.adapter = tracksAdapter
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ mainViewModel.savedTracks.collect { tracks ->
+ tracksAdapter.submitList(tracks)
+ layoutEmpty.visibility = if (tracks.isEmpty()) View.VISIBLE else View.GONE
+ rv.visibility = if (tracks.isEmpty()) View.GONE else View.VISIBLE
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ mainViewModel.safState.collect { state ->
+ when (state) {
+ SafState.NOT_CONFIGURED -> {
+ tvEmpty.text = "No saved tracks yet.\nRecord a trip to see it here."
+ btnSetup.text = "Set up track storage"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.PERMISSION_LOST -> {
+ tvEmpty.text = "Track storage needs re-authorization."
+ btnSetup.text = "Restore track access"
+ btnSetup.visibility = View.VISIBLE
+ }
+ SafState.CONFIGURED -> btnSetup.visibility = View.GONE
+ }
+ }
+ }
+ btnSetup.setOnClickListener {
+ (requireActivity() as MainActivity).launchSafPicker()
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ logViewModel.state.collect { state -> renderState(state) }
}
}
@@ -139,16 +195,14 @@ class VoiceLogFragment : Fragment() {
}
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(requireContext())
speechRecognizer.setRecognitionListener(object : RecognitionListener {
- override fun onReadyForSpeech(params: Bundle?) { viewModel.onListeningStarted() }
+ override fun onReadyForSpeech(params: Bundle?) { logViewModel.onListeningStarted() }
override fun onResults(results: Bundle?) {
val text = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
?.firstOrNull() ?: ""
- if (text.isNotBlank()) viewModel.onSpeechRecognized(text)
- else viewModel.onRecognitionError("Could not understand speech")
- }
- override fun onError(error: Int) {
- viewModel.onRecognitionError("Recognition error: $error")
+ if (text.isNotBlank()) logViewModel.onSpeechRecognized(text)
+ else logViewModel.onRecognitionError("Could not understand speech")
}
+ override fun onError(error: Int) { logViewModel.onRecognitionError("Recognition error: $error") }
override fun onBeginningOfSpeech() {}
override fun onBufferReceived(b: ByteArray?) {}
override fun onEndOfSpeech() {}
@@ -191,7 +245,7 @@ class VoiceLogFragment : Fragment() {
private fun clearEntry() {
etNote.setText("")
clearPhoto()
- viewModel.retry()
+ logViewModel.retry()
tvSavedConfirmation.text = ""
tvStatus.text = ""
}
@@ -209,7 +263,6 @@ class VoiceLogFragment : Fragment() {
fabMic.isEnabled = false
}
is VoiceLogState.Result -> {
- // Fill the text field; user can edit before saving
etNote.setText(state.recognized)
etNote.setSelection(state.recognized.length)
tvStatus.text = ""
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 6a2ba50..fafd75a 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
@@ -6,100 +6,121 @@
android:orientation="vertical"
android:background="?attr/colorSurface">
+<<<<<<< Updated upstream
<!-- Header -->
+=======
+ <!-- Track map — top half -->
+ <org.maplibre.android.maps.MapView
+ android:id="@+id/track_map_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <!-- Detail panel — bottom half -->
+>>>>>>> Stashed changes
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="56dp"
- android:orientation="horizontal"
- android:gravity="center_vertical"
- android:paddingStart="8dp"
- android:paddingEnd="16dp"
- android:elevation="4dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
android:background="?attr/colorSurface">
- <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" />
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="16dp"
+ android:elevation="4dp"
+ android:background="?attr/colorSurface">
- <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" />
+ <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" />
- </LinearLayout>
+ <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" />
- <!-- 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"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_distance" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="nm" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_duration" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="time" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
- </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="8dp"
+ android:paddingBottom="8dp">
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_max_sog" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="max kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
- </LinearLayout>
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_distance" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="nm" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_duration" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="time" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_max_sog" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="max kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
+
+ <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
+ android:layout_weight="1" android:orientation="vertical" android:gravity="center">
+ <TextView android:id="@+id/detail_avg_sog" android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:text="avg kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
+ </LinearLayout>
- <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content"
- android:layout_weight="1" android:orientation="vertical" android:gravity="center">
- <TextView android:id="@+id/detail_avg_sog" android:layout_width="wrap_content"
- android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" />
- <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:text="avg kt" android:textSize="11sp" android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
- </LinearLayout>
+ <View android:layout_width="match_parent" android:layout_height="1dp"
+ android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
- <View android:layout_width="match_parent" android:layout_height="1dp"
- android:background="?attr/colorOutlineVariant" android:layout_marginHorizontal="16dp" />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="20dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="4dp"
+ android:text="Event Log"
+ android:textSize="13sp"
+ android:textAllCaps="true"
+ android:letterSpacing="0.08"
+ android:textColor="?attr/colorOnSurfaceVariant" />
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingHorizontal="20dp"
- android:paddingTop="12dp"
- android:paddingBottom="4dp"
- android:text="Event Log"
- android:textSize="13sp"
- android:textAllCaps="true"
- android:letterSpacing="0.08"
- android:textColor="?attr/colorOnSurfaceVariant" />
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/rv_log_entries"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:clipToPadding="false"
- android:paddingBottom="24dp" />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/rv_log_entries"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:paddingBottom="24dp" />
+
+ </LinearLayout>
</LinearLayout>