Add a basic version of the GM screen.
This commit is contained in:
		
							parent
							
								
									35396b6069
								
							
						
					
					
						commit
						6b86a6c075
					
				
					 42 changed files with 969 additions and 784 deletions
				
			
		| 
						 | 
				
			
			@ -47,19 +47,15 @@ import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.navigation.window.WindowsNavHost
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetEditWindow
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetWindow
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.NetworkWindows
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.GameMasterWindow
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWindow
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.LwaTheme
 | 
			
		||||
import com.pixelized.desktop.lwa.utils.InstallCoil
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
| 
						 | 
				
			
			@ -153,10 +149,6 @@ fun ApplicationScope.LwaApplication() {
 | 
			
		|||
@Composable
 | 
			
		||||
private fun MainWindowScreen(
 | 
			
		||||
    dataSyncViewModel: DataSyncViewModel = koinViewModel(),
 | 
			
		||||
    networkViewModel: NetworkViewModel = koinViewModel(),
 | 
			
		||||
    campaignViewModel: CampaignViewModel = koinViewModel(),
 | 
			
		||||
    campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
 | 
			
		||||
    rollViewModel: RollHistoryViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    LaunchedEffect(Unit) {
 | 
			
		||||
        dataSyncViewModel.autoConnect()
 | 
			
		||||
| 
						 | 
				
			
			@ -221,7 +213,6 @@ private fun MainWindowScreen(
 | 
			
		|||
            )
 | 
			
		||||
            WindowsHandler(
 | 
			
		||||
                windowController = windowController,
 | 
			
		||||
                rollViewModel = rollViewModel,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -230,7 +221,6 @@ private fun MainWindowScreen(
 | 
			
		|||
@Composable
 | 
			
		||||
private fun WindowsHandler(
 | 
			
		||||
    windowController: WindowController,
 | 
			
		||||
    rollViewModel: RollHistoryViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    WindowsNavHost(
 | 
			
		||||
        controller = windowController,
 | 
			
		||||
| 
						 | 
				
			
			@ -248,11 +238,9 @@ private fun WindowsHandler(
 | 
			
		|||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                is RollHistoryWindow -> RollHistoryPage(
 | 
			
		||||
                    viewModel = rollViewModel,
 | 
			
		||||
                )
 | 
			
		||||
                is RollHistoryWindow -> RollHistoryPage()
 | 
			
		||||
 | 
			
		||||
                is NetworkWindows -> NetworkPage()
 | 
			
		||||
                is GameMasterWindow -> GameMasterScreen()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsStore
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.TextMessageFactory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory
 | 
			
		||||
| 
						 | 
				
			
			@ -31,9 +31,11 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEdi
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.main.MainPageViewModel
 | 
			
		||||
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.campaign.network.NetworkFactory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterActionUseCase
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterFactory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -112,13 +114,13 @@ val factoryDependencies
 | 
			
		|||
        factoryOf(::CharacterSheetCharacteristicDialogFactory)
 | 
			
		||||
        factoryOf(::TextMessageFactory)
 | 
			
		||||
        factoryOf(::LevelUpFactory)
 | 
			
		||||
        factoryOf(::GameMasterFactory)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
val viewModelDependencies
 | 
			
		||||
    get() = module {
 | 
			
		||||
        viewModelOf(::DataSyncViewModel)
 | 
			
		||||
        viewModelOf(::CampaignViewModel)
 | 
			
		||||
        viewModelOf(::MainPageViewModel)
 | 
			
		||||
        viewModelOf(::CampaignToolbarViewModel)
 | 
			
		||||
        viewModelOf(::CharacterSheetViewModel)
 | 
			
		||||
        viewModelOf(::CharacterSheetEditViewModel)
 | 
			
		||||
        viewModelOf(::RollViewModel)
 | 
			
		||||
| 
						 | 
				
			
			@ -131,9 +133,11 @@ val viewModelDependencies
 | 
			
		|||
        viewModelOf(::CampaignChatViewModel)
 | 
			
		||||
        viewModelOf(::SettingsViewModel)
 | 
			
		||||
        viewModelOf(::LevelUpViewModel)
 | 
			
		||||
        viewModelOf(::GameMasterViewModel)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
val useCaseDependencies
 | 
			
		||||
    get() = module {
 | 
			
		||||
        factoryOf(::SettingsUseCase)
 | 
			
		||||
        factoryOf(::GameMasterActionUseCase)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -17,11 +17,11 @@ interface LwaClient {
 | 
			
		|||
 | 
			
		||||
    suspend fun campaign(): CampaignJson
 | 
			
		||||
 | 
			
		||||
    suspend fun campaignAddCharacter(characterSheetId: String, instanceId: Int)
 | 
			
		||||
    suspend fun campaignAddCharacter(characterSheetId: String)
 | 
			
		||||
 | 
			
		||||
    suspend fun campaignDeleteCharacter(characterSheetId: String, instanceId: Int)
 | 
			
		||||
 | 
			
		||||
    suspend fun campaignAddNpc(characterSheetId: String, instanceId: Int)
 | 
			
		||||
    suspend fun campaignAddNpc(characterSheetId: String)
 | 
			
		||||
 | 
			
		||||
    suspend fun campaignDeleteNpc(characterSheetId: String, instanceId: Int)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,9 +45,8 @@ class LwaClientImpl(
 | 
			
		|||
 | 
			
		||||
    override suspend fun campaignAddCharacter(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ) = client
 | 
			
		||||
        .put("$root/campaign/character/update?characterSheetId=$characterSheetId&instanceId=$instanceId")
 | 
			
		||||
        .put("$root/campaign/character/update?characterSheetId=$characterSheetId")
 | 
			
		||||
        .body<Unit>()
 | 
			
		||||
 | 
			
		||||
    override suspend fun campaignDeleteCharacter(
 | 
			
		||||
| 
						 | 
				
			
			@ -59,9 +58,8 @@ class LwaClientImpl(
 | 
			
		|||
 | 
			
		||||
    override suspend fun campaignAddNpc(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ) = client
 | 
			
		||||
        .put("$root/campaign/npc/update?characterSheetId=$characterSheetId&instanceId=$instanceId")
 | 
			
		||||
        .put("$root/campaign/npc/update?characterSheetId=$characterSheetId")
 | 
			
		||||
        .body<Unit>()
 | 
			
		||||
 | 
			
		||||
    override suspend fun campaignDeleteNpc(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,4 +40,32 @@ class CampaignRepository(
 | 
			
		|||
    ): Campaign.CharacterInstance {
 | 
			
		||||
        return campaignFlow.value.character(characterInstanceId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun addCharacter(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
    ) = store.addCharacter(
 | 
			
		||||
        characterSheetId = characterSheetId,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    suspend fun removeCharacter(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ) = store.removeCharacter(
 | 
			
		||||
        characterSheetId = characterSheetId,
 | 
			
		||||
        instanceId = instanceId,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    suspend fun addNpc(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
    ) = store.addNpc(
 | 
			
		||||
        characterSheetId = characterSheetId,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    suspend fun removeNpc(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ) = store.removeNpc(
 | 
			
		||||
        characterSheetId = characterSheetId,
 | 
			
		||||
        instanceId = instanceId,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,8 +3,8 @@ package com.pixelized.desktop.lwa.repository.campaign
 | 
			
		|||
import com.pixelized.desktop.lwa.network.LwaClient
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.Campaign
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.character
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.npc
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.Message
 | 
			
		||||
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +47,42 @@ class CampaignStore(
 | 
			
		|||
        return data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun addCharacter(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
    ) {
 | 
			
		||||
        client.campaignAddCharacter(
 | 
			
		||||
            characterSheetId = characterSheetId
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun removeCharacter(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ) {
 | 
			
		||||
        client.campaignDeleteCharacter(
 | 
			
		||||
            characterSheetId = characterSheetId,
 | 
			
		||||
            instanceId = instanceId,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun addNpc(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
    ) {
 | 
			
		||||
        client.campaignAddNpc(
 | 
			
		||||
            characterSheetId = characterSheetId
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun removeNpc(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ) {
 | 
			
		||||
        client.campaignDeleteNpc(
 | 
			
		||||
            characterSheetId = characterSheetId,
 | 
			
		||||
            instanceId = instanceId,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // region : WebSocket message Handling.
 | 
			
		||||
 | 
			
		||||
    private suspend fun handleMessage(message: Message) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.repository.campaign.model
 | 
			
		||||
 | 
			
		||||
data class CharacterSheetPreview(
 | 
			
		||||
    val id: String,
 | 
			
		||||
    val characterSheetId: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val level: Int,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ class CharacterSheetRepository(
 | 
			
		|||
    val characterDetailFlow get() = store.detailFlow
 | 
			
		||||
 | 
			
		||||
    fun characterPreview(characterId: String?): CharacterSheetPreview? {
 | 
			
		||||
        return characterSheetPreviewFlow.value.firstOrNull { it.id == characterId }
 | 
			
		||||
        return characterSheetPreviewFlow.value.firstOrNull { it.characterSheetId == characterId }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun characterDetail(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ class CharacterSheetStore(
 | 
			
		|||
        val request = client.characters()
 | 
			
		||||
        val data = request.map {
 | 
			
		||||
            CharacterSheetPreview(
 | 
			
		||||
                id = it.id,
 | 
			
		||||
                characterSheetId = it.id,
 | 
			
		||||
                name = it.name,
 | 
			
		||||
                level = it.level,
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +105,7 @@ class CharacterSheetStore(
 | 
			
		|||
 | 
			
		||||
            is RestSynchronisation.CharacterDelete -> {
 | 
			
		||||
                _previewFlow.value = previewFlow.value.toMutableList()
 | 
			
		||||
                    .also { sheets -> sheets.removeIf { it.id == payload.characterId } }
 | 
			
		||||
                    .also { sheets -> sheets.removeIf { it.characterSheetId == payload.characterId } }
 | 
			
		||||
                _detailFlow.delete(payload.characterId)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ class SettingsFactory(
 | 
			
		|||
            autoHideDelay = settings.autoHideDelay,
 | 
			
		||||
            autoShowChat = settings.autoShowChat,
 | 
			
		||||
            autoScrollChat = settings.autoScrollChat,
 | 
			
		||||
            isGM = settings.isGM,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +46,7 @@ class SettingsFactory(
 | 
			
		|||
            autoHideDelay = json.autoHideDelay ?: default.autoHideDelay,
 | 
			
		||||
            autoShowChat = json.autoShowChat ?: default.autoShowChat,
 | 
			
		||||
            autoScrollChat = json.autoScrollChat ?: default.autoScrollChat,
 | 
			
		||||
            isGM = json.isGM ?: default.isGM,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ data class Settings(
 | 
			
		|||
    val autoHideDelay: Int,
 | 
			
		||||
    val autoShowChat: Boolean,
 | 
			
		||||
    val autoScrollChat: Boolean,
 | 
			
		||||
    val isGM: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    val root: String get() = "http://${"${host}:${port}".removePrefix("http://")}"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -12,4 +12,5 @@ data class SettingsJsonV1(
 | 
			
		|||
    val autoHideDelay: Int?,
 | 
			
		||||
    val autoShowChat: Boolean?,
 | 
			
		||||
    val autoScrollChat: Boolean?,
 | 
			
		||||
    val isGM: Boolean?,
 | 
			
		||||
) : SettingsJson
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,88 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.composable.textfield
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.TextField
 | 
			
		||||
import androidx.compose.material.TextFieldDefaults
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.focus.FocusDirection
 | 
			
		||||
import androidx.compose.ui.platform.LocalFocusManager
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.utils.rememberKeyboardActions
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class LwaTextFieldUio(
 | 
			
		||||
    val enable: Boolean,
 | 
			
		||||
    val labelFlow: StateFlow<String?>,
 | 
			
		||||
    val valueFlow: StateFlow<String>,
 | 
			
		||||
    val placeHolderFlow: StateFlow<String?>,
 | 
			
		||||
    val onValueChange: (String) -> Unit,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun LwaTextField(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    leadingIcon: @Composable (() -> Unit)? = null,
 | 
			
		||||
    trailingIcon: @Composable (() -> Unit)? = null,
 | 
			
		||||
    singleLine: Boolean = true,
 | 
			
		||||
    field: LwaTextFieldUio,
 | 
			
		||||
) {
 | 
			
		||||
    val focus = LocalFocusManager.current
 | 
			
		||||
    val colorScheme = MaterialTheme.colors
 | 
			
		||||
 | 
			
		||||
    val localModifier = if (singleLine) {
 | 
			
		||||
        Modifier.height(height = 56.dp)
 | 
			
		||||
    } else {
 | 
			
		||||
        Modifier
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val label = field.labelFlow.collectAsState()
 | 
			
		||||
    val value = field.valueFlow.collectAsState()
 | 
			
		||||
    val placeHolder = field.placeHolderFlow.collectAsState()
 | 
			
		||||
 | 
			
		||||
    TextField(
 | 
			
		||||
        modifier = localModifier.then(other = modifier),
 | 
			
		||||
        colors = TextFieldDefaults.textFieldColors(
 | 
			
		||||
            backgroundColor = remember(field.enable) {
 | 
			
		||||
                when (field.enable) {
 | 
			
		||||
                    true -> colorScheme.onSurface.copy(alpha = 0.03f)
 | 
			
		||||
                    else -> colorScheme.surface
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        keyboardActions = rememberKeyboardActions {
 | 
			
		||||
            focus.moveFocus(FocusDirection.Next)
 | 
			
		||||
        },
 | 
			
		||||
        enabled = field.enable,
 | 
			
		||||
        singleLine = singleLine,
 | 
			
		||||
        placeholder = placeHolder.value?.let {
 | 
			
		||||
            {
 | 
			
		||||
                Text(
 | 
			
		||||
                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                    maxLines = 1,
 | 
			
		||||
                    text = it
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        label = label.value?.let {
 | 
			
		||||
            {
 | 
			
		||||
                Text(
 | 
			
		||||
                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                    maxLines = 1,
 | 
			
		||||
                    text = it
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        leadingIcon = leadingIcon,
 | 
			
		||||
        trailingIcon = trailingIcon,
 | 
			
		||||
        onValueChange = { field.onValueChange(it) },
 | 
			
		||||
        value = value.value,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,12 +9,7 @@ import androidx.navigation.compose.rememberNavController
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.MainDestination
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableLevelUp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableMainPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableNetworkPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableOldMainPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableSettingsPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
 | 
			
		||||
 | 
			
		||||
val LocalScreenController = compositionLocalOf<NavHostController> {
 | 
			
		||||
    error("MainNavHost controller is not yet ready")
 | 
			
		||||
| 
						 | 
				
			
			@ -35,9 +30,6 @@ fun MainNavHost(
 | 
			
		|||
            composableMainPage()
 | 
			
		||||
            composableSettingsPage()
 | 
			
		||||
            composableLevelUp()
 | 
			
		||||
 | 
			
		||||
            composableNetworkPage()
 | 
			
		||||
            composableOldMainPage()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,29 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.navigation.screen.destination
 | 
			
		||||
 | 
			
		||||
import androidx.navigation.NavGraphBuilder
 | 
			
		||||
import androidx.navigation.NavHostController
 | 
			
		||||
import androidx.navigation.compose.composable
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkScreen
 | 
			
		||||
 | 
			
		||||
@Deprecated(message = "Part of the old UI")
 | 
			
		||||
object NetworkDestination {
 | 
			
		||||
    private const val ROUTE = "network"
 | 
			
		||||
 | 
			
		||||
    fun baseRoute() = ROUTE
 | 
			
		||||
    fun navigationRoute() = ROUTE
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Deprecated(message = "Part of the old UI")
 | 
			
		||||
fun NavGraphBuilder.composableNetworkPage() {
 | 
			
		||||
    composable(
 | 
			
		||||
        route = NetworkDestination.baseRoute(),
 | 
			
		||||
    ) {
 | 
			
		||||
        NetworkScreen()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Deprecated(message = "Part of the old UI")
 | 
			
		||||
fun NavHostController.navigateToNetwork() {
 | 
			
		||||
    val route = NetworkDestination.navigationRoute()
 | 
			
		||||
    navigate(route = route)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,29 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.navigation.screen.destination
 | 
			
		||||
 | 
			
		||||
import androidx.navigation.NavGraphBuilder
 | 
			
		||||
import androidx.navigation.NavHostController
 | 
			
		||||
import androidx.navigation.compose.composable
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.main.OldMainPage
 | 
			
		||||
 | 
			
		||||
@Deprecated(message = "Part of the old UI")
 | 
			
		||||
object OldMainDestination {
 | 
			
		||||
    private const val ROUTE = "old_main"
 | 
			
		||||
 | 
			
		||||
    fun baseRoute() = ROUTE
 | 
			
		||||
    fun navigationRoute() = ROUTE
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Deprecated(message = "Part of the old UI")
 | 
			
		||||
fun NavGraphBuilder.composableOldMainPage() {
 | 
			
		||||
    composable(
 | 
			
		||||
        route = OldMainDestination.baseRoute(),
 | 
			
		||||
    ) {
 | 
			
		||||
        OldMainPage()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Deprecated(message = "Part of the old UI")
 | 
			
		||||
fun NavHostController.navigateToOldMainPage() {
 | 
			
		||||
    val route = OldMainDestination.navigationRoute()
 | 
			
		||||
    navigate(route = route)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import androidx.compose.ui.unit.dp
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
class NetworkWindows(
 | 
			
		||||
class GameMasterWindow(
 | 
			
		||||
    title: String,
 | 
			
		||||
    size: DpSize,
 | 
			
		||||
) : Window(
 | 
			
		||||
| 
						 | 
				
			
			@ -14,14 +14,15 @@ class NetworkWindows(
 | 
			
		|||
    size = size,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
fun WindowController.navigateToNetwork(
 | 
			
		||||
    title: String = "",
 | 
			
		||||
fun WindowController.navigateToGameMasterWindow(
 | 
			
		||||
    title: String = "Game master",
 | 
			
		||||
) {
 | 
			
		||||
    showWindow(
 | 
			
		||||
        window = NetworkWindows(
 | 
			
		||||
            title = title, size = DpSize(
 | 
			
		||||
                width = 464.dp,
 | 
			
		||||
                height = 300.dp,
 | 
			
		||||
        window = GameMasterWindow(
 | 
			
		||||
            title = title,
 | 
			
		||||
            size = DpSize(
 | 
			
		||||
                width = 400.dp + 64.dp,
 | 
			
		||||
                height = maxWindowHeight - 32.dp,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +19,8 @@ fun WindowController.navigateToRollHistory(
 | 
			
		|||
) {
 | 
			
		||||
    showWindow(
 | 
			
		||||
        window = RollHistoryWindow(
 | 
			
		||||
            title = title, size = DpSize(
 | 
			
		||||
            title = title,
 | 
			
		||||
            size = DpSize(
 | 
			
		||||
                width = 400.dp + 64.dp,
 | 
			
		||||
                height = maxWindowHeight,
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,10 +42,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDimin
 | 
			
		|||
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +56,7 @@ fun CampaignScreen(
 | 
			
		|||
    characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
 | 
			
		||||
    characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
 | 
			
		||||
    dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
 | 
			
		||||
    campaignViewModel: CampaignViewModel = koinViewModel(),
 | 
			
		||||
    campaignViewModel: CampaignToolbarViewModel = koinViewModel(),
 | 
			
		||||
    networkViewModel: NetworkViewModel = koinViewModel(),
 | 
			
		||||
    campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
| 
						 | 
				
			
			@ -88,8 +86,7 @@ fun CampaignScreen(
 | 
			
		|||
                modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                top = {
 | 
			
		||||
                    CampaignToolbar(
 | 
			
		||||
                        campaignViewModel = campaignViewModel,
 | 
			
		||||
                        networkViewModel = networkViewModel,
 | 
			
		||||
                        viewModel = campaignViewModel,
 | 
			
		||||
                    )
 | 
			
		||||
                },
 | 
			
		||||
                bottom = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,58 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.campaign
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
class CampaignViewModel(
 | 
			
		||||
    private val characterRepository: CharacterSheetRepository,
 | 
			
		||||
    private val alterationRepository: AlterationRepository,
 | 
			
		||||
    private val campaignRepository: CampaignRepository,
 | 
			
		||||
    private val network: NetworkRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    val title: Flow<String> = campaignRepository.campaignFlow
 | 
			
		||||
        .map { it.scene.name }
 | 
			
		||||
 | 
			
		||||
    val networkStatus = network.status
 | 
			
		||||
 | 
			
		||||
    fun init() {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            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,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.network
 | 
			
		||||
package com.pixelized.desktop.lwa.ui.screen.campaign.network
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedContent
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
| 
						 | 
				
			
			@ -27,15 +27,11 @@ import androidx.compose.material.CircularProgressIndicator
 | 
			
		|||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.IconButton
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Scaffold
 | 
			
		||||
import androidx.compose.material.SnackbarDuration
 | 
			
		||||
import androidx.compose.material.Surface
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.TextButton
 | 
			
		||||
import androidx.compose.material.TextField
 | 
			
		||||
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.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
| 
						 | 
				
			
			@ -44,12 +40,10 @@ import androidx.compose.runtime.collectAsState
 | 
			
		|||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
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.LocalSnackHost
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnack
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +53,6 @@ import lwacharactersheet.composeapp.generated.resources.network__player_name__la
 | 
			
		|||
import lwacharactersheet.composeapp.generated.resources.network__port__label
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.network__title
 | 
			
		||||
import org.jetbrains.compose.resources.painterResource
 | 
			
		||||
import org.jetbrains.compose.resources.stringResource
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
| 
						 | 
				
			
			@ -100,93 +93,7 @@ data class NetworkPageUio(
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun NetworkScreen(
 | 
			
		||||
    viewModel: NetworkViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val screen = LocalScreenController.current
 | 
			
		||||
    val snack = LocalSnackHost.current
 | 
			
		||||
    val scope = rememberCoroutineScope()
 | 
			
		||||
 | 
			
		||||
    Surface(
 | 
			
		||||
        modifier = Modifier.fillMaxSize(),
 | 
			
		||||
    ) {
 | 
			
		||||
        Box(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
            contentAlignment = Alignment.Center,
 | 
			
		||||
        ) {
 | 
			
		||||
            BlurContent(
 | 
			
		||||
                modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                controller = viewModel.blurController,
 | 
			
		||||
            ) {
 | 
			
		||||
                Scaffold(
 | 
			
		||||
                    modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                    topBar = {
 | 
			
		||||
                        TopAppBar(
 | 
			
		||||
                            title = {
 | 
			
		||||
                                Text(
 | 
			
		||||
                                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                    maxLines = 1,
 | 
			
		||||
                                    text = stringResource(Res.string.network__title),
 | 
			
		||||
                                )
 | 
			
		||||
                            },
 | 
			
		||||
                            navigationIcon = {
 | 
			
		||||
                                IconButton(
 | 
			
		||||
                                    onClick = { screen.popBackStack() },
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    Icon(
 | 
			
		||||
                                        imageVector = Icons.AutoMirrored.Filled.ArrowBack,
 | 
			
		||||
                                        contentDescription = null,
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        )
 | 
			
		||||
                    },
 | 
			
		||||
                    content = { paddingValues ->
 | 
			
		||||
                        NetworkContent(
 | 
			
		||||
                            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
                            paddingValues = paddingValues,
 | 
			
		||||
                            network = viewModel.network.collectAsState(),
 | 
			
		||||
                            onPlayerChange = viewModel::onPlayerNameChange,
 | 
			
		||||
                            onHostChange = viewModel::onHostChange,
 | 
			
		||||
                            onResetPortChange = viewModel::onResetPortChange,
 | 
			
		||||
                            onPortChange = viewModel::onPortChange,
 | 
			
		||||
                            onResetHostChange = viewModel::onResetHostChange,
 | 
			
		||||
                            onConnect = { scope.launch { viewModel.connect() } },
 | 
			
		||||
                            onDisconnect = viewModel::disconnect,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AnimatedContent(
 | 
			
		||||
                modifier = Modifier.size(size = 64.dp),
 | 
			
		||||
                targetState = viewModel.isLoading.value,
 | 
			
		||||
                transitionSpec = { fadeIn() togetherWith fadeOut() },
 | 
			
		||||
            ) {
 | 
			
		||||
                when (it) {
 | 
			
		||||
                    true -> CircularProgressIndicator()
 | 
			
		||||
                    else -> Box(modifier = Modifier)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(Unit) {
 | 
			
		||||
            viewModel.message.collect {
 | 
			
		||||
                snack.showSnackbar(
 | 
			
		||||
                    message = it,
 | 
			
		||||
                    duration = SnackbarDuration.Short,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ErrorSnack(
 | 
			
		||||
            error = viewModel.networkError,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun NetworkPage(
 | 
			
		||||
fun NetworkDialog(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    viewModel: NetworkViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.network
 | 
			
		||||
package com.pixelized.desktop.lwa.ui.screen.campaign.network
 | 
			
		||||
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.network
 | 
			
		||||
package com.pixelized.desktop.lwa.ui.screen.campaign.network
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material.SnackbarDuration
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,18 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
import androidx.compose.animation.fadeOut
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.shape.CircleShape
 | 
			
		||||
import androidx.compose.material.DropdownMenu
 | 
			
		||||
import androidx.compose.material.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.IconButton
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.TextButton
 | 
			
		||||
import androidx.compose.material.TopAppBar
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.MoreVert
 | 
			
		||||
| 
						 | 
				
			
			@ -17,33 +22,32 @@ import androidx.compose.runtime.collectAsState
 | 
			
		|||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.text.font.FontWeight
 | 
			
		||||
import androidx.compose.ui.unit.DpOffset
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.LocalWindowController
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToOldMainPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToSettings
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToGameMasterWindow
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkDialog
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_settings_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_table_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_timeline_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_wifi_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_wifi_off_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.settings__title
 | 
			
		||||
import org.jetbrains.compose.resources.painterResource
 | 
			
		||||
import org.jetbrains.compose.resources.stringResource
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun CampaignToolbar(
 | 
			
		||||
    campaignViewModel: CampaignViewModel = koinViewModel(),
 | 
			
		||||
    networkViewModel: NetworkViewModel = koinViewModel(),
 | 
			
		||||
    viewModel: CampaignToolbarViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val windows = LocalWindowController.current
 | 
			
		||||
    val screen = LocalScreenController.current
 | 
			
		||||
| 
						 | 
				
			
			@ -51,60 +55,31 @@ fun CampaignToolbar(
 | 
			
		|||
    val isOverflowMenuOpen = remember { mutableStateOf(false) }
 | 
			
		||||
    val isNetworkMenuOpen = remember { mutableStateOf(false) }
 | 
			
		||||
 | 
			
		||||
    val title = viewModel.title.collectAsState()
 | 
			
		||||
    val status = viewModel.status.collectAsState()
 | 
			
		||||
    val isGM = viewModel.isGM.collectAsState()
 | 
			
		||||
 | 
			
		||||
    CampaignToolbarContent(
 | 
			
		||||
        title = campaignViewModel.title.collectAsState(initial = ""),
 | 
			
		||||
        networkStatus = campaignViewModel.networkStatus.collectAsState(),
 | 
			
		||||
        title = title,
 | 
			
		||||
        status = status,
 | 
			
		||||
        isGM = isGM,
 | 
			
		||||
        isNetworkMenuOpen = isNetworkMenuOpen,
 | 
			
		||||
        isOverflowMenuOpen = isOverflowMenuOpen,
 | 
			
		||||
        networkMenu = {
 | 
			
		||||
            NetworkPage(
 | 
			
		||||
                modifier = Modifier.size(384.dp + 96.dp, 240.dp),
 | 
			
		||||
                viewModel = networkViewModel
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        overflowMenu = {
 | 
			
		||||
            DropdownMenuItem(
 | 
			
		||||
                onClick = {
 | 
			
		||||
                    isOverflowMenuOpen.value = false
 | 
			
		||||
                    windows.navigateToRollHistory()
 | 
			
		||||
                },
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_timeline_24dp),
 | 
			
		||||
                    tint = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.padding(start = 8.dp),
 | 
			
		||||
                    color = MaterialTheme.colors.primary,
 | 
			
		||||
                    text = stringResource(Res.string.main_page__roll_history_action),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            DropdownMenuItem(
 | 
			
		||||
                onClick = {
 | 
			
		||||
                    isOverflowMenuOpen.value = false
 | 
			
		||||
                    screen.navigateToOldMainPage()
 | 
			
		||||
                },
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_table_24dp),
 | 
			
		||||
                    tint = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
                Text(
 | 
			
		||||
                    modifier = Modifier.padding(start = 8.dp),
 | 
			
		||||
                    color = MaterialTheme.colors.primary,
 | 
			
		||||
                    text = "Ancienne interface utilisateur",
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        onGM = {
 | 
			
		||||
            windows.navigateToGameMasterWindow()
 | 
			
		||||
        },
 | 
			
		||||
        onNetwork = {
 | 
			
		||||
            isNetworkMenuOpen.value = true
 | 
			
		||||
        },
 | 
			
		||||
        onOverflow = {
 | 
			
		||||
            isOverflowMenuOpen.value = isOverflowMenuOpen.value.not()
 | 
			
		||||
            isOverflowMenuOpen.value = true
 | 
			
		||||
        },
 | 
			
		||||
        onRollHistory = {
 | 
			
		||||
            isOverflowMenuOpen.value = false
 | 
			
		||||
            windows.navigateToRollHistory()
 | 
			
		||||
        },
 | 
			
		||||
        onSettings = {
 | 
			
		||||
            isOverflowMenuOpen.value = false
 | 
			
		||||
            screen.navigateToSettings()
 | 
			
		||||
        },
 | 
			
		||||
        onDismissNetworkMenu = {
 | 
			
		||||
| 
						 | 
				
			
			@ -120,13 +95,14 @@ fun CampaignToolbar(
 | 
			
		|||
private fun CampaignToolbarContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    title: State<String>,
 | 
			
		||||
    networkStatus: State<NetworkRepository.Status>,
 | 
			
		||||
    status: State<NetworkRepository.Status>,
 | 
			
		||||
    isGM: State<Boolean>,
 | 
			
		||||
    isNetworkMenuOpen: State<Boolean>,
 | 
			
		||||
    isOverflowMenuOpen: State<Boolean>,
 | 
			
		||||
    networkMenu: @Composable () -> Unit,
 | 
			
		||||
    overflowMenu: @Composable () -> Unit,
 | 
			
		||||
    onGM: () -> Unit,
 | 
			
		||||
    onNetwork: () -> Unit,
 | 
			
		||||
    onOverflow: () -> Unit,
 | 
			
		||||
    onRollHistory: () -> Unit,
 | 
			
		||||
    onSettings: () -> Unit,
 | 
			
		||||
    onDismissNetworkMenu: () -> Unit,
 | 
			
		||||
    onDismissOverflowMenu: () -> Unit,
 | 
			
		||||
| 
						 | 
				
			
			@ -139,38 +115,38 @@ private fun CampaignToolbarContent(
 | 
			
		|||
            )
 | 
			
		||||
        },
 | 
			
		||||
        actions = {
 | 
			
		||||
            AnimatedVisibility(
 | 
			
		||||
                visible = isGM.value,
 | 
			
		||||
                enter = fadeIn(),
 | 
			
		||||
                exit = fadeOut(),
 | 
			
		||||
            ) {
 | 
			
		||||
                TextButton(
 | 
			
		||||
                    modifier = Modifier.size(size = 48.dp).clip(shape = CircleShape),
 | 
			
		||||
                    onClick = onGM,
 | 
			
		||||
                ) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        fontWeight = FontWeight.SemiBold,
 | 
			
		||||
                        text = "GM",
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            IconButton(
 | 
			
		||||
                onClick = onNetwork
 | 
			
		||||
                onClick = onNetwork,
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    painter = painterResource(
 | 
			
		||||
                        when (networkStatus.value) {
 | 
			
		||||
                        when (status.value) {
 | 
			
		||||
                            NetworkRepository.Status.CONNECTED -> Res.drawable.ic_wifi_24dp
 | 
			
		||||
                            NetworkRepository.Status.DISCONNECTED -> Res.drawable.ic_wifi_off_24dp
 | 
			
		||||
                        }
 | 
			
		||||
                    ),
 | 
			
		||||
                    tint = when (networkStatus.value) {
 | 
			
		||||
                    tint = when (status.value) {
 | 
			
		||||
                        NetworkRepository.Status.CONNECTED -> MaterialTheme.lwa.colorScheme.base.primary
 | 
			
		||||
                        NetworkRepository.Status.DISCONNECTED -> MaterialTheme.lwa.colorScheme.base.error
 | 
			
		||||
                    },
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            DropdownMenu(
 | 
			
		||||
                offset = remember { DpOffset(x = -(48.dp * 2 + 8.dp), y = 8.dp) },
 | 
			
		||||
                expanded = isNetworkMenuOpen.value,
 | 
			
		||||
                onDismissRequest = onDismissNetworkMenu,
 | 
			
		||||
                content = { networkMenu() },
 | 
			
		||||
            )
 | 
			
		||||
            IconButton(
 | 
			
		||||
                onClick = onSettings
 | 
			
		||||
            ) {
 | 
			
		||||
                Icon(
 | 
			
		||||
                    painter = painterResource(Res.drawable.ic_settings_24dp),
 | 
			
		||||
                    tint = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            IconButton(
 | 
			
		||||
                onClick = onOverflow,
 | 
			
		||||
            ) {
 | 
			
		||||
| 
						 | 
				
			
			@ -180,11 +156,50 @@ private fun CampaignToolbarContent(
 | 
			
		|||
                    contentDescription = null,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            DropdownMenu(
 | 
			
		||||
                offset = remember { DpOffset(x = -(48.dp + 8.dp), y = 8.dp) },
 | 
			
		||||
                expanded = isNetworkMenuOpen.value,
 | 
			
		||||
                onDismissRequest = onDismissNetworkMenu,
 | 
			
		||||
                content = {
 | 
			
		||||
                    NetworkDialog(
 | 
			
		||||
                        modifier = Modifier.size(384.dp + 96.dp, 240.dp),
 | 
			
		||||
                    )
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            DropdownMenu(
 | 
			
		||||
                offset = remember { DpOffset(x = (-8).dp, y = 8.dp) },
 | 
			
		||||
                expanded = isOverflowMenuOpen.value,
 | 
			
		||||
                onDismissRequest = onDismissOverflowMenu,
 | 
			
		||||
                content = { overflowMenu() },
 | 
			
		||||
                content = {
 | 
			
		||||
                    DropdownMenuItem(
 | 
			
		||||
                        onClick = onRollHistory,
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Icon(
 | 
			
		||||
                            painter = painterResource(Res.drawable.ic_timeline_24dp),
 | 
			
		||||
                            tint = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                            contentDescription = null,
 | 
			
		||||
                        )
 | 
			
		||||
                        Text(
 | 
			
		||||
                            modifier = Modifier.padding(start = 8.dp),
 | 
			
		||||
                            color = MaterialTheme.colors.primary,
 | 
			
		||||
                            text = stringResource(Res.string.main_page__roll_history_action),
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    DropdownMenuItem(
 | 
			
		||||
                        onClick = onSettings,
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Icon(
 | 
			
		||||
                            painter = painterResource(Res.drawable.ic_settings_24dp),
 | 
			
		||||
                            tint = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                            contentDescription = null,
 | 
			
		||||
                        )
 | 
			
		||||
                        Text(
 | 
			
		||||
                            modifier = Modifier.padding(start = 8.dp),
 | 
			
		||||
                            color = MaterialTheme.colors.primary,
 | 
			
		||||
                            text = stringResource(Res.string.settings__title),
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
 | 
			
		||||
class CampaignToolbarViewModel(
 | 
			
		||||
    campaignRepository: CampaignRepository,
 | 
			
		||||
    networkRepository: NetworkRepository,
 | 
			
		||||
    settingsRepository: SettingsRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    val status = networkRepository.status
 | 
			
		||||
 | 
			
		||||
    val title = campaignRepository.campaignFlow
 | 
			
		||||
        .map { it.scene.name }
 | 
			
		||||
        .stateIn(
 | 
			
		||||
            scope = viewModelScope,
 | 
			
		||||
            started = SharingStarted.Lazily,
 | 
			
		||||
            initialValue = "",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    val isGM = settingsRepository.settingsFlow()
 | 
			
		||||
        .map { it.isGM }
 | 
			
		||||
        .stateIn(
 | 
			
		||||
            scope = viewModelScope,
 | 
			
		||||
            started = SharingStarted.Lazily,
 | 
			
		||||
            initialValue = false,
 | 
			
		||||
        )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +94,7 @@ class CharacterSheetViewModel(
 | 
			
		|||
            characterId = argument.characterInstanceId.characterSheetId
 | 
			
		||||
        ) ?: return
 | 
			
		||||
        _displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio(
 | 
			
		||||
            id = preview.id,
 | 
			
		||||
            id = preview.characterSheetId,
 | 
			
		||||
            name = preview.name,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.gamemaster
 | 
			
		||||
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
 | 
			
		||||
 | 
			
		||||
class GameMasterActionUseCase(
 | 
			
		||||
    private val campaignRepository: CampaignRepository,
 | 
			
		||||
) {
 | 
			
		||||
    suspend fun handleAction(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        action: Action,
 | 
			
		||||
    ) {
 | 
			
		||||
        when (action) {
 | 
			
		||||
            Action.DisplayPortrait -> TODO()
 | 
			
		||||
 | 
			
		||||
            Action.AddToGroup -> campaignRepository.addCharacter(
 | 
			
		||||
                characterSheetId = characterSheetId,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            Action.AddToNpc -> campaignRepository.addNpc(
 | 
			
		||||
                characterSheetId = characterSheetId,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            is Action.RemoveFromGroup -> campaignRepository.removeCharacter(
 | 
			
		||||
                characterSheetId = characterSheetId,
 | 
			
		||||
                instanceId = action.instanceId
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            is Action.RemoveFromNpc -> campaignRepository.removeNpc(
 | 
			
		||||
                characterSheetId = characterSheetId,
 | 
			
		||||
                instanceId = action.instanceId
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,119 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.gamemaster
 | 
			
		||||
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.Campaign
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_label
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_search
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_label
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_search
 | 
			
		||||
import org.jetbrains.compose.resources.getString
 | 
			
		||||
import java.text.Normalizer
 | 
			
		||||
 | 
			
		||||
class GameMasterFactory {
 | 
			
		||||
 | 
			
		||||
    suspend fun convertToGMCharacterPreviewUio(
 | 
			
		||||
        campaign: Campaign,
 | 
			
		||||
        characters: List<CharacterSheetPreview>,
 | 
			
		||||
        filter: String,
 | 
			
		||||
    ): List<GMCharacterPreviewUio> {
 | 
			
		||||
        val normalizedFilter = Normalizer.normalize(filter, Normalizer.Form.NFD)
 | 
			
		||||
 | 
			
		||||
        return characters.mapNotNull {
 | 
			
		||||
            convertToGMCharacterPreviewUio(
 | 
			
		||||
                campaign = campaign,
 | 
			
		||||
                character = it,
 | 
			
		||||
                filter = normalizedFilter,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun convertToGMCharacterPreviewUio(
 | 
			
		||||
        campaign: Campaign,
 | 
			
		||||
        character: CharacterSheetPreview,
 | 
			
		||||
        filter: String,
 | 
			
		||||
    ): GMCharacterPreviewUio? {
 | 
			
		||||
        val characterId = campaign.characters.keys.firstOrNull {
 | 
			
		||||
            it.characterSheetId == character.characterSheetId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val npcIds = campaign.npcs.keys.filter {
 | 
			
		||||
            it.characterSheetId == character.characterSheetId
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var playerTagHighlighted = false
 | 
			
		||||
        var npcTagHighlighted = false
 | 
			
		||||
 | 
			
		||||
        // Filter process.
 | 
			
		||||
        if (filter.isNotEmpty()) {
 | 
			
		||||
            val normalizedName = Normalizer.normalize(character.name, Normalizer.Form.NFD)
 | 
			
		||||
            // If the filter is not empty and the character is not
 | 
			
		||||
            val playerTag = getString(Res.string.game_master__character_tag__character_search)
 | 
			
		||||
            val npcTag = getString(Res.string.game_master__character_tag__npc_search)
 | 
			
		||||
 | 
			
		||||
            playerTagHighlighted = playerTag.contains(other = filter, ignoreCase = true)
 | 
			
		||||
            if (playerTagHighlighted && characterId == null) {
 | 
			
		||||
                return null
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            npcTagHighlighted = npcTag.contains(other = filter, ignoreCase = true)
 | 
			
		||||
            if (npcTagHighlighted && npcIds.isEmpty()) {
 | 
			
		||||
                return null
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val nameHighlight = normalizedName.contains(other = filter, ignoreCase = true)
 | 
			
		||||
            if (nameHighlight.not() && playerTagHighlighted.not() && npcTagHighlighted.not()) {
 | 
			
		||||
                return null
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val tags = buildList {
 | 
			
		||||
            if (characterId != null) {
 | 
			
		||||
                add(
 | 
			
		||||
                    GMCharacterPreviewUio.Tag(
 | 
			
		||||
                        label = getString(
 | 
			
		||||
                            Res.string.game_master__character_tag__character_label,
 | 
			
		||||
                            characterId.instanceId,
 | 
			
		||||
                        ),
 | 
			
		||||
                        highlight = playerTagHighlighted,
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            addAll(
 | 
			
		||||
                npcIds.map { npcId ->
 | 
			
		||||
                    GMCharacterPreviewUio.Tag(
 | 
			
		||||
                        label = getString(
 | 
			
		||||
                            Res.string.game_master__character_tag__npc_label,
 | 
			
		||||
                            npcId.instanceId
 | 
			
		||||
                        ),
 | 
			
		||||
                        highlight = npcTagHighlighted,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val actions = buildList {
 | 
			
		||||
            add(
 | 
			
		||||
                when (characterId) {
 | 
			
		||||
                    null -> Action.AddToGroup
 | 
			
		||||
                    else -> Action.RemoveFromGroup(instanceId = characterId.instanceId)
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            add(Action.AddToNpc)
 | 
			
		||||
            addAll(
 | 
			
		||||
                npcIds.map { npcId ->
 | 
			
		||||
                    Action.RemoveFromNpc(instanceId = npcId.instanceId)
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return GMCharacterPreviewUio(
 | 
			
		||||
            characterSheetId = character.characterSheetId,
 | 
			
		||||
            name = character.name, level = character.level,
 | 
			
		||||
            tags = tags,
 | 
			
		||||
            actions = actions,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.gamemaster
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
import androidx.compose.animation.fadeOut
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.lazy.LazyColumn
 | 
			
		||||
import androidx.compose.foundation.lazy.items
 | 
			
		||||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.IconButton
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Scaffold
 | 
			
		||||
import androidx.compose.material.Surface
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.TopAppBar
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreview
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
 | 
			
		||||
import org.jetbrains.compose.resources.painterResource
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun GameMasterScreen(
 | 
			
		||||
    viewModel: GameMasterViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val characters = viewModel.characters.collectAsState()
 | 
			
		||||
 | 
			
		||||
    Surface(
 | 
			
		||||
        modifier = Modifier.fillMaxSize()
 | 
			
		||||
    ) {
 | 
			
		||||
        GameMasterContent(
 | 
			
		||||
            modifier = Modifier.fillMaxSize(),
 | 
			
		||||
            filter = viewModel.filter,
 | 
			
		||||
            characters = characters,
 | 
			
		||||
            onCharacterAction = viewModel::onCharacterAction,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun GameMasterContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    filter: LwaTextFieldUio,
 | 
			
		||||
    characters: State<List<GMCharacterPreviewUio>>,
 | 
			
		||||
    onCharacterAction: (String, GMCharacterPreviewUio.Action) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        topBar = {
 | 
			
		||||
            TopAppBar(
 | 
			
		||||
                title = {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = "",
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        content = { paddingValues ->
 | 
			
		||||
            Column(
 | 
			
		||||
                modifier = Modifier.padding(paddingValues = paddingValues)
 | 
			
		||||
            ) {
 | 
			
		||||
                LwaTextField(
 | 
			
		||||
                    modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                    field = filter,
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        val value = filter.valueFlow.collectAsState()
 | 
			
		||||
                        AnimatedVisibility(
 | 
			
		||||
                            visible = value.value.isNotBlank(),
 | 
			
		||||
                            enter = fadeIn(),
 | 
			
		||||
                            exit = fadeOut(),
 | 
			
		||||
                        ) {
 | 
			
		||||
                            IconButton(
 | 
			
		||||
                                onClick = { filter.onValueChange.invoke("") },
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    painter = painterResource(Res.drawable.ic_cancel_24dp),
 | 
			
		||||
                                    tint = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                                    contentDescription = null,
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                LazyColumn(
 | 
			
		||||
                    modifier = Modifier.weight(1f),
 | 
			
		||||
                    contentPadding = PaddingValues(all = 8.dp),
 | 
			
		||||
                    verticalArrangement = Arrangement.spacedBy(space = 8.dp),
 | 
			
		||||
                ) {
 | 
			
		||||
                    items(
 | 
			
		||||
                        items = characters.value,
 | 
			
		||||
                    ) { character ->
 | 
			
		||||
                        GMCharacterPreview(
 | 
			
		||||
                            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                            character = character,
 | 
			
		||||
                            onAction = { action ->
 | 
			
		||||
                                onCharacterAction(character.characterSheetId, action)
 | 
			
		||||
                            }
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.gamemaster
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
class GameMasterViewModel(
 | 
			
		||||
    private val campaignRepository: CampaignRepository,
 | 
			
		||||
    private val characterSheetRepository: CharacterSheetRepository,
 | 
			
		||||
    private val gameMasterFactory: GameMasterFactory,
 | 
			
		||||
    private val useCase: GameMasterActionUseCase,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    private val _filter = MutableStateFlow("")
 | 
			
		||||
    val filter = LwaTextFieldUio(
 | 
			
		||||
        enable = true,
 | 
			
		||||
        labelFlow = MutableStateFlow("Filtre"),
 | 
			
		||||
        valueFlow = _filter,
 | 
			
		||||
        placeHolderFlow = MutableStateFlow(null),
 | 
			
		||||
        onValueChange = { _filter.value = it },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val characters = combine(
 | 
			
		||||
        campaignRepository.campaignFlow,
 | 
			
		||||
        characterSheetRepository.characterSheetPreviewFlow,
 | 
			
		||||
        filter.valueFlow,
 | 
			
		||||
        gameMasterFactory::convertToGMCharacterPreviewUio,
 | 
			
		||||
    ).stateIn(
 | 
			
		||||
        scope = viewModelScope,
 | 
			
		||||
        started = SharingStarted.Eagerly,
 | 
			
		||||
        initialValue = emptyList(),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    fun onCharacterAction(
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        action: GMCharacterPreviewUio.Action,
 | 
			
		||||
    ) {
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            try {
 | 
			
		||||
                useCase.handleAction(
 | 
			
		||||
                    characterSheetId = characterSheetId,
 | 
			
		||||
                    action = action,
 | 
			
		||||
                )
 | 
			
		||||
            } catch (exception: Exception) {
 | 
			
		||||
                // TODO
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,226 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.gamemaster.items
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.calculateStartPadding
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.shape.CircleShape
 | 
			
		||||
import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.material.DropdownMenu
 | 
			
		||||
import androidx.compose.material.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material.Icon
 | 
			
		||||
import androidx.compose.material.IconButton
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.MoreVert
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.draw.clip
 | 
			
		||||
import androidx.compose.ui.platform.LocalLayoutDirection
 | 
			
		||||
import androidx.compose.ui.unit.DpOffset
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.theme.lwa
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__add_to_group
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__add_to_npc
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__display_portrait
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__remove_from_group
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_action__remove_from_npc
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_level__label
 | 
			
		||||
import org.jetbrains.compose.resources.stringResource
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class GMCharacterPreviewUio(
 | 
			
		||||
    val characterSheetId: String,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val level: Int,
 | 
			
		||||
    val tags: List<Tag>,
 | 
			
		||||
    val actions: List<Action>,
 | 
			
		||||
) {
 | 
			
		||||
    @Stable
 | 
			
		||||
    data class Tag(
 | 
			
		||||
        val label: String,
 | 
			
		||||
        val highlight: Boolean,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @Stable
 | 
			
		||||
    sealed class Action {
 | 
			
		||||
        @Stable
 | 
			
		||||
        data object DisplayPortrait : Action()
 | 
			
		||||
 | 
			
		||||
        @Stable
 | 
			
		||||
        data object AddToGroup : Action()
 | 
			
		||||
 | 
			
		||||
        @Stable
 | 
			
		||||
        data class RemoveFromGroup(val instanceId: Int) : Action()
 | 
			
		||||
 | 
			
		||||
        @Stable
 | 
			
		||||
        data object AddToNpc : Action()
 | 
			
		||||
 | 
			
		||||
        @Stable
 | 
			
		||||
        data class RemoveFromNpc(val instanceId: Int) : Action()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object GMCharacterPreviewDefault {
 | 
			
		||||
    val padding = PaddingValues(horizontal = 16.dp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun GMCharacterPreview(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    padding: PaddingValues = GMCharacterPreviewDefault.padding,
 | 
			
		||||
    character: GMCharacterPreviewUio,
 | 
			
		||||
    onAction: (Action) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val layoutDirection = LocalLayoutDirection.current
 | 
			
		||||
    val startPadding = padding.calculateStartPadding(layoutDirection)
 | 
			
		||||
 | 
			
		||||
    Box(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
            .clip(shape = remember { RoundedCornerShape(8.dp) })
 | 
			
		||||
            .background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
 | 
			
		||||
            .then(other = modifier),
 | 
			
		||||
    ) {
 | 
			
		||||
        Column {
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier.padding(start = startPadding),
 | 
			
		||||
                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
            ) {
 | 
			
		||||
                Row(
 | 
			
		||||
                    modifier = Modifier.weight(weight = 1f),
 | 
			
		||||
                    horizontalArrangement = Arrangement.spacedBy(4.dp),
 | 
			
		||||
                ) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                        style = MaterialTheme.lwa.typography.base.body1,
 | 
			
		||||
                        text = character.name,
 | 
			
		||||
                    )
 | 
			
		||||
                    Text(
 | 
			
		||||
                        modifier = Modifier.alignByBaseline(),
 | 
			
		||||
                        style = MaterialTheme.lwa.typography.base.caption,
 | 
			
		||||
                        text = stringResource(
 | 
			
		||||
                            Res.string.game_master__character_level__label,
 | 
			
		||||
                            character.level,
 | 
			
		||||
                        ),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                OverflowActionMenu(
 | 
			
		||||
                    character = character,
 | 
			
		||||
                    onAction = onAction,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            Row(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .padding(paddingValues = padding)
 | 
			
		||||
                    .padding(bottom = 16.dp),
 | 
			
		||||
                horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
 | 
			
		||||
            ) {
 | 
			
		||||
                character.tags.forEach { tag ->
 | 
			
		||||
                    Tag(tag = tag)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun OverflowActionMenu(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    character: GMCharacterPreviewUio,
 | 
			
		||||
    onAction: (Action) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val overflowMenu = remember(character) {
 | 
			
		||||
        mutableStateOf(false)
 | 
			
		||||
    }
 | 
			
		||||
    IconButton(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        onClick = {
 | 
			
		||||
            overflowMenu.value = true
 | 
			
		||||
        },
 | 
			
		||||
    ) {
 | 
			
		||||
        Icon(
 | 
			
		||||
            imageVector = Icons.Default.MoreVert,
 | 
			
		||||
            tint = MaterialTheme.colors.primary,
 | 
			
		||||
            contentDescription = null,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    DropdownMenu(
 | 
			
		||||
        offset = remember { DpOffset(x = -(48.dp + 8.dp), y = -(48.dp)) },
 | 
			
		||||
        expanded = overflowMenu.value,
 | 
			
		||||
        onDismissRequest = {
 | 
			
		||||
            overflowMenu.value = false
 | 
			
		||||
        },
 | 
			
		||||
        content = {
 | 
			
		||||
            character.actions.forEach { action ->
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    onClick = {
 | 
			
		||||
                        overflowMenu.value = false
 | 
			
		||||
                        onAction(action)
 | 
			
		||||
                    },
 | 
			
		||||
                ) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        style = MaterialTheme.lwa.typography.base.body1,
 | 
			
		||||
                        color = MaterialTheme.lwa.colorScheme.base.primary,
 | 
			
		||||
                        text = when (action) {
 | 
			
		||||
                            Action.DisplayPortrait -> stringResource(
 | 
			
		||||
                                Res.string.game_master__character_action__display_portrait,
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                            Action.AddToGroup -> stringResource(
 | 
			
		||||
                                Res.string.game_master__character_action__add_to_group,
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                            Action.AddToNpc -> stringResource(
 | 
			
		||||
                                Res.string.game_master__character_action__add_to_npc,
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                            is Action.RemoveFromGroup -> stringResource(
 | 
			
		||||
                                Res.string.game_master__character_action__remove_from_group,
 | 
			
		||||
                                action.instanceId,
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                            is Action.RemoveFromNpc -> stringResource(
 | 
			
		||||
                                Res.string.game_master__character_action__remove_from_npc,
 | 
			
		||||
                                action.instanceId,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
private fun Tag(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    padding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
 | 
			
		||||
    tag: GMCharacterPreviewUio.Tag,
 | 
			
		||||
) {
 | 
			
		||||
    Text(
 | 
			
		||||
        modifier = modifier
 | 
			
		||||
            .background(
 | 
			
		||||
                color = MaterialTheme.lwa.colorScheme.elevated.base4dp,
 | 
			
		||||
                shape = CircleShape,
 | 
			
		||||
            )
 | 
			
		||||
            .padding(paddingValues = padding),
 | 
			
		||||
        style = MaterialTheme.lwa.typography.base.caption,
 | 
			
		||||
        color = when (tag.highlight) {
 | 
			
		||||
            true -> MaterialTheme.lwa.colorScheme.base.secondary
 | 
			
		||||
            else -> MaterialTheme.lwa.colorScheme.base.onSurface
 | 
			
		||||
        },
 | 
			
		||||
        text = tag.label,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,294 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.main
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.ScrollState
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.Spacer
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
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.Scaffold
 | 
			
		||||
import androidx.compose.material.Surface
 | 
			
		||||
import androidx.compose.material.Text
 | 
			
		||||
import androidx.compose.material.TextButton
 | 
			
		||||
import androidx.compose.material.TopAppBar
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.Stable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.text.style.TextAlign
 | 
			
		||||
import androidx.compose.ui.text.style.TextOverflow
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import com.pixelized.desktop.lwa.LocalWindowController
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToMainPage
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToNetwork
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheet
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit
 | 
			
		||||
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory
 | 
			
		||||
import com.pixelized.shared.lwa.model.campaign.Campaign
 | 
			
		||||
import kotlinx.coroutines.runBlocking
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.Res
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.app_name
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__create__title
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_file_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_folder_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_swords_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.ic_table_24dp
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.main_page__create_action
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.main_page__network_action
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.main_page__open_save_directory
 | 
			
		||||
import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action
 | 
			
		||||
import org.jetbrains.compose.resources.getString
 | 
			
		||||
import org.jetbrains.compose.resources.painterResource
 | 
			
		||||
import org.jetbrains.compose.resources.stringResource
 | 
			
		||||
import org.koin.compose.viewmodel.koinViewModel
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
data class CharacterUio(
 | 
			
		||||
    val id: Campaign.CharacterInstance.Id,
 | 
			
		||||
    val name: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun OldMainPage(
 | 
			
		||||
    viewModel: MainPageViewModel = koinViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val window = LocalWindowController.current
 | 
			
		||||
    val screen = LocalScreenController.current
 | 
			
		||||
    val characters = viewModel.characters.collectAsState()
 | 
			
		||||
    val npcs = viewModel.npcs.collectAsState()
 | 
			
		||||
    val enableRollHistory = viewModel.enableRollHistoryFlow.collectAsState()
 | 
			
		||||
 | 
			
		||||
    Surface(
 | 
			
		||||
        modifier = Modifier.fillMaxSize(),
 | 
			
		||||
    ) {
 | 
			
		||||
        MainPageContent(
 | 
			
		||||
            characters = characters,
 | 
			
		||||
            npcs = npcs,
 | 
			
		||||
            enableRollHistory = enableRollHistory,
 | 
			
		||||
            onCharacter = {
 | 
			
		||||
                window.navigateToCharacterSheet(
 | 
			
		||||
                    characterId = it.id,
 | 
			
		||||
                    title = it.name,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            onCreateCharacter = {
 | 
			
		||||
                window.navigateToCharacterSheetEdit(
 | 
			
		||||
                    characterId = null,
 | 
			
		||||
                    title = runBlocking { getString(Res.string.character_sheet_edit__create__title) },
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            onRollHistory = {
 | 
			
		||||
                window.navigateToRollHistory()
 | 
			
		||||
            },
 | 
			
		||||
            onOpenSaveDirectory = {
 | 
			
		||||
                viewModel.openSaveDirectory()
 | 
			
		||||
            },
 | 
			
		||||
            onNetwork = {
 | 
			
		||||
                screen.navigateToNetwork()
 | 
			
		||||
            },
 | 
			
		||||
            onMainPage = {
 | 
			
		||||
                screen.navigateToMainPage()
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun MainPageContent(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    scrollState: ScrollState = rememberScrollState(),
 | 
			
		||||
    characters: State<List<CharacterUio>>,
 | 
			
		||||
    npcs: State<List<CharacterUio>>,
 | 
			
		||||
    enableRollHistory: State<Boolean>,
 | 
			
		||||
    onCharacter: (CharacterUio) -> Unit,
 | 
			
		||||
    onCreateCharacter: () -> Unit,
 | 
			
		||||
    onRollHistory: () -> Unit,
 | 
			
		||||
    onOpenSaveDirectory: () -> Unit,
 | 
			
		||||
    onNetwork: () -> Unit,
 | 
			
		||||
    onMainPage: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        topBar = {
 | 
			
		||||
            TopAppBar(
 | 
			
		||||
                title = {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = runBlocking { getString(Res.string.app_name) },
 | 
			
		||||
                    )
 | 
			
		||||
                },
 | 
			
		||||
                actions = {
 | 
			
		||||
                    TextButton(
 | 
			
		||||
                        onClick = onMainPage,
 | 
			
		||||
                    ) {
 | 
			
		||||
                        Row(
 | 
			
		||||
                            horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
 | 
			
		||||
                            verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                painter = painterResource(Res.drawable.ic_swords_24dp),
 | 
			
		||||
                                contentDescription = null,
 | 
			
		||||
                            )
 | 
			
		||||
                            Text(
 | 
			
		||||
                                overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                textAlign = TextAlign.Start,
 | 
			
		||||
                                maxLines = 1,
 | 
			
		||||
                                text = "Nouvelle interface utilisateur",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        content = {
 | 
			
		||||
            Column {
 | 
			
		||||
                Box(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .verticalScroll(state = scrollState)
 | 
			
		||||
                        .fillMaxSize()
 | 
			
		||||
                        .padding(horizontal = 16.dp),
 | 
			
		||||
                    contentAlignment = Alignment.Center,
 | 
			
		||||
                ) {
 | 
			
		||||
                    Column {
 | 
			
		||||
                        if (characters.value.isNotEmpty()) {
 | 
			
		||||
                            Column {
 | 
			
		||||
                                characters.value.forEach { sheet ->
 | 
			
		||||
                                    TextButton(
 | 
			
		||||
                                        onClick = { onCharacter(sheet) },
 | 
			
		||||
                                    ) {
 | 
			
		||||
                                        Text(
 | 
			
		||||
                                            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                                            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                            textAlign = TextAlign.Start,
 | 
			
		||||
                                            maxLines = 1,
 | 
			
		||||
                                            text = sheet.name,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (characters.value.isNotEmpty() && npcs.value.isNotEmpty()) {
 | 
			
		||||
                            Spacer(modifier = Modifier.height(height = 24.dp))
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (npcs.value.isNotEmpty()) {
 | 
			
		||||
                            Column {
 | 
			
		||||
                                npcs.value.forEach { sheet ->
 | 
			
		||||
                                    TextButton(
 | 
			
		||||
                                        onClick = { onCharacter(sheet) },
 | 
			
		||||
                                    ) {
 | 
			
		||||
                                        Text(
 | 
			
		||||
                                            modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                                            overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                            textAlign = TextAlign.Start,
 | 
			
		||||
                                            maxLines = 1,
 | 
			
		||||
                                            text = sheet.name,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        Spacer(modifier = Modifier.height(height = 24.dp))
 | 
			
		||||
 | 
			
		||||
                        TextButton(
 | 
			
		||||
                            onClick = onCreateCharacter,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Row(
 | 
			
		||||
                                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                                horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
 | 
			
		||||
                                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    painter = painterResource(Res.drawable.ic_file_24dp),
 | 
			
		||||
                                    contentDescription = null,
 | 
			
		||||
                                )
 | 
			
		||||
                                Text(
 | 
			
		||||
                                    maxLines = 1,
 | 
			
		||||
                                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                    textAlign = TextAlign.Start,
 | 
			
		||||
                                    text = stringResource(Res.string.main_page__create_action),
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        TextButton(
 | 
			
		||||
                            onClick = onOpenSaveDirectory,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Row(
 | 
			
		||||
                                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                                horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
 | 
			
		||||
                                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    painter = painterResource(Res.drawable.ic_folder_24dp),
 | 
			
		||||
                                    contentDescription = null,
 | 
			
		||||
                                )
 | 
			
		||||
                                Text(
 | 
			
		||||
                                    maxLines = 1,
 | 
			
		||||
                                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                    textAlign = TextAlign.Start,
 | 
			
		||||
                                    text = stringResource(Res.string.main_page__open_save_directory),
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        TextButton(
 | 
			
		||||
                            enabled = enableRollHistory.value,
 | 
			
		||||
                            onClick = onRollHistory,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Row(
 | 
			
		||||
                                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                                horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
 | 
			
		||||
                                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    painter = painterResource(Res.drawable.ic_d20_24dp),
 | 
			
		||||
                                    contentDescription = null,
 | 
			
		||||
                                )
 | 
			
		||||
                                Text(
 | 
			
		||||
                                    maxLines = 1,
 | 
			
		||||
                                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                    textAlign = TextAlign.Start,
 | 
			
		||||
                                    text = stringResource(Res.string.main_page__roll_history_action),
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        TextButton(
 | 
			
		||||
                            onClick = onNetwork,
 | 
			
		||||
                        ) {
 | 
			
		||||
                            Row(
 | 
			
		||||
                                modifier = Modifier.fillMaxWidth(),
 | 
			
		||||
                                horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
 | 
			
		||||
                                verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    painter = painterResource(Res.drawable.ic_table_24dp),
 | 
			
		||||
                                    contentDescription = null,
 | 
			
		||||
                                )
 | 
			
		||||
                                Text(
 | 
			
		||||
                                    overflow = TextOverflow.Ellipsis,
 | 
			
		||||
                                    textAlign = TextAlign.Start,
 | 
			
		||||
                                    maxLines = 1,
 | 
			
		||||
                                    text = stringResource(Res.string.main_page__network_action),
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,96 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.ui.screen.main
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.lordcodes.turtle.shellRun
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
 | 
			
		||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
 | 
			
		||||
import com.pixelized.shared.lwa.utils.OperatingSystem
 | 
			
		||||
import com.pixelized.shared.lwa.utils.PathProvider
 | 
			
		||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
 | 
			
		||||
import kotlinx.coroutines.flow.SharingStarted
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.combine
 | 
			
		||||
import kotlinx.coroutines.flow.flatMapMerge
 | 
			
		||||
import kotlinx.coroutines.flow.flowOf
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
 | 
			
		||||
class MainPageViewModel(
 | 
			
		||||
    private val pathProvider: PathProvider,
 | 
			
		||||
    private val characterSheetRepository: CharacterSheetRepository,
 | 
			
		||||
    private val campaignRepository: CampaignRepository,
 | 
			
		||||
    networkRepository: NetworkRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    @OptIn(ExperimentalCoroutinesApi::class)
 | 
			
		||||
    val characters: StateFlow<List<CharacterUio>> = campaignRepository.campaignFlow
 | 
			
		||||
        .flatMapMerge { campaign ->
 | 
			
		||||
            combine(
 | 
			
		||||
                campaign.characters
 | 
			
		||||
                    .map { entry ->
 | 
			
		||||
                        characterSheetRepository
 | 
			
		||||
                            .characterDetailFlow(characterSheetId = entry.key.characterSheetId)
 | 
			
		||||
                            .map transform@{ sheet ->
 | 
			
		||||
                                if (sheet == null) return@transform null
 | 
			
		||||
                                CharacterUio(id = entry.key, name = sheet.name)
 | 
			
		||||
                            }
 | 
			
		||||
                    }
 | 
			
		||||
                    .ifEmpty {
 | 
			
		||||
                        listOf(flowOf(null))
 | 
			
		||||
                    }
 | 
			
		||||
            ) { data ->
 | 
			
		||||
                data.mapNotNull { it }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .stateIn(
 | 
			
		||||
            scope = viewModelScope,
 | 
			
		||||
            started = SharingStarted.Eagerly,
 | 
			
		||||
            initialValue = emptyList(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @OptIn(ExperimentalCoroutinesApi::class)
 | 
			
		||||
    val npcs: StateFlow<List<CharacterUio>> = campaignRepository.campaignFlow
 | 
			
		||||
        .flatMapMerge { campaign ->
 | 
			
		||||
            combine(
 | 
			
		||||
                campaign.npcs
 | 
			
		||||
                    .map { entry ->
 | 
			
		||||
                        characterSheetRepository
 | 
			
		||||
                            .characterDetailFlow(characterSheetId = entry.key.characterSheetId)
 | 
			
		||||
                            .map transform@{ sheet ->
 | 
			
		||||
                                if (sheet == null) return@transform null
 | 
			
		||||
                                CharacterUio(id = entry.key, name = sheet.name)
 | 
			
		||||
                            }
 | 
			
		||||
                    }
 | 
			
		||||
                    .ifEmpty {
 | 
			
		||||
                        listOf(flowOf(null))
 | 
			
		||||
                    }
 | 
			
		||||
            ) { data ->
 | 
			
		||||
                data.mapNotNull { it }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        .stateIn(
 | 
			
		||||
            scope = viewModelScope,
 | 
			
		||||
            started = SharingStarted.Eagerly,
 | 
			
		||||
            initialValue = emptyList(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    val enableRollHistoryFlow = networkRepository.status
 | 
			
		||||
        .map { it == NetworkRepository.Status.CONNECTED }
 | 
			
		||||
        .stateIn(
 | 
			
		||||
            scope = viewModelScope,
 | 
			
		||||
            started = SharingStarted.Lazily,
 | 
			
		||||
            initialValue = false,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    fun openSaveDirectory(
 | 
			
		||||
        os: OperatingSystem = OperatingSystem.current,
 | 
			
		||||
    ) {
 | 
			
		||||
        val path = pathProvider.storePath(os = os)
 | 
			
		||||
        when (os) {
 | 
			
		||||
            OperatingSystem.Windows -> shellRun("explorer.exe", listOf(path))
 | 
			
		||||
            OperatingSystem.Macintosh -> shellRun("open", listOf(path))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ class RollHistoryViewModel(
 | 
			
		|||
        characterRepository.characterSheetPreviewFlow,
 | 
			
		||||
        rollRepository.rolls,
 | 
			
		||||
    ) { sheets, message ->
 | 
			
		||||
        val name = sheets.firstOrNull { it.id == message.characterSheetId }?.name ?: ""
 | 
			
		||||
        val name = sheets.firstOrNull { it.characterSheetId == message.characterSheetId }?.name ?: ""
 | 
			
		||||
        val roll = RollHistoryItemUio(
 | 
			
		||||
            character = name,
 | 
			
		||||
            skillLabel = message.skillLabel,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ class SettingsUseCase {
 | 
			
		|||
        autoHideDelay = 8,
 | 
			
		||||
        autoShowChat = true,
 | 
			
		||||
        autoScrollChat = true,
 | 
			
		||||
        isGM = false,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue