From d6a2e71c155f62a03e1ab466198a256c96c13b3b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 08:38:33 +0000 Subject: Fix GPX file opening, share sheet visibility, and add SAF track storage Intent filter fixes: - Remove pathSuffix=".gpx" + scheme restriction from the application/octet-stream ACTION_VIEW filter; pathSuffix matches the URI path, not the filename, so it never fired against file manager content URIs (opaque paths like /provider/uuid). The existing display-name check in handleIncomingGpx() is the correct gate. - Add ACTION_SEND filter for application/octet-stream; Nav was invisible in share sheets because most file managers send this type for .gpx files. SAF-based track storage (API 29+): - Replace fragile SharedPrefs URI cache + OWNER_PACKAGE_NAME MediaStore query with Storage Access Framework as the primary write/read path. The user grants access to their tracks folder once via ACTION_OPEN_DOCUMENT_TREE; DocumentFile.listFiles() finds all GPX files without relying on MediaStore ownership (which Android clears on uninstall). After reinstall, a one-tap "Restore track access" button in Saved Tracks re-authorizes the same folder and all tracks immediately reappear. - MediaStore path (existing) is kept as fallback for tracks saved before this change - Add SafState enum (CONFIGURED / PERMISSION_LOST / NOT_CONFIGURED) in TrackStorage - TrackRepository exposes safState() and initSafDirectory() thin wrappers - MainViewModel exposes safState StateFlow and onSafDirectorySelected() - SavedTracksFragment observes safState and shows contextual setup / re-auth button - MainActivity registers safPickerLauncher and exposes launchSafPicker() - Add androidx.documentfile:documentfile:1.0.1 dependency https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .../src/main/res/layout/fragment_saved_tracks.xml | 29 +++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/fragment_saved_tracks.xml b/android-app/app/src/main/res/layout/fragment_saved_tracks.xml index 7485e59..a95b368 100644 --- a/android-app/app/src/main/res/layout/fragment_saved_tracks.xml +++ b/android-app/app/src/main/res/layout/fragment_saved_tracks.xml @@ -34,15 +34,32 @@ - + android:padding="32dp" + android:visibility="gone"> + + + + + + Date: Wed, 20 May 2026 09:33:31 +0000 Subject: Move Saved Tracks to Log tab; restore conditions sheet on back - hideOverlays() now resets bottomSheetBehavior to COLLAPSED/not-hideable, so the conditions sheet reappears correctly when returning from any overlay (previously it stayed hidden after navigating away via the track browser) - Remove btn_saved_tracks from the conditions sheet (layout + click handler) - Add btn_saved_tracks to the Log screen layout and wire it in VoiceLogFragment via MainActivity.showSavedTracks(); tracks live with the log, not with weather https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .../app/src/main/kotlin/org/terst/nav/MainActivity.kt | 16 ++++++---------- .../kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt | 6 ++++++ .../app/src/main/res/layout/fragment_voice_log.xml | 9 +++++++++ .../app/src/main/res/layout/layout_instruments_sheet.xml | 14 -------------- 4 files changed, 21 insertions(+), 24 deletions(-) (limited to 'android-app/app/src/main/res/layout') 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 a4453be..7ce417d 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 @@ -303,14 +303,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } - // "Saved Tracks" button in the instrument sheet - findViewById(R.id.btn_saved_tracks).setOnClickListener { - showOverlay(SavedTracksFragment()) - bottomSheetBehavior.isHideable = true - bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN - // Note: do NOT set selectedItemId here — it triggers the nav listener - // which calls hideOverlays(), immediately undoing showOverlay(). - } } /** Updates all static unit label TextViews to match current [unitPrefs]. */ @@ -435,13 +427,17 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { .commit() } + fun showSavedTracks() { + showOverlay(SavedTracksFragment()) + } + internal fun hideOverlays() { fragmentContainer.visibility = View.GONE fabRecordTrack.visibility = View.VISIBLE fabLayers.visibility = View.VISIBLE - // fabRecenter visibility is managed by the isFollowing observer; just restore alpha fabRecenter.alpha = 1f - // Re-apply cached conditions in case they arrived while the overlay was covering the sheet + bottomSheetBehavior.isHideable = false + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED lastConditions?.let { applyConditions(it) } } 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..105057c 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 @@ -23,9 +23,11 @@ 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.SavedTracksFragment import java.io.File import java.io.FileOutputStream import java.util.Locale @@ -124,6 +126,10 @@ class VoiceLogFragment : Fragment() { .commit() } + view.findViewById(R.id.btn_saved_tracks).setOnClickListener { + (requireActivity() as MainActivity).showSavedTracks() + } + viewLifecycleOwner.lifecycleScope.launch { viewModel.state.collect { state -> renderState(state) } } diff --git a/android-app/app/src/main/res/layout/fragment_voice_log.xml b/android-app/app/src/main/res/layout/fragment_voice_log.xml index c1275a6..16c3574 100644 --- a/android-app/app/src/main/res/layout/fragment_voice_log.xml +++ b/android-app/app/src/main/res/layout/fragment_voice_log.xml @@ -153,4 +153,13 @@ android:layout_height="60dp" android:text="GENERATE TRIP REPORT" /> + + diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index f8dc365..31c445e 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -339,19 +339,5 @@ - - -- cgit v1.2.3 From 6dc2b16da127dd0a68907c037e8219eaacc8806c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 09:40:20 +0000 Subject: Fix gap between wave animation and ocean conditions panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the spurious constraintBottom_toBottomOf="parent" from forecast_row: ConstraintLayout was vertically centering it between the wave and the sheet bottom (default bias 0.5) instead of placing it flush below the wave. Also aligned light-mode wave_sea_bottom (#074B68 → #0D2137) with the forecast_row background so the wave tail blends seamlessly in light theme. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- android-app/app/src/main/res/layout/layout_instruments_sheet.xml | 3 +-- android-app/app/src/main/res/values/colors.xml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'android-app/app/src/main/res/layout') diff --git a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml index 31c445e..5e58009 100644 --- a/android-app/app/src/main/res/layout/layout_instruments_sheet.xml +++ b/android-app/app/src/main/res/layout/layout_instruments_sheet.xml @@ -183,8 +183,7 @@ android:layout_marginEnd="-16dp" app:layout_constraintTop_toBottomOf="@id/wave_divider" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintEnd_toEndOf="parent"> #2B8FC4 #87C8DF #0D7A9A - #074B68 + #0D2137 #50FFFFFF #80FFFFFF -- cgit v1.2.3 From cdb3fa7c6167e108f46b4d411655f78a22e3bebe Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 09:52:18 +0000 Subject: Fix Recenter bleed-through; drop noisy speed events; track detail as bottom sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recenter button was reappearing over overlays because the isFollowing flow kept calling fadeIn(fabRecenter) even when an overlay was visible. Added overlayShowing flag — the flow skips FAB updates while an overlay is shown. hideOverlays() now correctly restores fabRecenter visibility based on the actual following state instead of just resetting alpha. Speed up/Slow down events fired on every 1.5 kt GPS jitter, flooding the log with noise. Removed the detection loop entirely — no reliable motor on/off signal is available from GPS alone. TrackDetailSheet converted from a full-screen Fragment to a BottomSheetDialogFragment: opens at half-screen height so the track route is visible on the map behind it, draggable to full height, 20% background dim. SavedTracksFragment now calls hideOverlays() then sheet.show() instead of fragment-replacing the overlay container. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 19 ++++++++---- .../org/terst/nav/track/SavedTracksFragment.kt | 7 ++--- .../kotlin/org/terst/nav/track/TrackDetailSheet.kt | 36 +++++++++------------- .../main/res/layout/layout_track_detail_sheet.xml | 7 +++++ 4 files changed, 36 insertions(+), 33 deletions(-) (limited to 'android-app/app/src/main/res/layout') 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 7ce417d..caabe40 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 @@ -114,6 +114,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private val viewModel: MainViewModel by viewModels() private var pendingServiceStart = false private var serviceStarted = false + private var overlayShowing = false override fun onResume() { super.onResume() @@ -418,6 +419,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } private fun showOverlay(fragment: androidx.fragment.app.Fragment) { + overlayShowing = true fragmentContainer.visibility = View.VISIBLE fabRecordTrack.visibility = View.GONE fabLayers.visibility = View.GONE @@ -432,9 +434,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } internal fun hideOverlays() { + overlayShowing = false fragmentContainer.visibility = View.GONE fabRecordTrack.visibility = View.VISIBLE fabLayers.visibility = View.VISIBLE + val following = mapHandler?.isFollowing?.value ?: true + fabRecenter.visibility = if (following) View.GONE else View.VISIBLE fabRecenter.alpha = 1f bottomSheetBehavior.isHideable = false bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED @@ -592,12 +597,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { lifecycleScope.launch { mapHandler!!.isFollowing.collect { following -> mapCrosshair.visibility = if (following) View.GONE else View.VISIBLE - if (following) { - fadeOut(fabRecenter, gone = true) - fadeIn(fabRecordTrack) - } else { - fadeOut(fabRecordTrack, gone = true) - fadeIn(fabRecenter) + if (!overlayShowing) { + if (following) { + fadeOut(fabRecenter, gone = true) + fadeIn(fabRecordTrack) + } else { + fadeOut(fabRecordTrack, gone = true) + fadeIn(fabRecenter) + } } } } 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 d8b9858..f992e1e 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 @@ -44,11 +44,8 @@ class SavedTracksFragment : Fragment() { val adapter = SavedTrackAdapter { track -> viewModel.selectTrack(track) - parentFragmentManager - .beginTransaction() - .replace(R.id.fragment_container, TrackDetailSheet.newInstance()) - .addToBackStack(null) - .commit() + (requireActivity() as MainActivity).hideOverlays() + TrackDetailSheet.newInstance().show(parentFragmentManager, "track_detail") } 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 c1764a8..627ce89 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,13 @@ 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.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.terst.nav.R import org.terst.nav.ui.MainViewModel import java.time.Instant @@ -28,7 +30,7 @@ data class LogEvent( val detail: String ) -class TrackDetailSheet : Fragment() { +class TrackDetailSheet : BottomSheetDialogFragment() { private val viewModel: MainViewModel by activityViewModels() @@ -41,13 +43,11 @@ class TrackDetailSheet : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val track = viewModel.selectedTrack.value ?: run { - parentFragmentManager.popBackStack() + dismiss() return } - view.findViewById(R.id.btn_back).setOnClickListener { - parentFragmentManager.popBackStack() - } + view.findViewById(R.id.btn_back).setOnClickListener { dismiss() } val titleFmt = DateTimeFormatter.ofPattern("dd MMM yyyy · HH:mm", Locale.getDefault()) .withZone(ZoneId.systemDefault()) @@ -70,6 +70,14 @@ class TrackDetailSheet : Fragment() { rv.adapter = LogEventAdapter(events) { event -> viewModel.panToTrackPoint(event.lat, event.lon) } + + (dialog as? BottomSheetDialog)?.behavior?.apply { + isFitToContents = false + halfExpandedRatio = 0.5f + skipCollapsed = true + state = BottomSheetBehavior.STATE_HALF_EXPANDED + } + dialog?.window?.setDimAmount(0.2f) } override fun onDestroyView() { @@ -96,22 +104,6 @@ class TrackDetailSheet : Fragment() { "%.0f° → %.0f° (Δ%.0f°)".format(t.cogBefore, t.cogAfter, abs(delta))) } - for (i in 1 until points.size - 1) { - val prev = points[i - 1].sogKnots - val curr = points[i].sogKnots - val next = points[i + 1].sogKnots - val rise = curr - prev - val fall = curr - next - val p = points[i] - if (rise > 1.5 && rise > fall) { - events += LogEvent(p.lat, p.lon, p.timestampMs, "↑", - "Speed up", "%.1f → %.1f kt".format(prev, curr)) - } else if (fall > 1.5 && fall > rise) { - events += LogEvent(p.lat, p.lon, p.timestampMs, "↓", - "Slow down", "%.1f → %.1f kt".format(curr, next)) - } - } - points.lastOrNull()?.let { p -> events += LogEvent(p.lat, p.lon, p.timestampMs, "🏁", "End", formatPointDetail(p)) } 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..97b1da8 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,6 +6,13 @@ android:orientation="vertical" android:background="?attr/colorSurface"> + + + Date: Wed, 20 May 2026 10:06:14 +0000 Subject: Inline saved tracks on Log tab; collapse log entry when not recording Removed the 'Saved Tracks' button that opened a separate overlay. Tracks now appear directly on the Log screen in a RecyclerView below the Generate Trip Report button. Tapping a track hides the overlay and opens the existing TrackDetailSheet bottom sheet over the map. The log entry section (text field, mic, camera, save/clear) is hidden with visibility=gone when not recording and shown when a recording is in progress. The storage-setup prompt is now surfaced here too. SavedTrackAdapter and DATE_FMT widened to internal so VoiceLogFragment can reuse them directly. SavedTracksFragment kept for the GPX import flow. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --- .../org/terst/nav/track/SavedTracksFragment.kt | 4 +- .../org/terst/nav/ui/voicelog/VoiceLogFragment.kt | 125 +++++--- .../app/src/main/res/layout/fragment_voice_log.xml | 350 ++++++++++++--------- 3 files changed, 290 insertions(+), 189 deletions(-) (limited to 'android-app/app/src/main/res/layout') 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 f992e1e..44055ce 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 @@ -86,11 +86,11 @@ class SavedTracksFragment : Fragment() { } } -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() { 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 105057c..cf6805a 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,7 +19,11 @@ 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 @@ -27,17 +32,22 @@ 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.SavedTracksFragment +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 @@ -50,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? -> @@ -71,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?, @@ -92,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() @@ -111,27 +111,75 @@ 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(R.id.btn_generate_report).setOnClickListener { parentFragmentManager.beginTransaction() .replace(R.id.fragment_container, org.terst.nav.tripreport.TripReportFragment()) .addToBackStack(null) .commit() } - view.findViewById(R.id.btn_saved_tracks).setOnClickListener { - (requireActivity() as MainActivity).showSavedTracks() + // Show log-entry section only while recording + viewLifecycleOwner.lifecycleScope.launch { + mainViewModel.isRecording.collect { recording -> + layoutLogEntry.visibility = if (recording) View.VISIBLE else View.GONE + } + } + + // Inline track list + val rv = view.findViewById(R.id.rv_saved_tracks) + val layoutEmpty = view.findViewById(R.id.layout_empty) + val tvEmpty = view.findViewById(R.id.tv_empty) + val btnSetup = view.findViewById(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) + (requireActivity() as MainActivity).hideOverlays() + TrackDetailSheet.newInstance().show(parentFragmentManager, "track_detail") + } + 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 { - viewModel.state.collect { state -> renderState(state) } + logViewModel.state.collect { state -> renderState(state) } } } @@ -145,16 +193,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() {} @@ -197,7 +243,7 @@ class VoiceLogFragment : Fragment() { private fun clearEntry() { etNote.setText("") clearPhoto() - viewModel.retry() + logViewModel.retry() tvSavedConfirmation.text = "" tvStatus.text = "" } @@ -215,7 +261,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/fragment_voice_log.xml b/android-app/app/src/main/res/layout/fragment_voice_log.xml index 16c3574..389add1 100644 --- a/android-app/app/src/main/res/layout/fragment_voice_log.xml +++ b/android-app/app/src/main/res/layout/fragment_voice_log.xml @@ -1,165 +1,221 @@ - + android:layout_height="match_parent"> - - - - - - - - - - + android:orientation="vertical" + android:padding="20dp"> - - - + - + android:orientation="vertical" + android:visibility="gone"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:id="@+id/btn_generate_report" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="match_parent" + android:layout_height="60dp" + android:text="GENERATE TRIP REPORT" + android:layout_marginBottom="24dp" /> + - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + android:nestedScrollingEnabled="false" + android:clipToPadding="false" + android:paddingBottom="80dp" /> - - - - - - - - - - + -- cgit v1.2.3