Refactor the RibbonViewModels to add an abstraction layer

This commit is contained in:
Thomas Andres Gomez 2025-03-16 13:36:31 +01:00
parent 27dba5438e
commit a59444c610
3 changed files with 148 additions and 199 deletions

View file

@ -0,0 +1,122 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
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.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRollUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance
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.launchIn
import kotlinx.coroutines.flow.stateIn
import java.text.Collator
abstract class CharacterRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository,
private val settingsRepository: SettingsRepository,
characterRepository: CharacterSheetRepository,
alterationRepository: AlterationRepository,
campaignRepository: CampaignRepository,
private val ribbonFactory: CharacterRibbonFactory,
) : ViewModel() {
private val rolls = hashMapOf<CharacterInstance.Id, MutableState<CharacterPortraitRollUio?>>()
abstract val Campaign.data: Map<CharacterInstance.Id, CharacterInstance>
/**
* This flow is a tad complex so there is an explanation of wtf it's about :
* On a campaign update it go through every element of the abstract [data] map and either:
* - build and flow of an empty list to handle the case where the map is empty.
* - build a flow of flow bind to each character & alteration to update portrait details.
* Then sort the result.
*/
@OptIn(ExperimentalCoroutinesApi::class)
val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow
.flatMapMerge { campaign ->
when (campaign.data.isEmpty()) {
true -> flowOf(emptyList())
else -> combine<CharacterPortraitUio?, List<CharacterPortraitUio>>(
flows = campaign.data.map { entry ->
combine(
characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId),
alterationRepository.alterationsFlow(characterInstanceId = entry.key),
) { sheet, alterations ->
ribbonFactory.convertToPlayerPortraitUio(
characterSheet = sheet,
alterations = alterations,
characterInstanceId = entry.key,
characterInstance = entry.value,
enableDetail = settingsRepository.settings().isGM,
displayCharacterStats = settingsRepository.settings().isGM,
)
}
},
transform = { headers ->
headers.mapNotNull { it }
.sortedWith(compareBy(Collator.getInstance()) { it.name })
.toList()
}
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
@Composable
@Stable
fun roll(
characterId: CharacterInstance.Id,
): State<CharacterPortraitRollUio?> {
val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
LaunchedEffect(characterId) {
combine(
settingsRepository.settingsFlow(),
rollHistoryRepository.rolls,
) { settings, roll ->
if (settings.dynamicDice &&
characterId.equals(roll.prefix, roll.characterSheetId, roll.instanceId)
) {
state.value = CharacterPortraitRollUio(
characterId = CharacterInstance.Id(
prefix = characterId.prefix,
characterSheetId = characterId.characterSheetId,
instanceId = characterId.instanceId,
),
value = roll.rollValue,
label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it }
)
}
}.launchIn(this)
}
return state
}
fun onPortraitRollRightClick(
characterId: CharacterInstance.Id,
) {
rolls[characterId]?.value = null
}
}

View file

@ -1,114 +1,28 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
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.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRollUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign
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.launchIn
import kotlinx.coroutines.flow.stateIn
import java.text.Collator
private typealias CharacterId = Campaign.CharacterInstance.Id
class NpcRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository,
private val settingsRepository: SettingsRepository,
rollHistoryRepository: RollHistoryRepository,
settingsRepository: SettingsRepository,
characterRepository: CharacterSheetRepository,
alterationRepository: AlterationRepository,
campaignRepository: CampaignRepository,
private val ribbonFactory: CharacterRibbonFactory,
) : ViewModel() {
private val rolls = hashMapOf<CharacterId, MutableState<CharacterPortraitRollUio?>>()
@OptIn(ExperimentalCoroutinesApi::class)
val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow
.flatMapMerge { campaign ->
if (campaign.npcs.isEmpty()) {
flowOf(emptyList())
} else {
combine<CharacterPortraitUio?, List<CharacterPortraitUio>>(
flows = campaign.npcs.map { entry ->
combine(
characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId),
alterationRepository.alterationsFlow(characterInstanceId = entry.key),
) { sheet, alterations ->
ribbonFactory.convertToPlayerPortraitUio(
characterSheet = sheet,
alterations = alterations,
characterInstanceId = entry.key,
characterInstance = entry.value,
enableDetail = settingsRepository.settings().isGM,
displayCharacterStats = settingsRepository.settings().isGM,
)
}
},
transform = { headers ->
headers.mapNotNull { it }
.sortedWith(compareBy(Collator.getInstance()) { it.name })
.toList()
}
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
@Composable
@Stable
fun roll(
characterId: Campaign.CharacterInstance.Id,
): State<CharacterPortraitRollUio?> {
val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
LaunchedEffect(characterId) {
combine(
settingsRepository.settingsFlow(),
rollHistoryRepository.rolls,
) { settings, roll ->
if (settings.dynamicDice &&
characterId.equals(roll.prefix, roll.characterSheetId, roll.instanceId)
) {
state.value = CharacterPortraitRollUio(
characterId = Campaign.CharacterInstance.Id(
prefix = characterId.prefix,
characterSheetId = characterId.characterSheetId,
instanceId = characterId.instanceId,
),
value = roll.rollValue,
label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it }
)
}
}.launchIn(this)
}
return state
}
fun onPortraitRollRightClick(
characterId: Campaign.CharacterInstance.Id,
) {
rolls[characterId]?.value = null
}
ribbonFactory: CharacterRibbonFactory,
) : CharacterRibbonViewModel(
rollHistoryRepository = rollHistoryRepository,
settingsRepository = settingsRepository,
characterRepository = characterRepository,
alterationRepository = alterationRepository,
campaignRepository = campaignRepository,
ribbonFactory = ribbonFactory,
) {
override val Campaign.data get() = npcs
}

View file

@ -1,115 +1,28 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
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.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRollUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign
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.launchIn
import kotlinx.coroutines.flow.stateIn
import java.text.Collator
private typealias CharacterId = Campaign.CharacterInstance.Id
class PlayerRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository,
private val settingsRepository: SettingsRepository,
rollHistoryRepository: RollHistoryRepository,
settingsRepository: SettingsRepository,
characterRepository: CharacterSheetRepository,
alterationRepository: AlterationRepository,
campaignRepository: CampaignRepository,
private val ribbonFactory: CharacterRibbonFactory,
) : ViewModel() {
private val rolls = hashMapOf<CharacterId, MutableState<CharacterPortraitRollUio?>>()
@OptIn(ExperimentalCoroutinesApi::class)
val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow
.flatMapMerge { campaign ->
if (campaign.characters.isEmpty()) {
flowOf(emptyList())
} else {
combine<CharacterPortraitUio?, List<CharacterPortraitUio>>(
flows = campaign.characters.map { entry ->
combine(
characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId),
alterationRepository.alterationsFlow(characterInstanceId = entry.key),
) { sheet, alterations ->
ribbonFactory.convertToPlayerPortraitUio(
characterSheet = sheet,
alterations = alterations,
characterInstanceId = entry.key,
characterInstance = entry.value,
enableDetail = true,
displayCharacterStats = true,
)
}
},
transform = { headers ->
headers.mapNotNull { it }
.sortedWith(compareBy(Collator.getInstance()) { it.name })
.toList()
}
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
@Composable
@Stable
fun roll(
characterId: Campaign.CharacterInstance.Id,
): State<CharacterPortraitRollUio?> {
val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
LaunchedEffect(characterId) {
combine(
settingsRepository.settingsFlow(),
rollHistoryRepository.rolls,
) { settings, roll ->
if (settings.dynamicDice &&
characterId.equals(roll.prefix, roll.characterSheetId, roll.instanceId)
) {
state.value = CharacterPortraitRollUio(
characterId = Campaign.CharacterInstance.Id(
prefix = characterId.prefix,
characterSheetId = characterId.characterSheetId,
instanceId = characterId.instanceId,
),
value = roll.rollValue,
label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it }
)
}
}.launchIn(this)
}
return state
}
fun onPortraitRollRightClick(
characterId: Campaign.CharacterInstance.Id,
) {
rolls[characterId]?.value = null
}
ribbonFactory: CharacterRibbonFactory,
) : CharacterRibbonViewModel(
rollHistoryRepository = rollHistoryRepository,
settingsRepository = settingsRepository,
characterRepository = characterRepository,
alterationRepository = alterationRepository,
campaignRepository = campaignRepository,
ribbonFactory = ribbonFactory,
) {
override val Campaign.data get() = characters
}