diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 6361bbf..4251b6a 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -177,6 +177,15 @@ Passage du niveau %1$d ▸ %2$d niv : %1$d - - + niv: %1$d + joueur + joueur: %1$d + npc + npc: %1$d + Afficher le portrait + Ajouter au groupe + Retirer du groupe (id: %1$d) + Ajouter aux Npcs + Retirer des Npcs (id: %1$d) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt index f3f7ddc..313d952 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -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() } } ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 6cd6669..d49e879 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -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) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index 1f2ae80..173e37a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -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) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index bb0373c..19e2b3c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -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() 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() override suspend fun campaignDeleteNpc( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt index c98cc3a..776c1ea 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt @@ -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, + ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt index 61dddfe..f2558bc 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt @@ -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) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/model/CharacterSheetPreview.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/model/CharacterSheetPreview.kt index 7c9f403..f9aea42 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/model/CharacterSheetPreview.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/model/CharacterSheetPreview.kt @@ -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, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt index 0cdb070..c08f6bf 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index a5eba7d..784ae18 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -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) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt index 9c8c92b..6876e36 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt @@ -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, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt index 2693887..eebeeb4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/Settings.kt @@ -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://")}" } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt index b83937e..2c9a7fd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/model/SettingsJsonV1.kt @@ -12,4 +12,5 @@ data class SettingsJsonV1( val autoHideDelay: Int?, val autoShowChat: Boolean?, val autoScrollChat: Boolean?, + val isGM: Boolean?, ) : SettingsJson \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt new file mode 100644 index 0000000..5bf6b5b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt @@ -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, + val valueFlow: StateFlow, + val placeHolderFlow: StateFlow, + 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, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt index 2d326ad..e2937b5 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt @@ -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 { error("MainNavHost controller is not yet ready") @@ -35,9 +30,6 @@ fun MainNavHost( composableMainPage() composableSettingsPage() composableLevelUp() - - composableNetworkPage() - composableOldMainPage() } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt deleted file mode 100644 index 15b53a3..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt deleted file mode 100644 index f572b43..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/NetworkWindows.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/GameMasterWindow.kt similarity index 58% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/NetworkWindows.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/GameMasterWindow.kt index d70afd4..d2e158e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/NetworkWindows.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/GameMasterWindow.kt @@ -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, ) ) ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/RollHistoryWindow.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/RollHistoryWindow.kt index bbd9cc1..712dcde 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/RollHistoryWindow.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/RollHistoryWindow.kt @@ -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, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index bb3bf52..2eff35d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -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 = { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt deleted file mode 100644 index de9a871..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignViewModel.kt +++ /dev/null @@ -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 = 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, - ) - } - } - } - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkDialog.kt similarity index 72% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkDialog.kt index ec766bd..37d295d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkDialog.kt @@ -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(), ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkFactory.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkFactory.kt index 1c2f209..3738445 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkFactory.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkViewModel.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkViewModel.kt index 0b521fa..fa64620 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/network/NetworkViewModel.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt index 6a45ed9..d6bd5d1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt @@ -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, - networkStatus: State, + status: State, + isGM: State, isNetworkMenuOpen: State, isOverflowMenuOpen: State, - 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), + ) + } + }, ) }, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt new file mode 100644 index 0000000..0bf23c9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt @@ -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, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt index 1a093ff..0cf3826 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt @@ -94,7 +94,7 @@ class CharacterSheetViewModel( characterId = argument.characterInstanceId.characterSheetId ) ?: return _displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio( - id = preview.id, + id = preview.characterSheetId, name = preview.name, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterActionUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterActionUseCase.kt new file mode 100644 index 0000000..703efc8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterActionUseCase.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt new file mode 100644 index 0000000..ddca343 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterFactory.kt @@ -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, + filter: String, + ): List { + 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, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt new file mode 100644 index 0000000..0698ad4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt @@ -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>, + 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) + } + ) + } + } + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt new file mode 100644 index 0000000..ddf37ad --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterViewModel.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt new file mode 100644 index 0000000..34a8945 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacterPreview.kt @@ -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, + val actions: List, +) { + @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, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt deleted file mode 100644 index b8a7c37..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt +++ /dev/null @@ -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>, - npcs: State>, - enableRollHistory: State, - 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), - ) - } - } - } - } - } - }, - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt deleted file mode 100644 index 4101021..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt +++ /dev/null @@ -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> = 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> = 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)) - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt index 2547c1c..51a1d0d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt index 7d46183..eff28b5 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt @@ -13,6 +13,7 @@ class SettingsUseCase { autoHideDelay = 8, autoShowChat = true, autoScrollChat = true, + isGM = false, ) companion object { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt index 2661132..bea1a98 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt @@ -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 } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt index e2a949c..8dc0403 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt @@ -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()) } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt index bd9f4a8..c5368bb 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt @@ -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, + ) + } + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt index 4f73a89..dcc1248 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt @@ -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, + ) + } + } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt index 669fba3..58af874 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt @@ -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, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt index 632d77b..76e14a2 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt @@ -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, ) \ No newline at end of file