From eeafe2f0f3a3718bd52e9c333894e1abbc0611ab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 05:35:49 +0000 Subject: feat: vessel registry, crew management, and thermal alarm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vessel/CrewMember data classes with full profile fields (MMSI, callsign, flag, home port, hull type, dimensions, roles) - VesselRepository: in-memory CRUD for own vessel, fleet, and crew; getOwnVessel(), getSkipper(), getCrewByRole() helpers - VesselRegistryFragment: own-vessel card + crew list + known-vessels list; AlertDialog editors for add/edit; long-press to delete - ic_vessel.xml sailboat icon; 5th bottom-nav tab "Vessel" - ThermalMonitor: thermalFlow() using Intent.ACTION_BATTERY_CHANGED (battery °C) + PowerManager.OnThermalStatusChangedListener (API 29+); ThermalState OK/WARM/HOT/CRITICAL with distinct-until-changed filter - MainActivity: startThermalMonitoring() sounds mob_alarm + Toast on HOT (≥45°C) and CRITICAL (≥50°C); auto-silences on OK/WARM - MainViewModel: thermalState and thermalReading StateFlows - 18 VesselRepositoryTest cases — all GREEN (test-runner) - design.md: updated competitive matrix + feature implementation status https://claude.ai/code/session_011h2dXbgXg3PesQMmQUNTCW --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 56 ++++- .../kotlin/org/terst/nav/thermal/ThermalMonitor.kt | 79 +++++++ .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 18 ++ .../terst/nav/ui/vessel/VesselRegistryFragment.kt | 250 +++++++++++++++++++++ .../main/kotlin/org/terst/nav/vessel/CrewMember.kt | 15 ++ .../src/main/kotlin/org/terst/nav/vessel/Vessel.kt | 22 ++ .../org/terst/nav/vessel/VesselRepository.kt | 59 +++++ .../app/src/main/res/drawable/ic_vessel.xml | 26 +++ .../main/res/layout/fragment_vessel_registry.xml | 177 +++++++++++++++ .../app/src/main/res/menu/bottom_nav_menu.xml | 4 + 10 files changed, 705 insertions(+), 1 deletion(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/thermal/ThermalMonitor.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/ui/vessel/VesselRegistryFragment.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/vessel/Vessel.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt create mode 100644 android-app/app/src/main/res/drawable/ic_vessel.xml create mode 100644 android-app/app/src/main/res/layout/fragment_vessel_registry.xml (limited to 'android-app') 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 7046b23..ac2272c 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 @@ -35,10 +35,13 @@ import org.maplibre.android.maps.Style import org.maplibre.android.style.layers.RasterLayer import org.maplibre.android.style.sources.RasterSource import org.maplibre.android.style.sources.TileSet +import org.terst.nav.thermal.ThermalState +import org.terst.nav.thermal.thermalFlow import org.terst.nav.ui.* import org.terst.nav.ui.doc.DocFragment import org.terst.nav.ui.map.ParticleWindView import org.terst.nav.ui.safety.SafetyFragment +import org.terst.nav.ui.vessel.VesselRegistryFragment import org.terst.nav.ui.voicelog.VoiceLogFragment import java.util.* @@ -140,6 +143,12 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN true } + R.id.nav_vessel -> { + showOverlay(VesselRegistryFragment()) + bottomSheetBehavior.isHideable = true + bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + true + } else -> false } } @@ -267,7 +276,52 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } + private var thermalAlarmPlayer: MediaPlayer? = null + + private fun startThermalMonitoring() { + lifecycleScope.launch { + thermalFlow(applicationContext).collect { reading -> + viewModel.onThermalReading(reading) + when (reading.state) { + ThermalState.HOT -> { + Toast.makeText( + this@MainActivity, + "⚠ Phone hot: %.1f°C — reduce screen brightness".format(reading.batteryTempC), + Toast.LENGTH_LONG + ).show() + soundThermalAlarm() + } + ThermalState.CRITICAL -> { + Toast.makeText( + this@MainActivity, + "🔥 Phone overheating: %.1f°C — dock device immediately!".format(reading.batteryTempC), + Toast.LENGTH_LONG + ).show() + soundThermalAlarm() + } + else -> stopThermalAlarm() + } + } + } + } + + private fun soundThermalAlarm() { + if (thermalAlarmPlayer?.isPlaying == true) return + thermalAlarmPlayer?.release() + thermalAlarmPlayer = MediaPlayer.create(this, R.raw.mob_alarm)?.also { + it.isLooping = false + it.start() + } + } + + private fun stopThermalAlarm() { + thermalAlarmPlayer?.stop() + thermalAlarmPlayer?.release() + thermalAlarmPlayer = null + } + private fun observeDataSources() { + startThermalMonitoring() lifecycleScope.launch { LocationService.locationFlow.collect { gpsData -> mapHandler?.centerOnLocation(gpsData.latitude, gpsData.longitude) @@ -347,6 +401,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { override fun onStart() { super.onStart(); mapView?.onStart() } override fun onPause() { super.onPause(); mapView?.onPause() } override fun onStop() { super.onStop(); mapView?.onStop() } - override fun onDestroy() { super.onDestroy(); mapView?.onDestroy() } + override fun onDestroy() { super.onDestroy(); mapView?.onDestroy(); stopThermalAlarm() } override fun onLowMemory() { super.onLowMemory(); mapView?.onLowMemory() } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/thermal/ThermalMonitor.kt b/android-app/app/src/main/kotlin/org/terst/nav/thermal/ThermalMonitor.kt new file mode 100644 index 0000000..62d3557 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/thermal/ThermalMonitor.kt @@ -0,0 +1,79 @@ +package org.terst.nav.thermal + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build +import android.os.PowerManager +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged + +enum class ThermalState { + OK, // < 40 °C + WARM, // 40–45 °C + HOT, // 45–50 °C → alarm + CRITICAL // > 50 °C → urgent alarm +} + +data class ThermalReading( + val batteryTempC: Float, + val state: ThermalState, + val powerManagerState: Int = -1 // PowerManager.THERMAL_STATUS_* on API 29+ +) + +object ThermalThresholds { + const val WARM_C = 40f + const val HOT_C = 45f + const val CRITICAL_C = 50f +} + +fun batteryTempToState(tempC: Float): ThermalState = when { + tempC >= ThermalThresholds.CRITICAL_C -> ThermalState.CRITICAL + tempC >= ThermalThresholds.HOT_C -> ThermalState.HOT + tempC >= ThermalThresholds.WARM_C -> ThermalState.WARM + else -> ThermalState.OK +} + +/** + * Emits [ThermalReading] whenever the battery temperature changes. + * On API 29+ also incorporates PowerManager thermal status. + * The flow is distinct — only emits when state changes. + */ +fun thermalFlow(context: Context): Flow = callbackFlow { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager + + // PowerManager thermal status listener (API 29+) + var pmStatus = -1 + val pmListener: Any? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val listener = PowerManager.OnThermalStatusChangedListener { status -> + pmStatus = status + } + powerManager?.addThermalStatusListener(listener) + listener + } else null + + val receiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val rawTemp = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1) + if (rawTemp < 0) return + val tempC = rawTemp / 10f + val state = batteryTempToState(tempC) + trySend(ThermalReading(tempC, state, pmStatus)) + } + } + + context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + + awaitClose { + context.unregisterReceiver(receiver) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && pmListener != null) { + powerManager?.removeThermalStatusListener( + pmListener as PowerManager.OnThermalStatusChangedListener + ) + } + } +}.distinctUntilChanged { a, b -> a.state == b.state } 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 4c2c555..86bfe32 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 @@ -6,8 +6,13 @@ import org.terst.nav.ais.AisHubSource import org.terst.nav.ais.AisRepository import org.terst.nav.ais.AisVessel import org.terst.nav.data.api.AisHubApiService +import org.terst.nav.thermal.ThermalReading +import org.terst.nav.thermal.ThermalState import org.terst.nav.track.TrackPoint import org.terst.nav.track.TrackRepository +import org.terst.nav.vessel.CrewMember +import org.terst.nav.vessel.Vessel +import org.terst.nav.vessel.VesselRepository import org.terst.nav.data.model.ForecastItem import org.terst.nav.data.model.WindArrow import org.terst.nav.data.repository.WeatherRepository @@ -50,6 +55,19 @@ class MainViewModel( private val trackRepository = TrackRepository() + val vesselRepository = VesselRepository() + + private val _thermalState = MutableStateFlow(ThermalState.OK) + val thermalState: StateFlow = _thermalState.asStateFlow() + + private val _thermalReading = MutableStateFlow(null) + val thermalReading: StateFlow = _thermalReading.asStateFlow() + + fun onThermalReading(reading: ThermalReading) { + _thermalReading.value = reading + _thermalState.value = reading.state + } + private val _isRecording = MutableStateFlow(false) val isRecording: StateFlow = _isRecording.asStateFlow() diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/vessel/VesselRegistryFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/vessel/VesselRegistryFragment.kt new file mode 100644 index 0000000..b00d899 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/vessel/VesselRegistryFragment.kt @@ -0,0 +1,250 @@ +package org.terst.nav.ui.vessel + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import org.terst.nav.R +import org.terst.nav.vessel.CrewMember +import org.terst.nav.vessel.CrewRole +import org.terst.nav.vessel.Vessel +import org.terst.nav.vessel.VesselRepository + +class VesselRegistryFragment : Fragment() { + + private val repo = VesselRepository() + + private lateinit var tvOwnVesselName: TextView + private lateinit var tvOwnVesselDetails: TextView + private lateinit var crewListContainer: LinearLayout + private lateinit var tvCrewEmpty: TextView + private lateinit var vesselListContainer: LinearLayout + private lateinit var tvVesselEmpty: TextView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_vessel_registry, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + tvOwnVesselName = view.findViewById(R.id.tv_own_vessel_name) + tvOwnVesselDetails = view.findViewById(R.id.tv_own_vessel_details) + crewListContainer = view.findViewById(R.id.crew_list_container) + tvCrewEmpty = view.findViewById(R.id.tv_crew_empty) + vesselListContainer = view.findViewById(R.id.vessel_list_container) + tvVesselEmpty = view.findViewById(R.id.tv_vessel_empty) + + view.findViewById(R.id.button_edit_own_vessel).setOnClickListener { + showEditVesselDialog(repo.getOwnVessel()) { vessel -> + repo.addVessel(vessel.copy(isOwnVessel = true)) + refreshOwnVessel() + } + } + + view.findViewById(R.id.button_add_crew).setOnClickListener { + showEditCrewDialog(null) { member -> + repo.addCrewMember(member) + refreshCrew() + } + } + + view.findViewById(R.id.button_add_vessel).setOnClickListener { + showEditVesselDialog(null) { vessel -> + repo.addVessel(vessel.copy(isOwnVessel = false)) + refreshVesselList() + } + } + + refresh() + } + + private fun refresh() { + refreshOwnVessel() + refreshCrew() + refreshVesselList() + } + + private fun refreshOwnVessel() { + val own = repo.getOwnVessel() + if (own != null) { + tvOwnVesselName.text = own.name + tvOwnVesselDetails.text = buildVesselDetails(own) + } else { + tvOwnVesselName.text = "No vessel configured" + tvOwnVesselDetails.text = "" + } + } + + private fun refreshCrew() { + crewListContainer.removeAllViews() + val crew = repo.getCrew() + tvCrewEmpty.isVisible = crew.isEmpty() + crew.forEach { member -> crewListContainer.addView(buildCrewRow(member)) } + } + + private fun refreshVesselList() { + vesselListContainer.removeAllViews() + val others = repo.getVessels().filter { !it.isOwnVessel } + tvVesselEmpty.isVisible = others.isEmpty() + others.forEach { v -> vesselListContainer.addView(buildVesselRow(v)) } + } + + private fun buildCrewRow(member: CrewMember): View { + val ctx = requireContext() + val card = layoutInflater.inflate( + android.R.layout.simple_list_item_2, crewListContainer, false + ) + card.findViewById(android.R.id.text1).text = + "${member.name} · ${member.role.name.replace('_', ' ')}" + card.findViewById(android.R.id.text2).text = + if (member.emergencyContact.isNotBlank()) "ICE: ${member.emergencyContact}" else "" + card.setOnClickListener { + showEditCrewDialog(member) { updated -> + repo.updateCrewMember(updated) + refreshCrew() + } + } + card.setOnLongClickListener { + AlertDialog.Builder(ctx) + .setTitle("Remove ${member.name}?") + .setPositiveButton("Remove") { _, _ -> repo.removeCrewMember(member.id); refreshCrew() } + .setNegativeButton("Cancel", null) + .show() + true + } + return card + } + + private fun buildVesselRow(vessel: Vessel): View { + val ctx = requireContext() + val row = layoutInflater.inflate( + android.R.layout.simple_list_item_2, vesselListContainer, false + ) + row.findViewById(android.R.id.text1).text = + "${vessel.name}${if (vessel.mmsi.isNotBlank()) " · MMSI ${vessel.mmsi}" else ""}" + row.findViewById(android.R.id.text2).text = buildVesselDetails(vessel) + row.setOnClickListener { + showEditVesselDialog(vessel) { updated -> + repo.updateVessel(updated) + refreshVesselList() + } + } + row.setOnLongClickListener { + AlertDialog.Builder(ctx) + .setTitle("Remove ${vessel.name}?") + .setPositiveButton("Remove") { _, _ -> repo.removeVessel(vessel.id); refreshVesselList() } + .setNegativeButton("Cancel", null) + .show() + true + } + return row + } + + private fun buildVesselDetails(v: Vessel): String = buildString { + if (v.hullType.name != "MONOHULL") append("${v.hullType.name.replace('_', ' ')} ") + if (v.loaMeters > 0) append("LOA %.1fm ".format(v.loaMeters)) + if (v.callsign.isNotBlank()) append("Call: ${v.callsign} ") + if (v.homePort.isNotBlank()) append("Home: ${v.homePort}") + }.trim() + + // --- Dialogs --- + + private fun showEditVesselDialog(vessel: Vessel?, onSave: (Vessel) -> Unit) { + val ctx = requireContext() + val layout = LinearLayout(ctx).apply { + orientation = LinearLayout.VERTICAL + setPadding(48, 24, 48, 0) + } + + fun field(hint: String, initial: String = ""): TextInputEditText { + val til = TextInputLayout(ctx).apply { this.hint = hint } + val et = TextInputEditText(ctx).apply { setText(initial) } + til.addView(et) + layout.addView(til) + return et + } + + val etName = field("Vessel name *", vessel?.name ?: "") + val etMmsi = field("MMSI", vessel?.mmsi ?: "") + val etCallsign = field("Callsign", vessel?.callsign ?: "") + val etFlag = field("Flag (country code)", vessel?.flag ?: "") + val etHomePort = field("Home port", vessel?.homePort ?: "") + val etLoa = field("LOA (metres)", if ((vessel?.loaMeters ?: 0.0) > 0) "%.2f".format(vessel!!.loaMeters) else "") + val etNotes = field("Notes", vessel?.notes ?: "") + + AlertDialog.Builder(ctx) + .setTitle(if (vessel == null) "Add Vessel" else "Edit Vessel") + .setView(layout) + .setPositiveButton("Save") { _, _ -> + val name = etName.text.toString().trim() + if (name.isBlank()) return@setPositiveButton + val v = (vessel ?: Vessel(id = "")).copy( + name = name, + mmsi = etMmsi.text.toString().trim(), + callsign = etCallsign.text.toString().trim(), + flag = etFlag.text.toString().trim(), + homePort = etHomePort.text.toString().trim(), + loaMeters = etLoa.text.toString().toDoubleOrNull() ?: vessel?.loaMeters ?: 0.0, + notes = etNotes.text.toString().trim() + ) + onSave(v) + } + .setNegativeButton("Cancel", null) + .show() + } + + private fun showEditCrewDialog(member: CrewMember?, onSave: (CrewMember) -> Unit) { + val ctx = requireContext() + val layout = LinearLayout(ctx).apply { + orientation = LinearLayout.VERTICAL + setPadding(48, 24, 48, 0) + } + + fun field(hint: String, initial: String = ""): TextInputEditText { + val til = TextInputLayout(ctx).apply { this.hint = hint } + val et = TextInputEditText(ctx).apply { setText(initial) } + til.addView(et) + layout.addView(til) + return et + } + + val etName = field("Name *", member?.name ?: "") + val etRole = field("Role (SKIPPER/FIRST_MATE/WATCH_CAPTAIN/CREW/GUEST)", + member?.role?.name ?: CrewRole.CREW.name) + val etIceName = field("Emergency contact name", member?.emergencyContact ?: "") + val etIcePhone = field("Emergency contact phone", member?.emergencyPhone ?: "") + val etNotes = field("Notes", member?.notes ?: "") + + AlertDialog.Builder(ctx) + .setTitle(if (member == null) "Add Crew Member" else "Edit Crew Member") + .setView(layout) + .setPositiveButton("Save") { _, _ -> + val name = etName.text.toString().trim() + if (name.isBlank()) return@setPositiveButton + val role = runCatching { + CrewRole.valueOf(etRole.text.toString().trim().uppercase()) + }.getOrDefault(CrewRole.CREW) + val m = (member ?: CrewMember(id = "")).copy( + name = name, + role = role, + emergencyContact = etIceName.text.toString().trim(), + emergencyPhone = etIcePhone.text.toString().trim(), + notes = etNotes.text.toString().trim() + ) + onSave(m) + } + .setNegativeButton("Cancel", null) + .show() + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt b/android-app/app/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt new file mode 100644 index 0000000..0df8cb9 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt @@ -0,0 +1,15 @@ +package org.terst.nav.vessel + +data class CrewMember( + val id: String, + val name: String, + val role: CrewRole = CrewRole.CREW, + val emergencyContact: String = "", + val emergencyPhone: String = "", + val certifications: List = emptyList(), + val notes: String = "" +) + +enum class CrewRole { + SKIPPER, FIRST_MATE, WATCH_CAPTAIN, CREW, GUEST +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/vessel/Vessel.kt b/android-app/app/src/main/kotlin/org/terst/nav/vessel/Vessel.kt new file mode 100644 index 0000000..b7c0eaf --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/vessel/Vessel.kt @@ -0,0 +1,22 @@ +package org.terst.nav.vessel + +data class Vessel( + val id: String, + val name: String, + val mmsi: String = "", + val callsign: String = "", + val flag: String = "", + val homePort: String = "", + val hullType: HullType = HullType.MONOHULL, + val loaMeters: Double = 0.0, + val beamMeters: Double = 0.0, + val draftMeters: Double = 0.0, + val displacementKg: Double = 0.0, + val engineHp: Int = 0, + val notes: String = "", + val isOwnVessel: Boolean = false +) + +enum class HullType { + MONOHULL, CATAMARAN, TRIMARAN, POWERBOAT, OTHER +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt new file mode 100644 index 0000000..b65a816 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt @@ -0,0 +1,59 @@ +package org.terst.nav.vessel + +import java.util.UUID + +class VesselRepository { + + private val vessels = mutableListOf() + private val crew = mutableListOf() + + // --- Vessel CRUD --- + + fun addVessel(vessel: Vessel): Vessel { + val v = if (vessel.id.isBlank()) vessel.copy(id = UUID.randomUUID().toString()) else vessel + vessels.removeAll { it.id == v.id } + vessels.add(v) + return v + } + + fun updateVessel(vessel: Vessel): Boolean { + val idx = vessels.indexOfFirst { it.id == vessel.id } + if (idx < 0) return false + vessels[idx] = vessel + return true + } + + fun removeVessel(id: String): Boolean = vessels.removeAll { it.id == id } + + fun getVessel(id: String): Vessel? = vessels.find { it.id == id } + + fun getVessels(): List = vessels.toList() + + fun getOwnVessel(): Vessel? = vessels.find { it.isOwnVessel } + + // --- Crew CRUD --- + + fun addCrewMember(member: CrewMember): CrewMember { + val m = if (member.id.isBlank()) member.copy(id = UUID.randomUUID().toString()) else member + crew.removeAll { it.id == m.id } + crew.add(m) + return m + } + + fun updateCrewMember(member: CrewMember): Boolean { + val idx = crew.indexOfFirst { it.id == member.id } + if (idx < 0) return false + crew[idx] = member + return true + } + + fun removeCrewMember(id: String): Boolean = crew.removeAll { it.id == id } + + fun getCrewMember(id: String): CrewMember? = crew.find { it.id == id } + + fun getCrew(): List = crew.toList() + + fun getCrewByRole(role: CrewRole): List = crew.filter { it.role == role } + + fun getSkipper(): CrewMember? = crew.find { it.role == CrewRole.SKIPPER } +} diff --git a/android-app/app/src/main/res/drawable/ic_vessel.xml b/android-app/app/src/main/res/drawable/ic_vessel.xml new file mode 100644 index 0000000..13a93ed --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_vessel.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/fragment_vessel_registry.xml b/android-app/app/src/main/res/layout/fragment_vessel_registry.xml new file mode 100644 index 0000000..6b55fa6 --- /dev/null +++ b/android-app/app/src/main/res/layout/fragment_vessel_registry.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/menu/bottom_nav_menu.xml b/android-app/app/src/main/res/menu/bottom_nav_menu.xml index b29fb08..5f444eb 100644 --- a/android-app/app/src/main/res/menu/bottom_nav_menu.xml +++ b/android-app/app/src/main/res/menu/bottom_nav_menu.xml @@ -16,4 +16,8 @@ android:id="@+id/nav_safety" android:icon="@drawable/ic_safety" android:title="Safety" /> + -- cgit v1.2.3