mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-22 10:54:19 -04:00
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:
parent
437f89bffc
commit
16c561b9a1
52
core/src/com/unciv/logic/AlternatingStateManager.kt
Normal file
52
core/src/com/unciv/logic/AlternatingStateManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ import java.util.UUID
|
|||||||
data class Chat(
|
data class Chat(
|
||||||
val gameId: UUID,
|
val gameId: UUID,
|
||||||
) {
|
) {
|
||||||
var read = true
|
var unreadCount = 0
|
||||||
|
|
||||||
// <civName, message> pairs
|
// <civName, message> pairs
|
||||||
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
|
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())) {
|
if (gameId.equals(UncivGame.Current.worldScreen?.gameInfo?.gameId?.toUUIDOrNull())) {
|
||||||
// ensures that you are not getting notified for your own messages
|
// ensures that you are not getting notified for your own messages
|
||||||
if (UncivGame.Current.worldScreen?.gameInfo?.currentPlayer != incomingChatMsg.civName) {
|
if (UncivGame.Current.worldScreen?.gameInfo?.currentPlayer != incomingChatMsg.civName) {
|
||||||
chat.read = false
|
chat.unreadCount++
|
||||||
UncivGame.Current.worldScreen?.chatButton?.startFlashing()
|
UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// user is out of world screen or
|
// user is out of world screen or
|
||||||
// some other game not currently on screen has a message
|
// some other game not currently on screen has a message
|
||||||
chat.read = false
|
chat.unreadCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ object ChatStore {
|
|||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
if (civName != "System") {
|
if (civName != "System") {
|
||||||
hasGlobalMessage = chatPopup == null
|
hasGlobalMessage = chatPopup == null
|
||||||
if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.startFlashing()
|
if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication()
|
||||||
}
|
}
|
||||||
|
|
||||||
chatPopup?.addMessage(civName, message, suffix = "one time")
|
chatPopup?.addMessage(civName, message, suffix = "one time")
|
||||||
|
@ -2,36 +2,88 @@ package com.unciv.ui.screens.worldscreen.chat
|
|||||||
|
|
||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.graphics.Color
|
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.UncivGame
|
||||||
|
import com.unciv.logic.AlternatingStateManager
|
||||||
import com.unciv.logic.multiplayer.chat.ChatStore
|
import com.unciv.logic.multiplayer.chat.ChatStore
|
||||||
import com.unciv.logic.multiplayer.chat.ChatWebSocket
|
import com.unciv.logic.multiplayer.chat.ChatWebSocket
|
||||||
import com.unciv.logic.multiplayer.chat.Message
|
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.components.input.onClick
|
||||||
import com.unciv.ui.images.IconTextButton
|
import com.unciv.ui.images.IconTextButton
|
||||||
import com.unciv.ui.images.ImageGetter
|
import com.unciv.ui.images.ImageGetter
|
||||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
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(
|
class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
|
||||||
"Chat", ImageGetter.getImage("OtherIcons/Chat"), 23
|
"Chat", ImageGetter.getImage("OtherIcons/Chat"), 23
|
||||||
), Disposable {
|
) {
|
||||||
val chat = ChatStore.getChatByGameId(worldScreen.gameInfo.gameId)
|
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 {
|
init {
|
||||||
width = 95f
|
width = 95f
|
||||||
iconCell.pad(3f).center()
|
iconCell.pad(3f).center()
|
||||||
|
addActor(badge)
|
||||||
|
|
||||||
if (!chat.read || ChatStore.hasGlobalMessage) {
|
if (chat.unreadCount > 0 || ChatStore.hasGlobalMessage) {
|
||||||
startFlashing()
|
updateBadge(true)
|
||||||
|
flash.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
stopFlashing()
|
chat.unreadCount = 0
|
||||||
|
updateBadge()
|
||||||
|
flash.stop()
|
||||||
|
|
||||||
ChatPopup(chat, worldScreen).open()
|
ChatPopup(chat, worldScreen).open()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +102,7 @@ class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
|
|||||||
Message.Join(listOf(worldScreen.gameInfo.gameId)),
|
Message.Join(listOf(worldScreen.gameInfo.gameId)),
|
||||||
)
|
)
|
||||||
updatePosition()
|
updatePosition()
|
||||||
|
updateBadge()
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
ChatWebSocket.stop()
|
ChatWebSocket.stop()
|
||||||
@ -61,38 +114,4 @@ class ChatButton(val worldScreen: WorldScreen) : IconTextButton(
|
|||||||
worldScreen.techPolicyAndDiplomacy.x.coerceAtLeast(1f),
|
worldScreen.techPolicyAndDiplomacy.x.coerceAtLeast(1f),
|
||||||
worldScreen.techPolicyAndDiplomacy.y - height - 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ class ChatPopup(
|
|||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
chat.read = true
|
|
||||||
ChatStore.chatPopup = this
|
ChatStore.chatPopup = this
|
||||||
ChatStore.hasGlobalMessage = false
|
ChatStore.hasGlobalMessage = false
|
||||||
chatTable.defaults().growX().pad(5f).center()
|
chatTable.defaults().growX().pad(5f).center()
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
package com.unciv.ui.screens.worldscreen.status
|
package com.unciv.ui.screens.worldscreen.status
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Button
|
import com.badlogic.gdx.scenes.scene2d.ui.Button
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup
|
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.scenes.scene2d.ui.Label
|
||||||
import com.badlogic.gdx.utils.Disposable
|
import com.badlogic.gdx.utils.Disposable
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
|
import com.unciv.logic.AlternatingStateManager
|
||||||
import com.unciv.logic.event.EventBus
|
import com.unciv.logic.event.EventBus
|
||||||
import com.unciv.logic.multiplayer.HasMultiplayerGameName
|
import com.unciv.logic.multiplayer.HasMultiplayerGameName
|
||||||
import com.unciv.logic.multiplayer.MultiplayerGameNameChanged
|
import com.unciv.logic.multiplayer.MultiplayerGameNameChanged
|
||||||
|
import com.unciv.logic.multiplayer.MultiplayerGamePreview
|
||||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdateEnded
|
import com.unciv.logic.multiplayer.MultiplayerGameUpdateEnded
|
||||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdateStarted
|
import com.unciv.logic.multiplayer.MultiplayerGameUpdateStarted
|
||||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdated
|
import com.unciv.logic.multiplayer.MultiplayerGameUpdated
|
||||||
import com.unciv.logic.multiplayer.MultiplayerGamePreview
|
|
||||||
import com.unciv.logic.multiplayer.isUsersTurn
|
import com.unciv.logic.multiplayer.isUsersTurn
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.components.extensions.setSize
|
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.images.ImageGetter
|
||||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||||
import com.unciv.utils.Concurrency
|
import com.unciv.utils.Concurrency
|
||||||
import com.unciv.utils.launchOnGLThread
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
class MultiplayerStatusButton(
|
class MultiplayerStatusButton(
|
||||||
screen: BaseScreen,
|
screen: BaseScreen,
|
||||||
curGame: MultiplayerGamePreview?
|
curGame: MultiplayerGamePreview?
|
||||||
) : Button(BaseScreen.skin), Disposable {
|
) : Button(BaseScreen.skin), Disposable {
|
||||||
private var curGameName = curGame?.name
|
private var curGameName = curGame?.name
|
||||||
private val loadingImage = LoadingImage(style = LoadingImage.Style(
|
private val loadingImage = LoadingImage(
|
||||||
idleImageName = "OtherIcons/Multiplayer",
|
style = LoadingImage.Style(
|
||||||
idleIconColor = Color.WHITE,
|
idleImageName = "OtherIcons/Multiplayer",
|
||||||
minShowTime = 500
|
idleIconColor = Color.WHITE,
|
||||||
))
|
minShowTime = 500
|
||||||
|
)
|
||||||
|
)
|
||||||
private val turnIndicator = TurnIndicator()
|
private val turnIndicator = TurnIndicator()
|
||||||
private val turnIndicatorCell: Cell<Actor>
|
private val turnIndicatorCell: Cell<Actor>
|
||||||
private val gameNamesWithCurrentTurn = getInitialGamesWithCurrentTurn()
|
private val gameNamesWithCurrentTurn = getInitialGamesWithCurrentTurn()
|
||||||
@ -96,7 +97,7 @@ class MultiplayerStatusButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTurnIndicator(flash: Boolean = true) {
|
private fun updateTurnIndicator(flash: Boolean = true) {
|
||||||
if (gameNamesWithCurrentTurn.size == 0) {
|
if (gameNamesWithCurrentTurn.isEmpty()) {
|
||||||
turnIndicatorCell.clearActor()
|
turnIndicatorCell.clearActor()
|
||||||
} else {
|
} else {
|
||||||
turnIndicatorCell.setActor(turnIndicator)
|
turnIndicatorCell.setActor(turnIndicator)
|
||||||
@ -105,7 +106,7 @@ class MultiplayerStatusButton(
|
|||||||
|
|
||||||
// flash so the user sees an better update
|
// flash so the user sees an better update
|
||||||
if (flash) {
|
if (flash) {
|
||||||
turnIndicator.flash()
|
turnIndicator.flash.start(3.seconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,10 +119,9 @@ class MultiplayerStatusButton(
|
|||||||
|
|
||||||
private class TurnIndicator : HorizontalGroup(), Disposable {
|
private class TurnIndicator : HorizontalGroup(), Disposable {
|
||||||
val gameAmount = Label("2", BaseScreen.skin)
|
val gameAmount = Label("2", BaseScreen.skin)
|
||||||
val image: Image
|
val image = ImageGetter.getImage("OtherIcons/ExclamationMark")
|
||||||
private var job: Job? = null
|
|
||||||
init {
|
init {
|
||||||
image = ImageGetter.getImage("OtherIcons/ExclamationMark")
|
|
||||||
image.setSize(30f)
|
image.setSize(30f)
|
||||||
addActor(image)
|
addActor(image)
|
||||||
}
|
}
|
||||||
@ -135,23 +135,25 @@ private class TurnIndicator : HorizontalGroup(), Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flash() {
|
val flash = AlternatingStateManager(
|
||||||
// using a gdx Action would be nicer, but we don't necessarily have continuousRendering on and we still want to flash
|
name = "StatusButton color flash",
|
||||||
flash(6, Color.WHITE, Color.ORANGE)
|
state = object {
|
||||||
}
|
val initialColor = Color.WHITE
|
||||||
private fun flash(alternations: Int, curColor: Color, nextColor: Color) {
|
val targetColor = Color.ORANGE
|
||||||
if (alternations == 0) return
|
}, originalState = { state ->
|
||||||
gameAmount.color = nextColor
|
Gdx.app.postRunnable {
|
||||||
image.color = nextColor
|
image.color = state.initialColor
|
||||||
job = Concurrency.run("StatusButton color flash") {
|
gameAmount.color = state.initialColor
|
||||||
delay(500)
|
}
|
||||||
launchOnGLThread {
|
}, alternateState = { state ->
|
||||||
flash(alternations - 1, nextColor, curColor)
|
Gdx.app.postRunnable {
|
||||||
|
image.color = state.targetColor
|
||||||
|
gameAmount.color = state.targetColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
job?.cancel()
|
flash.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user