diff options
| author | Claude <noreply@anthropic.com> | 2026-05-27 05:32:36 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-27 05:32:36 +0000 |
| commit | 1f9f0ec3f7ce986134ed75836fc42e43582aa34f (patch) | |
| tree | 2f9662743ac6ae655aba0ab2ff2aa1511cb7c8a7 /android-app/app/src | |
| parent | 0db33911ca4841c658b22912d3ecf32477e5e827 (diff) | |
Add pirate mode Easter egg to trip report
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
Diffstat (limited to 'android-app/app/src')
3 files changed, 78 insertions, 3 deletions
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<String> { + 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"> <TextView + android:id="@+id/tv_report_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Trip Report" android:textSize="24sp" android:textStyle="bold" - android:layout_marginBottom="24dp" /> + android:layout_marginBottom="24dp" + android:longClickable="true" /> <com.google.android.material.card.MaterialCardView android:layout_width="match_parent" |
