Change the access to some old screen from the new UI.

This commit is contained in:
Thomas Andres Gomez 2025-02-28 13:31:01 +01:00
parent 1bf0cb7c0a
commit 3941c5dd76
25 changed files with 657 additions and 450 deletions

View file

@ -42,6 +42,8 @@ kotlin {
implementation(libs.ktor.client.negotiation) implementation(libs.ktor.client.negotiation)
// shell // shell
implementation(libs.turtle) implementation(libs.turtle)
// log
implementation(libs.logback)
} }
commonTest.dependencies { commonTest.dependencies {

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m370,880 l-16,-128q-13,-5 -24.5,-12T307,725l-119,50L78,585l103,-78q-1,-7 -1,-13.5v-27q0,-6.5 1,-13.5L78,375l110,-190 119,50q11,-8 23,-15t24,-12l16,-128h220l16,128q13,5 24.5,12t22.5,15l119,-50 110,190 -103,78q1,7 1,13.5v27q0,6.5 -2,13.5l103,78 -110,190 -118,-50q-11,8 -23,15t-24,12L590,880L370,880ZM440,800h79l14,-106q31,-8 57.5,-23.5T639,633l99,41 39,-68 -86,-65q5,-14 7,-29.5t2,-31.5q0,-16 -2,-31.5t-7,-29.5l86,-65 -39,-68 -99,42q-22,-23 -48.5,-38.5T533,266l-13,-106h-79l-14,106q-31,8 -57.5,23.5T321,327l-99,-41 -39,68 86,64q-5,15 -7,30t-2,32q0,16 2,31t7,30l-86,65 39,68 99,-42q22,23 48.5,38.5T427,694l13,106ZM482,620q58,0 99,-41t41,-99q0,-58 -41,-99t-99,-41q-59,0 -99.5,41T342,480q0,58 40.5,99t99.5,41ZM480,480Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M120,720q-33,0 -56.5,-23.5T40,640q0,-33 23.5,-56.5T120,560h10.5q4.5,0 9.5,2l182,-182q-2,-5 -2,-9.5L320,360q0,-33 23.5,-56.5T400,280q33,0 56.5,23.5T480,360q0,2 -2,20l102,102q5,-2 9.5,-2h21q4.5,0 9.5,2l142,-142q-2,-5 -2,-9.5L760,320q0,-33 23.5,-56.5T840,240q33,0 56.5,23.5T920,320q0,33 -23.5,56.5T840,400h-10.5q-4.5,0 -9.5,-2L678,540q2,5 2,9.5v10.5q0,33 -23.5,56.5T600,640q-33,0 -56.5,-23.5T520,560v-10.5q0,-4.5 2,-9.5L420,438q-5,2 -9.5,2L400,440q-2,0 -20,-2L198,620q2,5 2,9.5v10.5q0,33 -23.5,56.5T120,720Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -7,10 +7,9 @@
<string name="main_page__create_action">Créer une feuille de personnage</string> <string name="main_page__create_action">Créer une feuille de personnage</string>
<string name="main_page__open_save_directory">Ouvrir le dossier de sauvegarde</string> <string name="main_page__open_save_directory">Ouvrir le dossier de sauvegarde</string>
<string name="main_page__roll_history_action">Consulter l'historique des lancers</string> <string name="main_page__roll_history_action">Historique des lancers</string>
<string name="main_page__network_action">Configuration de la table</string> <string name="main_page__network_action">Configuration de la table</string>
<string name="roll_page__roll__label">Jet de :</string> <string name="roll_page__roll__label">Jet de :</string>
<string name="roll_page__roll__success_label">Réussite si lancer inférieur ou égal à : %1$s</string> <string name="roll_page__roll__success_label">Réussite si lancer inférieur ou égal à : %1$s</string>
<string name="roll_page__critical_success">Réussite critique</string> <string name="roll_page__critical_success">Réussite critique</string>

View file

@ -26,9 +26,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler
@ -40,12 +42,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.WindowsNavHost
import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetEditWindow 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.CharacterSheetWindow
import com.pixelized.desktop.lwa.ui.navigation.window.destination.NetworkWindows
import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWindow 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.navigation.window.rememberMaxWindowHeight
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost 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.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage 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.ui.theme.LwaTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -88,74 +93,82 @@ fun ApplicationScope.App() {
) { ) {
Window( Window(
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
state = rememberWindowState(size = DpSize(width = 800.dp, height = maxWindowHeight)),
title = runBlocking { getString(Res.string.app_name) }, title = runBlocking { getString(Res.string.app_name) },
onKeyEvent = { event -> onKeyEvent = { event ->
keyEventHandlers.reversed().any { it(event) } keyEventHandlers.reversed().any { it(event) }
}, },
) { ) {
LwaTheme { MainWindowScreen()
Surface(
modifier = Modifier.fillMaxSize()
) {
Scaffold(
snackbarHost = {
Column(
modifier = Modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
SnackbarHost(
hostState = snackHostState,
snackbar = {
Snackbar(
snackbarData = it,
)
}
)
SnackbarHost(
hostState = errorSnackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
)
}
},
content = {
MainScreen()
}
)
NetworkSnackHandler(
snack = snackHostState,
)
WindowsHandler(
windowController = windowController,
)
}
}
} }
} }
} }
@Composable @Composable
private fun MainScreen( private fun MainWindowScreen(
campaignViewModel: CampaignViewModel = koinViewModel(), campaignViewModel: CampaignViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(), networkViewModel: NetworkViewModel = koinViewModel(),
rollViewModel: RollHistoryViewModel = koinViewModel(),
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
networkViewModel.connect() networkViewModel.connect()
campaignViewModel.init() campaignViewModel.init()
} }
MainNavHost() val snackHostState = LocalSnackHost.current
val errorSnackHostState = LocalErrorSnackHost.current
val windowController = LocalWindowController.current
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
Scaffold(
snackbarHost = {
Column(
modifier = Modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
SnackbarHost(
hostState = snackHostState,
snackbar = {
Snackbar(
snackbarData = it,
)
}
)
SnackbarHost(
hostState = errorSnackHostState,
snackbar = {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError,
actionColor = MaterialTheme.colors.onError,
)
}
)
}
},
content = {
MainNavHost()
}
)
NetworkSnackHandler(
snack = snackHostState,
)
WindowsHandler(
windowController = windowController,
rollViewModel = rollViewModel,
)
}
}
} }
@Composable @Composable
private fun WindowsHandler( private fun WindowsHandler(
windowController: WindowController, windowController: WindowController,
rollViewModel: RollHistoryViewModel = koinViewModel(),
) { ) {
WindowsNavHost( WindowsNavHost(
controller = windowController, controller = windowController,
@ -173,7 +186,11 @@ private fun WindowsHandler(
), ),
) )
is RollHistoryWindow -> RollHistoryPage() is RollHistoryWindow -> RollHistoryPage(
viewModel = rollViewModel,
)
is NetworkWindows -> NetworkPage()
} }
} }
) )

