~cytrogen/vbhelper

cf272e80308b44fbe66e7c3fd92d7147a02f76bf — nacabaro 5 months ago 1d9879f + ffa6958
Merge pull request #42 from nacabaro/dex/possible_fusions

Few things
19 files changed, 781 insertions(+), 487 deletions(-)

M app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt
M app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt
M app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt
M app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt
M app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt
M app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt
M app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt
A app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt
M app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt
A app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt
A app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt
M app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt
M app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt => app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt +3 -2
@@ 3,6 3,7 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.CardDtos
import kotlinx.coroutines.flow.Flow

@Dao
interface CardAdventureDao {


@@ 52,7 53,7 @@ interface CardAdventureDao {
        WHERE
            cc.cardId = :cardId
    """)
    suspend fun getAdventureForCard(
    fun getAdventureForCard(
        cardId: Long
    ): List<CardDtos.CardAdventureWithSprites>
    ): Flow<List<CardDtos.CardAdventureWithSprites>>
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt => app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt +28 -12
@@ 2,6 2,9 @@ package com.github.nacabaro.vbhelper.daos

import androidx.room.Dao
import androidx.room.Query
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow

@Dao
interface CardFusionsDao {


@@ 9,24 12,37 @@ interface CardFusionsDao {
        INSERT INTO
            CardFusions (
                fromCharaId,
                attribute1Fusion,
                attribute2Fusion,
                attribute3Fusion,
                attribute4Fusion
                attribute,
                toCharaId
            )
        SELECT
            (SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :fromCharaId),
            (SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdAttr1),
            (SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdAttr2),
            (SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdAttr3),
            (SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdAttr4)            
            :attribute,
            (SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaId)
    """)
    suspend fun insertNewFusion(
        cardId: Long,
        fromCharaId: Int,
        toCharaIdAttr1: Int,
        toCharaIdAttr2: Int,
        toCharaIdAttr3: Int,
        toCharaIdAttr4: Int
        attribute: NfcCharacter.Attribute,
        toCharaId: Int
    )

