Refactor the RibbonViewModels to add an abstraction layer
This commit is contained in:
		
							parent
							
								
									27dba5438e
								
							
						
					
					
						commit
						a59444c610
					
				
					 3 changed files with 148 additions and 199 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue