summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/SavedTracksFragment.kt4
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt125
2 files changed, 87 insertions, 42 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 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<SavedTrackAdapter.VH>() {
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<MaterialButton>(R.id.btn_generate_report).setOnClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, org.terst.nav.tripreport.TripReportFragment())
.addToBackStack(null)
.commit()
}
- view.findViewById<MaterialButton>(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<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)
+ (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 = ""