From ffa6958a8967bc55e4c288fcd46a3f4deb830d52 Mon Sep 17 00:00:00 2001 From: Nacho Date: Sat, 15 Nov 2025 20:10:17 +0100 Subject: [PATCH] Few things - Finished working with the card fusions - Added the GUI part to access the data - Cleaned up a bit of the code, separated a few things from the SettingsScreenControllerImpl.kt into separate classes. - Removed a few LaunchedEffect in exchange of Flows (UI now updates automatically when an action happens) --- .../vbhelper/daos/CardAdventureDao.kt | 5 +- .../nacabaro/vbhelper/daos/CardFusionsDao.kt | 40 +- .../nacabaro/vbhelper/daos/CardProgressDao.kt | 3 +- .../nacabaro/vbhelper/daos/CharacterDao.kt | 5 +- .../github/nacabaro/vbhelper/daos/DexDao.kt | 5 +- .../vbhelper/domain/card/CardFusions.kt | 27 +- .../nacabaro/vbhelper/dtos/CharacterDtos.kt | 10 + .../screens/cardScreen/CardAdventureScreen.kt | 31 +- .../cardScreen/CardScreenController.kt | 7 +- .../cardScreen/CardScreenControllerImpl.kt | 13 +- .../screens/cardScreen/CardViewScreen.kt | 22 +- .../screens/cardScreen/CardsScreen.kt | 16 +- .../dialogs/DexCharaDetailsDialog.kt | 58 ++- .../dialogs/DexCharaFusionsDialog.kt | 191 +++++++++ .../screens/homeScreens/HomeScreen.kt | 3 - .../SettingsScreenControllerImpl.kt | 384 +----------------- .../controllers/CardImportController.kt | 320 +++++++++++++++ .../DatabaseManagementController.kt | 115 ++++++ .../nacabaro/vbhelper/source/DexRepository.kt | 13 +- 19 files changed, 781 insertions(+), 487 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt index 03db5ac..3496ec2 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardAdventureDao.kt @@ -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 + ): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt index e9f7e34..86d6a9a 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardFusionsDao.kt @@ -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> } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt index 80d5bc8..3fde614 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CardProgressDao.kt @@ -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 @Insert fun insertCardProgress(cardProgress: CardProgress) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt index 261fa00..378e92f 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/CharacterDao.kt @@ -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 + fun getEvolutionRequirementsForCard(characterId: Long): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt b/app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt index f31b365..c343796 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/daos/DexDao.kt @@ -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 + fun getSingleCardProgress(cardId: Long): Flow> @Query( """ @@ -55,5 +56,5 @@ interface DexDao { FROM Card c """ ) - suspend fun getCardsWithProgress(): List + fun getCardsWithProgress(): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt index 0ced483..1769eda 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/domain/card/CardFusions.kt @@ -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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt b/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt index dd4a6ab..0837dbb 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/dtos/CharacterDtos.kt @@ -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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt index 084ea4e..6f5527a 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardAdventureScreen.kt @@ -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()) } - 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt index 6e53a60..836746f 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenController.kt @@ -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 - suspend fun getCardProgress(cardId: Long): Int + fun getCardAdventureMissions(cardId: Long): Flow> + fun getCardProgress(cardId: Long): Flow + fun getFusionsForCharacters(characterId: Long): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt index 0fe45b5..ded0375 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardScreenControllerImpl.kt @@ -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 { + override fun getCardAdventureMissions(cardId: Long): Flow> { return database .cardAdventureDao() .getAdventureForCard(cardId) } - override suspend fun getCardProgress(cardId: Long): Int { + override fun getCardProgress(cardId: Long): Flow { return database .cardProgressDao() .getCardProgress(cardId) } + + override fun getFusionsForCharacters(characterId: Long): Flow> { + return database + .cardFusionsDao() + .getFusionsForCharacter(characterId) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt index 2cd5838..da95460 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardViewScreen.kt @@ -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>(emptyList()) } - val cardPossibleTransformations = remember { mutableStateOf>(emptyList()) } + val characterList by dexRepository.getCharactersByCardId(cardId).collectAsState(emptyList()) val selectedCharacter = remember { mutableStateOf(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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt index e639444..fcb24b5 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt @@ -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>(emptyList()) } + val cardList by dexRepository.getAllDims().collectAsState(emptyList()) val selectedCard = remember { mutableStateOf(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( diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt index 92beb74..364982e 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaDetailsDialog.kt @@ -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, 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt new file mode 100644 index 0000000..be2296d --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/dialogs/DexCharaFusionsDialog.kt @@ -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, + 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt index 4a5d3a0..53dd40f 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/homeScreens/HomeScreen.kt @@ -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!!, diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt index 0e2a039..dd78c14 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreenControllerImpl.kt @@ -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 private val filePickerOpenerLauncher: ActivityResultLauncher> private val filePickerApk: ActivityResultLauncher> @@ -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() - - 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 { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt new file mode 100644 index 0000000..1626405 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt @@ -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() + + 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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt new file mode 100644 index 0000000..f3fb931 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt @@ -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 diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt index dcec614..585392c 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/DexRepository.kt @@ -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 { + fun getAllDims(): Flow> { return db.dexDao().getCardsWithProgress() } - suspend fun getCharactersByCardId(cardId: Long): List { + fun getCharactersByCardId(cardId: Long): Flow> { return db.dexDao().getSingleCardProgress(cardId) } - suspend fun getCardPossibleTransformations(cardId: Long): List { - return db.characterDao().getEvolutionRequirementsForCard(cardId) + fun getCharacterPossibleTransformations(characterId: Long): Flow> { + return db.characterDao().getEvolutionRequirementsForCard(characterId) + } + + fun getCharacterPossibleFusions(characterId: Long): Flow> { + return db.cardFusionsDao().getFusionsForCharacter(characterId) } } \ No newline at end of file