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
				
			
		|  | @ -177,6 +177,15 @@ | |||
|     <string name="level_up__character_level_description">Passage du niveau %1$d ▸ %2$d</string> | ||||
|     <string name="level_up__skill_level">niv : %1$d -</string> | ||||
| 
 | ||||
| 
 | ||||
|     <string name="game_master__character_level__label">niv: %1$d</string> | ||||
|     <string name="game_master__character_tag__character_search">joueur</string> | ||||
|     <string name="game_master__character_tag__character_label">joueur: %1$d</string> | ||||
|     <string name="game_master__character_tag__npc_search">npc</string> | ||||
|     <string name="game_master__character_tag__npc_label">npc: %1$d</string> | ||||
|     <string name="game_master__character_action__display_portrait">Afficher le portrait</string> | ||||
|     <string name="game_master__character_action__add_to_group">Ajouter au groupe</string> | ||||
|     <string name="game_master__character_action__remove_from_group">Retirer du groupe (id: %1$d)</string> | ||||
|     <string name="game_master__character_action__add_to_npc">Ajouter aux Npcs</string> | ||||
|     <string name="game_master__character_action__remove_from_npc">Retirer des Npcs (id: %1$d)</string> | ||||
| 
 | ||||
| </resources> | ||||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -33,7 +33,11 @@ class CampaignService( | |||
|             initialValue = factory.convertToJson(campaignFlow.value), | ||||
|         ) | ||||
| 
 | ||||