View file

@ -34,7 +34,6 @@ import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
import com.pixelized.desktop.lwa.usecase.SettingsUseCase import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.plugins.websocket.WebSockets
@ -56,12 +55,9 @@ val appModuleDependencies
val toolsDependencies val toolsDependencies
get() = module { get() = module {
single<HttpClientEngine> {
OkHttp.create()
}
single { single {
HttpClient( HttpClient(
engine = get() engine = OkHttp.create(),
) { ) {
install(WebSockets) { install(WebSockets) {
pingIntervalMillis = 20_000 pingIntervalMillis = 20_000

View file

@ -7,6 +7,10 @@ import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.serialization.kotlinx.json.json
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -22,10 +26,11 @@ import kotlinx.serialization.json.Json
class NetworkRepository( class NetworkRepository(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val client: HttpClient, private val json: Json,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
private var networkJob: Job? = null private var networkJob: Job? = null
private var client: HttpClient? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>() private val outgoingMessageBuffer = MutableSharedFlow<Message>()
private val incomingMessageBuffer = MutableSharedFlow<Message>() private val incomingMessageBuffer = MutableSharedFlow<Message>()
@ -41,10 +46,11 @@ class NetworkRepository(
onFailure: (Exception) -> Unit = { }, onFailure: (Exception) -> Unit = { },
onClose: () -> Unit = { }, onClose: () -> Unit = { },
) { ) {
client = buildNewClient()
networkJob?.cancel() networkJob?.cancel()
networkJob = scope.launch { networkJob = scope.launch(Dispatchers.IO) {
try { try {
client.connectWebSocket(host = host, port = port) { client?.connectWebSocket(host = host, port = port) {
_status.value = Status.CONNECTED _status.value = Status.CONNECTED
onConnect() onConnect()
@ -78,7 +84,8 @@ class NetworkRepository(
fun disconnect() { fun disconnect() {
networkJob?.cancel() networkJob?.cancel()
scope.launch { scope.launch {
client.close() client?.close()
client = null
} }
} }
@ -96,6 +103,17 @@ class NetworkRepository(
} }
} }
private fun buildNewClient() = HttpClient(
engine = OkHttp.create(),
) {
install(WebSockets) {
pingIntervalMillis = 20_000
}
install(ContentNegotiation) {
json(json)
}
}
enum class Status { enum class Status {
CONNECTED, CONNECTED,
DISCONNECTED DISCONNECTED

View file

@ -13,7 +13,7 @@ suspend fun HttpClient.connectWebSocket(
) { ) {
webSocket( webSocket(
method = HttpMethod.Get, method = HttpMethod.Get,
host = host, host = host.removePrefix("http://"),
port = port, port = port,
path = "/ws", path = "/ws",
block = block, block = block,

View file

@ -1,15 +1,14 @@
package com.pixelized.desktop.lwa.repository.settings package com.pixelized.desktop.lwa.repository.settings
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV1 import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV1
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
class SettingsFactory( class SettingsFactory(
private val useCase: SettingsUseCase, private val useCase: SettingsUseCase,
) { ) {
fun convertToJson( fun convertToJson(
settings: Settings, settings: Settings,
): SettingsJson { ): SettingsJson {

View file

@ -5,5 +5,5 @@ data class Settings(
val port: Int, val port: Int,
val playerName: String, val playerName: String,
) { ) {
val root: String get() = "http://${host}:${port}" val root: String get() = "http://${"${host}:${port}".removePrefix("http://")}"
} }

View file

@ -3,7 +3,7 @@ package com.pixelized.desktop.lwa.ui.navigation.screen.destination
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage import com.pixelized.desktop.lwa.ui.screen.network.NetworkScreen
object NetworkDestination { object NetworkDestination {
private const val ROUTE = "network" private const val ROUTE = "network"
@ -16,7 +16,7 @@ fun NavGraphBuilder.composableNetworkPage() {
composable( composable(
route = NetworkDestination.baseRoute(), route = NetworkDestination.baseRoute(),
) { ) {
NetworkPage() NetworkScreen()
} }
} }

View file

@ -0,0 +1,28 @@
package com.pixelized.desktop.lwa.ui.navigation.window.destination
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
@Stable
class NetworkWindows(
title: String,
size: DpSize,
) : Window(
title = title,
size = size,
)
fun WindowController.navigateToNetwork(
title: String = "",
) {
showWindow(
window = NetworkWindows(
title = title, size = DpSize(
width = 464.dp,
height = 300.dp,
)
)
)
}

View file

@ -13,12 +13,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -34,33 +31,24 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialog import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialog
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
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.window.destination.navigateToRollHistory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.Toolbar 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.characterSheet.detail.dialog.DiminishedStatDialog
import com.pixelized.desktop.lwa.ui.screen.roll.RollPage import com.pixelized.desktop.lwa.ui.screen.roll.RollPage
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
import lwacharactersheet.composeapp.generated.resources.ic_table_24dp
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun MainPage( fun MainPage(
campaignViewModel: CampaignViewModel = koinViewModel(),
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
@ -76,9 +64,7 @@ fun MainPage(
else -> false else -> false
} }
} }
val windows = LocalWindowController.current
val screen = LocalScreenController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val blurController = rememberBlurContentController() val blurController = rememberBlurContentController()
@ -92,31 +78,7 @@ fun MainPage(
CampaignScreenLayout( CampaignScreenLayout(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
top = { top = {
Toolbar( CampaignToolbar()
title = campaignViewModel.title.collectAsState(initial = "").value,
actions = {
IconButton(
onClick = {
windows.navigateToRollHistory()
},
) {
Icon(
painter = painterResource(Res.drawable.ic_d20_24dp),
contentDescription = null,
)
}
IconButton(
onClick = {
screen.navigateToOldMainPage()
},
) {
Icon(
painter = painterResource(Res.drawable.ic_table_24dp),
contentDescription = null,
)
}
},
)
}, },
bottom = { bottom = {
Surface( Surface(

View file

@ -5,8 +5,10 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository 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.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -14,6 +16,7 @@ class CampaignViewModel(
private val characterRepository: CharacterSheetRepository, private val characterRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository, private val alterationRepository: AlterationRepository,
private val campaignRepository: CampaignRepository, private val campaignRepository: CampaignRepository,
private val network: NetworkRepository,
) : ViewModel() { ) : ViewModel() {
val title: Flow<String> = campaignRepository.campaignFlow val title: Flow<String> = campaignRepository.campaignFlow
@ -21,15 +24,22 @@ class CampaignViewModel(
fun init() { fun init() {
viewModelScope.launch { viewModelScope.launch {
campaignRepository.campaignFlow.collectLatest { combine(
it.characters.keys.forEach { id -> network.status,
characterRepository.characterDetail( campaignRepository.campaignFlow,
characterSheetId = id.characterSheetId, ) { status, campaign ->
forceUpdate = true, status to campaign
) }.collectLatest { (status, campaign) ->
alterationRepository.updateActiveAlterations( if (status == NetworkRepository.Status.CONNECTED) {
characterInstanceId = id, campaign.characters.keys.forEach { id ->
) characterRepository.characterDetail(
characterSheetId = id.characterSheetId,
forceUpdate = true,
)
alterationRepository.updateActiveAlterations(
characterInstanceId = id,
)
}
} }
} }
} }

View file

@ -0,0 +1,145 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.compose.foundation.layout.padding
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.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.navigateToOldMainPage
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToNetwork
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.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.main_page__network_action
import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun CampaignToolbar(
campaignViewModel: CampaignViewModel = koinViewModel(),
) {
val windows = LocalWindowController.current
val screen = LocalScreenController.current
val isOverflowMenuOpen = remember { mutableStateOf(false) }
CampaignToolbarContent(
title = campaignViewModel.title.collectAsState(initial = ""),
isOverflowMenuOpen = isOverflowMenuOpen,
onOverflow = {
isOverflowMenuOpen.value = isOverflowMenuOpen.value.not()
},
onDismissOverflowMenu = {
isOverflowMenuOpen.value = false
},
onRollHistory = {
windows.navigateToRollHistory()
},
onNetwork = {
windows.navigateToNetwork()
},
onOlUi = {
screen.navigateToOldMainPage()
},
)
}
@Composable
private fun CampaignToolbarContent(
modifier: Modifier = Modifier,
title: State<String>,
isOverflowMenuOpen: State<Boolean>,
onOverflow: () -> Unit,
onDismissOverflowMenu: () -> Unit,
onRollHistory: () -> Unit,
onNetwork: () -> Unit,
onOlUi: () -> Unit,
) {
TopAppBar(
modifier = modifier,
title = {
Text(
text = title.value,
)
},
actions = {
IconButton(
onClick = onOverflow,
) {
Icon(
imageVector = Icons.Default.MoreVert,
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
DropdownMenu(
expanded = isOverflowMenuOpen.value,
onDismissRequest = onDismissOverflowMenu,
) {
DropdownMenuItem(
onClick = { onDismissOverflowMenu(); 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 = { onDismissOverflowMenu(); onNetwork() },
) {
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.main_page__network_action),
)
}
DropdownMenuItem(
onClick = { onDismissOverflowMenu(); onOlUi() },
) {
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",
)
}
}
},
)
}

View file

@ -1,52 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
@Composable
fun Toolbar(
modifier: Modifier = Modifier,
title: String,
actions: @Composable RowScope.() -> Unit = { },
) {
Surface(
modifier = modifier,
elevation = 1.dp,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.minimumInteractiveComponentSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.h6,
text = title,
)
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.lwa.colorScheme.base.primary,
) {
Row(
modifier = Modifier.padding(end = 8.dp),
content = actions,
)
}
}
}
}

View file

@ -124,7 +124,7 @@ data class CharacterSheetPageUio(
data class Roll( data class Roll(
val label: String, val label: String,
val value: String, val value: String,
val roll: RollActionUio?, val roll: RollActionUio,
) )
} }
@ -187,7 +187,7 @@ fun CharacterSheetPage(
onUseSkill = viewModel::onUseSkill, onUseSkill = viewModel::onUseSkill,
onRoll = { roll -> onRoll = { roll ->
blurController.show() blurController.show()
rollViewModel.prepareRoll(sheet = sheet, roll = roll) rollViewModel.prepareRoll(roll.roll)
viewModel.showRollOverlay() viewModel.showRollOverlay()
}, },
) )

View file

@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
@ -32,7 +34,6 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToNetw
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheet 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.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.Toolbar
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
@ -117,168 +118,177 @@ fun MainPageContent(
onNetwork: () -> Unit, onNetwork: () -> Unit,
onMainPage: () -> Unit, onMainPage: () -> Unit,
) { ) {
Column( Scaffold(
modifier = modifier, modifier = modifier,
) { topBar = {
Toolbar( TopAppBar(
title = runBlocking { getString(Res.string.app_name) }, title = {
actions = { Text(
TextButton( text = runBlocking { getString(Res.string.app_name) },
onClick = onMainPage, )
) { },
Row( actions = {
horizontalArrangement = Arrangement.spacedBy(space = 8.dp), TextButton(
verticalAlignment = Alignment.CenterVertically, onClick = onMainPage,
) { ) {
Icon( Row(
painter = painterResource(Res.drawable.ic_swords_24dp), horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
contentDescription = null, verticalAlignment = Alignment.CenterVertically,
) ) {
Text( Icon(
overflow = TextOverflow.Ellipsis, painter = painterResource(Res.drawable.ic_swords_24dp),
textAlign = TextAlign.Start, contentDescription = null,
maxLines = 1, )
text = "Nouvelle interface utilisateur", Text(
) overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = "Nouvelle interface utilisateur",
)
}
} }
} }
} )
) },
Box( content = {
modifier = Modifier
.verticalScroll(state = scrollState)
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
Column { Column {
if (characters.value.isNotEmpty()) { Box(
modifier = Modifier
.verticalScroll(state = scrollState)
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
Column { Column {
characters.value.forEach { sheet -> if (characters.value.isNotEmpty()) {
TextButton( Column {
onClick = { onCharacter(sheet) }, 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( Text(
modifier = Modifier.fillMaxWidth(), maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
maxLines = 1, text = stringResource(Res.string.main_page__create_action),
text = sheet.name,
) )
} }
} }
}
}
if (characters.value.isNotEmpty() && npcs.value.isNotEmpty()) { TextButton(
Spacer(modifier = Modifier.height(height = 24.dp)) onClick = onOpenSaveDirectory,
} ) {
Row(
if (npcs.value.isNotEmpty()) { modifier = Modifier.fillMaxWidth(),
Column { horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
npcs.value.forEach { sheet -> verticalAlignment = Alignment.CenterVertically,
TextButton(
onClick = { onCharacter(sheet) },
) { ) {
Icon(
painter = painterResource(Res.drawable.ic_folder_24dp),
contentDescription = null,
)
Text( Text(
modifier = Modifier.fillMaxWidth(), maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
maxLines = 1, text = stringResource(Res.string.main_page__open_save_directory),
text = sheet.name,
) )
} }
} }
}
}
Spacer(modifier = Modifier.height(height = 24.dp)) 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( TextButton(
onClick = onCreateCharacter, onClick = onNetwork,
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp), horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
painter = painterResource(Res.drawable.ic_file_24dp), painter = painterResource(Res.drawable.ic_table_24dp),
contentDescription = null, contentDescription = null,
) )
Text( Text(
maxLines = 1, overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Start,
textAlign = TextAlign.Start, maxLines = 1,
text = stringResource(Res.string.main_page__create_action), text = stringResource(Res.string.main_page__network_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),
)
} }
} }
} }
} },
} )
} }

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -82,7 +83,7 @@ data class NetworkPageUio(
} }
@Composable @Composable
fun NetworkPage( fun NetworkScreen(
viewModel: NetworkViewModel = koinViewModel(), viewModel: NetworkViewModel = koinViewModel(),
) { ) {
val screen = LocalScreenController.current val screen = LocalScreenController.current
@ -97,17 +98,43 @@ fun NetworkPage(
) { ) {
BlurContent( BlurContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
controller = viewModel.controller, controller = viewModel.blurController,
) { ) {
NetworkContent( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
player = viewModel.network.collectAsState(), topBar = {
onBack = { screen.popBackStack() }, TopAppBar(
onPlayerChange = viewModel::onPlayerNameChange, title = {
onHostChange = viewModel::onHostChange, Text(
onPortChange = viewModel::onPortChange, overflow = TextOverflow.Ellipsis,
onConnect = viewModel::connect, maxLines = 1,
onDisconnect = viewModel::disconnect, 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,
player = viewModel.network.collectAsState(),
onPlayerChange = viewModel::onPlayerNameChange,
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect,
onDisconnect = viewModel::disconnect,
)
}
) )
} }
@ -138,96 +165,119 @@ fun NetworkPage(
} }
} }
@Composable
fun NetworkPage(
viewModel: NetworkViewModel = koinViewModel(),
) {
val snack = LocalSnackHost.current
Surface {
BlurContent(
modifier = Modifier.fillMaxSize(),
controller = viewModel.blurController,
) {
NetworkContent(
modifier = Modifier.fillMaxSize(),
player = viewModel.network.collectAsState(),
onPlayerChange = viewModel::onPlayerNameChange,
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = 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 @Composable
private fun NetworkContent( private fun NetworkContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(), scrollState: ScrollState = rememberScrollState(),
paddingValues: PaddingValues = PaddingValues(),
player: State<NetworkPageUio>, player: State<NetworkPageUio>,
onBack: () -> Unit,
onPlayerChange: (String) -> Unit, onPlayerChange: (String) -> Unit,
onHostChange: (String) -> Unit, onHostChange: (String) -> Unit,
onPortChange: (String) -> Unit, onPortChange: (String) -> Unit,
onConnect: () -> Unit, onConnect: () -> Unit,
onDisconnect: () -> Unit, onDisconnect: () -> Unit,
) { ) {
Scaffold( Column(
modifier = modifier, modifier = Modifier
topBar = { .verticalScroll(state = scrollState)
TopAppBar( .padding(paddingValues)
title = { .padding(all = 16.dp)
Text( .then(other = modifier),
overflow = TextOverflow.Ellipsis, ) {
maxLines = 1, TextField(
text = stringResource(Res.string.network__title), modifier = Modifier.fillMaxWidth(),
) singleLine = true,
}, enabled = player.value.enableFields,
navigationIcon = { label = { Text(text = stringResource(Res.string.network__player_name__label)) },
IconButton( onValueChange = { onPlayerChange(it) },
onClick = onBack, value = player.value.player,
) { )
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, Spacer(
contentDescription = null, modifier = Modifier.height(16.dp),
) )
}
} Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
TextField(
modifier = Modifier.weight(1f),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__host__label)) },
onValueChange = { onHostChange(it) },
value = player.value.host,
)
TextField(
modifier = Modifier.width(100.dp),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__port__label)) },
onValueChange = { onPortChange(it) },
value = player.value.port,
) )
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(paddingValues)
.padding(all = 16.dp),
) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__player_name__label)) },
onValueChange = { onPlayerChange(it) },
value = player.value.player,
)
Spacer(
modifier = Modifier.height(16.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
TextField(
modifier = Modifier.weight(1f),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__host__label)) },
onValueChange = { onHostChange(it) },
value = player.value.host,
)
TextField(
modifier = Modifier.width(100.dp),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__port__label)) },
onValueChange = { onPortChange(it) },
value = player.value.port,
)
}
TextButton(
enabled = player.value.enableActions,
onClick = onConnect,
) {
Text(text = stringResource(Res.string.network__socket__connect_action))
}
TextButton(
enabled = player.value.enableCancel,
onClick = onDisconnect,
) {
Text(text = stringResource(Res.string.network__socket__disconnect_action))
}
}
} }
)
TextButton(
enabled = player.value.enableActions,
onClick = onConnect,
) {
Text(text = stringResource(Res.string.network__socket__connect_action))
}
TextButton(
enabled = player.value.enableCancel,
onClick = onDisconnect,
) {
Text(text = stringResource(Res.string.network__socket__disconnect_action))
}
}
} }

View file

@ -9,6 +9,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -22,8 +23,8 @@ class NetworkViewModel(
private val factory: NetworkFactory, private val factory: NetworkFactory,
) : ViewModel() { ) : ViewModel() {
private val settings = settingsRepository.settings() private val settings = settingsRepository.settings()
private val host = mutableStateOf(settings.host) private val hostFlow = MutableStateFlow(settings.host)
private val port = mutableStateOf(settings.port) private val portFlow = MutableStateFlow(settings.port)
private val _networkError = MutableSharedFlow<ErrorSnackUio>() private val _networkError = MutableSharedFlow<ErrorSnackUio>()
val networkError: SharedFlow<ErrorSnackUio> get() = _networkError val networkError: SharedFlow<ErrorSnackUio> get() = _networkError
@ -34,17 +35,19 @@ class NetworkViewModel(
private val _isLoading = mutableStateOf(false) private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading val isLoading: State<Boolean> get() = _isLoading
val controller: BlurContentController = BlurContentController() val blurController: BlurContentController = BlurContentController()
val network: StateFlow<NetworkPageUio> = combine( val network: StateFlow<NetworkPageUio> = combine(
settingsRepository.settingsFlow(), settingsRepository.settingsFlow(),
networkRepository.status, networkRepository.status,
) { settings, status -> hostFlow,
portFlow,
) { settings, status, host, port ->
factory.convertToUio( factory.convertToUio(
player = settings.playerName, player = settings.playerName,
status = status, status = status,
host = host.value, host = host,
port = port.value, port = port,
) )
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
@ -61,36 +64,36 @@ class NetworkViewModel(
} }
fun onPortChange(port: String) { fun onPortChange(port: String) {
this.port.value = port.toIntOrNull() ?: settings.port this.portFlow.value = port.toIntOrNull() ?: settings.port
} }
fun onHostChange(host: String) { fun onHostChange(host: String) {
this.host.value = host this.hostFlow.value = host
} }
fun connect() { fun connect() {
controller.show() blurController.show()
_isLoading.value = true _isLoading.value = true
if (settings.host != host.value || settings.port != port.value) { if (settings.host != hostFlow.value || settings.port != portFlow.value) {
settingsRepository.update( settingsRepository.update(
settings = settings.copy( settings = settings.copy(
host = host.value, host = hostFlow.value,
port = port.value port = portFlow.value
) )
) )
} }
networkRepository.connect( networkRepository.connect(
host = host.value, host = hostFlow.value,
port = port.value, port = portFlow.value,
onConnect = { onConnect = {
_isLoading.value = false _isLoading.value = false
controller.hide() blurController.hide()
}, },
onFailure = { onFailure = {
_isLoading.value = false _isLoading.value = false
controller.hide() blurController.hide()
viewModelScope.launch { viewModelScope.launch {
_networkError.emit(ErrorSnackUio(it)) _networkError.emit(ErrorSnackUio(it))
} }

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
@ -33,6 +34,7 @@ import org.jetbrains.compose.resources.getString
class RollViewModel( class RollViewModel(
private val characterSheetRepository: CharacterSheetRepository, private val characterSheetRepository: CharacterSheetRepository,
private val settingsRepository: SettingsRepository,
private val skillComputation: ExpressionUseCase, private val skillComputation: ExpressionUseCase,
private val skillStepUseCase: SkillStepUseCase, private val skillStepUseCase: SkillStepUseCase,
private val networkRepository: NetworkRepository, private val networkRepository: NetworkRepository,

View file

@ -42,7 +42,7 @@ fun RollHistoryItem(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) { ) {
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),

View file

@ -13,6 +13,7 @@ import androidx.compose.material.Text
import androidx.compose.material.TopAppBar import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -26,12 +27,13 @@ import org.koin.compose.viewmodel.koinViewModel
fun RollHistoryPage( fun RollHistoryPage(
viewModel: RollHistoryViewModel = koinViewModel(), viewModel: RollHistoryViewModel = koinViewModel(),
) { ) {
val rolls = viewModel.rolls.collectAsState()
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
RollHistoryContent( RollHistoryContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
rolls = viewModel.rolls, rolls = rolls,
) )
} }
} }

View file

@ -1,47 +1,44 @@
package com.pixelized.desktop.lwa.ui.screen.rollhistory package com.pixelized.desktop.lwa.ui.screen.rollhistory
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.stateIn
class RollHistoryViewModel( class RollHistoryViewModel(
private val characterRepository: CharacterSheetRepository, characterRepository: CharacterSheetRepository,
private val rollRepository: RollHistoryRepository, rollRepository: RollHistoryRepository,
) : ViewModel() { ) : ViewModel() {
private val _rolls = mutableStateOf((emptyList<RollHistoryItemUio>())) private var _rolls: List<RollHistoryItemUio> = emptyList()
val rolls: State<List<RollHistoryItemUio>> get() = _rolls val rolls = combine(
characterRepository.characterSheetPreviewFlow,
init { rollRepository.rolls,
viewModelScope.launch { ) { sheets, message ->
combine( val name = sheets.firstOrNull { it.id == message.characterId }?.name ?: ""
characterRepository.characterSheetPreviewFlow, val roll = RollHistoryItemUio(
rollRepository.rolls, character = name,
) { sheets: List<CharacterSheetPreview>, content -> skillLabel = message.skillLabel,
_rolls.value.toMutableList().apply { rollDifficulty = message.rollDifficulty,
val name = sheets.firstOrNull { it.id == content.characterId }?.name ?: "" resultLabel = message.resultLabel,
add( rollValue = message.rollValue,
index = 0, rollSuccessLimit = message.rollSuccessLimit,
element = RollHistoryItemUio( )
character = name, _rolls = _rolls.toMutableList().also {
skillLabel = content.skillLabel, if (it.size == MAX_ROLL_HISTORY_SIZE) it.removeAt(index = it.lastIndex)
rollDifficulty = content.rollDifficulty, it.add(index = 0, element = roll)
resultLabel = content.resultLabel,
rollValue = content.rollValue,
rollSuccessLimit = content.rollSuccessLimit,
)
)
}
}.collect { content ->
_rolls.value = content
}
} }
_rolls
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = _rolls,
)
companion object {
const val MAX_ROLL_HISTORY_SIZE = 100
} }
} }

View file

@ -7,8 +7,8 @@ androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10" androidx-navigation = "2.8.0-alpha10"
ktor = "3.0.1" ktor = "3.0.1"
koin = "4.0.0" koin = "4.0.0"
turtle = "0.5.0" turtle = "0.10.0"
logback = "1.5.11" logback = "1.5.17"
coil = "3.1.0" coil = "3.1.0"
[plugins] [plugins]
@ -47,6 +47,7 @@ ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref =
ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" } ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" } ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" }
ktor-server-negotiation = { group = 'io.ktor', name = 'ktor-server-content-negotiation', version.ref = "ktor" } ktor-server-negotiation = { group = 'io.ktor', name = 'ktor-server-content-negotiation', version.ref = "ktor" }
# Utilitary run command line.
turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" } turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
# Utilitary logging use by Ktor.
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }