~cytrogen/vbhelper

1a38cefb9241fa66df115bdb4f2c5d93d340aac1 — nacabaro 1 year, 3 months ago 0939487 + a974bd3
Merge pull request #23 from nacabaro/cleanup

Cleanup MainActivity
M app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt => app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +5 -250
@@ 1,44 1,21 @@
package com.github.nacabaro.vbhelper

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vb.dim.card.BemCard
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.characters.Card
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.domain.characters.Character
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.util.GregorianCalendar

class MainActivity : ComponentActivity() {

    private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)

    private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>

class MainActivity : ComponentActivity() {
    private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()

    private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {


@@ 53,12 30,9 @@ class MainActivity : ComponentActivity() {
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        registerFileActivityResult()

        val application = applicationContext as VBHelper
        val scanScreenController = ScanScreenControllerImpl(
            application.container.dataStoreSecretsRepository.secretsFlow,
            this::handleReceivedNfcCharacter,
            this,
            this::registerActivityLifecycleListener,
            this::unregisterActivityLifecycleListener


@@ 67,7 41,9 @@ class MainActivity : ComponentActivity() {
        val itemsScreenController = ItemsScreenControllerImpl(this)

        super.onCreate(savedInstanceState)

        enableEdgeToEdge()

        setContent {
            VBHelperTheme {
                MainApplication(


@@ 77,6 53,7 @@ class MainActivity : ComponentActivity() {
                )
            }
        }

        Log.i("MainActivity", "Activity onCreated")
    }



@@ 96,240 73,18 @@ class MainActivity : ComponentActivity() {
        }
    }

    private fun registerFileActivityResult() {
        activityResultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()
        ) {
            lifecycleScope.launch {
                val application = applicationContext as VBHelper
                val storageRepository = application.container.db

                if (it.resultCode != RESULT_OK) {
                    Toast.makeText(applicationContext, "Import operation cancelled.", Toast.LENGTH_SHORT).show()
                }
                val contentResolver = applicationContext.contentResolver
                val inputStream = contentResolver.openInputStream(it.data!!.data!!)
                inputStream.use { fileReader ->
                    val dimReader = DimReader()
                    val card = dimReader.readCard(fileReader, false)

                    Log.i("MainActivity", "Card name: ${card is BemCard}")

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

                    val dimId = storageRepository
                        .dimDao()
                        .insertNewDim(cardModel)

                    val characters = card.characterStats.characterEntries

                    var spriteCounter = when (card is BemCard) {
                        true -> 55
                        false -> 10
                    }

                    val domainCharacters = mutableListOf<Character>()

                    for (index in 0 until characters.size) {
                        domainCharacters.add(
                            Character(
                                dimId = dimId,
                                monIndex = index,
                                name = card.spriteData.sprites[spriteCounter].pixelData,
                                stage = characters[index].stage,
                                attribute = characters[index].attribute,
                                baseHp = characters[index].hp,
                                baseBp = characters[index].dp,
                                baseAp = characters[index].ap,
                                sprite1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                                sprite2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
                                nameWidth = card.spriteData.sprites[spriteCounter].width,
                                nameHeight = card.spriteData.sprites[spriteCounter].height,
                                spritesWidth = card.spriteData.sprites[spriteCounter + 1].width,
                                spritesHeight = card.spriteData.sprites[spriteCounter + 1].height
                            )
                        )

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

                    storageRepository
                        .characterDao()
                        .insertCharacter(*domainCharacters.toTypedArray())

                    val sprites = card.spriteData.sprites.map { sprite ->
                        Sprites(
                            id = 0,
                            sprite = sprite.pixelData,
                            width = sprite.width,
                            height = sprite.height
                        )
                    }
                    storageRepository
                        .characterDao()
                        .insertSprite(*sprites.toTypedArray())
                }
                inputStream?.close()
                Toast.makeText(applicationContext, "Import successful!", Toast.LENGTH_SHORT).show()
            }
        }
    }

    @Composable
    private fun MainApplication(
        scanScreenController: ScanScreenControllerImpl,
        settingsScreenController: SettingsScreenControllerImpl,
        itemsScreenController: ItemsScreenControllerImpl
    ) {

        AppNavigation(
            applicationNavigationHandlers = AppNavigationHandlers(
                settingsScreenController,
                scanScreenController,
                itemsScreenController
            ),
            onClickImportCard = {
                val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                    addCategory(Intent.CATEGORY_OPENABLE)
                    type = "*/*"
                }
                activityResultLauncher.launch(intent)
            }
        )
    }

    private fun handleReceivedNfcCharacter(character: NfcCharacter): String {
        nfcCharacter.value = character

        val importStatus = addCharacterScannedIntoDatabase()

        return importStatus
    }

    //
    /*
    TODO:
    - Support for regular VB

    The good news is that the theory behind inserting to the database should be working
    now, it's a matter of implementing the functionality to parse dim/bem cards and use my
    domain model.
     */
    private fun addCharacterScannedIntoDatabase(): String {
        val application = applicationContext as VBHelper
        val storageRepository = application.container.db

        val dimData = storageRepository
            .dimDao()
            .getDimById(nfcCharacter.value!!.dimId.toInt())

        if (dimData == null) return "Card not found"

        val cardCharData = storageRepository
            .characterDao()
            .getCharacterByMonIndex(nfcCharacter.value!!.charIndex.toInt(), dimData.id)

        val characterData = UserCharacter(
            charId = cardCharData.id,
            stage = nfcCharacter.value!!.stage.toInt(),
            attribute = nfcCharacter.value!!.attribute,
            ageInDays = nfcCharacter.value!!.ageInDays.toInt(),
            nextAdventureMissionStage = nfcCharacter.value!!.nextAdventureMissionStage.toInt(),
            mood = nfcCharacter.value!!.mood.toInt(),
            vitalPoints = nfcCharacter.value!!.vitalPoints.toInt(),
            transformationCountdown = nfcCharacter.value!!.transformationCountdownInMinutes.toInt(),
            injuryStatus = nfcCharacter.value!!.injuryStatus,
            trophies = nfcCharacter.value!!.trophies.toInt(),
            currentPhaseBattlesWon = nfcCharacter.value!!.currentPhaseBattlesWon.toInt(),
            currentPhaseBattlesLost = nfcCharacter.value!!.currentPhaseBattlesLost.toInt(),
            totalBattlesWon = nfcCharacter.value!!.totalBattlesWon.toInt(),
            totalBattlesLost = nfcCharacter.value!!.totalBattlesLost.toInt(),
            activityLevel = nfcCharacter.value!!.activityLevel.toInt(),
            heartRateCurrent = nfcCharacter.value!!.heartRateCurrent.toInt(),
            characterType = when (nfcCharacter.value) {
                is BENfcCharacter -> DeviceType.BEDevice
                else -> DeviceType.VBDevice
            },
            isActive = true
        )

        storageRepository
            .userCharacterDao()
            .clearActiveCharacter()

        val characterId: Long = storageRepository
            .userCharacterDao()
            .insertCharacterData(characterData)

        if (nfcCharacter.value is BENfcCharacter) {
            val beCharacter = nfcCharacter.value as BENfcCharacter
            val extraCharacterData = BECharacterData(
                id = characterId,
                trainingHp = beCharacter.trainingHp.toInt(),
                trainingAp = beCharacter.trainingAp.toInt(),
                trainingBp = beCharacter.trainingBp.toInt(),
                remainingTrainingTimeInMinutes = beCharacter.remainingTrainingTimeInMinutes.toInt(),
                itemEffectActivityLevelValue = beCharacter.itemEffectActivityLevelValue.toInt(),
                itemEffectMentalStateValue = beCharacter.itemEffectMentalStateValue.toInt(),
                itemEffectMentalStateMinutesRemaining = beCharacter.itemEffectMentalStateMinutesRemaining.toInt(),
                itemEffectActivityLevelMinutesRemaining = beCharacter.itemEffectActivityLevelMinutesRemaining.toInt(),
                itemEffectVitalPointsChangeValue = beCharacter.itemEffectVitalPointsChangeValue.toInt(),
                itemEffectVitalPointsChangeMinutesRemaining = beCharacter.itemEffectVitalPointsChangeMinutesRemaining.toInt(),
                abilityRarity = beCharacter.abilityRarity,
                abilityType = beCharacter.abilityType.toInt(),
                abilityBranch = beCharacter.abilityBranch.toInt(),
                abilityReset = beCharacter.abilityReset.toInt(),
                rank = beCharacter.abilityReset.toInt(),
                itemType = beCharacter.itemType.toInt(),
                itemMultiplier = beCharacter.itemMultiplier.toInt(),
                itemRemainingTime = beCharacter.itemRemainingTime.toInt(),
                otp0 = "", //beCharacter.value!!.otp0.toString(),
                otp1 = "", //beCharacter.value!!.otp1.toString(),
                minorVersion = beCharacter.characterCreationFirmwareVersion.minorVersion.toInt(),
                majorVersion = beCharacter.characterCreationFirmwareVersion.majorVersion.toInt(),
            )

            storageRepository
                .userCharacterDao()
                .insertBECharacterData(extraCharacterData)

            val transformationHistoryWatch = beCharacter.transformationHistory
            transformationHistoryWatch.map { item ->
                if (item.toCharIndex.toInt() != 255) {
                    val date = GregorianCalendar(item.year.toInt(), item.month.toInt(), item.day.toInt())
                        .time
                        .time

                    storageRepository
                        .characterDao()
                        .insertTransformation(characterId, item.toCharIndex.toInt(), dimData.id, date)

                    storageRepository
                        .dexDao()
                        .insertCharacter(item.toCharIndex.toInt(), dimData.id, date)
                }
            }
        } else if (nfcCharacter.value is VBNfcCharacter) {
            return "Not implemented yet"
        }

        return "Done reading character!"
        )
    }
}

M app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt => app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt +1 -3
@@ 30,7 30,6 @@ data class AppNavigationHandlers(
@Composable
fun AppNavigation(
    applicationNavigationHandlers: AppNavigationHandlers,
    onClickImportCard: () -> Unit,
) {
    val navController = rememberNavController()



@@ 76,8 75,7 @@ fun AppNavigation(
            composable(NavigationItems.Settings.route) {
                SettingsScreen(
                    navController = navController,
                    settingsScreenController = applicationNavigationHandlers.settingsScreenController,
                    onClickImportCard = onClickImportCard,
                    settingsScreenController = applicationNavigationHandlers.settingsScreenController
                )
            }
            composable(NavigationItems.Viewer.route) {

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


@@ 72,7 71,6 @@ fun HomeScreen(
            }
        } else {
            if (activeMon.value!!.isBemCard) {
                Log.d("HomeScreen", "BEDeviceBEm")
                BEBEmHomeScreen(
                    activeMon = activeMon.value!!,
                    beData = beData.value!!,


@@ 80,7 78,6 @@ fun HomeScreen(
                    contentPadding = contentPadding
                )
            } else if (!activeMon.value!!.isBemCard && activeMon.value!!.characterType == DeviceType.BEDevice) {
                Log.d("HomeScreen", "BEDevice")
                BEDiMHomeScreen(
                    activeMon = activeMon.value!!,
                    beData = beData.value!!,

M app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt +123 -17
@@ 11,20 11,26 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.source.getCryptographicTransformerMap
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.GregorianCalendar

class ScanScreenControllerImpl(
    override val secretsFlow: Flow<Secrets>,
    private val nfcHandler: (NfcCharacter)->String,
    private val context: ComponentActivity,
    private val componentActivity: ComponentActivity,
    private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
    private val unregisterActivityLifecycleListener: (String)->Unit,
): ScanScreenController {


@@ 32,9 38,9 @@ class ScanScreenControllerImpl(
    private val nfcAdapter: NfcAdapter

    init {
        val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(context)
        val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(componentActivity)
        if (maybeNfcAdapter == null) {
            Toast.makeText(context, "No NFC on device!", Toast.LENGTH_SHORT).show()
            Toast.makeText(componentActivity, "No NFC on device!", Toast.LENGTH_SHORT).show()
        }
        nfcAdapter = maybeNfcAdapter
        checkSecrets()


@@ 43,7 49,7 @@ class ScanScreenControllerImpl(
    override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {
        handleTag(secrets) { tagCommunicator ->
            val character = tagCommunicator.receiveCharacter()
            val resultMessage = nfcHandler(character)
            val resultMessage = addCharacterScannedIntoDatabase(character)
            onComplete.invoke()
            resultMessage
        }


@@ 51,7 57,7 @@ class ScanScreenControllerImpl(

    override fun cancelRead() {
        if(nfcAdapter.isEnabled) {
            nfcAdapter.disableReaderMode(context)
            nfcAdapter.disableReaderMode(componentActivity)
        }
    }



@@ 74,7 80,7 @@ class ScanScreenControllerImpl(
            val options = Bundle()
            // Work around for some broken Nfc firmware implementations that poll the card too fast
            options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
            nfcAdapter.enableReaderMode(context, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
            nfcAdapter.enableReaderMode(componentActivity, buildOnReadTag(secrets, handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
                options
            )
        }


@@ 85,26 91,26 @@ class ScanScreenControllerImpl(
        return { tag->
            val nfcData = NfcA.get(tag)
            if (nfcData == null) {
                context.runOnUiThread {
                    Toast.makeText(context, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
                }
            }
            nfcData.connect()
            nfcData.use {
                val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
                val successText = handlerFunc(tagCommunicator)
                context.runOnUiThread {
                    Toast.makeText(context, successText, Toast.LENGTH_SHORT).show()
                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun checkSecrets() {
        context.lifecycleScope.launch(Dispatchers.IO) {
            if(secretsFlow.stateIn(context.lifecycleScope).value.isMissingSecrets()) {
                context.runOnUiThread {
                    Toast.makeText(context, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
        componentActivity.lifecycleScope.launch(Dispatchers.IO) {
            if(secretsFlow.stateIn(componentActivity.lifecycleScope).value.isMissingSecrets()) {
                componentActivity.runOnUiThread {
                    Toast.makeText(componentActivity, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
                }
            }
        }


@@ 141,7 147,107 @@ class ScanScreenControllerImpl(

    // EXTRACTED DIRECTLY FROM EXAMPLE APP
    private fun showWirelessSettings() {
        Toast.makeText(context, "NFC must be enabled", Toast.LENGTH_SHORT).show()
        context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
        Toast.makeText(componentActivity, "NFC must be enabled", Toast.LENGTH_SHORT).show()
        componentActivity.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
    }

    private fun addCharacterScannedIntoDatabase(nfcCharacter: NfcCharacter): String {
        val application = componentActivity.applicationContext as VBHelper
        val storageRepository = application.container.db

        val dimData = storageRepository
            .dimDao()
            .getDimById(nfcCharacter.dimId.toInt())

        if (dimData == null) return "Card not found"

        val cardCharData = storageRepository
            .characterDao()
            .getCharacterByMonIndex(nfcCharacter.charIndex.toInt(), dimData.id)

        val characterData = UserCharacter(
            charId = cardCharData.id,
            stage = nfcCharacter.stage.toInt(),
            attribute = nfcCharacter.attribute,
            ageInDays = nfcCharacter.ageInDays.toInt(),
            nextAdventureMissionStage = nfcCharacter.nextAdventureMissionStage.toInt(),
            mood = nfcCharacter.mood.toInt(),
            vitalPoints = nfcCharacter.vitalPoints.toInt(),
            transformationCountdown = nfcCharacter.transformationCountdownInMinutes.toInt(),
            injuryStatus = nfcCharacter.injuryStatus,
            trophies = nfcCharacter.trophies.toInt(),
            currentPhaseBattlesWon = nfcCharacter.currentPhaseBattlesWon.toInt(),
            currentPhaseBattlesLost = nfcCharacter.currentPhaseBattlesLost.toInt(),
            totalBattlesWon = nfcCharacter.totalBattlesWon.toInt(),
            totalBattlesLost = nfcCharacter.totalBattlesLost.toInt(),
            activityLevel = nfcCharacter.activityLevel.toInt(),
            heartRateCurrent = nfcCharacter.heartRateCurrent.toInt(),
            characterType = when (nfcCharacter) {
                is BENfcCharacter -> DeviceType.BEDevice
                else -> DeviceType.VBDevice
            },
            isActive = true
        )

        storageRepository
            .userCharacterDao()
            .clearActiveCharacter()

        val characterId: Long = storageRepository
            .userCharacterDao()
            .insertCharacterData(characterData)

        if (nfcCharacter is BENfcCharacter) {
            val extraCharacterData = BECharacterData(
                id = characterId,
                trainingHp = nfcCharacter.trainingHp.toInt(),
                trainingAp = nfcCharacter.trainingAp.toInt(),
                trainingBp = nfcCharacter.trainingBp.toInt(),
                remainingTrainingTimeInMinutes = nfcCharacter.remainingTrainingTimeInMinutes.toInt(),
                itemEffectActivityLevelValue = nfcCharacter.itemEffectActivityLevelValue.toInt(),
                itemEffectMentalStateValue = nfcCharacter.itemEffectMentalStateValue.toInt(),
                itemEffectMentalStateMinutesRemaining = nfcCharacter.itemEffectMentalStateMinutesRemaining.toInt(),
                itemEffectActivityLevelMinutesRemaining = nfcCharacter.itemEffectActivityLevelMinutesRemaining.toInt(),
                itemEffectVitalPointsChangeValue = nfcCharacter.itemEffectVitalPointsChangeValue.toInt(),
                itemEffectVitalPointsChangeMinutesRemaining = nfcCharacter.itemEffectVitalPointsChangeMinutesRemaining.toInt(),
                abilityRarity = nfcCharacter.abilityRarity,
                abilityType = nfcCharacter.abilityType.toInt(),
                abilityBranch = nfcCharacter.abilityBranch.toInt(),
                abilityReset = nfcCharacter.abilityReset.toInt(),
                rank = nfcCharacter.abilityReset.toInt(),
                itemType = nfcCharacter.itemType.toInt(),
                itemMultiplier = nfcCharacter.itemMultiplier.toInt(),
                itemRemainingTime = nfcCharacter.itemRemainingTime.toInt(),
                otp0 = "", //nfcCharacter.value!!.otp0.toString(),
                otp1 = "", //nfcCharacter.value!!.otp1.toString(),
                minorVersion = nfcCharacter.characterCreationFirmwareVersion.minorVersion.toInt(),
                majorVersion = nfcCharacter.characterCreationFirmwareVersion.majorVersion.toInt(),
            )

            storageRepository
                .userCharacterDao()
                .insertBECharacterData(extraCharacterData)

            val transformationHistoryWatch = nfcCharacter.transformationHistory
            transformationHistoryWatch.map { item ->
                if (item.toCharIndex.toInt() != 255) {
                    val date = GregorianCalendar(item.year.toInt(), item.month.toInt(), item.day.toInt())
                        .time
                        .time

                    storageRepository
                        .characterDao()
                        .insertTransformation(characterId, item.toCharIndex.toInt(), dimData.id, date)

                    storageRepository
                        .dexDao()
                        .insertCharacter(item.toCharIndex.toInt(), dimData.id, date)
                }
            }
        } else if (nfcCharacter is VBNfcCharacter) {
            return "Not implemented yet"
        }

        return "Done reading character!"
    }
}
\ No newline at end of file

M app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt +3 -2
@@ 23,7 23,6 @@ import com.github.nacabaro.vbhelper.components.TopBanner
fun SettingsScreen(
    navController: NavController,
    settingsScreenController: SettingsScreenControllerImpl,
    onClickImportCard: () -> Unit
) {
    Scaffold (
        topBar = {


@@ 55,7 54,9 @@ fun SettingsScreen(
                settingsScreenController.onClickImportDatabase()
            }
            SettingsSection("DiM/BEm management")
            SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
            SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file") {
                settingsScreenController.onClickImportCard()
            }
            SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
            SettingsSection("About and credits")
            SettingsEntry(title = "Credits", description = "Credits") { }

M app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt => app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenController.kt +1 -0
@@ 4,4 4,5 @@ interface SettingsScreenController {
    fun onClickOpenDirectory()
    fun onClickImportDatabase()
    fun onClickImportApk()
    fun onClickImportCard()
}
\ No newline at end of file

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 +110 -0
@@ 9,7 9,13 @@ 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.DimReader
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.domain.characters.Card
import com.github.nacabaro.vbhelper.domain.characters.Character
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository


@@ 27,9 33,11 @@ class SettingsScreenControllerImpl(
    private val filePickerLauncher: ActivityResultLauncher<String>
    private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
    private val filePickerApk: ActivityResultLauncher<Array<String>>
    private val filePickerCard: ActivityResultLauncher<Array<String>>
    private val secretsImporter: SecretsImporter = ApkSecretsImporter()
    private val application = context.applicationContext as VBHelper
    private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
    private val database: AppDatabase = application.container.db

    init {
        filePickerLauncher = context.registerForActivityResult(


@@ 68,6 76,18 @@ class SettingsScreenControllerImpl(
                }
            }
        }

        filePickerCard = context.registerForActivityResult(
            ActivityResultContracts.OpenDocument()
        ) { uri ->
            if (uri != null) {
                importCard(uri)
            } else {
                context.runOnUiThread {
                    Toast.makeText(context, "Card import cancelled", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    override fun onClickOpenDirectory() {


@@ 82,6 102,96 @@ class SettingsScreenControllerImpl(
        filePickerApk.launch(arrayOf("*/*"))
    }

    override fun onClickImportCard() {
        filePickerCard.launch(arrayOf("*/*"))
    }

    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(
                    dimId = card.header.dimId,
                    logo = card.spriteData.sprites[0].pixelData,
                    name = card.spriteData.text, // TODO Make user write card name// TODO Make user write card name
                    stageCount = card.adventureLevels.levels.size,
                    logoHeight = card.spriteData.sprites[0].height,
                    logoWidth = card.spriteData.sprites[0].width,
                    isBEm = card is BemCard
                )

                val dimId = database
                    .dimDao()
                    .insertNewDim(cardModel)

                val characters = card.characterStats.characterEntries

                var spriteCounter = when (card is BemCard) {
                    true -> 55
                    false -> 10
                }

                val domainCharacters = mutableListOf<Character>()

                for (index in 0 until characters.size) {
                    domainCharacters.add(
                        Character(
                            dimId = dimId,
                            monIndex = index,
                            name = card.spriteData.sprites[spriteCounter].pixelData,
                            stage = characters[index].stage,
                            attribute = characters[index].attribute,
                            baseHp = characters[index].hp,
                            baseBp = characters[index].dp,
                            baseAp = characters[index].ap,
                            sprite1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
                            sprite2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
                            nameWidth = card.spriteData.sprites[spriteCounter].width,
                            nameHeight = card.spriteData.sprites[spriteCounter].height,
                            spritesWidth = card.spriteData.sprites[spriteCounter + 1].width,
                            spritesHeight = card.spriteData.sprites[spriteCounter + 1].height
                        )
                    )

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

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

                val sprites = card.spriteData.sprites.map { sprite ->
                    Sprites(
                        id = 0,
                        sprite = sprite.pixelData,
                        width = sprite.width,
                        height = sprite.height
                    )
                }
                database
                    .characterDao()
                    .insertSprite(*sprites.toTypedArray())
            }

            inputStream?.close()
            context.runOnUiThread {
                Toast.makeText(context, "Import successful!", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun exportDatabase(destinationUri: Uri) {
        context.lifecycleScope.launch(Dispatchers.IO) {
            try {