From 62cc93086498d57ef7298eddb40cc382dfc10c6d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 22:18:38 +0000 Subject: Fix Long overflow in TackDetector: var lastTackMs = Long.MIN_VALUE caused subtraction overflow, filtering every tack --- .../org/terst/nav/track/TackDetectorGpxTest.kt | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 android-app/app/src/test/kotlin/org/terst/nav/track/TackDetectorGpxTest.kt (limited to 'android-app/app/src/test/kotlin/org') diff --git a/android-app/app/src/test/kotlin/org/terst/nav/track/TackDetectorGpxTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/track/TackDetectorGpxTest.kt new file mode 100644 index 0000000..83ca259 --- /dev/null +++ b/android-app/app/src/test/kotlin/org/terst/nav/track/TackDetectorGpxTest.kt @@ -0,0 +1,25 @@ +package org.terst.nav.track + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class TackDetectorGpxTest { + + @Test + fun `detects 6 tacks in April 22 test file`() { + val stream = javaClass.classLoader!! + .getResourceAsStream("nav_20260422_190543.gpx") + ?: error("GPX test file not found in test resources") + val points = GpxParser.parse(stream) + assertTrue("Expected >100 points, got ${points.size}", points.size > 100) + + val tacks = TackDetector.detectTacks(points) + assertEquals( + "Expected 6 tacks in test track; got ${tacks.size}: ${ + tacks.map { "%.0f→%.0f".format(it.cogBefore, it.cogAfter) } + }", + 6, tacks.size + ) + } +} -- cgit v1.2.3 From 04b5b7f6aa469a368bcf8f1d75046770d3f83d63 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 19:02:33 +0000 Subject: Fix bugs from static analysis; add test coverage for new code Bugs fixed: - LogbookStorage.copyFromUri: return null when ContentResolver returns null stream instead of returning a path to an unwritten file - VoiceLogFragment adapter: decode photos as downsampled thumbnails (128x96 target) using BitmapFactory inSampleSize instead of loading full-resolution bitmaps on the main thread Tests added: - LogbookRepositoryTest: ID sequencing, insertion order, storage delegation, reload from persisted state, lat/lon preservation - TripReportGeneratorTest: log entry time-range filtering, distance calculation, empty-points edge case, all narrative styles smoke-tested https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .../kotlin/org/terst/nav/logbook/LogbookStorage.kt | 5 +- .../org/terst/nav/ui/voicelog/VoiceLogFragment.kt | 13 +++- .../org/terst/nav/logbook/LogbookRepositoryTest.kt | 69 +++++++++++++++++ .../nav/tripreport/TripReportGeneratorTest.kt | 89 ++++++++++++++++++++++ 4 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 android-app/app/src/test/kotlin/org/terst/nav/logbook/LogbookRepositoryTest.kt create mode 100644 android-app/app/src/test/kotlin/org/terst/nav/tripreport/TripReportGeneratorTest.kt (limited to 'android-app/app/src/test/kotlin/org') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt index 35349f0..6b90542 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt @@ -44,9 +44,8 @@ class LogbookStorage(private val context: Context) { fun copyFromUri(uri: Uri): String? = runCatching { val dest = File(photoDir, "log_${System.currentTimeMillis()}.jpg") - context.contentResolver.openInputStream(uri)?.use { input -> - FileOutputStream(dest).use { output -> input.copyTo(output) } - } + val stream = context.contentResolver.openInputStream(uri) ?: return null + stream.use { input -> FileOutputStream(dest).use { output -> input.copyTo(output) } } dest.absolutePath }.getOrElse { null } } 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 31d9715..3638f8f 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 @@ -368,6 +368,16 @@ class VoiceLogFragment : Fragment() { } } +// ── Helpers ──────────────────────────────────────────────────────────────────── + +private fun decodeThumbnail(path: String, targetW: Int, targetH: Int): android.graphics.Bitmap? { + val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(path, opts) + if (opts.outWidth <= 0) return null + val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1) + return BitmapFactory.decodeFile(path, BitmapFactory.Options().apply { inSampleSize = scale }) +} + // ── Logbook entries adapter ──────────────────────────────────────────────────── private val TIME_FMT: DateTimeFormatter = @@ -409,11 +419,12 @@ private class LogbookEntryAdapter : RecyclerView.Adapter val photoPath = e.photoPath if (photoPath != null && File(photoPath).exists()) { - val bm = BitmapFactory.decodeFile(photoPath) + val bm = decodeThumbnail(photoPath, 128, 96) if (bm != null) { holder.ivPhoto.setImageBitmap(bm) holder.ivPhoto.visibility = View.VISIBLE } else { + holder.ivPhoto.setImageDrawable(null) holder.ivPhoto.visibility = View.GONE } } else { diff --git a/android-app/app/src/test/kotlin/org/terst/nav/logbook/LogbookRepositoryTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/logbook/LogbookRepositoryTest.kt new file mode 100644 index 0000000..1161065 --- /dev/null +++ b/android-app/app/src/test/kotlin/org/terst/nav/logbook/LogbookRepositoryTest.kt @@ -0,0 +1,69 @@ +package org.terst.nav.logbook + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class LogbookRepositoryTest { + + private lateinit var storage: LogbookStorage + private lateinit var repo: LogbookRepository + + @Before fun setUp() { + storage = mockk(relaxed = true) + every { storage.loadAll() } returns emptyList() + repo = LogbookRepository(storage) + } + + @Test fun `save assigns unique sequential ids`() { + val a = repo.save(entry("a")) + val b = repo.save(entry("b")) + assertNotEquals(a.id, b.id) + assertEquals(a.id + 1, b.id) + } + + @Test fun `getAll returns entries in insertion order`() { + repo.save(entry("first")) + repo.save(entry("second")) + assertEquals(listOf("first", "second"), repo.getAll().map { it.text }) + } + + @Test fun `save calls storage saveAll`() { + repo.save(entry("stored")) + verify { storage.saveAll(any()) } + } + + @Test fun `repository loads initial entries from storage on init`() { + val preloaded = entry("preloaded").copy(id = 99L) + every { storage.loadAll() } returns listOf(preloaded) + val repo2 = LogbookRepository(storage) + assertTrue(repo2.getAll().any { it.text == "preloaded" }) + } + + @Test fun `ids continue from max stored id after reload`() { + every { storage.loadAll() } returns listOf(entry("old").copy(id = 50L)) + val repo2 = LogbookRepository(storage) + val saved = repo2.save(entry("new")) + assertEquals(51L, saved.id) + } + + @Test fun `saved entry inherits text and location`() { + val e = entry("test note").copy(lat = 19.669, lon = -156.027) + val saved = repo.save(e) + assertEquals("test note", saved.text) + assertEquals(19.669, saved.lat!!, 0.0001) + assertEquals(-156.027, saved.lon!!, 0.0001) + } + + private fun entry(text: String) = LogEntry( + timestampMs = System.currentTimeMillis(), + text = text, + entryType = EntryType.GENERAL + ) +} diff --git a/android-app/app/src/test/kotlin/org/terst/nav/tripreport/TripReportGeneratorTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/tripreport/TripReportGeneratorTest.kt new file mode 100644 index 0000000..eea3234 --- /dev/null +++ b/android-app/app/src/test/kotlin/org/terst/nav/tripreport/TripReportGeneratorTest.kt @@ -0,0 +1,89 @@ +package org.terst.nav.tripreport + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.terst.nav.logbook.EntryType +import org.terst.nav.logbook.LogEntry +import org.terst.nav.track.TrackPoint + +class TripReportGeneratorTest { + + private val gen = TripReportGenerator() + + private fun pt(lat: Double, lon: Double, sog: Double = 5.0, ts: Long) = + TrackPoint(lat = lat, lon = lon, sogKnots = sog, cogDeg = 0.0, timestampMs = ts) + + private fun entry(ts: Long, text: String = "note") = LogEntry( + timestampMs = ts, text = text, entryType = EntryType.GENERAL + ) + + // ── generateSummary ─────────────────────────────────────────────────────── + + @Test + fun `empty points returns zero summary`() { + val s = gen.generateSummary(emptyList(), emptyList()) + assertEquals(0.0, s.distanceNm, 0.0) + assertEquals(0.0, s.maxSogKts, 0.0) + assertTrue(s.logEntries.isEmpty()) + } + + @Test + fun `distance between two points one nm apart`() { + // 1 nm north along prime meridian ≈ 0.01667° latitude + val points = listOf(pt(0.0, 0.0, ts = 0L), pt(0.016667, 0.0, ts = 60_000L)) + val s = gen.generateSummary(points, emptyList()) + assertEquals(1.0, s.distanceNm, 0.02) + } + + @Test + fun `max sog picks highest value`() { + val points = listOf(pt(0.0, 0.0, sog = 3.0, ts = 0L), pt(0.0, 0.0, sog = 8.5, ts = 1000L)) + val s = gen.generateSummary(points, emptyList()) + assertEquals(8.5, s.maxSogKts, 0.001) + } + + @Test + fun `log entries within track time range are included`() { + val points = listOf(pt(0.0, 0.0, ts = 1000L), pt(0.0, 0.0, ts = 5000L)) + val logs = listOf(entry(500L, "before"), entry(1000L, "start"), entry(3000L, "middle"), entry(5000L, "end"), entry(6000L, "after")) + val s = gen.generateSummary(points, logs) + assertEquals(listOf("start", "middle", "end"), s.logEntries.map { it.text }) + } + + @Test + fun `log entries outside track time range are excluded`() { + val points = listOf(pt(0.0, 0.0, ts = 10_000L), pt(0.0, 0.0, ts = 20_000L)) + val logs = listOf(entry(5_000L, "too early"), entry(25_000L, "too late")) + val s = gen.generateSummary(points, logs) + assertTrue(s.logEntries.isEmpty()) + } + + @Test + fun `start and end times match first and last point`() { + val points = listOf(pt(0.0, 0.0, ts = 1234L), pt(0.0, 0.0, ts = 9876L)) + val s = gen.generateSummary(points, emptyList()) + assertEquals(1234L, s.startTimeMs) + assertEquals(9876L, s.endTimeMs) + } + + // ── generateNarrative ───────────────────────────────────────────────────── + + @Test + fun `professional narrative contains distance and speed`() { + val points = listOf(pt(0.0, 0.0, sog = 5.0, ts = 0L), pt(0.016667, 0.0, sog = 5.0, ts = 3_600_000L)) + val s = gen.generateSummary(points, emptyList()) + val narrative = gen.generateNarrative(s, NarrativeStyle.PROFESSIONAL) + assertTrue("Expected distance in narrative", narrative.contains("NM") || narrative.contains("nm")) + } + + @Test + fun `all narrative styles produce non-empty output`() { + val points = listOf(pt(0.0, 0.0, ts = 0L), pt(0.016667, 0.0, ts = 3_600_000L)) + val s = gen.generateSummary(points, listOf(entry(1_000L))) + for (style in NarrativeStyle.values()) { + val narrative = gen.generateNarrative(s, style) + assertTrue("Narrative for $style should not be blank", narrative.isNotBlank()) + } + } +} -- cgit v1.2.3 From e34df09b96f4ded3c25c5a14a0332a874205b64a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 05:13:48 +0000 Subject: Add notes to finished tracks, single Nav voice, fix speed coloring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TrackDetailSheet: + Add note button opens a dialog with text, camera, and gallery. Entry is saved with timestampMs=track.endMs so it lands inside the track's time window and appears automatically in the event log. logEventAdapter.update() refreshes the list after save. - TripReportGenerator: removed NarrativeStyle enum and 4-style branch. generateNarrative() now emits a concise PASSAGE header (date, time range, duration), stats line (nm / avg kt / max kt), optional conditions (seas, temp), and a timestamped deck log. Fragment and layout updated to remove the ChipGroup style picker. - TrackColors: replaced Expression.get("color") — which silently fails for lineColor on LineLayer in MapLibre Android 11.x — with Expression.step(Expression.get("speed"), ...) that maps the numeric "speed" property directly to color literals. "color" string property removed from speedSegments() features. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .agent/worklog.md | 14 +- .../main/kotlin/org/terst/nav/track/TrackColors.kt | 26 ++-- .../kotlin/org/terst/nav/track/TrackDetailSheet.kt | 143 ++++++++++++++++++++- .../org/terst/nav/tripreport/TripReportFragment.kt | 20 +-- .../terst/nav/tripreport/TripReportGenerator.kt | 114 ++++++++-------- .../terst/nav/tripreport/TripReportViewModel.kt | 12 +- .../src/main/res/layout/dialog_add_track_note.xml | 69 ++++++++++ .../src/main/res/layout/fragment_trip_report.xml | 62 +-------- .../main/res/layout/layout_track_detail_sheet.xml | 35 +++-- .../nav/tripreport/TripReportGeneratorTest.kt | 32 +++-- 10 files changed, 349 insertions(+), 178 deletions(-) create mode 100644 android-app/app/src/main/res/layout/dialog_add_track_note.xml (limited to 'android-app/app/src/test/kotlin/org') diff --git a/.agent/worklog.md b/.agent/worklog.md index cfae053..a31dbd0 100644 --- a/.agent/worklog.md +++ b/.agent/worklog.md @@ -2,15 +2,19 @@ ## Backlog (fix later) -- **Add notes/photos to a finished track** — UI to attach logbook entries to a past track by timestamp/geo proximity. Must flow into the trip report (currently only live-session entries are scoped). +- **Tack detection is buggy, especially at low speed** — False positives at low SOG (GPS heading noise looks like tacks). Need minimum-SOG gate (e.g. skip detection when SOG < 1.5 kt) and review the stability window / MIN_DELTA thresholds against real tracks. -- **Trip report voice** — The current narrative styles (Professional / Adventurous / Journal / Pirate) are generic and not right for Nav. Nav is an opinionated sailing app; the report should have a single voice that reflects that — concise, knowledgeable, seamanlike. Rip out the style picker and write one good template. +## Current Task Goal +Logbook + Trip Report Improvements — COMPLETE (2026-05-26) -- **Tack detection is buggy, especially at low speed** — False positives at low SOG (GPS heading noise looks like tacks). Need minimum-SOG gate (e.g. skip detection when SOG < 1.5 kt) and review the stability window / MIN_DELTA thresholds against real tracks. +Delivered: +- **Add notes/photos to finished track** — `+ Add note` button in `TrackDetailSheet` opens a dialog with text entry, camera, and gallery. Notes are saved with `timestampMs = track.endMs` so they fall within the track window and appear in `buildLogEvents()` automatically. The map is re-built via `logEventAdapter.update()` after save. +- **Trip report single Nav voice** — Removed `NarrativeStyle` enum and the 4-style ChipGroup. `generateNarrative(summary)` now emits a concise `PASSAGE` header with date/time range, stats, conditions, and a timestamped deck log. `fragment_trip_report.xml` no longer has the style picker. +- **Speed coloring fixed** — Replaced the broken `Expression.get("color")` (reads a string feature property, silently fails for `lineColor` on `LineLayer` in MapLibre Android 11.x) with `Expression.step(Expression.get("speed"), ...)` which maps the numeric `"speed"` property directly to color literals. `speedSegments()` no longer writes the unused `"color"` string property. -- **Speed coloring on the map doesn't work** — `Expression.get("color")` on a FeatureCollection of 2-point LineStrings fails silently in MapLibre Android 11.x. The focused-track layer in MapHandler still uses this broken approach. Needs investigation: either find a working data-driven expression syntax for this MapLibre version, or pre-segment the GeoJSON differently. +--- -## Current Task Goal +## Previous Task Goal Instrument Sheet Visual Redesign — IN PROGRESS (2026-04-06) **Plan:** `docs/superpowers/plans/2026-04-04-instrument-sheet-visual-redesign.md` diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt index 292240a..5e5e85d 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackColors.kt @@ -5,17 +5,9 @@ import org.maplibre.geojson.Feature import org.maplibre.geojson.LineString import org.maplibre.geojson.Point -private fun speedToHex(speedKt: Double): String = when { - speedKt >= 13 -> "#F44336" // red - speedKt >= 10 -> "#FFC107" // amber - speedKt >= 7 -> "#4CAF50" // green - speedKt >= 4 -> "#00BCD4" // cyan - else -> "#2196F3" // blue -} - /** * Breaks [points] into consecutive 2-point LineString Features. - * Each feature carries "speed" (knots) and "color" (CSS hex) properties. + * Each feature carries a numeric "speed" property (knots) for data-driven coloring. */ fun speedSegments(points: List): List = (0 until points.size - 1).map { i -> @@ -27,12 +19,20 @@ fun speedSegments(points: List): List = )) ).apply { addNumberProperty("speed", sog) - addStringProperty("color", speedToHex(sog)) } } /** - * Returns the lineColor expression for speed-colored segments. - * Reads the pre-computed "color" string property from each feature. + * Returns a step expression mapping the "speed" numeric property to a color. + * Uses Expression.step() which reads the numeric property directly, avoiding + * the silent failure of Expression.get("color") for string properties on LineLayer + * in MapLibre Android 11.x. */ -fun speedColorExpression(): Expression = Expression.get("color") +fun speedColorExpression(): Expression = Expression.step( + Expression.get("speed"), + Expression.literal("#2196F3"), // < 4 kt: blue + Expression.stop(4, Expression.literal("#00BCD4")), // 4–7 kt: cyan + Expression.stop(7, Expression.literal("#4CAF50")), // 7–10 kt: green + Expression.stop(10, Expression.literal("#FFC107")), // 10–13 kt: amber + Expression.stop(13, Expression.literal("#F44336")) // 13+ kt: red +) 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 25e7b15..1d92e18 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 @@ -1,17 +1,31 @@ package org.terst.nav.track +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent import android.graphics.BitmapFactory +import android.net.Uri import android.os.Bundle +import android.provider.MediaStore import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Button +import android.widget.EditText +import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.FileProvider 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 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 @@ -28,8 +42,11 @@ import org.maplibre.geojson.LineString import org.maplibre.geojson.Point import org.terst.nav.NavApplication import org.terst.nav.R +import org.terst.nav.logbook.EntryType +import org.terst.nav.logbook.LogEntry import org.terst.nav.ui.MainViewModel import java.io.File +import java.io.FileOutputStream import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -53,6 +70,52 @@ class TrackDetailSheet : Fragment() { private lateinit var trackMapView: MapView private var trackMap: MapLibreMap? = null + // Note-adding state + private var pendingNotePhotoPath: String? = null + private var noteDialogPhotoFrame: FrameLayout? = null + private var noteDialogPhotoView: ImageView? = null + private var logEventAdapter: LogEventAdapter? = null + private var currentTrack: SavedTrack? = null + + private val noteCameraLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val path = pendingNotePhotoPath ?: return@registerForActivityResult + val bm = BitmapFactory.decodeFile(path) + if (bm != null) noteDialogPhotoView?.setImageBitmap(bm) + else noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path))) + noteDialogPhotoFrame?.visibility = View.VISIBLE + } else { + pendingNotePhotoPath?.let { File(it).delete() } + pendingNotePhotoPath = null + } + } + + private val noteGalleryLauncher = registerForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + if (uri != null) { + lifecycleScope.launch { + val path = withContext(Dispatchers.IO) { + runCatching { + val dir = NavApplication.logbookRepository.photoDir + val dest = File(dir, "log_${System.currentTimeMillis()}.jpg") + requireContext().contentResolver.openInputStream(uri)?.use { inp -> + FileOutputStream(dest).use { out -> inp.copyTo(out) } + } + dest.absolutePath + }.getOrNull() + } + if (path != null) { + pendingNotePhotoPath = path + noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path))) + noteDialogPhotoFrame?.visibility = View.VISIBLE + } + } + } + } + companion object { fun newInstance() = TrackDetailSheet() } @@ -65,6 +128,7 @@ class TrackDetailSheet : Fragment() { parentFragmentManager.popBackStack() return } + currentTrack = track view.findViewById(R.id.btn_back).setOnClickListener { parentFragmentManager.popBackStack() @@ -88,9 +152,14 @@ class TrackDetailSheet : Fragment() { val rv = view.findViewById(R.id.rv_log_entries) rv.layoutManager = LinearLayoutManager(requireContext()) rv.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) - rv.adapter = LogEventAdapter(events) { event -> + logEventAdapter = LogEventAdapter(events) { event -> trackMap?.easeCamera(CameraUpdateFactory.newLatLng(LatLng(event.lat, event.lon)), 300) } + rv.adapter = logEventAdapter + + view.findViewById(R.id.btn_add_note).setOnClickListener { + showAddNoteDialog(track) + } trackMapView = view.findViewById(R.id.track_map_view) trackMapView.onCreate(savedInstanceState) @@ -106,6 +175,67 @@ class TrackDetailSheet : Fragment() { } } + private fun showAddNoteDialog(track: SavedTrack) { + val dialogView = LayoutInflater.from(requireContext()) + .inflate(R.layout.dialog_add_track_note, null) + val etNote = dialogView.findViewById(R.id.et_note_text) + noteDialogPhotoFrame = dialogView.findViewById(R.id.frame_note_photo) + noteDialogPhotoView = dialogView.findViewById(R.id.iv_note_photo) + pendingNotePhotoPath = null + noteDialogPhotoFrame?.visibility = View.GONE + + dialogView.findViewById