    @Query("""
        SELECT 
            cf.toCharaId as charaId,
            cf.fromCharaId as fromCharaId,
            s.spriteIdle1 as spriteIdle,
            cc.attribute as attribute,
            s.width as spriteWidth,
            s.height as spriteHeight,
            d.discoveredOn as discoveredOn,
            cf.attribute as fusionAttribute
        FROM CardFusions cf
        JOIN CardCharacter cc ON cc.id = cf.toCharaId
        JOIN Sprite s ON s.id = cc.id
        LEFT JOIN Dex d ON d.id = cc.id
        WHERE cf.fromCharaId = :charaId
        ORDER BY cc.charaIndex
    """)
    fun getFusionsForCharacter(charaId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>>
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt => app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt +2 -1
@@ 5,6 5,7 @@ import androidx.room.Insert
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow

@Dao
interface CardProgressDao {


@@ 21,7 22,7 @@ interface CardProgressDao {
    @Query(
        "SELECT currentStage FROM CardProgress WHERE cardId = :cardId"
    )
    fun getCardProgress(cardId: Long): Int
    fun getCardProgress(cardId: Long): Flow<Int>

    @Insert
    fun insertCardProgress(cardProgress: CardProgress)

M app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt => app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt +3 -2
@@ 6,6 6,7 @@ import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow

@Dao
interface CharacterDao {


@@ 82,8 83,8 @@ interface CharacterDao {
        JOIN Sprite s ON s.id = c.spriteId
        LEFT JOIN Dex d ON d.id = pt.toCharaId
        WHERE
            c.cardId = :cardId
            pt.charaId = :characterId
    """
    )
    suspend fun getEvolutionRequirementsForCard(cardId: Long): List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>
    fun getEvolutionRequirementsForCard(characterId: Long): Flow<List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>>
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt => app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt +3 -2
@@ 4,6 4,7 @@ import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow

@Dao
interface DexDao {


@@ 40,7 41,7 @@ interface DexDao {
        WHERE c.cardId = :cardId
    """
    )
    suspend fun getSingleCardProgress(cardId: Long): List<CharacterDtos.CardCharaProgress>
    fun getSingleCardProgress(cardId: Long): Flow<List<CharacterDtos.CardCharaProgress>>

    @Query(
        """


@@ 55,5 56,5 @@ interface DexDao {
        FROM Card c
    """
    )
    suspend fun getCardsWithProgress(): List<CardDtos.CardProgress>
    fun getCardsWithProgress(): Flow<List<CardDtos.CardProgress>>
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt => app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt +4 -23
@@ 3,6 3,7 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter

@Entity(
    foreignKeys = [


@@ 15,25 16,7 @@ import androidx.room.PrimaryKey
        ForeignKey(
            entity = CardCharacter::class,
            parentColumns = ["id"],
            childColumns = ["attribute1Fusion"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = CardCharacter::class,
            parentColumns = ["id"],
            childColumns = ["attribute2Fusion"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = CardCharacter::class,
            parentColumns = ["id"],
            childColumns = ["attribute3Fusion"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = CardCharacter::class,
            parentColumns = ["id"],
            childColumns = ["attribute4Fusion"],
            childColumns = ["toCharaId"],
            onDelete = ForeignKey.CASCADE
        )
    ]


@@ 41,8 24,6 @@ import androidx.room.PrimaryKey
data class CardFusions(
    @PrimaryKey(autoGenerate = true) val id: Long,
    val fromCharaId: Long,
    val attribute1Fusion: Long?,
    val attribute2Fusion: Long?,
    val attribute3Fusion: Long?,
    val attribute4Fusion: Long?
    val attribute: NfcCharacter.Attribute,
    val toCharaId: Long
)
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt => app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt +10 -0
@@ 114,4 114,14 @@ object CharacterDtos {
        val changeTimerHours: Int,
        val requiredAdventureLevelCompleted: Int
    )

    data class FusionsWithSpritesAndObtained(
        val charaId: Long,
        val fromCharaId: Long,
        val spriteIdle: ByteArray,
        val spriteWidth: Int,
        val spriteHeight: Int,
        val discoveredOn: Long?,
        val fusionAttribute: NfcCharacter.Attribute
    )
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt +8 -23
@@ 6,18 6,11 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.dtos.CardDtos
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@Composable
fun CardAdventureScreen(


@@ 25,20 18,12 @@ fun CardAdventureScreen(
    cardScreenController: CardScreenControllerImpl,
    cardId: Long
) {
    val cardAdventureMissions = remember { mutableStateOf(emptyList<CardDtos.CardAdventureWithSprites>()) }
    var currentCardAdventure by remember { mutableIntStateOf(0) }

    LaunchedEffect(cardId) {
        withContext(Dispatchers.IO) {
            cardAdventureMissions.value =
                cardScreenController
                    .getCardAdventureMissions(cardId)

            currentCardAdventure =
                cardScreenController
                    .getCardProgress(cardId)
        }
    }
    val cardAdventureMissions by cardScreenController
        .getCardAdventureMissions(cardId)
        .collectAsState(emptyList())
    val currentCardAdventure by cardScreenController
        .getCardProgress(cardId)
        .collectAsState(0)

    Scaffold (
        topBar = {


@@ 55,7 40,7 @@ fun CardAdventureScreen(
                .padding(top = contentPadding.calculateTopPadding())
                .verticalScroll(state = rememberScrollState())
        ) {
            cardAdventureMissions.value.mapIndexed { index, it ->
            cardAdventureMissions.mapIndexed { index, it ->
                CardAdventureEntry(
                    cardAdventureEntry = it,
                    obscure = index > currentCardAdventure - 1

M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt +5 -2
@@ 1,10 1,13 @@
package com.github.nacabaro.vbhelper.screens.cardScreen

import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow

interface CardScreenController {
    fun renameCard(cardId: Long, newName: String, onRenamed: (String) -> Unit)
    fun deleteCard(cardId: Long, onDeleted: () -> Unit)
    suspend fun getCardAdventureMissions(cardId: Long): List<CardDtos.CardAdventureWithSprites>
    suspend fun getCardProgress(cardId: Long): Int
    fun getCardAdventureMissions(cardId: Long): Flow<List<CardDtos.CardAdventureWithSprites>>
    fun getCardProgress(cardId: Long): Flow<Int>
    fun getFusionsForCharacters(characterId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>>
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt +11 -2
@@ 4,6 4,8 @@ import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

class CardScreenControllerImpl(


@@ 32,15 34,22 @@ class CardScreenControllerImpl(
        }
    }

    override suspend fun getCardAdventureMissions(cardId: Long): List<CardDtos.CardAdventureWithSprites> {
    override fun getCardAdventureMissions(cardId: Long): Flow<List<CardDtos.CardAdventureWithSprites>> {
        return database
            .cardAdventureDao()
            .getAdventureForCard(cardId)
    }

    override suspend fun getCardProgress(cardId: Long): Int {
    override fun getCardProgress(cardId: Long): Flow<Int> {
        return database
            .cardProgressDao()
            .getCardProgress(cardId)
    }

    override fun getFusionsForCharacters(characterId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>> {
        return database
            .cardFusionsDao()
            .getFusionsForCharacter(characterId)
    }

}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt +4 -18
@@ 5,10 5,10 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.utils.BitmapData


@@ 19,32 19,19 @@ import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.cardScreen.dialogs.DexCharaDetailsDialog
import com.github.nacabaro.vbhelper.source.DexRepository
import kotlinx.coroutines.launch

@Composable
fun CardViewScreen(
    navController: NavController,
    cardId: Long
) {
    val coroutineScope = rememberCoroutineScope()
    val application = LocalContext.current.applicationContext as VBHelper
    val dexRepository = DexRepository(application.container.db)

    val characterList = remember { mutableStateOf<List<CharacterDtos.CardCharaProgress>>(emptyList()) }
    val cardPossibleTransformations = remember { mutableStateOf<List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>>(emptyList()) }
    val characterList by dexRepository.getCharactersByCardId(cardId).collectAsState(emptyList())

    val selectedCharacter = remember { mutableStateOf<CharacterDtos.CardCharaProgress?>(null) }

    LaunchedEffect(dexRepository) {
        coroutineScope.launch {
            val newCharacterList = dexRepository.getCharactersByCardId(cardId)
            characterList.value = newCharacterList

            val newCardPossibleTransformations = dexRepository.getCardPossibleTransformations(cardId)
            cardPossibleTransformations.value = newCardPossibleTransformations
        }
    }

    Scaffold (
        topBar = {
            TopBanner(


@@ 70,7 57,7 @@ fun CardViewScreen(
            columns = GridCells.Fixed(3),
            contentPadding = contentPadding
        ) {
            items(characterList.value) { character ->
            items(characterList) { character ->
                CharacterEntry(
                    onClick = {
                        selectedCharacter.value = character


@@ 88,7 75,6 @@ fun CardViewScreen(
        if (selectedCharacter.value != null) {
            DexCharaDetailsDialog(
                currentChara = selectedCharacter.value!!,
                possibleTransformations = cardPossibleTransformations.value,
                obscure = selectedCharacter.value!!.discoveredOn == null,
                onClickClose = {
                    selectedCharacter.value = null

M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt +3 -13
@@ 7,11 7,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext


@@ 25,17 24,15 @@ import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.cardScreen.dialogs.CardDeleteDialog
import com.github.nacabaro.vbhelper.screens.cardScreen.dialogs.CardRenameDialog
import com.github.nacabaro.vbhelper.source.DexRepository
import kotlinx.coroutines.launch

@Composable
fun CardsScreen(
    navController: NavController,
    cardScreenController: CardScreenControllerImpl
) {
    val coroutineScope = rememberCoroutineScope()
    val application = LocalContext.current.applicationContext as VBHelper
    val dexRepository = DexRepository(application.container.db)
    val cardList = remember { mutableStateOf<List<CardDtos.CardProgress>>(emptyList()) }
    val cardList by dexRepository.getAllDims().collectAsState(emptyList())

    val selectedCard = remember { mutableStateOf<CardDtos.CardProgress?>(null) }
    var clickedDelete by remember { mutableStateOf(false) }


@@ 43,13 40,6 @@ fun CardsScreen(

    var modifyCards by remember { mutableStateOf(false) }

    LaunchedEffect(dexRepository) {
        coroutineScope.launch {
            val newDimList = dexRepository.getAllDims()
            cardList.value = newDimList
        }
    }

    Scaffold (
        topBar = {
            TopBanner(


@@ 64,7 54,7 @@ fun CardsScreen(
            modifier = Modifier
                .padding(top = contentPadding.calculateTopPadding())
        ) {
            items(cardList.value) {
            items(cardList) {
                CardEntry(
                    name = it.cardName,
                    logo = BitmapData(

M app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt +52 -6
@@ 16,6 16,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter


@@ 23,7 28,9 @@ import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.source.DexRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getImageBitmap



@@ 31,14 38,25 @@ import com.github.nacabaro.vbhelper.utils.getImageBitmap
@Composable
fun DexCharaDetailsDialog(
    currentChara: CharacterDtos.CardCharaProgress,
    possibleTransformations: List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>,
    obscure: Boolean,
    onClickClose: () -> Unit
) {
    val nameMultiplier = 3
    val charaMultiplier = 4

    val currentCharaPossibleTransformations = possibleTransformations.filter { it.fromCharaId == currentChara.id }
    val application = LocalContext.current.applicationContext as VBHelper
    val database = application.container.db
    val dexRepository = DexRepository(database)

    var showFusions by remember { mutableStateOf(false) }

    val currentCharaPossibleTransformations by dexRepository
        .getCharacterPossibleTransformations(currentChara.id)
        .collectAsState(emptyList())

    val currentCharaPossibleFusions by dexRepository
        .getCharacterPossibleFusions(currentChara.id)
        .collectAsState(emptyList())

    val romanNumeralsStage = when (currentChara.stage) {
        1 -> "II"


@@ 204,12 222,40 @@ fun DexCharaDetailsDialog(
                    }
                }

                Button(
                    onClick = onClickClose
                ) {
                    Text("Close")
                Row {
                    if (currentCharaPossibleFusions.isNotEmpty()) {
                        Button(
                            onClick = {
                                showFusions = true
                            }
                        ) {
                            Text("Fusions")
                        }
                    }

                    Spacer(
                        modifier = Modifier
                            .padding(4.dp)
                    )

                    Button(
                        onClick = onClickClose
                    ) {
                        Text("Close")
                    }
                }
            }
        }
    }

    if (showFusions) {
        DexCharaFusionsDialog(
            currentChara = currentChara,
            currentCharaPossibleFusions = currentCharaPossibleFusions,
            onClickDismiss = {
                showFusions = false
            },
            obscure = obscure
        )
    }
}
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt +191 -0
@@ 0,0 1,191 @@
package com.github.nacabaro.vbhelper.screens.cardScreen.dialogs

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getImageBitmap

@Composable
fun DexCharaFusionsDialog(
    currentChara: CharacterDtos.CardCharaProgress,
    currentCharaPossibleFusions: List<CharacterDtos.FusionsWithSpritesAndObtained>,
    obscure: Boolean,
    onClickDismiss: () -> Unit,
) {
    val nameMultiplier = 3
    val charaMultiplier = 4

    val charaBitmapData = BitmapData(
        bitmap = currentChara.spriteIdle,
        width = currentChara.spriteWidth,
        height = currentChara.spriteHeight
    )
    val charaImageBitmapData = charaBitmapData.getImageBitmap(
        context = LocalContext.current,
        multiplier = charaMultiplier,
        obscure = obscure
    )

    val nameBitmapData = BitmapData(
        bitmap = currentChara.nameSprite,
        width = currentChara.nameSpriteWidth,
        height = currentChara.nameSpriteHeight
    )
    val nameImageBitmapData = nameBitmapData.getImageBitmap(
        context = LocalContext.current,
        multiplier = nameMultiplier,
        obscure = obscure
    )

    Dialog(
        onDismissRequest = onClickDismiss,
    ) {
        Card(
            modifier = Modifier
                .fillMaxWidth()
        ) {
            Column(
                modifier = Modifier
                    .padding(16.dp)
            ) {
                Row {
                    Card (
                        colors = CardColors(
                            containerColor = MaterialTheme.colorScheme.surfaceVariant,
                            contentColor = MaterialTheme.colorScheme.contentColorFor(
                                backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
                            ),
                            disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
                            disabledContentColor = MaterialTheme.colorScheme.contentColorFor(
                                backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
                            )
                        )
                    ) {
                        Image(
                            bitmap = charaImageBitmapData.imageBitmap,
                            contentDescription = "Icon",
                            modifier = Modifier
                                .size(charaImageBitmapData.dpWidth)
                                .padding(8.dp),
                            colorFilter = when (obscure) {
                                true -> ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
                                false -> null
                            },
                            filterQuality = FilterQuality.None
                        )
                    }

                    Spacer(
                        modifier = Modifier
                            .padding(16.dp)
                    )

                    if (!obscure) {
                        Column {
                            Image(
                                bitmap = nameImageBitmapData.imageBitmap,
                                contentDescription = "Icon",
                                modifier = Modifier
                                    .width(nameImageBitmapData.dpWidth)
                                    .height(nameImageBitmapData.dpHeight),
                                filterQuality = FilterQuality.None
                            )
                        }
                    } else {
                        Column {
                            Text(text = "????????????????")
                        }
                    }
                }

                Spacer(modifier = Modifier.padding(16.dp))
                Column {
                    currentCharaPossibleFusions.map {
                        val selectedCharaBitmap = BitmapData(
                            bitmap = it.spriteIdle,
                            width = it.spriteWidth,
                            height = it.spriteHeight
                        )
                        val selectedCharaImageBitmap = selectedCharaBitmap.getImageBitmap(
                            context = LocalContext.current,
                            multiplier = 4,
                            obscure = it.discoveredOn == null
                        )

                        Card (
                            modifier = Modifier
                                .padding(vertical = 8.dp)
                        ) {
                            Row (
                                verticalAlignment = Alignment.CenterVertically,
                                modifier = Modifier
                                    .fillMaxWidth()
                            ) {
                                Card (
                                    colors = CardColors(
                                        containerColor = MaterialTheme.colorScheme.surfaceVariant,
                                        contentColor = MaterialTheme.colorScheme.contentColorFor(
                                            backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
                                        ),
                                        disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
                                        disabledContentColor = MaterialTheme.colorScheme.contentColorFor(
                                            backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
                                        )
                                    )
                                ) {
                                    Image(
                                        bitmap = selectedCharaImageBitmap.imageBitmap,
                                        contentDescription = "Icon",
                                        modifier = Modifier
                                            .size(selectedCharaImageBitmap.dpWidth)
                                            .padding(8.dp),
                                        colorFilter = when (it.discoveredOn == null) {
                                            true -> ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
                                            false -> null
                                        },
                                        filterQuality = FilterQuality.None
                                    )
                                }
                                Spacer(
                                    modifier = Modifier
                                        .padding(16.dp)
                                )
                                Column {
                                    Text("Combine with ${it.fusionAttribute}")
                                }
                            }
                        }
                    }
                }

                Button(
                    onClick = onClickDismiss
                ) {
                    Text("Close")
                }
            }
        }
    }
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt +0 -3
@@ 1,6 1,5 @@
package com.github.nacabaro.vbhelper.screens.homeScreens

import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize


@@ 92,7 91,6 @@ fun HomeScreen(
        }
    ) { contentPadding ->
        if (activeMon.value == null || (beData.value == null && vbData.value == null) || transformationHistory.value == null) {
            Log.d("TetTet", "Something is null")
            Column (
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center,


@@ 103,7 101,6 @@ fun HomeScreen(
                Text(text = "Nothing to see here")
            }
        } else {
            Log.d("TetTet", "Something is not null")
            if (activeMon.value!!.isBemCard) {
                BEBEmHomeScreen(
                    activeMon = activeMon.value!!,

M app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt +10 -374
@@ 3,36 3,24 @@ package com.github.nacabaro.vbhelper.screens.settingsScreen
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.github.cfogrady.vb.dim.card.BemCard
import com.github.cfogrady.vb.dim.card.DimCard
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.CardImportController
import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.DatabaseManagementController
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import java.io.File
import java.io.InputStream
import java.io.OutputStream


class SettingsScreenControllerImpl(
    private val context: ComponentActivity,
): SettingsScreenController {
    private val roomDbName = "internalDb"
    private val filePickerLauncher: ActivityResultLauncher<String>
    private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
    private val filePickerApk: ActivityResultLauncher<Array<String>>


@@ 41,13 29,17 @@ class SettingsScreenControllerImpl(
    private val application = context.applicationContext as VBHelper
    private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
    private val database: AppDatabase = application.container.db
    private val databaseManagementController = DatabaseManagementController(
        componentActivity = context,
        application = application
    )

    init {
        filePickerLauncher = context.registerForActivityResult(
            ActivityResultContracts.CreateDocument("application/octet-stream")
        ) { uri ->
                if (uri != null) {
                    exportDatabase(uri)
                    databaseManagementController.exportDatabase(uri)
                } else {
                    context.runOnUiThread {
                        Toast.makeText(context, "No destination selected", Toast.LENGTH_SHORT)


@@ 60,7 52,7 @@ class SettingsScreenControllerImpl(
            ActivityResultContracts.OpenDocument()
        ) { uri ->
            if (uri != null) {
                importDatabase(uri)
                databaseManagementController.importDatabase(uri)
            } else {
                context.runOnUiThread {
                    Toast.makeText(context, "No source selected", Toast.LENGTH_SHORT).show()


@@ 109,276 101,14 @@ class SettingsScreenControllerImpl(
        filePickerCard.launch(arrayOf("*/*"))
    }

    private suspend fun importEvoData(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        for (index in 0 until card.transformationRequirements.transformationEntries.size) {
            val evo = card.transformationRequirements.transformationEntries[index]

            var transformationTimerHours: Int
            var unlockAdventureLevel: Int

            if (card is BemCard) {
                transformationTimerHours = card
                    .transformationRequirements
                    .transformationEntries[index]
                    .minutesUntilTransformation / 60
                unlockAdventureLevel = if (
                    card
                        .transformationRequirements
                        .transformationEntries[index]
                        .requiredCompletedAdventureLevel == 65535
                ) {
                    0
                } else {
                    card
                        .transformationRequirements
                        .transformationEntries[index]
                        .requiredCompletedAdventureLevel
                }
            } else {
                transformationTimerHours = (card as DimCard)
                    .transformationRequirements
                    .transformationEntries[index]
                    .hoursUntilEvolution
                unlockAdventureLevel = if (
                    card
                        .adventureLevels
                        .levels
                        .last()
                        .bossCharacterIndex == card.transformationRequirements.transformationEntries[index].toCharacterIndex
                ) {
                    14
                    /*
                    Magic number incoming!!

                    In the case of DiMCards, stage 15 is the one that unlocks the locked character.
                    We know it is a locked character if the last adventure level's boss character index
                    is the current index. If it is, we add stage 15 complete as a requirement for transformation.
                     */
                } else {
                    0
                    /*
                    Another magic number...

                    The rest of the characters are not locked.
                     */
                }
            }

            database
                .characterDao()
                .insertPossibleTransformation(
                    cardId = cardId,
                    fromChraraIndex = evo.fromCharacterIndex,
                    toChraraIndex = evo.toCharacterIndex,
                    requiredVitals = evo.requiredVitalValues,
                    requiredTrophies = evo.requiredTrophies,
                    requiredBattles = evo.requiredBattles,
                    requiredWinRate = evo.requiredWinRatio,
                    requiredAdventureLevelCompleted = unlockAdventureLevel,
                    changeTimerHours = transformationTimerHours
                )
        }
    }

    private suspend fun importCharacterData(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        var spriteCounter = when (card is BemCard) {
            true -> 54
            false -> 10
        }

        val domainCharacters = mutableListOf<CardCharacter>()

        val characters = card
            .characterStats
            .characterEntries

        for (index in 0 until characters.size) {
            var domainSprite: Sprite?
            if (index < 2 && card is DimCard) {
                domainSprite = Sprite(
                    width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
                    height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
                    spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
                    spriteWalk1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteWalk2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteRun1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteRun2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteTrain1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteTrain2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteHappy = card.spriteData.sprites[spriteCounter + 4].pixelData,
                    spriteSleep = card.spriteData.sprites[spriteCounter + 5].pixelData,
                    spriteAttack = card.spriteData.sprites[spriteCounter + 2].pixelData,
                    spriteDodge = card.spriteData.sprites[spriteCounter + 3].pixelData
                )
            } else {
                domainSprite = Sprite(
                    width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
                    height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
                    spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
                    spriteWalk1 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteWalk2 = card.spriteData.sprites[spriteCounter + 4].pixelData,
                    spriteRun1 = card.spriteData.sprites[spriteCounter + 5].pixelData,
                    spriteRun2 = card.spriteData.sprites[spriteCounter + 6].pixelData,
                    spriteTrain1 = card.spriteData.sprites[spriteCounter + 7].pixelData,
                    spriteTrain2 = card.spriteData.sprites[spriteCounter + 8].pixelData,
                    spriteHappy = card.spriteData.sprites[spriteCounter + 9].pixelData,
                    spriteSleep = card.spriteData.sprites[spriteCounter + 10].pixelData,
                    spriteAttack = card.spriteData.sprites[spriteCounter + 11].pixelData,
                    spriteDodge = card.spriteData.sprites[spriteCounter + 12].pixelData
                )
            }

            val spriteId = database
                .spriteDao()
                .insertSprite(domainSprite)


            domainCharacters.add(
                CardCharacter(
                    cardId = cardId,
                    spriteId = spriteId,
                    charaIndex = index,
                    nameSprite = card.spriteData.sprites[spriteCounter].pixelData,
                    stage = characters[index].stage,
                    attribute = NfcCharacter.Attribute.entries[characters[index].attribute],
                    baseHp = characters[index].hp,
                    baseBp = characters[index].dp,
                    baseAp = characters[index].ap,
                    nameWidth = card.spriteData.sprites[spriteCounter].spriteDimensions.width,
                    nameHeight = card.spriteData.sprites[spriteCounter].spriteDimensions.height
                )
            )

            spriteCounter += if (card is BemCard) {
                14
            } else {
                when (index) {
                    0 -> 6
                    1 -> 7
                    else -> 14
                }
            }
        }

        database
            .characterDao()
            .insertCharacter(*domainCharacters.toTypedArray())
    }

    private suspend fun importAdventureMissions(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        Log.d("importAdventureMissions", "Importing adventure missions")
        if (card is BemCard) {
            card.adventureLevels.levels.forEach {
                database
                    .cardAdventureDao()
                    .insertNewAdventure(
                        cardId = cardId,
                        characterId = it.bossCharacterIndex,
                        steps = it.steps,
                        bossAp = it.bossAp,
                        bossHp = it.bossHp,
                        bossDp = it.bossDp,
                        bossBp = it.bossBp
                    )
            }
        } else if (card is DimCard) {
            card.adventureLevels.levels.map {
                database
                    .cardAdventureDao()
                    .insertNewAdventure(
                        cardId = cardId,
                        characterId = it.bossCharacterIndex,
                        steps = it.steps,
                        bossAp = it.bossAp,
                        bossHp = it.bossHp,
                        bossDp = it.bossDp,
                        bossBp = null
                    )
            }
        }
    }

    private suspend fun importCardFusions(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        Log.d("importCardFusions", "Importing card fusions")
        if (card is DimCard) {
            card
                .attributeFusions
                .entries
                .forEach {
                    database
                        .cardFusionsDao()
                        .insertNewFusion(
                            cardId = cardId,
                            fromCharaId = it.characterIndex,
                            toCharaIdAttr1 = it.attribute1Fusion,
                            toCharaIdAttr2 = it.attribute2Fusion,
                            toCharaIdAttr3 = it.attribute3Fusion,
                            toCharaIdAttr4 = it.attribute4Fusion
                        )
                }
        }
    }

    private fun updateCardProgress(
        cardId: Long,
    ) {
        database
            .cardProgressDao()
            .insertCardProgress(
                CardProgress(
                    cardId = cardId,
                    currentStage = 1,
                    unlocked = false
                )
            )
    }

    private fun importCard(uri: Uri) {
        context.lifecycleScope.launch(Dispatchers.IO) {
            val contentResolver = context.contentResolver
            val inputStream = contentResolver.openInputStream(uri)

            inputStream.use { fileReader ->
                val dimReader = DimReader()
                val card = dimReader.readCard(fileReader, false)

                val cardModel = Card(
                    cardId = card.header.dimId,
                    logo = card.spriteData.sprites[0].pixelData,
                    name = card.spriteData.text,
                    stageCount = card.adventureLevels.levels.size,
                    logoHeight = card.spriteData.sprites[0].height,
                    logoWidth = card.spriteData.sprites[0].width,
                    isBEm = card is BemCard
                )

                val cardId = database
                    .cardDao()
                    .insertNewCard(cardModel)

                updateCardProgress(cardId = cardId)

                importCharacterData(cardId, card)

                importEvoData(cardId, card)

                importAdventureMissions(cardId, card)

                importCardFusions(cardId, card)
                val cardImportController = CardImportController(database)
                cardImportController.importCard(fileReader)
            }

            inputStream?.close()


@@ 388,100 118,6 @@ class SettingsScreenControllerImpl(
        }
    }

    private fun exportDatabase(destinationUri: Uri) {
        context.lifecycleScope.launch(Dispatchers.IO) {
            try {
                val dbFile = File(context.getDatabasePath(roomDbName).absolutePath)
                if (!dbFile.exists()) {
                    throw IllegalStateException("Database file does not exist!")
                }

                application.container.db.close()

                context.contentResolver.openOutputStream(destinationUri)?.use { outputStream ->
                    dbFile.inputStream().use { inputStream ->
                        copyFile(inputStream, outputStream)
                    }
                } ?: throw IllegalArgumentException("Unable to open destination Uri for writing")

                context.runOnUiThread {
                    Toast.makeText(context, "Database exported successfully!", Toast.LENGTH_SHORT).show()
                    Toast.makeText(context, "Closing application to avoid changes.", Toast.LENGTH_LONG).show()
                    context.finishAffinity()
                }
            } catch (e: Exception) {
                Log.e("ScanScreenController", "Error exporting database $e")
                context.runOnUiThread {
                    Toast.makeText(context, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show()
                }
            }
        }
    }

    private fun importDatabase(sourceUri: Uri) {
        context.lifecycleScope.launch(Dispatchers.IO) {
            try {
                if (!getFileNameFromUri(sourceUri)!!.endsWith(".vbhelper")) {
                    context.runOnUiThread {
                        Toast.makeText(context, "Invalid file format", Toast.LENGTH_SHORT).show()
                    }
                    return@launch
                }

                application.container.db.close()

                val dbPath = context.getDatabasePath(roomDbName)
                val shmFile = File(dbPath.parent, "$roomDbName-shm")
                val walFile = File(dbPath.parent, "$roomDbName-wal")

                // Delete existing database files
                if (dbPath.exists()) dbPath.delete()
                if (shmFile.exists()) shmFile.delete()
                if (walFile.exists()) walFile.delete()

                val dbFile = File(dbPath.absolutePath)

                context.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
                    dbFile.outputStream().use { outputStream ->
                        copyFile(inputStream, outputStream)
                    }
                } ?: throw IllegalArgumentException("Unable to open source Uri for reading")

                context.runOnUiThread {
                    Toast.makeText(context, "Database imported successfully!", Toast.LENGTH_SHORT).show()
                    Toast.makeText(context, "Reopen the app to finish import process!", Toast.LENGTH_LONG).show()
                    context.finishAffinity()
                }
            } catch (e: Exception) {
                Log.e("ScanScreenController", "Error importing database $e")
                context.runOnUiThread {
                    Toast.makeText(context, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show()
                }
            }
        }
    }

    private fun getFileNameFromUri(uri: Uri): String? {
        var fileName: String? = null
        val cursor = context.contentResolver.query(uri, null, null, null, null)
        cursor?.use {
            if (it.moveToFirst()) {
                val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
                fileName = it.getString(nameIndex)
            }
        }
        return fileName
    }

    private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
        val buffer = ByteArray(1024)
        var bytesRead: Int
        while (inputStream.read(buffer).also { bytesRead = it } != -1) {
            outputStream.write(buffer, 0, bytesRead)
        }
        outputStream.flush()
    }

    private fun importApk(uri: Uri) {
        context.lifecycleScope.launch(Dispatchers.IO) {
            context.contentResolver.openInputStream(uri).use {

A app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt +320 -0
@@ 0,0 1,320 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers

import android.util.Log
import com.github.cfogrady.vb.dim.card.BemCard
import com.github.cfogrady.vb.dim.card.DimCard
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import java.io.InputStream

class CardImportController(
    private val database: AppDatabase
) {
    suspend fun importCard(
        fileReader: InputStream?
    ) {
        val dimReader = DimReader()
        val card = dimReader.readCard(fileReader, false)

        val cardModel = Card(
            cardId = card.header.dimId,
            logo = card.spriteData.sprites[0].pixelData,
            name = card.spriteData.text,
            stageCount = card.adventureLevels.levels.size,
            logoHeight = card.spriteData.sprites[0].height,
            logoWidth = card.spriteData.sprites[0].width,
            isBEm = card is BemCard
        )

        val cardId = database
            .cardDao()
            .insertNewCard(cardModel)

        updateCardProgress(cardId = cardId)

        importCharacterData(cardId, card)

        importEvoData(cardId, card)

        importAdventureMissions(cardId, card)

        importCardFusions(cardId, card)
    }

    private fun updateCardProgress(
        cardId: Long,
    ) {
        database
            .cardProgressDao()
            .insertCardProgress(
                CardProgress(
                    cardId = cardId,
                    currentStage = 1,
                    unlocked = false
                )
            )
    }

    private suspend fun importCharacterData(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        var spriteCounter = when (card is BemCard) {
            true -> 54
            false -> 10
        }

        val domainCharacters = mutableListOf<CardCharacter>()

        val characters = card
            .characterStats
            .characterEntries

        for (index in 0 until characters.size) {
            var domainSprite: Sprite?
            if (index < 2 && card is DimCard) {
                domainSprite = Sprite(
                    width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
                    height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
                    spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
                    spriteWalk1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteWalk2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteRun1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteRun2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteTrain1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteTrain2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteHappy = card.spriteData.sprites[spriteCounter + 4].pixelData,
                    spriteSleep = card.spriteData.sprites[spriteCounter + 5].pixelData,
                    spriteAttack = card.spriteData.sprites[spriteCounter + 2].pixelData,
                    spriteDodge = card.spriteData.sprites[spriteCounter + 3].pixelData
                )
            } else {
                domainSprite = Sprite(
                    width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
                    height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
                    spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                    spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
                    spriteWalk1 = card.spriteData.sprites[spriteCounter + 3].pixelData,
                    spriteWalk2 = card.spriteData.sprites[spriteCounter + 4].pixelData,
                    spriteRun1 = card.spriteData.sprites[spriteCounter + 5].pixelData,
                    spriteRun2 = card.spriteData.sprites[spriteCounter + 6].pixelData,
                    spriteTrain1 = card.spriteData.sprites[spriteCounter + 7].pixelData,
                    spriteTrain2 = card.spriteData.sprites[spriteCounter + 8].pixelData,
                    spriteHappy = card.spriteData.sprites[spriteCounter + 9].pixelData,
                    spriteSleep = card.spriteData.sprites[spriteCounter + 10].pixelData,
                    spriteAttack = card.spriteData.sprites[spriteCounter + 11].pixelData,
                    spriteDodge = card.spriteData.sprites[spriteCounter + 12].pixelData
                )
            }

            val spriteId = database
                .spriteDao()
                .insertSprite(domainSprite)


            domainCharacters.add(
                CardCharacter(
                    cardId = cardId,
                    spriteId = spriteId,
                    charaIndex = index,
                    nameSprite = card.spriteData.sprites[spriteCounter].pixelData,
                    stage = characters[index].stage,
                    attribute = NfcCharacter.Attribute.entries[characters[index].attribute],
                    baseHp = characters[index].hp,
                    baseBp = characters[index].dp,
                    baseAp = characters[index].ap,
                    nameWidth = card.spriteData.sprites[spriteCounter].spriteDimensions.width,
                    nameHeight = card.spriteData.sprites[spriteCounter].spriteDimensions.height
                )
            )

            spriteCounter += if (card is BemCard) {
                14
            } else {
                when (index) {
                    0 -> 6
                    1 -> 7
                    else -> 14
                }
            }
        }

        database
            .characterDao()
            .insertCharacter(*domainCharacters.toTypedArray())
    }

    private suspend fun importAdventureMissions(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        Log.d("importAdventureMissions", "Importing adventure missions")
        if (card is BemCard) {
            card.adventureLevels.levels.forEach {
                database
                    .cardAdventureDao()
                    .insertNewAdventure(
                        cardId = cardId,
                        characterId = it.bossCharacterIndex,
                        steps = it.steps,
                        bossAp = it.bossAp,
                        bossHp = it.bossHp,
                        bossDp = it.bossDp,
                        bossBp = it.bossBp
                    )
            }
        } else if (card is DimCard) {
            card.adventureLevels.levels.map {
                database
                    .cardAdventureDao()
                    .insertNewAdventure(
                        cardId = cardId,
                        characterId = it.bossCharacterIndex,
                        steps = it.steps,
                        bossAp = it.bossAp,
                        bossHp = it.bossHp,
                        bossDp = it.bossDp,
                        bossBp = null
                    )
            }
        }
    }

    private suspend fun importCardFusions(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        Log.d("importCardFusions", "Importing card fusions")
        if (card is DimCard) {
            card
                .attributeFusions
                .entries
                .forEach {
                    Log.d("importCardFusions", "Importing fusion: ${it.attribute1Fusion}")
                    if (it.attribute1Fusion != 65535 && it.characterIndex != 65535) {
                        database
                            .cardFusionsDao()
                            .insertNewFusion(
                                cardId = cardId,
                                fromCharaId = it.characterIndex,
                                attribute = NfcCharacter.Attribute.Virus,
                                toCharaId = it.attribute1Fusion,
                            )
                    }

                    if (it.attribute2Fusion != 65535 && it.characterIndex != 65535) {
                        database
                            .cardFusionsDao()
                            .insertNewFusion(
                                cardId = cardId,
                                fromCharaId = it.characterIndex,
                                attribute = NfcCharacter.Attribute.Data,
                                toCharaId = it.attribute2Fusion,
                            )
                    }

                    if (it.attribute3Fusion != 65535 && it.characterIndex != 65535) {
                        database
                            .cardFusionsDao()
                            .insertNewFusion(
                                cardId = cardId,
                                fromCharaId = it.characterIndex,
                                attribute = NfcCharacter.Attribute.Vaccine,
                                toCharaId = it.attribute3Fusion,
                            )
                    }

                    if (it.attribute4Fusion != 65535 && it.characterIndex != 65535) {
                        database
                            .cardFusionsDao()
                            .insertNewFusion(
                                cardId = cardId,
                                fromCharaId = it.characterIndex,
                                attribute = NfcCharacter.Attribute.Free,
                                toCharaId = it.attribute4Fusion,
                            )
                    }
                }
        }
    }

    private suspend fun importEvoData(
        cardId: Long,
        card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
    ) {
        for (index in 0 until card.transformationRequirements.transformationEntries.size) {
            val evo = card.transformationRequirements.transformationEntries[index]

            var transformationTimerHours: Int
            var unlockAdventureLevel: Int

            if (card is BemCard) {
                transformationTimerHours = card
                    .transformationRequirements
                    .transformationEntries[index]
                    .minutesUntilTransformation / 60
                unlockAdventureLevel = if (
                    card
                        .transformationRequirements
                        .transformationEntries[index]
                        .requiredCompletedAdventureLevel == 65535
                ) {
                    0
                } else {
                    card
                        .transformationRequirements
                        .transformationEntries[index]
                        .requiredCompletedAdventureLevel
                }
            } else {
                transformationTimerHours = (card as DimCard)
                    .transformationRequirements
                    .transformationEntries[index]
                    .hoursUntilEvolution
                unlockAdventureLevel = if (
                    card
                        .adventureLevels
                        .levels
                        .last()
                        .bossCharacterIndex == card.transformationRequirements.transformationEntries[index].toCharacterIndex
                ) {
                    14
                    /*
                    Magic number incoming!!

                    In the case of DiMCards, stage 15 is the one that unlocks the locked character.
                    We know it is a locked character if the last adventure level's boss character index
                    is the current index. If it is, we add stage 15 complete as a requirement for transformation.
                     */
                } else {
                    0
                    /*
                    Another magic number...

                    The rest of the characters are not locked.
                     */
                }
            }

            database
                .characterDao()
                .insertPossibleTransformation(
                    cardId = cardId,
                    fromChraraIndex = evo.fromCharacterIndex,
                    toChraraIndex = evo.toCharacterIndex,
                    requiredVitals = evo.requiredVitalValues,
                    requiredTrophies = evo.requiredTrophies,
                    requiredBattles = evo.requiredBattles,
                    requiredWinRate = evo.requiredWinRatio,
                    requiredAdventureLevelCompleted = unlockAdventureLevel,
                    changeTimerHours = transformationTimerHours
                )
        }
    }
}
\ No newline at end of file

A app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt +115 -0
@@ 0,0 1,115 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers

import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.di.VBHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
import java.io.OutputStream

class DatabaseManagementController(
    val componentActivity: ComponentActivity,
    val application: VBHelper
) {
    private val roomDbName = "internalDb"

    fun exportDatabase( destinationUri: Uri) {
        componentActivity.lifecycleScope.launch(Dispatchers.IO) {
            try {
                val dbFile = File(componentActivity.getDatabasePath(roomDbName).absolutePath)
                if (!dbFile.exists()) {
                    throw IllegalStateException("Database file does not exist!")
                }

                application.container.db.close()

                componentActivity.contentResolver.openOutputStream(destinationUri)?.use { outputStream ->
                    dbFile.inputStream().use { inputStream ->
                        copyFile(inputStream, outputStream)
                    }
                } ?: throw IllegalArgumentException("Unable to open destination Uri for writing")

                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, "Database exported successfully!", Toast.LENGTH_SHORT).show()
                    Toast.makeText(componentActivity, "Closing application to avoid changes.", Toast.LENGTH_LONG).show()
                    componentActivity.finishAffinity()
                }
            } catch (e: Exception) {
                Log.e("ScanScreenController", "Error exporting database $e")
                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show()
                }
            }
        }
    }

    fun importDatabase(sourceUri: Uri) {
        componentActivity.lifecycleScope.launch(Dispatchers.IO) {
            try {
                if (!getFileNameFromUri(sourceUri)!!.endsWith(".vbhelper")) {
                    componentActivity.runOnUiThread {
                        Toast.makeText(componentActivity, "Invalid file format", Toast.LENGTH_SHORT).show()
                    }
                    return@launch
                }

                application.container.db.close()

                val dbPath = componentActivity.getDatabasePath(roomDbName)
                val shmFile = File(dbPath.parent, "$roomDbName-shm")
                val walFile = File(dbPath.parent, "$roomDbName-wal")

                // Delete existing database files
                if (dbPath.exists()) dbPath.delete()
                if (shmFile.exists()) shmFile.delete()
                if (walFile.exists()) walFile.delete()

                val dbFile = File(dbPath.absolutePath)

                componentActivity.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
                    dbFile.outputStream().use { outputStream ->
                        copyFile(inputStream, outputStream)
                    }
                } ?: throw IllegalArgumentException("Unable to open source Uri for reading")

                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, "Database imported successfully!", Toast.LENGTH_SHORT).show()
                    Toast.makeText(componentActivity, "Reopen the app to finish import process!", Toast.LENGTH_LONG).show()
                    componentActivity.finishAffinity()
                }
            } catch (e: Exception) {
                Log.e("ScanScreenController", "Error importing database $e")
                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show()
                }
            }
        }
    }

    private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
        val buffer = ByteArray(1024)
        var bytesRead: Int
        while (inputStream.read(buffer).also { bytesRead = it } != -1) {
            outputStream.write(buffer, 0, bytesRead)
        }
        outputStream.flush()
    }

    private fun getFileNameFromUri(uri: Uri): String? {
        var fileName: String? = null
        val cursor = componentActivity.contentResolver.query(uri, null, null, null, null)
        cursor?.use {
            if (it.moveToFirst()) {
                val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
                fileName = it.getString(nameIndex)
            }
        }
        return fileName
    }
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt +9 -4
@@ 3,19 3,24 @@ package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow

class DexRepository (
    private val db: AppDatabase
) {
    suspend fun getAllDims(): List<CardDtos.CardProgress> {
    fun getAllDims(): Flow<List<CardDtos.CardProgress>> {
        return db.dexDao().getCardsWithProgress()
    }

    suspend fun getCharactersByCardId(cardId: Long): List<CharacterDtos.CardCharaProgress> {
    fun getCharactersByCardId(cardId: Long): Flow<List<CharacterDtos.CardCharaProgress>> {
        return db.dexDao().getSingleCardProgress(cardId)
    }

    suspend fun getCardPossibleTransformations(cardId: Long): List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained> {
        return db.characterDao().getEvolutionRequirementsForCard(cardId)
    fun getCharacterPossibleTransformations(characterId: Long): Flow<List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>> {
        return db.characterDao().getEvolutionRequirementsForCard(characterId)
    }

    fun getCharacterPossibleFusions(characterId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>> {
        return db.cardFusionsDao().getFusionsForCharacter(characterId)
    }
}
\ No newline at end of file