diff --git a/app-desktop/src/main/i18n/strings_de.properties b/app-desktop/src/main/i18n/strings_de.properties index be165ea5..c0e23c67 100644 --- a/app-desktop/src/main/i18n/strings_de.properties +++ b/app-desktop/src/main/i18n/strings_de.properties @@ -334,6 +334,8 @@ title.connecting=Verbindung zu ON4KST wird aufgebaut… title.authenticating=Verbunden – Anmeldung bei ON4KST läuft… title.synchronizing=Verbunden – ON4KST-Chatdaten werden abgeglichen… title.disconnected=NICHT VERBUNDEN! +title.online=Verbunden mit: {0} als {1} ({2}) in {3} ({4} Benutzer online, {5} angezeigt), {6} Nachrichten insgesamt. +title.bothCategories={0} und {1} status.noConnection=Keine ON4KST-Verbindung status.linkTooltip=ON4KST-Verbindung: {0}\n{1} status.linkConnected=🟢 LINK: Verbunden diff --git a/app-desktop/src/main/i18n/strings_en.properties b/app-desktop/src/main/i18n/strings_en.properties index 4f50396a..58725812 100644 --- a/app-desktop/src/main/i18n/strings_en.properties +++ b/app-desktop/src/main/i18n/strings_en.properties @@ -343,6 +343,8 @@ title.connecting=Connecting to ON4KST… title.authenticating=Connected – authenticating with ON4KST… title.synchronizing=Connected – synchronizing ON4KST chat data… title.disconnected=DISCONNECTED! +title.online=Connected to: {0} as {1} ({2}) in {3} ({4} users online, {5} shown), {6} messages total. +title.bothCategories={0} and {1} status.noConnection=No ON4KST connection status.linkTooltip=ON4KST link: {0}\n{1} status.linkConnected=🟢 LINK: Connected diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowFrame.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowFrame.kt index 7de35bf9..2ac676c1 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowFrame.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowFrame.kt @@ -1,6 +1,7 @@ package kst4contest.view.compose import kst4contest.controller.On4KstConnectionState +import kst4contest.model.ChatCategory /** How big the main window opens. */ data class MainWindowSize(val heightDp: Double, val widthDp: Double) @@ -56,30 +57,75 @@ object MainWindowFrame { fun title(chatState: String, profileName: String?): String = if (profileName.isNullOrBlank()) chatState else "$chatState - $profileName" + /** + * What the title's first half says. + * + * The live connection line while the chat is usable -- exactly what the JavaFX title + * carried -- and a wording about the attempt otherwise. The connection manager's own + * detail ("session is authenticated and synchronized") is a diagnostic for the status + * bar, not a title: it says nothing about category, call or traffic. + * + * @param state the connection state, null before the first push + * @param onlineLine the line built by [onlineChatState], asked for only while online + * @return the chat half of the title + */ + fun chatState(state: On4KstConnectionState?, onlineLine: () -> String): String = + if (state == On4KstConnectionState.ONLINE) onlineLine() else offlineChatState(state) + + /** + * The connection line while the chat is usable, as the JavaFX title had it: + * "Connected to: 2: 144/432 MHz as DO1JKS (Johannes 70/2) in JN59MU (106 users online, + * 106 shown), 14 messages total." + * + * Counts are passed as text so MessageFormat does not group them by locale. Without a + * main category there is nothing to name, so the line falls back to the offline wording + * for ONLINE instead of reading "Connected to: as ...". + * + * @param mainCategory the main chat category, null when none is configured + * @param secondCategory the second category, null when it is not logged in + * @param callSign the login callsign + * @param name the login name of the main category + * @param locator the login locator + * @param usersOnline every station in the chat + * @param shown the stations the filters let through + * @param messagesTotal every chat message held, the global list behind the public tab + */ + fun onlineChatState( + mainCategory: ChatCategory?, + secondCategory: ChatCategory?, + callSign: String?, + name: String?, + locator: String?, + usersOnline: Int, + shown: Int, + messagesTotal: Int, + ): String { + if (mainCategory == null) { + return offlineChatState(On4KstConnectionState.ONLINE) + } + val strings = kst4contest.view.i18n.CurrentStrings.get() + val categories = if (secondCategory == null) { + mainCategory.toString() + } else { + strings.titleBothCategories(mainCategory.toString(), secondCategory.toString()) + } + return strings.titleOnline( + categories, + callSign.orEmpty(), + name.orEmpty(), + locator.orEmpty(), + usersOnline.toString(), + shown.toString(), + messagesTotal.toString(), + ) + } + /** * What the title says while the chat is not usable. * * The wording separates a scheduled reconnect from a dead session, which is the * difference between waiting and doing something about it. */ - /** - * What the title's first half says. - * - * The live connection detail while the chat is usable -- which is the whole status line, - * exactly what the JavaFX title carried -- and a wording about the attempt otherwise. A - * blank detail falls through to the offline wording rather than leaving the title empty. - * - * @param state the connection state, null before the first push - * @param detail the status line the controller pushes - * @return the chat half of the title - */ - fun chatState(state: On4KstConnectionState?, detail: String?): String = - if (state == On4KstConnectionState.ONLINE && !detail.isNullOrBlank()) { - detail - } else { - offlineChatState(state) - } - fun offlineChatState(state: On4KstConnectionState?): String = when (state ?: On4KstConnectionState.DISCONNECTED) { On4KstConnectionState.RECONNECT_WAIT -> kst4contest.view.i18n.CurrentStrings.get().titleConnectionLost diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowHost.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowHost.kt index c4fd6e9f..a4f6ddc3 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowHost.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/MainWindowHost.kt @@ -111,14 +111,29 @@ object MainWindowHost { * Read inside the composition, so the title follows the connection the way * the JavaFX one did: "Connected to: 2: 144/432 MHz as DN5PW in JO50JP * (73 users online, 73 shown), 29 messages total." A captured string would - * say "disconnected" for the life of the session. + * say "disconnected" for the life of the session. The counts are Compose + * state, so the title moves with every station and message. visibleRows + * costs nothing extra: it is cached per change, and the station table + * reads the same list anyway. */ - val surroundings = currentState?.surroundings + val state = currentState MainWindowFrame.title( - chatState = MainWindowFrame.chatState( - surroundings?.connectionState, - surroundings?.connectionDetail, - ), + chatState = if (state == null) { + MainWindowFrame.offlineChatState(null) + } else MainWindowFrame.chatState(state.surroundings.connectionState) { + val prefs = state.prefs + MainWindowFrame.onlineChatState( + mainCategory = prefs.getLoginChatCategoryMain(), + secondCategory = prefs.getLoginChatCategorySecond() + .takeIf { prefs.isLoginToSecondChatEnabled() }, + callSign = prefs.getStn_loginCallSign(), + name = prefs.getStn_loginNameMainCat(), + locator = prefs.getStn_loginLocatorMainCat(), + usersOnline = state.stations.rows.size, + shown = state.stations.visibleRows.size, + messagesTotal = state.publicMessages.rows.size, + ) + }, profileName = profileName, ) }, diff --git a/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowFrameTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowFrameTest.kt index ae353538..01b1a7fe 100644 --- a/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowFrameTest.kt +++ b/app-desktop/src/test/kotlin/kst4contest/view/compose/MainWindowFrameTest.kt @@ -1,6 +1,7 @@ package kst4contest.view.compose import kst4contest.controller.On4KstConnectionState +import kst4contest.model.ChatCategory import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -123,36 +124,79 @@ class MainWindowFrameTest { * The title carries the whole connection line while the chat is usable. * * The JavaFX window did that -- "Connected to: 2: 144/432 MHz as DN5PW (Testing) in - * JO50JP (73 users online, 73 shown), 29 messages total." -- and the Compose window - * opened with a fixed "KST4Contest (Compose)" instead, which told a tiling window - * manager nothing and told the operator less. + * JO50JP (73 users online, 73 shown), 29 messages total." The first Compose title took + * the connection manager's diagnostic instead and read "ON4KST session is authenticated + * and synchronized" for the whole contest. */ @Test fun `the title carries the live connection line once the chat is usable`() { - val detail = "Connected to: 2: 144/432 MHz as DN5PW (Testing) in JO50JP " + - "(73 users online, 73 shown), 29 messages total." + val line = MainWindowFrame.onlineChatState( + mainCategory = ChatCategory(ChatCategory.VUHF), + secondCategory = null, + callSign = "DN5PW", + name = "Testing", + locator = "JO50JP", + usersOnline = 73, + shown = 70, + messagesTotal = 29, + ) - assertEquals(detail, MainWindowFrame.chatState(On4KstConnectionState.ONLINE, detail)) + assertEquals( + "Connected to: 2: 144/432 MHz as DN5PW (Testing) in JO50JP " + + "(73 users online, 70 shown), 29 messages total.", + line, + ) + assertEquals(line, MainWindowFrame.chatState(On4KstConnectionState.ONLINE) { line }) + } + + @Test + fun `both categories are named when the second chat is logged in`() { + val line = MainWindowFrame.onlineChatState( + mainCategory = ChatCategory(ChatCategory.VUHF), + secondCategory = ChatCategory(ChatCategory.MICROWAVE), + callSign = "DN5PW", + name = "", + locator = "JO50JP", + usersOnline = 1, + shown = 1, + messagesTotal = 1, + ) + + assertEquals( + "Connected to: 2: 144/432 MHz and 3: Microwave as DN5PW () in JO50JP " + + "(1 users online, 1 shown), 1 messages total.", + line, + ) + } + + @Test + fun `without a main category the title does not name an empty one`() { + assertEquals( + MainWindowFrame.offlineChatState(On4KstConnectionState.ONLINE), + MainWindowFrame.onlineChatState(null, null, "DN5PW", "x", "JO50JP", 1, 1, 1), + ) + } + + @Test + fun `large counts are not grouped by locale`() { + val line = MainWindowFrame.onlineChatState(ChatCategory(ChatCategory.VUHF), null, "DN5PW", "x", "JO50JP", 1234, 1200, 23456) + + assertTrue(line.contains("(1234 users online, 1200 shown), 23456 messages total."), line) } @Test fun `an unusable chat says what it is doing instead`() { assertEquals( MainWindowFrame.offlineChatState(On4KstConnectionState.CONNECTING), - MainWindowFrame.chatState(On4KstConnectionState.CONNECTING, "ignored while connecting"), + MainWindowFrame.chatState(On4KstConnectionState.CONNECTING) { error("not online") }, ) } @Test - fun `a blank detail falls through rather than leaving the title empty`() { - // The feed substitutes the state name for a blank detail, but it may not have run yet. - assertEquals( - MainWindowFrame.offlineChatState(On4KstConnectionState.ONLINE), - MainWindowFrame.chatState(On4KstConnectionState.ONLINE, " "), - ) + fun `no state at all reads as disconnected without building the online line`() { assertEquals( MainWindowFrame.offlineChatState(null), - MainWindowFrame.chatState(null, null), + MainWindowFrame.chatState(null) { error("not online") }, ) } }