Move SSD detection status text to typed Kotlin

ober

6f51da876dc7d4cc4e40a5b1ef4e4337dd4e0b1f

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 3114d8a..b20fa5d 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -3730,7 +3730,9 @@
                 manualGroupLabel manualCellCount editedGroupCount
                 clampPageIndex shiftPageIndex displayGroupCanvasLabel
                 dropdownHeight dialogPageNumber nullableLowercaseOrEmpty
-                selectModeStatus selectionResultStatus
+                selectModeStatus selectionResultStatus detectStartStatus
+                detectorSourceWithTruth detectionTruthNote detectionOcrNote
+                detectionLearnedNote
                 reviewPrimaryColor reviewWhiteColor reviewLabelTextColor
                 reviewLabelBackgroundColor reviewCanvasBackgroundColor
                 reviewAccentColor reviewModeColor reviewDarkTextColor
@@ -3909,6 +3911,37 @@
                   (int32->string matchingCount)
                   " detected boxes"))
               "Shrank rough selection to SSD boxes")))
+        (def (detectStartStatus (forceDetect : Bool)) : String
+          (if forceDetect "Detecting boxes..." "Loading truth..."))
+        (def (detectorSourceWithTruth (forceDetect : Bool)
+                                      (existingTruthGroupCount : Int32)
+                                      (forcedSource : String)
+                                      (fallbackSource : String)) : String
+          (if (and forceDetect (> existingTruthGroupCount (int32 0)))
+            forcedSource
+            fallbackSource))
+        (def (detectionTruthNote (hasExistingTruth : Bool)) : String
+          (if hasExistingTruth ", replayed truth" ""))
+        (def (detectionOcrNote (ocrGuesses : Int32) (ocrWordCount : Int32)) : String
+          (if (> ocrGuesses (int32 0))
+            (string-append
+              ", "
+              (string-append
+                (int32->string ocrGuesses)
+                " OCR guesses"))
+            (if (> ocrWordCount (int32 0))
+              (string-append
+                ", OCR "
+                (string-append (int32->string ocrWordCount) " words"))
+              "")))
+        (def (detectionLearnedNote (learnedGuesses : Int32)) : String
+          (if (> learnedGuesses (int32 0))
+            (string-append
+              ", "
+              (string-append
+                (int32->string learnedGuesses)
+                " learned guesses"))
+            ""))
         (def (reviewPrimaryColor) : Int32
           (androidColorRgb (int32 37) (int32 99) (int32 235)))
         (def (reviewWhiteColor) : Int32
@@ -5427,7 +5460,7 @@
        "    ) {"
        "        val bitmap = currentBitmap ?: return"
        "        val uri = expectedUri ?: return"
-       "        setStatus(if (forceDetect) \"Detecting boxes...\" else \"Loading truth...\")"
+       "        setStatus(detectStartStatus(forceDetect))"
        "        thread {"
        "            try {"
        "                if (!isCurrentLoad(expectedGeneration, uri, expectedPageIndex)) return@thread"
@@ -5489,10 +5522,10 @@
        "                    if (nativeDetection != null) {"
        "                        detectedCells = nativeDetection.cells"
        "                        detectorNote = \", rust detector\""
-       "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-rust-plus-truth\" else \"rust-detector\""
+       "                        detectorSource = detectorSourceWithTruth(forceDetect, existingTruthGroupCount, \"forced-rust-plus-truth\", \"rust-detector\")"
        "                    } else {"
        "                        detectedCells = detectCells(bitmap)"
-       "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-android-plus-truth\" else \"android-fallback\""
+       "                        detectorSource = detectorSourceWithTruth(forceDetect, existingTruthGroupCount, \"forced-android-plus-truth\", \"android-fallback\")"
        "                    }"
        "                    ssdSessionAddDetectedCellsInsideArea(session, detectedCells)"
        "                    session.groups.addAll(initialGroups(session.cells).take(120))"
@@ -5512,9 +5545,9 @@
 	       "                    reviewView.session = session"
 	       "                    reviewView.selectedGroupId = null"
 	       "                    reviewView.ssdArea = session.ssdArea"
-       "                    val truthNote = if (existingTruth != null) \", replayed truth\" else \"\""
-       "                    val ocrNote = if (ocrGuesses > 0) \", $ocrGuesses OCR guesses\" else if (session.ocrWords.isNotEmpty()) \", OCR ${session.ocrWords.size} words\" else \"\""
-       "                    val learnedNote = if (learnedGuesses > 0) \", $learnedGuesses learned guesses\" else \"\""
+       "                    val truthNote = detectionTruthNote(existingTruth != null)"
+       "                    val ocrNote = detectionOcrNote(ocrGuesses, session.ocrWords.size)"
+       "                    val learnedNote = detectionLearnedNote(learnedGuesses)"
        "                    setStatus(sessionCountsStatus(session, detectorNote + truthNote + ocrNote + learnedNote))"
        "                    logDetectResult(\"page-load\", session, detectorSource, learnedGuesses, ocrGuesses)"
        "                    if (existingTruth == null && !forceDetect) mergeRemoteTruthAsync(session, expectedGeneration, uri, expectedPageIndex)"