|     fun campaign(): CampaignJson { | ||||
|     fun campaign(): Campaign { | ||||
|         return campaignFlow.value | ||||
|     } | ||||
| 
 | ||||
|     fun campaignJson(): CampaignJson { | ||||
|         return campaignJsonFlow.value | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,6 @@ import io.ktor.server.response.respond | |||
| 
 | ||||
| fun Engine.getCampaign(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { | ||||
|     return { | ||||
|         call.respond(campaignService.campaign()) | ||||
|         call.respond(campaignService.campaignJson()) | ||||
|     } | ||||
| } | ||||
|  | @ -9,30 +9,43 @@ import io.ktor.server.response.respondText | |||
| 
 | ||||
| fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { | ||||
|     return { | ||||
|         val characterSheetId = call.queryParameters["characterSheetId"] | ||||
|         val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() | ||||
|         val id = if (characterSheetId != null && instanceId != null) { | ||||
|             Campaign.CharacterInstance.Id( | ||||
|         try { | ||||
|             val characterSheetId = call.queryParameters["characterSheetId"] | ||||
|                 ?: error("missing character sheet id") | ||||
| 
 | ||||
|             val instanceId = campaignService.campaign().characters.keys | ||||
|                 .firstOrNull { key -> key.characterSheetId == characterSheetId } | ||||
| 
 | ||||
|             if (instanceId != null) { | ||||
|                 error("Character Already in party") | ||||
|             } | ||||
| 
 | ||||
|             val id = Campaign.CharacterInstance.Id( | ||||
|                 characterSheetId = characterSheetId, | ||||
|                 instanceId = instanceId | ||||
|                 instanceId = 0, | ||||
|             ) | ||||
|         } else { | ||||
|             null | ||||
|         } | ||||
|         val updated = id?.let { campaignService.addCharacter(it) } ?: false | ||||
|         val code = when (updated) { | ||||
|             true -> HttpStatusCode.Accepted | ||||
|             else -> HttpStatusCode.UnprocessableEntity | ||||
|         } | ||||
|         call.respondText( | ||||
|             text = "$code", | ||||
|             status = code, | ||||
|         ) | ||||
|         webSocket.emit( | ||||
|             Message( | ||||
|                 from = "Server", | ||||
|                 value = RestSynchronisation.Campaign, | ||||
| 
 | ||||
|             if (campaignService.addCharacter(id).not()) { | ||||
|                 error("Unexpected error occurred when the character instance was added to the party") | ||||
|             } | ||||
| 
 | ||||
|             call.respondText( | ||||
|                 text = "Character $characterSheetId successfully added to the party", | ||||
|                 status = HttpStatusCode.Accepted, | ||||
|             ) | ||||
|         ) | ||||
|             webSocket.emit( | ||||
|                 Message( | ||||
|                     from = "Server", | ||||
|                     value = RestSynchronisation.Campaign, | ||||
|                 ) | ||||
|             ) | ||||
|         } catch (exception: Exception) { | ||||
|             call.run { | ||||
|                 respondText( | ||||
|                     text = "${exception.message}", | ||||
|                     status = HttpStatusCode.UnprocessableEntity, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -9,35 +9,46 @@ import io.ktor.server.response.respondText | |||
| 
 | ||||
| fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { | ||||
|     return { | ||||
|         val characterSheetId = call.queryParameters["characterSheetId"] | ||||
|         val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() | ||||
|         try { | ||||
|             val characterSheetId = call.queryParameters["characterSheetId"] | ||||
|                 ?: error("missing character sheet id") | ||||
| 
 | ||||
|         val id = if (characterSheetId != null && instanceId != null) { | ||||
|             Campaign.CharacterInstance.Id( | ||||
|             val instanceId = campaignService.campaign().npcs.keys | ||||
|                 .filter { it.characterSheetId == characterSheetId } | ||||
|                 .reduceOrNull { acc, id -> | ||||
|                     if (acc.instanceId < id.instanceId) { | ||||
|                         id | ||||
|                     } else { | ||||
|                         acc | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|             val id = Campaign.CharacterInstance.Id( | ||||
|                 characterSheetId = characterSheetId, | ||||
|                 instanceId = instanceId | ||||
|                 instanceId = instanceId?.let { it.instanceId + 1 } ?: 0, | ||||
|             ) | ||||
|         } else { | ||||
|             null | ||||
|         } | ||||
| 
 | ||||
|         val updated = id?.let { campaignService.addNpc(it) } ?: false | ||||
|             if (campaignService.addNpc(id).not()) { | ||||
|                 error("Unexpected error occurred when the character instance was added to the npcs") | ||||
|             } | ||||
| 
 | ||||
|         val code = when (updated) { | ||||
|             true -> HttpStatusCode.Accepted | ||||
|             else -> HttpStatusCode.UnprocessableEntity | ||||
|         } | ||||
| 
 | ||||
|         call.respondText( | ||||
|             text = "$code", | ||||
|             status = code, | ||||
|         ) | ||||
| 
 | ||||
|         webSocket.emit( | ||||
|             Message( | ||||
|                 from = "Server", | ||||
|                 value = RestSynchronisation.Campaign, | ||||
|             call.respondText( | ||||
|                 text = "Character $characterSheetId successfully added to the npcs", | ||||
|                 status = HttpStatusCode.Accepted, | ||||
|             ) | ||||
|         ) | ||||
|             webSocket.emit( | ||||
|                 Message( | ||||
|                     from = "Server", | ||||
|                     value = RestSynchronisation.Campaign, | ||||
|                 ) | ||||
|             ) | ||||
|         } catch (exception: Exception) { | ||||
|             call.run { | ||||
|                 respondText( | ||||
|                     text = "${exception.message}", | ||||
|                     status = HttpStatusCode.UnprocessableEntity, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -87,6 +87,8 @@ class CharacterSheetJsonFactory( | |||
|     ): CharacterPreviewJson { | ||||
|         return CharacterPreviewJson( | ||||
|             id = sheet.id, | ||||
|             portrait = sheet.portrait, | ||||
|             thumbnail = sheet.thumbnail, | ||||
|             name = sheet.name, | ||||
|             level = sheet.level, | ||||
|         ) | ||||
|  | @ -98,8 +100,8 @@ class CharacterSheetJsonFactory( | |||
|         val json = CharacterSheetJsonV1( | ||||
|             id = sheet.id, | ||||
|             name = sheet.name, | ||||
|             thumbnail = sheet.thumbnail, | ||||
|             portrait = sheet.portrait, | ||||
|             thumbnail = sheet.thumbnail, | ||||
|             level = sheet.level, | ||||
|             shouldLevelUp = sheet.shouldLevelUp, | ||||
|             strength = sheet.strength, | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ import kotlinx.serialization.Serializable | |||
| @Serializable | ||||
| class CharacterPreviewJson( | ||||
|     val id: String, | ||||
|     val portrait: String?, | ||||
|     val thumbnail: String?, | ||||
|     val name: String, | ||||
|     val level: Int, | ||||
| ) | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue