From 1f9f0ec3f7ce986134ed75836fc42e43582aa34f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 05:32:36 +0000 Subject: Add pirate mode Easter egg to trip report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-press the "Trip Report" header flips the narrative into pirate dialect (☠ VOYAGE header, "Made X nm by the log", "Avast!" log-entry prefixes, closing "Fair winds and following seas. Arr."). Long-press again returns to standard nav voice. Seamanlike output remains the default; pirate mode is purely opt-in. https://claude.ai/code/session_01KXYBuAHZkvv63DeUG6XaZD --- .../org/terst/nav/tripreport/TripReportFragment.kt | 17 +++++- .../terst/nav/tripreport/TripReportGenerator.kt | 60 +++++++++++++++++++++- .../src/main/res/layout/fragment_trip_report.xml | 4 +- 3 files changed, 78 insertions(+), 3 deletions(-) (limited to 'android-app') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt index 632f616..d567ed6 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportFragment.kt @@ -22,10 +22,14 @@ class TripReportFragment : Fragment() { ) } + private lateinit var tvReportTitle: TextView private lateinit var tvNarrativeContent: TextView private lateinit var btnRefresh: MaterialButton private lateinit var progress: ProgressBar + private var pirateMode = false + private val generator = TripReportGenerator() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -35,12 +39,23 @@ class TripReportFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + tvReportTitle = view.findViewById(R.id.tv_report_title) tvNarrativeContent = view.findViewById(R.id.tv_narrative_content) btnRefresh = view.findViewById(R.id.btn_refresh_report) progress = view.findViewById(R.id.progress_report) btnRefresh.setOnClickListener { viewModel.generateReport() } + tvReportTitle.setOnLongClickListener { + pirateMode = !pirateMode + tvReportTitle.text = if (pirateMode) "☠ Trip Report ☠" else "Trip Report" + val s = viewModel.state.value + if (s is TripReportState.Success) { + tvNarrativeContent.text = generator.generateNarrative(s.summary, pirate = pirateMode) + } + true + } + viewLifecycleOwner.lifecycleScope.launch { viewModel.state.collect { state -> renderState(state) } } @@ -60,7 +75,7 @@ class TripReportFragment : Fragment() { is TripReportState.Success -> { progress.visibility = View.GONE btnRefresh.isEnabled = true - tvNarrativeContent.text = state.narrative + tvNarrativeContent.text = generator.generateNarrative(state.summary, pirate = pirateMode) } is TripReportState.Error -> { progress.visibility = View.GONE diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt index 2c7f77f..a101152 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/TripReportGenerator.kt @@ -73,7 +73,10 @@ class TripReportGenerator { return r * c } - fun generateNarrative(summary: TripSummary): String { + fun generateNarrative(summary: TripSummary, pirate: Boolean = false): String = + if (pirate) generatePirateNarrative(summary) else generateStandardNarrative(summary) + + private fun generateStandardNarrative(summary: TripSummary): String { val zone = ZoneId.systemDefault() val dateFmt = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.getDefault()).withZone(zone) val timeFmt = DateTimeFormatter.ofPattern("HHmm", Locale.getDefault()).withZone(zone) @@ -120,4 +123,59 @@ class TripReportGenerator { return sb.toString().trimEnd() } + + private fun generatePirateNarrative(summary: TripSummary): String { + val zone = ZoneId.systemDefault() + val dateFmt = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.getDefault()).withZone(zone) + val timeFmt = DateTimeFormatter.ofPattern("HHmm", Locale.getDefault()).withZone(zone) + val startInst = Instant.ofEpochMilli(summary.startTimeMs) + val endInst = Instant.ofEpochMilli(summary.endTimeMs) + + val durationMs = summary.endTimeMs - summary.startTimeMs + val durationStr = if (durationMs >= 3_600_000) { + val h = durationMs / 3_600_000 + val m = (durationMs % 3_600_000) / 60_000 + "${h}h ${m}m" + } else { + "${durationMs / 60_000}m" + } + + val sb = StringBuilder() + sb.appendLine("☠ VOYAGE · ${dateFmt.format(startInst).uppercase(Locale.getDefault())}") + sb.appendLine("${timeFmt.format(startInst)}–${timeFmt.format(endInst)} · $durationStr") + sb.appendLine() + sb.appendLine("Made %.1f nm by the log · avg %.1f kt · peaked %.1f kt".format( + summary.distanceNm, summary.avgSogKts, summary.maxSogKts)) + + val conditions = buildList { + summary.maxWaveHeightM?.let { add("seas ran %.1f m".format(it)) } + val minT = summary.minAirTempC + val maxT = summary.maxAirTempC + if (minT != null && maxT != null) { + add(if (minT == maxT) "%.0f°C".format(minT) else "%.0f–%.0f°C".format(minT, maxT)) + } + } + if (conditions.isNotEmpty()) { + sb.appendLine() + sb.appendLine(conditions.joinToString(" ")) + } + + if (summary.logEntries.isNotEmpty()) { + val exclamations = listOf( + "Arr, ", "Blimey! ", "Shiver me timbers — ", + "Avast! ", "By Neptune's beard, ", "Splice the mainbrace! " + ) + sb.appendLine() + for ((i, entry) in summary.logEntries.sortedBy { it.timestampMs }.withIndex()) { + val time = timeFmt.format(Instant.ofEpochMilli(entry.timestampMs)) + val photoMarker = if (entry.photoPath != null) " [portrait]" else "" + val prefix = exclamations[i % exclamations.size] + sb.appendLine("$time $prefix${entry.text}$photoMarker") + } + } + + sb.appendLine() + sb.append("— Arr. Fair winds and following seas. —") + return sb.toString().trimEnd() + } } diff --git a/android-app/app/src/main/res/layout/fragment_trip_report.xml b/android-app/app/src/main/res/layout/fragment_trip_report.xml index 7228fa9..c8092ba 100644 --- a/android-app/app/src/main/res/layout/fragment_trip_report.xml +++ b/android-app/app/src/main/res/layout/fragment_trip_report.xml @@ -12,12 +12,14 @@ android:padding="24dp"> + android:layout_marginBottom="24dp" + android:longClickable="true" />