diff options
| author | Claude <noreply@anthropic.com> | 2026-04-22 08:33:41 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-04-22 08:33:41 +0000 |
| commit | 447c274999b6b412985b640f841ab92d8c23c0f8 (patch) | |
| tree | 68ba329f0cadd64333fb5d72587f684fc38a500a /android-app | |
| parent | 3cddcd3b0a8719da5e828734a1d3054cef803235 (diff) | |
| parent | eeafe2f0f3a3718bd52e9c333894e1abbc0611ab (diff) | |
Merge branch 'claude/vessel-crew-thermal-features-Qbk4z'
Vessel registry, crew management, and thermal alarm.
All 18 new VesselRepositoryTest cases GREEN (59 total across suite).
https://claude.ai/code/session_011h2dXbgXg3PesQMmQUNTCW
Diffstat (limited to 'android-app')
10 files changed, 705 insertions, 1 deletions
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<ThermalReading> = 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> = _thermalState.asStateFlow() + + private val _thermalReading = MutableStateFlow<ThermalReading?>(null) + val thermalReading: StateFlow<ThermalReading?> = _thermalReading.asStateFlow() + + fun onThermalReading(reading: ThermalReading) { + _thermalReading.value = reading + _thermalState.value = reading.state + } + private val _isRecording = MutableStateFlow(false) val isRecording: StateFlow<Boolean> = _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<MaterialButton>(R.id.button_edit_own_vessel).setOnClickListener { + showEditVesselDialog(repo.getOwnVessel()) { vessel -> + repo.addVessel(vessel.copy(isOwnVessel = true)) + refreshOwnVessel() + } + } + + view.findViewById<MaterialButton>(R.id.button_add_crew).setOnClickListener { + showEditCrewDialog(null) { member -> + repo.addCrewMember(member) + refreshCrew() + } + } + + view.findViewById<MaterialButton>(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<TextView>(android.R.id.text1).text = + "${member.name} · ${member.role.name.replace('_', ' ')}" + card.findViewById<TextView>(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<TextView>(android.R.id.text1).text = + "${vessel.name}${if (vessel.mmsi.isNotBlank()) " · MMSI ${vessel.mmsi}" else ""}" + row.findViewById<TextView>(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<String> = 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<Vessel>() + private val crew = mutableListOf<CrewMember>() + + // --- 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<Vessel> = 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<CrewMember> = crew.toList() + + fun getCrewByRole(role: CrewRole): List<CrewMember> = 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Simple sailboat silhouette icon --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <!-- Hull --> + <path + android:fillColor="@android:color/white" + android:pathData="M3,17 L5,20 L19,20 L21,17 Z" /> + <!-- Mast --> + <path + android:fillColor="@android:color/white" + android:pathData="M12,4 L12,17 M11.5,4 L11.5,17" + android:strokeColor="@android:color/white" + android:strokeWidth="1.5" /> + <!-- Main sail --> + <path + android:fillColor="@android:color/white" + android:pathData="M12,5 L18,15 L12,15 Z" /> + <!-- Jib --> + <path + android:fillColor="@android:color/white" + android:pathData="M12,7 L7,15 L12,15 Z" /> +</vector> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.core.widget.NestedScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/colorSurface"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="24dp"> + + <!-- Title row --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Vessel Registry" + android:textSize="24sp" + android:textStyle="bold" + android:textColor="?attr/colorOnSurface" /> + + </LinearLayout> + + <!-- OWN VESSEL card --> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:layout_marginBottom="8dp" + android:text="OWN VESSEL" + android:textSize="12sp" + android:textStyle="bold" + android:letterSpacing="0.1" + android:textColor="?attr/colorPrimary" /> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/card_own_vessel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="12dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/tv_own_vessel_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="No vessel configured" + android:textSize="18sp" + android:textStyle="bold" + android:textColor="?attr/colorOnSurface" /> + + <TextView + android:id="@+id/tv_own_vessel_details" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:text="" + android:textSize="14sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_edit_own_vessel" + style="@style/Widget.Material3.Button.TonalButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:text="EDIT VESSEL" /> + + </LinearLayout> + + </com.google.android.material.card.MaterialCardView> + + <!-- CREW section --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginTop="24dp" + android:layout_marginBottom="8dp"> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="CREW" + android:textSize="12sp" + android:textStyle="bold" + android:letterSpacing="0.1" + android:textColor="?attr/colorPrimary" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_add_crew" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="+ ADD" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/crew_list_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + + <TextView + android:id="@+id/tv_crew_empty" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="No crew members added yet." + android:textSize="14sp" + android:textColor="?attr/colorOnSurfaceVariant" + android:visibility="visible" /> + + <!-- FLEET section --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginTop="24dp" + android:layout_marginBottom="8dp"> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="KNOWN VESSELS" + android:textSize="12sp" + android:textStyle="bold" + android:letterSpacing="0.1" + android:textColor="?attr/colorPrimary" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_add_vessel" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="+ ADD" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/vessel_list_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + + <TextView + android:id="@+id/tv_vessel_empty" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="No additional vessels recorded." + android:textSize="14sp" + android:textColor="?attr/colorOnSurfaceVariant" + android:visibility="visible" /> + + </LinearLayout> + +</androidx.core.widget.NestedScrollView> 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" /> + <item + android:id="@+id/nav_vessel" + android:icon="@drawable/ic_vessel" + android:title="Vessel" /> </menu> |
