add badge to display unread messages count, and set flash time to 5s, isolate flash logic in a new class AlternatingStateManager

This commit is contained in:
Md. Touhidur Rahman 2025-09-14 16:13:07 +06:00
parent 437f89bffc
commit 16c561b9a1
No known key found for this signature in database
GPG Key ID: 431978882FE25058
5 changed files with 151 additions and 79 deletions

View File

@ -0,0 +1,52 @@
package com.unciv.logic
import com.unciv.utils.Concurrency
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
class AlternatingStateManager<State>(
private val name: String,
private val state: State,
private val originalState: (State) -> Unit,
private val alternateState: (State) -> Unit
) {
private var job: Job? = null
@OptIn(ExperimentalTime::class)
fun start(
duration: Duration = 5.seconds, interval: Duration = 500.milliseconds
) {
job?.cancel()
job = Concurrency.run(name) {
val startTime = Clock.System.now()
var isAlternateState = true
while (true) {
if (isAlternateState) {
alternateState(state)
isAlternateState = false
} else {
originalState(state)
isAlternateState = true
}
if (Clock.System.now() - startTime >= duration) {
originalState(state)
return@run
}
delay(interval)
}
}
}
fun stop() {
job?.cancel()
originalState(state)
}
}

View File

@ -12,7 +12,7 @@ import java.util.UUID
data class Chat(
val gameId: UUID,
) {
var read = true
var unreadCount = 0
// <civName, message> pairs
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
@ -100,13 +100,13 @@ object ChatStore {
if (gameId.equals(UncivGame.Current.worldScreen?.gameInfo?.gameId?.toUUIDOrNull())) {
// ensures that you are not getting notified for your own messages
if (UncivGame.Current.worldScreen?.gameInfo?.currentPlayer != incomingChatMsg.civName) {
chat.read = false
UncivGame.Current.worldScreen?.chatButton?.startFlashing()
chat.unreadCount++
UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication()
}
} else {
// user is out of world screen or
// some other game not currently on screen has a message
chat.read = false
chat.unreadCount++
}
}
}
@ -126,7 +126,7 @@ object ChatStore {
Gdx.app.postRunnable {
if (civName != "System") {
hasGlobalMessage = chatPopup == null
if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.startFlashing()
if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication()
}
chatPopup?.addMessage(civName, message, suffix = "one time")

View File

@ -2,36 +2,88 @@ package com.unciv.ui.screens.worldscreen.chat
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.utils.Disposable
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.AlternatingStateManager
import com.unciv.logic.multiplayer.chat.ChatStore
import com.unciv.logic.multiplayer.chat.ChatWebSocket
import com.unciv.logic.multiplayer.chat.Message
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.utils.Concurrency
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
"Chat", ImageGetter.getImage("OtherIcons/Chat"), 23
), Disposable {
) {
val chat = ChatStore.getChatByGameId(worldScreen.gameInfo.gameId)
val badge = "".toTextButton().apply {
disable()
setColor(Color.valueOf("da1e28"))
label.setColor(Color.WHITE)
label.setAlignment(Align.center)
label.setFontScale(0.2f)
}
val flash = AlternatingStateManager(
name = "ChatButton color flash",
state = object {
val initialColor = fontColor
val targetColor = Color.ORANGE
}, originalState = { state ->
Gdx.app.postRunnable {
icon?.color = state.initialColor
label.color = state.initialColor
}
}, alternateState = { state ->
Gdx.app.postRunnable {
icon?.color = state.targetColor
label.color = state.targetColor
}
}
)
private fun updateBadge(visible: Boolean = false) {
badge.height = height / 3
badge.setPosition(
width - badge.width / 1.5f,
height - badge.height / 1.5f
)
badge.isVisible = visible || chat.unreadCount > 0 || ChatStore.hasGlobalMessage
if (badge.isVisible) {
var text = chat.unreadCount.toString()
if (ChatStore.hasGlobalMessage) {
text += '+'
}
badge.setText(text)
}
}
fun triggerChatIndication() {
updateBadge()
flash.start()
}
init {
width = 95f
iconCell.pad(3f).center()
addActor(badge)
if (!chat.read || ChatStore.hasGlobalMessage) {
startFlashing()
if (chat.unreadCount > 0 || ChatStore.hasGlobalMessage) {
updateBadge(true)
flash.stop()
}
onClick {
stopFlashing()
chat.unreadCount = 0
updateBadge()
flash.stop()
ChatPopup(chat, worldScreen).open()
}
@ -50,6 +102,7 @@ class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
Message.Join(listOf(worldScreen.gameInfo.gameId)),
)
updatePosition()
updateBadge()
true
} else {
ChatWebSocket.stop()
@ -61,38 +114,4 @@ class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
worldScreen.techPolicyAndDiplomacy.x.coerceAtLeast(1f),
worldScreen.techPolicyAndDiplomacy.y - height - 1f
)
private var flashJob: Job? = null
fun startFlashing(targetColor: Color = Color.ORANGE, interval: Duration = 500.milliseconds) {
flashJob?.cancel()
flashJob = Concurrency.run("ChatButton color flash") {
var isAlternatingColor = true
while (true) {
Gdx.app.postRunnable {
if (isAlternatingColor) {
icon?.color = targetColor
label.color = targetColor
} else {
icon?.color = fontColor
label.color = fontColor
}
isAlternatingColor = !isAlternatingColor
}
delay(interval)
}
}
}
private fun stopFlashing() {
flashJob?.cancel()
icon?.color = fontColor
label.color = fontColor
}
override fun dispose() {
flashJob?.cancel()
}
}

View File

@ -44,7 +44,6 @@ class ChatPopup(
)
init {
chat.read = true
ChatStore.chatPopup = this
ChatStore.hasGlobalMessage = false
chatTable.defaults().growX().pad(5f).center()

View File

@ -1,21 +1,22 @@
package com.unciv.ui.screens.worldscreen.status
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.utils.Disposable
import com.unciv.UncivGame
import com.unciv.logic.AlternatingStateManager
import com.unciv.logic.event.EventBus
import com.unciv.logic.multiplayer.HasMultiplayerGameName
import com.unciv.logic.multiplayer.MultiplayerGameNameChanged
import com.unciv.logic.multiplayer.MultiplayerGamePreview
import com.unciv.logic.multiplayer.MultiplayerGameUpdateEnded
import com.unciv.logic.multiplayer.MultiplayerGameUpdateStarted
import com.unciv.logic.multiplayer.MultiplayerGameUpdated
import com.unciv.logic.multiplayer.MultiplayerGamePreview
import com.unciv.logic.multiplayer.isUsersTurn
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.setSize
@ -24,20 +25,20 @@ import com.unciv.ui.components.widgets.LoadingImage
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.utils.Concurrency
import com.unciv.utils.launchOnGLThread
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
class MultiplayerStatusButton(
screen: BaseScreen,
curGame: MultiplayerGamePreview?
) : Button(BaseScreen.skin), Disposable {
private var curGameName = curGame?.name
private val loadingImage = LoadingImage(style = LoadingImage.Style(
private val loadingImage = LoadingImage(
style = LoadingImage.Style(
idleImageName = "OtherIcons/Multiplayer",
idleIconColor = Color.WHITE,
minShowTime = 500
))
)
)
private val turnIndicator = TurnIndicator()
private val turnIndicatorCell: Cell<Actor>
private val gameNamesWithCurrentTurn = getInitialGamesWithCurrentTurn()
@ -96,7 +97,7 @@ class MultiplayerStatusButton(
}
private fun updateTurnIndicator(flash: Boolean = true) {
if (gameNamesWithCurrentTurn.size == 0) {
if (gameNamesWithCurrentTurn.isEmpty()) {
turnIndicatorCell.clearActor()
} else {
turnIndicatorCell.setActor(turnIndicator)
@ -105,7 +106,7 @@ class MultiplayerStatusButton(
// flash so the user sees an better update
if (flash) {
turnIndicator.flash()
turnIndicator.flash.start(3.seconds)
}
}
@ -118,10 +119,9 @@ class MultiplayerStatusButton(
private class TurnIndicator : HorizontalGroup(), Disposable {
val gameAmount = Label("2", BaseScreen.skin)
val image: Image
private var job: Job? = null
val image = ImageGetter.getImage("OtherIcons/ExclamationMark")
init {
image = ImageGetter.getImage("OtherIcons/ExclamationMark")
image.setSize(30f)
addActor(image)
}
@ -135,23 +135,25 @@ private class TurnIndicator : HorizontalGroup(), Disposable {
}
}
fun flash() {
// using a gdx Action would be nicer, but we don't necessarily have continuousRendering on and we still want to flash
flash(6, Color.WHITE, Color.ORANGE)
}
private fun flash(alternations: Int, curColor: Color, nextColor: Color) {
if (alternations == 0) return
gameAmount.color = nextColor
image.color = nextColor
job = Concurrency.run("StatusButton color flash") {
delay(500)
launchOnGLThread {
flash(alternations - 1, nextColor, curColor)
val flash = AlternatingStateManager(
name = "StatusButton color flash",
state = object {
val initialColor = Color.WHITE
val targetColor = Color.ORANGE
}, originalState = { state ->
Gdx.app.postRunnable {
image.color = state.initialColor
gameAmount.color = state.initialColor
}
}, alternateState = { state ->
Gdx.app.postRunnable {
image.color = state.targetColor
gameAmount.color = state.targetColor
}
}
)
override fun dispose() {
job?.cancel()
flash.stop()
}
}