Add some chat log messages (diminished + hp/pp changes)
This commit is contained in:
		
							parent
							
								
									7a9dd97123
								
							
						
					
					
						commit
						f60a58f71e
					
				
					 22 changed files with 503 additions and 38 deletions
				
			
		| 
						 | 
				
			
			@ -28,7 +28,6 @@ import androidx.compose.ui.graphics.Shape
 | 
			
		|||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.DpSize
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.compose.ui.unit.max
 | 
			
		||||
import androidx.compose.ui.unit.min
 | 
			
		||||
import androidx.compose.ui.window.ApplicationScope
 | 
			
		||||
import androidx.compose.ui.window.Window
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ import com.pixelized.desktop.lwa.ui.screen.network.NetworkFactory
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.SettingsViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
 | 
			
		||||
import com.pixelized.shared.lwa.utils.PathProvider
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +125,7 @@ val viewModelDependencies
 | 
			
		|||
        viewModelOf(::CharacterDiminishedViewModel)
 | 
			
		||||
        viewModelOf(::CharacterDetailCharacteristicDialogViewModel)
 | 
			
		||||
        viewModelOf(::CampaignChatViewModel)
 | 
			
		||||
        viewModelOf(::SettingsViewModel)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
val useCaseDependencies
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,10 @@ class CampaignRepository(
 | 
			
		|||
 | 
			
		||||
    val campaignFlow get() = store.campaignFlow
 | 
			
		||||
 | 
			
		||||
    suspend fun update() {
 | 
			
		||||
        store.update()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun characterInstanceFlow(
 | 
			
		||||
        id: Campaign.CharacterInstance.Id,
 | 
			
		||||
    ): StateFlow<Campaign.CharacterInstance> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ class CampaignStore(
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun update() {
 | 
			
		||||
    suspend fun update() {
 | 
			
		||||
        _campaignFlow.value = load()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,10 @@ class SettingsFactory(
 | 
			
		|||
            host = settings.host,
 | 
			
		||||
            port = settings.port,
 | 
			
		||||
            playerName = settings.playerName,
 | 
			
		||||
            dynamicDice = settings.dynamicDice,
 | 
			
		||||
            autoHideChat = settings.autoHideChat,
 | 
			
		||||
            autoShowChat = settings.autoShowChat,
 | 
			
		||||
            autoScrollChat = settings.autoScrollChat,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +38,11 @@ class SettingsFactory(
 | 
			
		|||
        return Settings(
 | 
			
		||||
            host = json.host ?: default.host,
 | 
			
		||||
            port = json.port ?: default.port,
 | 
			
		||||
            playerName = json.playerName ?: default.playerName
 | 
			
		||||
            playerName = json.playerName ?: default.playerName,
 | 
			
		||||
            dynamicDice = json.dynamicDice ?: default.dynamicDice,
 | 
			
		||||
            autoHideChat = json.autoHideChat ?: default.autoHideChat,
 | 
			
		||||
            autoShowChat = json.autoShowChat ?: default.autoShowChat,
 | 
			
		||||
            autoScrollChat = json.autoScrollChat ?: default.autoScrollChat,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,10 @@ data class Settings(
 | 
			
		|||
    val host: String,
 | 
			
		||||
    val port: Int,
 | 
			
		||||
    val playerName: String,
 | 
			
		||||
    val dynamicDice: Boolean,
 | 
			
		||||
    val autoHideChat: Boolean,
 | 
			
		||||
    val autoShowChat: Boolean,
 | 
			
		||||
    val autoScrollChat: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    val root: String get() = "http://${"${host}:${port}".removePrefix("http://")}"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,4 +7,8 @@ data class SettingsJsonV1(
 | 
			
		|||
    val host: String?,
 | 
			
		||||
    val port: Int?,
 | 
			
		||||
    val playerName: String?,
 | 
			
		||||
    val dynamicDice: Boolean?,
 | 
			
		||||
    val autoHideChat: Boolean?,
 | 
			
		||||
    val autoShowChat: Boolean?,
 | 
			
		||||
    val autoScrollChat: Boolean?,
 | 
			
		||||
) : SettingsJson
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +92,7 @@ fun MainPage(
 | 
			
		|||
                top = {
 | 
			
		||||
                    CampaignToolbar(
 | 
			
		||||
                        campaignViewModel = campaignViewModel,
 | 
			
		||||
                        networkViewModel = networkViewModel,
 | 
			
		||||
                    )
 | 
			
		||||
                },
 | 
			
		||||
                bottom = {
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +104,7 @@ fun MainPage(
 | 
			
		|||
                chat = {
 | 
			
		||||
                    CampaignChat(
 | 
			
		||||
                        modifier = Modifier.padding(all = 8.dp),
 | 
			
		||||
                        campaignChatViewModel = campaignChatViewModel,
 | 
			
		||||
                        chatViewModel = campaignChatViewModel,
 | 
			
		||||
                    )
 | 
			
		||||
                },
 | 
			
		||||
                leftOverlay = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,21 +26,30 @@ class CampaignViewModel(
 | 
			
		|||
 | 
			
		||||
    fun init() {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            combine(
 | 
			
		||||
                network.status,
 | 
			
		||||
                campaignRepository.campaignFlow,
 | 
			
		||||
            ) { status, campaign ->
 | 
			
		||||
                status to campaign
 | 
			
		||||
            }.collectLatest { (status, campaign) ->
 | 
			
		||||
                if (status == NetworkRepository.Status.CONNECTED) {
 | 
			
		||||
                    campaign.characters.keys.forEach { id ->
 | 
			
		||||
                        characterRepository.characterDetail(
 | 
			
		||||
                            characterSheetId = id.characterSheetId,
 | 
			
		||||
                            forceUpdate = true,
 | 
			
		||||
                        )
 | 
			
		||||
                        alterationRepository.updateActiveAlterations(
 | 
			
		||||
                            characterInstanceId = id,
 | 
			
		||||
                        )
 | 
			
		||||
            launch {
 | 
			
		||||
                network.status.collect { status ->
 | 
			
		||||
                    if (status == NetworkRepository.Status.CONNECTED) {
 | 
			
		||||
                        campaignRepository.update()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                combine(
 | 
			
		||||
                    network.status,
 | 
			
		||||
                    campaignRepository.campaignFlow,
 | 
			
		||||
                ) { status, campaign ->
 | 
			
		||||
                    status to campaign
 | 
			
		||||
                }.collectLatest { (status, campaign) ->
 | 
			
		||||
                    if (status == NetworkRepository.Status.CONNECTED) {
 | 
			
		||||
                        campaign.characters.keys.forEach { id ->
 | 
			
		||||
                            characterRepository.characterDetail(
 | 
			
		||||
                                characterSheetId = id.characterSheetId,
 | 
			
		||||
                                forceUpdate = true,
 | 
			
		||||
                            )
 | 
			
		||||
                            alterationRepository.updateActiveAlterations(
 | 
			
		||||
                                characterInstanceId = id,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,9 +32,14 @@ import androidx.compose.ui.unit.dp
 | 
			
		|||
import androidx.compose.ui.unit.max
 | 
			
		||||
import androidx.compose.ui.unit.min
 | 
			
		||||
import androidx.compose.ui.window.WindowState
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.settings.model.Settings
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindowState
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignLayoutScope
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessageUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessageUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage
 | 
			
		||||
| 
						 | 
				
			
			@ -47,19 +52,21 @@ import org.koin.compose.viewmodel.koinViewModel
 | 
			
		|||
@Composable
 | 
			
		||||
fun CampaignChat(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
 | 
			
		||||
    chatViewModel: CampaignChatViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val scope = rememberCoroutineScope()
 | 
			
		||||
    val lazyState = rememberLazyListState()
 | 
			
		||||
    val animatedChatWidth = rememberAnimatedChatWidth()
 | 
			
		||||
    val colorScheme = MaterialTheme.lwa.colorScheme
 | 
			
		||||
    val messages = campaignChatViewModel.messages.collectAsState()
 | 
			
		||||
    val messages = chatViewModel.messages.collectAsState()
 | 
			
		||||
    val settings = chatViewModel.settings.collectAsState()
 | 
			
		||||
 | 
			
		||||
    ChatScrollDownEffect(
 | 
			
		||||
        lazyState = lazyState,
 | 
			
		||||
        messages = messages,
 | 
			
		||||
        displayChat = campaignChatViewModel::displayChat,
 | 
			
		||||
        hideChat = campaignChatViewModel::hideChat,
 | 
			
		||||
        settings = settings,
 | 
			
		||||
        displayChat = chatViewModel::displayChat,
 | 
			
		||||
        hideChat = chatViewModel::hideChat,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    Box(
 | 
			
		||||
| 
						 | 
				
			
			@ -69,17 +76,19 @@ fun CampaignChat(
 | 
			
		|||
                height = PlayerRibbon.Default.size.height * 2 + 8.dp,
 | 
			
		||||
            )
 | 
			
		||||
            .graphicsLayer {
 | 
			
		||||
                alpha = campaignChatViewModel.chatAnimatedVisibility.value
 | 
			
		||||
                alpha = chatViewModel.chatAnimatedVisibility.value
 | 
			
		||||
            }
 | 
			
		||||
            .background(
 | 
			
		||||
                shape = remember { RoundedCornerShape(8.dp) },
 | 
			
		||||
                color = remember { colorScheme.elevated.base1dp.copy(alpha = 0.5f) },
 | 
			
		||||
            )
 | 
			
		||||
            .onPointerEvent(eventType = PointerEventType.Enter) {
 | 
			
		||||
                scope.launch { campaignChatViewModel.displayChat() }
 | 
			
		||||
                scope.launch { chatViewModel.displayChat() }
 | 
			
		||||
            }
 | 
			
		||||
            .onPointerEvent(eventType = PointerEventType.Exit) {
 | 
			
		||||
                scope.launch { campaignChatViewModel.hideChat() }
 | 
			
		||||
                if (settings.value.autoHideChat) {
 | 
			
		||||
                    scope.launch { chatViewModel.hideChat() }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
    ) {
 | 
			
		||||
        LazyColumn(
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +107,8 @@ fun CampaignChat(
 | 
			
		|||
            ) {
 | 
			
		||||
                when (it) {
 | 
			
		||||
                    is RollTextMessageUio -> RollTextMessage(message = it)
 | 
			
		||||
                    is DiminishedTextMessageUio -> DiminishedTextMessage(message = it)
 | 
			
		||||
                    is CharacteristicTextMessageUio -> CharacteristicTextMessage(message = it)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +119,7 @@ fun CampaignChat(
 | 
			
		|||
private fun ChatScrollDownEffect(
 | 
			
		||||
    lazyState: LazyListState,
 | 
			
		||||
    messages: State<List<TextMessage>>,
 | 
			
		||||
    settings: State<Settings>,
 | 
			
		||||
    displayChat: suspend () -> Unit,
 | 
			
		||||
    hideChat: suspend () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,11 +127,17 @@ private fun ChatScrollDownEffect(
 | 
			
		|||
        key1 = messages.value.lastOrNull()?.id,
 | 
			
		||||
    ) {
 | 
			
		||||
        if (messages.value.isNotEmpty()) {
 | 
			
		||||
            displayChat()
 | 
			
		||||
            lazyState.animateScrollToItem(
 | 
			
		||||
                index = messages.value.lastIndex + 1,
 | 
			
		||||
            )
 | 
			
		||||
            hideChat()
 | 
			
		||||
            if (settings.value.autoShowChat) {
 | 
			
		||||
                displayChat()
 | 
			
		||||
            }
 | 
			
		||||
            if (settings.value.autoScrollChat) {
 | 
			
		||||
                lazyState.animateScrollToItem(
 | 
			
		||||
                    index = messages.value.lastIndex + 1,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (settings.value.autoHideChat) {
 | 
			
		||||
                hideChat()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween
 | 
			
		|||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
| 
						 | 
				
			
			@ -15,8 +16,11 @@ import kotlinx.coroutines.flow.stateIn
 | 
			
		|||
class CampaignChatViewModel(
 | 
			
		||||
    networkRepository: NetworkRepository,
 | 
			
		||||
    textMessageFactory: TextMessageFactory,
 | 
			
		||||
    settingsRepository: SettingsRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    val settings = settingsRepository.settingsFlow()
 | 
			
		||||
 | 
			
		||||
    val chatAnimatedVisibility = Animatable(0f)
 | 
			
		||||
 | 
			
		||||
    private var _messages = emptyList<TextMessage>()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,40 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.campaign.chat
 | 
			
		||||
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessageUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessageUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage
 | 
			
		||||
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.Campaign
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Damage
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Power
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.Message
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.chat__characteristic__hp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.chat__characteristic__pp
 | 
			
		||||
import org.jetbrains.compose.resources.getString
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
 | 
			
		||||
class TextMessageFactory(
 | 
			
		||||
    private val characterSheetRepository: CharacterSheetRepository,
 | 
			
		||||
    private val alterationRepository: AlterationRepository,
 | 
			
		||||
    private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
 | 
			
		||||
) {
 | 
			
		||||
    private val formatId = SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS")
 | 
			
		||||
    private val formatTime = SimpleDateFormat("HH:mm:ss")
 | 
			
		||||
 | 
			
		||||
    fun convertToTextMessage(
 | 
			
		||||
    suspend fun convertToTextMessage(
 | 
			
		||||
        message: Message,
 | 
			
		||||
    ): TextMessage? {
 | 
			
		||||
        val time = System.currentTimeMillis()
 | 
			
		||||
        val id = formatId.format(time)
 | 
			
		||||
 | 
			
		||||
        return when (val payload = message.value) {
 | 
			
		||||
            is RollMessage -> {
 | 
			
		||||
                val sheetPreview = characterSheetRepository
 | 
			
		||||
| 
						 | 
				
			
			@ -47,8 +61,54 @@ class TextMessageFactory(
 | 
			
		|||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is CampaignMessage.UpdateCharacteristic -> null
 | 
			
		||||
            is CampaignMessage.UpdateDiminished -> null
 | 
			
		||||
            is CampaignMessage.UpdateDiminished -> {
 | 
			
		||||
                val characterInstanceId = Campaign.CharacterInstance.Id(
 | 
			
		||||
                    characterSheetId = payload.characterSheetId,
 | 
			
		||||
                    instanceId = payload.instanceId,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                val sheetPreview = characterSheetRepository
 | 
			
		||||
                    .characterPreview(characterId = payload.characterSheetId)
 | 
			
		||||
                    ?: return null
 | 
			
		||||
 | 
			
		||||
                DiminishedTextMessageUio(
 | 
			
		||||
                    id = id,
 | 
			
		||||
                    timestamp = formatTime.format(time),
 | 
			
		||||
                    character = sheetPreview.name,
 | 
			
		||||
                    diminished = payload.diminished,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            is CampaignMessage.UpdateCharacteristic -> {
 | 
			
		||||
                val sheet = characterSheetRepository.characterDetail(
 | 
			
		||||
                    characterSheetId = payload.characterSheetId,
 | 
			
		||||
                ) ?: return null
 | 
			
		||||
                val characterInstanceId = Campaign.CharacterInstance.Id(
 | 
			
		||||
                    characterSheetId = payload.characterSheetId,
 | 
			
		||||
                    instanceId = payload.instanceId,
 | 
			
		||||
                )
 | 
			
		||||
                val alterations = alterationRepository.alterations(
 | 
			
		||||
                    characterInstanceId = characterInstanceId,
 | 
			
		||||
                )
 | 
			
		||||
                val alteredSheet = alteredCharacterSheetFactory.sheet(
 | 
			
		||||
                    characterSheet = sheet,
 | 
			
		||||
                    alterations = alterations,
 | 
			
		||||
                )
 | 
			
		||||
                CharacteristicTextMessageUio(
 | 
			
		||||
                    id = id,
 | 
			
		||||
                    timestamp = formatTime.format(time),
 | 
			
		||||
                    character = sheet.name,
 | 
			
		||||
                    value = when (payload.characteristic) {
 | 
			
		||||
                        Damage -> alteredSheet.maxHp - payload.value
 | 
			
		||||
                        Power -> alteredSheet.maxPp - payload.value
 | 
			
		||||
                    },
 | 
			
		||||
                    characteristic = when (payload.characteristic) {
 | 
			
		||||
                        Damage -> getString(Res.string.chat__characteristic__hp)
 | 
			
		||||
                        Power -> getString(Res.string.chat__characteristic__pp)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            RestSynchronisation.Campaign -> null
 | 
			
		||||
            is RestSynchronisation.CharacterDelete -> null
 | 
			
		||||
            is RestSynchronisation.CharacterUpdate -> null
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change
 | 
			
		||||
import org.jetbrains.compose.resources.stringResource
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CharacteristicTextMessageUio(
 | 
			
		||||
    override val id: String,
 | 
			
		||||
    override val timestamp: String,
 | 
			
		||||
    val character: String,
 | 
			
		||||
    val value: Int,
 | 
			
		||||
    val characteristic: String,
 | 
			
		||||
) : TextMessage
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CharacteristicTextMessage(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    message: CharacteristicTextMessageUio,
 | 
			
		||||
) {
 | 
			
		||||
    Row(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(space = 3.dp),
 | 
			
		||||
        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
    ) {
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier.alignByBaseline(),
 | 
			
		||||
            style = MaterialTheme.lwa.typography.chat.timestamp,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            text = message.timestamp,
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier.alignByBaseline(),
 | 
			
		||||
            style = MaterialTheme.lwa.typography.chat.timestamp,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            text = ">",
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier.alignByBaseline(),
 | 
			
		||||
            style = MaterialTheme.lwa.typography.chat.text,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            text = stringResource(
 | 
			
		||||
                Res.string.chat__characteristic_change,
 | 
			
		||||
                message.character,
 | 
			
		||||
                message.value,
 | 
			
		||||
                message.characteristic,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.chat__diminished_change
 | 
			
		||||
import org.jetbrains.compose.resources.stringResource
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class DiminishedTextMessageUio(
 | 
			
		||||
    override val id: String,
 | 
			
		||||
    override val timestamp: String,
 | 
			
		||||
    val character: String,
 | 
			
		||||
    val diminished: Int,
 | 
			
		||||
) : TextMessage
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun DiminishedTextMessage(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    message: DiminishedTextMessageUio,
 | 
			
		||||
) {
 | 
			
		||||
    Row(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(space = 3.dp),
 | 
			
		||||
        verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
    ) {
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier.alignByBaseline(),
 | 
			
		||||
            style = MaterialTheme.lwa.typography.chat.timestamp,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            text = message.timestamp,
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier.alignByBaseline(),
 | 
			
		||||
            style = MaterialTheme.lwa.typography.chat.timestamp,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            text = ">",
 | 
			
		||||
        )
 | 
			
		||||
        Text(
 | 
			
		||||
            modifier = Modifier.alignByBaseline(),
 | 
			
		||||
            style = MaterialTheme.lwa.typography.chat.text,
 | 
			
		||||
            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
            maxLines = 1,
 | 
			
		||||
            text = stringResource(
 | 
			
		||||
                Res.string.chat__diminished_change,
 | 
			
		||||
                message.character,
 | 
			
		||||
                message.diminished
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,26 +1,43 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.settings
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.ScrollState
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.rememberScrollState
 | 
			
		||||
import androidx.compose.foundation.verticalScroll
 | 
			
		||||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.IconButton
 | 
			
		||||
import androidx.compose.material.Scaffold
 | 
			
		||||
import androidx.compose.material.Surface
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.TopAppBar
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingItemUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingSection
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingSectionUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingToggleItem
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingToggleItemUio
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SettingsScreen() {
 | 
			
		||||
fun SettingsScreen(
 | 
			
		||||
    viewModel: SettingsViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val screen = LocalScreenController.current
 | 
			
		||||
 | 
			
		||||
    Surface {
 | 
			
		||||
        SettingsContent(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
            items = viewModel.items,
 | 
			
		||||
            onBack = {
 | 
			
		||||
                screen.popBackStack()
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -31,13 +48,18 @@ fun SettingsScreen() {
 | 
			
		|||
@Composable
 | 
			
		||||
private fun SettingsContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    state: ScrollState = rememberScrollState(),
 | 
			
		||||
    spacing: Dp = 8.dp,
 | 
			
		||||
    items: List<SettingItemUio>,
 | 
			
		||||
    onBack: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        topBar = {
 | 
			
		||||
            TopAppBar(
 | 
			
		||||
                title = { },
 | 
			
		||||
                title = {
 | 
			
		||||
                    Text(text = "Paramètres de l\'application")
 | 
			
		||||
                },
 | 
			
		||||
                navigationIcon = {
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                        onClick = onBack,
 | 
			
		||||
| 
						 | 
				
			
			@ -52,10 +74,24 @@ private fun SettingsContent(
 | 
			
		|||
        },
 | 
			
		||||
        content = { paddingValues ->
 | 
			
		||||
            Column(
 | 
			
		||||
                modifier = Modifier.padding(paddingValues = paddingValues),
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(paddingValues = paddingValues)
 | 
			
		||||
                    .verticalScroll(state = state),
 | 
			
		||||
                verticalArrangement = Arrangement.spacedBy(space = spacing),
 | 
			
		||||
            ) {
 | 
			
		||||
                items.forEach {
 | 
			
		||||
                    when (it) {
 | 
			
		||||
                        is SettingSectionUio -> {
 | 
			
		||||
                            SettingSection(item = it)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        is SettingToggleItemUio -> {
 | 
			
		||||
                            SettingToggleItem(item = it)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.settings
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.MutableState
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingSectionUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.settings.composable.SettingToggleItemUio
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
class SettingsViewModel(
 | 
			
		||||
    private val settingsRepository: SettingsRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    private val settings = settingsRepository.settingsFlow()
 | 
			
		||||
    private val states = hashMapOf<String, MutableState<Boolean>>()
 | 
			
		||||
 | 
			
		||||
    val items = listOf(
 | 
			
		||||
        SettingSectionUio(
 | 
			
		||||
            title = "Portrait joueurs."
 | 
			
		||||
        ),
 | 
			
		||||
        SettingToggleItemUio(
 | 
			
		||||
            title = "Dés dynamiques",
 | 
			
		||||
            description = "Affiche un dés à côté du portrait d'un personnage lorsqu\'un jet est fait par ce dernier.",
 | 
			
		||||
            checked = states.dynamicDice,
 | 
			
		||||
            onToggle = {
 | 
			
		||||
                settingsRepository.update(settings = settings.value.copy(dynamicDice = it))
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        SettingSectionUio(
 | 
			
		||||
            title = "Chatlog options."
 | 
			
		||||
        ),
 | 
			
		||||
        SettingToggleItemUio(
 | 
			
		||||
            title = "Afficher automatiquement",
 | 
			
		||||
            description = "Affiche automatiquement le chat lors de la réception d'un message.",
 | 
			
		||||
            checked = states.autoShowChat,
 | 
			
		||||
            onToggle = {
 | 
			
		||||
                settingsRepository.update(settings = settings.value.copy(autoShowChat = it))
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        SettingToggleItemUio(
 | 
			
		||||
            title = "Cacher automatiquement",
 | 
			
		||||
            description = "Cache automatiquement le chat au bout d'un certain temps.",
 | 
			
		||||
            checked = states.autoHideChat,
 | 
			
		||||
            onToggle = {
 | 
			
		||||
                settingsRepository.update(settings = settings.value.copy(autoHideChat = it))
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        SettingToggleItemUio(
 | 
			
		||||
            title = "Défilement automatique",
 | 
			
		||||
            description = "Défilement automatique de chat vers le dernier message reçu lors de la réception de ce dernier.",
 | 
			
		||||
            checked = states.autoScrollChat,
 | 
			
		||||
            onToggle = {
 | 
			
		||||
                settingsRepository.update(settings = settings.value.copy(autoScrollChat = it))
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            settingsRepository.settingsFlow().collect { settings ->
 | 
			
		||||
                states.dynamicDice.value = settings.dynamicDice
 | 
			
		||||
                states.autoShowChat.value = settings.autoShowChat
 | 
			
		||||
                states.autoHideChat.value = settings.autoHideChat
 | 
			
		||||
                states.autoScrollChat.value = settings.autoScrollChat
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val HashMap<String, MutableState<Boolean>>.dynamicDice
 | 
			
		||||
        get() = getOrPut("DYNAMIC_DICE") { mutableStateOf(settings.value.dynamicDice) }
 | 
			
		||||
 | 
			
		||||
    private val HashMap<String, MutableState<Boolean>>.autoShowChat
 | 
			
		||||
        get() = getOrPut("AUTO_SHOW_CHAT") { mutableStateOf(settings.value.autoShowChat) }
 | 
			
		||||
 | 
			
		||||
    private val HashMap<String, MutableState<Boolean>>.autoHideChat
 | 
			
		||||
        get() = getOrPut("AUTO_HIDE_CHAT") { mutableStateOf(settings.value.autoHideChat) }
 | 
			
		||||
 | 
			
		||||
    private val HashMap<String, MutableState<Boolean>>.autoScrollChat
 | 
			
		||||
        get() = getOrPut("AUTO_SCROLL_CHAT") { mutableStateOf(settings.value.autoScrollChat) }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.settings.composable
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
sealed interface SettingItemUio
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.settings.composable
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class SettingSectionUio(
 | 
			
		||||
    val title: String,
 | 
			
		||||
) : SettingItemUio
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SettingSection(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    padding: PaddingValues = PaddingValues(start = 16.dp, top = 32.dp, end = 16.dp),
 | 
			
		||||
    item: SettingSectionUio,
 | 
			
		||||
) {
 | 
			
		||||
    Text(
 | 
			
		||||
        modifier = modifier.padding(paddingValues = padding),
 | 
			
		||||
        style = MaterialTheme.lwa.typography.base.h6,
 | 
			
		||||
        maxLines = 1,
 | 
			
		||||
        text = item.title,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.settings.composable
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Switch
 | 
			
		||||
import androidx.compose.material.SwitchDefaults
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.text.font.FontStyle
 | 
			
		||||
import androidx.compose.ui.unit.Dp
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class SettingToggleItemUio(
 | 
			
		||||
    val title: String,
 | 
			
		||||
    val description: String,
 | 
			
		||||
    val checked: State<Boolean>,
 | 
			
		||||
    val onToggle: (Boolean) -> Unit,
 | 
			
		||||
) : SettingItemUio
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SettingToggleItem(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    padding: PaddingValues = PaddingValues(start = 16.dp, top = 8.dp, end = 16.dp),
 | 
			
		||||
    spacing: Dp = 8.dp,
 | 
			
		||||
    item: SettingToggleItemUio,
 | 
			
		||||
) {
 | 
			
		||||
    Row(
 | 
			
		||||
        modifier = modifier.padding(paddingValues = padding),
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(space = spacing),
 | 
			
		||||
    ) {
 | 
			
		||||
        Column(
 | 
			
		||||
            modifier = Modifier.weight(weight = 1f),
 | 
			
		||||
            verticalArrangement = Arrangement.spacedBy(space = spacing),
 | 
			
		||||
        ) {
 | 
			
		||||
            Text(
 | 
			
		||||
                style = MaterialTheme.lwa.typography.base.body1,
 | 
			
		||||
                maxLines = 1,
 | 
			
		||||
                text = item.title,
 | 
			
		||||
            )
 | 
			
		||||
            Text(
 | 
			
		||||
                style = MaterialTheme.lwa.typography.base.caption,
 | 
			
		||||
                fontStyle = FontStyle.Italic,
 | 
			
		||||
                text = item.description,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Switch(
 | 
			
		||||
            colors = SwitchDefaults.colors(
 | 
			
		||||
                checkedThumbColor = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
            ),
 | 
			
		||||
            onCheckedChange = item.onToggle,
 | 
			
		||||
            checked = item.checked.value
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +8,10 @@ class SettingsUseCase {
 | 
			
		|||
        host = DEFAULT_HOST,
 | 
			
		||||
        port = DEFAULT_PORT,
 | 
			
		||||
        playerName = "",
 | 
			
		||||
        dynamicDice = true,
 | 
			
		||||
        autoHideChat = true,
 | 
			
		||||
        autoShowChat = true,
 | 
			
		||||
        autoScrollChat = true,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue