Compare commits

...
18 Commits
Author SHA1 Message Date
Nacho 93f568dd8d Create a small test widget, I just wanted to learn hoe Jetpack Compose Glance works? 2025-07-27 19:48:01 +02:00
Nacho d46769b0cb Added credits for the app in preparation for version 0.1 2025-07-26 22:26:41 +02:00
Nacho fce05870c5 This should allow the app to build, otherwise it won't or it will crash 2025-07-26 14:23:53 +02:00
nacabaroandGitHub d749af0a9c Merge pull request #27 from cfogrady/FixStageAndAttributeFromCharacter
Fix stage and attribute coming from character instead of UserCharacter
2025-04-08 15:35:13 +02:00
Christopher O'Grady 076f6bfacf Fix stage and attribute coming from character instead of UserCharacter 2025-04-05 20:31:03 -04:00
nacabaroandGitHub a4f3b701e5 Merge pull request #26 from cfogrady/RemoveDuplicatedData
Remove duplicated stage and attribute from UserCharacter
2025-03-22 00:34:07 +01:00
Christopher O'Grady da134247b1 Remove duplicated stage and attribute from UserCharacter
These fields are on the Character entity.
2025-03-19 15:33:52 -04:00
nacabaroandGitHub e36a700d9f Merge pull request #25 from nacabaro/database/adventure
Adventure mode
2025-01-26 02:01:24 +01:00
Nacho fb6b6d2825 While I'm at it...
- Resized some buttons in the dialogs
- Added a dismiss button in get item dialog after adventure and in the send on adventure choose time dialogs
- Moved the export/import data lower in the settings screen
2025-01-26 01:59:43 +01:00
Nacho e17f6c23e4 Renaming terms and other bits and bobs
- Changed adventure database so that the original intended time is also stored with the final time. This will come useful once the algorithm for determining which object to give is made.
2025-01-26 01:40:32 +01:00
Nacho 409474b5d1 Phew
- Added adventure screen, dialogs, components and controllers needed to rock.
- Modified DTOs so that it is possible to tell if a character is in adventure or not
- Updated layout of items database
- Fixed an issue with importing bems where the character name would be read incorrectly
- Also added a ton of checks
2025-01-26 01:25:25 +01:00
nacabaroandGitHub 547318864b Merge pull request #24 from nacabaro/cleanup
Bigger icons plus material obscurity
2025-01-21 13:35:43 +01:00
Nacho 5e92895656 Made item icons a bit smaller in order to be similarly sized to character icons 2025-01-21 13:34:49 +01:00
Nacho 25500e9cb4 Made character icons bigger and also made the obscured characters follow material guidelines, as being pure black can be difficult to see in dark theme 2025-01-21 13:28:13 +01:00
nacabaroandGitHub 1a38cefb92 Merge pull request #23 from nacabaro/cleanup
Cleanup MainActivity
2025-01-21 13:12:38 +01:00
Nacho a974bd366e Moved read character from watch to its corresponding controller 2025-01-21 13:02:45 +01:00
Nacho bb5f66d167 Moved import card logic outside of the MainActivity 2025-01-21 12:42:52 +01:00
nacabaroandGitHub 09394871e5 Merge pull request #22 from nacabaro/database/items
Add items functionality
2025-01-20 15:49:22 +01:00
54 changed files with 1871 additions and 568 deletions
+4 -1
View File
@@ -15,7 +15,7 @@ android {
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "Alpha 0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -91,4 +91,7 @@ dependencies {
implementation("com.google.android.material:material:1.2.0") implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite) implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material") implementation("androidx.compose.material:material")
implementation("androidx.glance:glance:1.1.1")
implementation("androidx.glance:glance-appwidget:1.1.1")
implementation("androidx.work:work-runtime-ktx:2.9.0")
} }
+9
View File
@@ -27,6 +27,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name=".widget.CharacterDisplayWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
</application> </application>
</manifest> </manifest>
Binary file not shown.
@@ -1,44 +1,24 @@
package com.github.nacabaro.vbhelper package com.github.nacabaro.vbhelper
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable 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.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.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.navigation.AppNavigationHandlers
import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme 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() { class MainActivity : ComponentActivity() {
private var nfcCharacter = MutableStateFlow<NfcCharacter?>(null)
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>() private val onActivityLifecycleListeners = HashMap<String, ActivityLifecycleListener>()
private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) { private fun registerActivityLifecycleListener(key: String, activityLifecycleListener: ActivityLifecycleListener) {
@@ -53,30 +33,36 @@ class MainActivity : ComponentActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
registerFileActivityResult()
val application = applicationContext as VBHelper val application = applicationContext as VBHelper
val scanScreenController = ScanScreenControllerImpl( val scanScreenController = ScanScreenControllerImpl(
application.container.dataStoreSecretsRepository.secretsFlow, application.container.dataStoreSecretsRepository.secretsFlow,
this::handleReceivedNfcCharacter,
this, this,
this::registerActivityLifecycleListener, this::registerActivityLifecycleListener,
this::unregisterActivityLifecycleListener this::unregisterActivityLifecycleListener
) )
val settingsScreenController = SettingsScreenControllerImpl(this) val settingsScreenController = SettingsScreenControllerImpl(this)
val itemsScreenController = ItemsScreenControllerImpl(this) val itemsScreenController = ItemsScreenControllerImpl(this)
val adventureScreenController = AdventureScreenControllerImpl(this)
val storageScreenController = StorageScreenControllerImpl(this)
val homeScreenController = HomeScreenControllerImpl(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
VBHelperTheme { VBHelperTheme {
MainApplication( MainApplication(
scanScreenController = scanScreenController, scanScreenController = scanScreenController,
settingsScreenController = settingsScreenController, settingsScreenController = settingsScreenController,
itemsScreenController = itemsScreenController itemsScreenController = itemsScreenController,
adventureScreenController = adventureScreenController,
homeScreenController = homeScreenController,
storageScreenController = storageScreenController
) )
} }
} }
Log.i("MainActivity", "Activity onCreated") Log.i("MainActivity", "Activity onCreated")
} }
@@ -96,240 +82,25 @@ 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 @Composable
private fun MainApplication( private fun MainApplication(
scanScreenController: ScanScreenControllerImpl, scanScreenController: ScanScreenControllerImpl,
settingsScreenController: SettingsScreenControllerImpl, settingsScreenController: SettingsScreenControllerImpl,
itemsScreenController: ItemsScreenControllerImpl itemsScreenController: ItemsScreenControllerImpl,
) { adventureScreenController: AdventureScreenControllerImpl,
storageScreenController: StorageScreenControllerImpl,
homeScreenController: HomeScreenControllerImpl,
) {
AppNavigation( AppNavigation(
applicationNavigationHandlers = AppNavigationHandlers( applicationNavigationHandlers = AppNavigationHandlers(
settingsScreenController, settingsScreenController,
scanScreenController, scanScreenController,
itemsScreenController itemsScreenController,
), adventureScreenController,
onClickImportCard = { storageScreenController,
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { homeScreenController
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!"
} }
} }
@@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -34,8 +35,9 @@ fun CharacterEntry(
icon: BitmapData, icon: BitmapData,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
obscure: Boolean = false, obscure: Boolean = false,
disabled: Boolean = false,
shape: Shape = MaterialTheme.shapes.medium, shape: Shape = MaterialTheme.shapes.medium,
multiplier: Int = 3, multiplier: Int = 4,
onClick: () -> Unit = { } onClick: () -> Unit = { }
) { ) {
val bitmap = remember (icon.bitmap) { val bitmap = remember (icon.bitmap) {
@@ -47,7 +49,10 @@ fun CharacterEntry(
Card( Card(
shape = shape, shape = shape,
onClick = onClick, onClick = when (disabled) {
true -> { {} }
false -> onClick
},
modifier = modifier modifier = modifier
.aspectRatio(1f) .aspectRatio(1f)
.padding(8.dp) .padding(8.dp)
@@ -56,12 +61,16 @@ fun CharacterEntry(
contentAlignment = Alignment.BottomCenter, contentAlignment = Alignment.BottomCenter,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(4.dp)
) { ) {
Image( Image(
bitmap = imageBitmap, bitmap = imageBitmap,
contentDescription = "Icon", contentDescription = "Icon",
filterQuality = FilterQuality.None, filterQuality = FilterQuality.None,
colorFilter = when (obscure) {
true -> ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
false -> null
},
modifier = Modifier modifier = Modifier
.size(dpSize) .size(dpSize)
) )
@@ -23,7 +23,8 @@ fun TopBanner(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onGearClick: (() -> Unit)? = null, onGearClick: (() -> Unit)? = null,
onBackClick: (() -> Unit)? = null, onBackClick: (() -> Unit)? = null,
onScanClick: (() -> Unit)? = null onScanClick: (() -> Unit)? = null,
onAdventureClick: (() -> Unit)? = null
) { ) {
Box( // Use Box to overlay elements Box( // Use Box to overlay elements
modifier = modifier modifier = modifier
@@ -49,6 +50,17 @@ fun TopBanner(
contentDescription = "Settings" contentDescription = "Settings"
) )
} }
} else if (onAdventureClick != null) {
IconButton(
onClick = onAdventureClick,
modifier = Modifier
.align(Alignment.CenterEnd) // Place gear icon at the end
) {
Icon(
painter = painterResource(R.drawable.baseline_fort_24), // Use a gear icon
contentDescription = "Adventure"
)
}
} }
if (onScanClick != null) { if (onScanClick != null) {
@@ -0,0 +1,44 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
@Dao
interface AdventureDao {
@Query("""
INSERT INTO Adventure (characterId, originalDuration, finishesAdventure)
VALUES (:characterId, :originalDuration, strftime('%s', 'now') + :timeInSeconds)
""")
fun insertNewAdventure(characterId: Long, originalDuration: Long, timeInSeconds: Long)
@Query("""
SELECT COUNT(*) FROM Adventure
""")
fun getAdventureCount(): Int
@Query("""
SELECT
uc.*,
c.stage,
c.attribute,
c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight,
d.isBEm as isBemCard,
a.finishesAdventure AS finishesAdventure,
a.originalDuration AS originalTimeInMinutes
FROM UserCharacter uc
JOIN Character c ON uc.charId = c.id
JOIN Card d ON c.dimId = d.id
JOIN Adventure a ON uc.id = a.characterId
""")
suspend fun getAdventureCharacters(): List<CharacterDtos.AdventureCharacterWithSprites>
@Query("""
DELETE FROM Adventure
WHERE characterId = :characterId
""")
suspend fun deleteAdventure(characterId: Long)
}
@@ -6,40 +6,48 @@ import com.github.nacabaro.vbhelper.dtos.ItemDtos
@Dao @Dao
interface ItemDao { interface ItemDao {
@Query(""" @Query(
SELECT Items.*, UserItems.quantity """
SELECT *
FROM Items FROM Items
LEFT JOIN UserItems ON Items.id = UserItems.itemId
ORDER BY Items.itemIcon ASC ORDER BY Items.itemIcon ASC
""") """
)
suspend fun getAllItems(): List<ItemDtos.ItemsWithQuantities> suspend fun getAllItems(): List<ItemDtos.ItemsWithQuantities>
@Query(""" @Query(
SELECT Items.*, UserItems.quantity """
SELECT *
FROM Items FROM Items
JOIN UserItems ON Items.id = UserItems.itemId WHERE quantity > 0
""") """
)
suspend fun getAllUserItems(): List<ItemDtos.ItemsWithQuantities> suspend fun getAllUserItems(): List<ItemDtos.ItemsWithQuantities>
@Query(""" @Query(
SELECT Items.*, UserItems.quantity """
SELECT *
FROM Items FROM Items
JOIN UserItems ON Items.id = UserItems.itemId WHERE Items.id = :itemId
WHERE UserItems.itemId = :itemId """
""") )
fun getUserItem(itemId: Long): ItemDtos.ItemsWithQuantities fun getItem(itemId: Long): ItemDtos.ItemsWithQuantities
@Query(""" @Query(
UPDATE UserItems """
UPDATE Items
SET quantity = quantity - 1 SET quantity = quantity - 1
WHERE itemId = :itemId WHERE id = :itemId
""") """
)
fun useItem(itemId: Long) fun useItem(itemId: Long)
@Query(""" @Query(
UPDATE UserItems """
SET quantity = quantity - :itemAmount UPDATE Items
WHERE itemId = :itemId SET quantity = quantity + :itemAmount
""") WHERE id = :itemId
"""
)
suspend fun purchaseItem(itemId: Long, itemAmount: Int) suspend fun purchaseItem(itemId: Long, itemAmount: Int)
} }
@@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert import androidx.room.Upsert
import com.github.nacabaro.vbhelper.domain.characters.Character
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
@@ -45,17 +46,46 @@ interface UserCharacterDao {
""" """
SELECT SELECT
uc.*, uc.*,
c.stage,
c.attribute,
c.sprite1 AS spriteIdle, c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth, c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight, c.spritesHeight AS spriteHeight,
d.isBEm as isBemCard c.name as nameSprite,
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId = uc.id as isInAdventure
FROM UserCharacter uc FROM UserCharacter uc
JOIN Character c ON uc.charId = c.id JOIN Character c ON uc.charId = c.id
JOIN Card d ON c.dimId = d.id JOIN Card d ON c.dimId = d.id
LEFT JOIN Adventure a ON a.characterId = uc.id
""" """
) )
suspend fun getAllCharacters(): List<CharacterDtos.CharacterWithSprites> suspend fun getAllCharacters(): List<CharacterDtos.CharacterWithSprites>
@Query(
"""
SELECT
uc.*,
c.stage,
c.attribute,
c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight,
c.name as nameSprite,
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId = uc.id as isInAdventure
FROM UserCharacter uc
JOIN Character c ON uc.charId = c.id
JOIN Card d ON c.dimId = d.id
LEFT JOIN Adventure a ON a.characterId = uc.id
WHERE uc.id = :id
""")
suspend fun getCharacterWithSprites(id: Long): CharacterDtos.CharacterWithSprites
@Query("SELECT * FROM UserCharacter WHERE id = :id") @Query("SELECT * FROM UserCharacter WHERE id = :id")
suspend fun getCharacter(id: Long): UserCharacter suspend fun getCharacter(id: Long): UserCharacter
@@ -66,17 +96,23 @@ interface UserCharacterDao {
""" """
SELECT SELECT
uc.*, uc.*,
c.stage,
c.attribute,
c.sprite1 AS spriteIdle, c.sprite1 AS spriteIdle,
c.spritesWidth AS spriteWidth, c.spritesWidth AS spriteWidth,
c.spritesHeight AS spriteHeight, c.spritesHeight AS spriteHeight,
d.isBEm as isBemCard c.name as nameSprite,
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId as isInAdventure
FROM UserCharacter uc FROM UserCharacter uc
JOIN Character c ON uc.charId = c.id JOIN Character c ON uc.charId = c.id
JOIN Card d ON c.dimId = d.id JOIN Card d ON c.dimId = d.id
LEFT JOIN Adventure a ON a.characterId = uc.id
WHERE uc.isActive = 1 WHERE uc.isActive = 1
LIMIT 1 LIMIT 1
""" """)
)
suspend fun getActiveCharacter(): CharacterDtos.CharacterWithSprites? suspend fun getActiveCharacter(): CharacterDtos.CharacterWithSprites?
@Query("DELETE FROM UserCharacter WHERE id = :id") @Query("DELETE FROM UserCharacter WHERE id = :id")
@@ -87,4 +123,15 @@ interface UserCharacterDao {
@Query("UPDATE UserCharacter SET isActive = 1 WHERE id = :id") @Query("UPDATE UserCharacter SET isActive = 1 WHERE id = :id")
fun setActiveCharacter(id: Long) fun setActiveCharacter(id: Long)
@Query(
"""
SELECT c.*
FROM Character c
join UserCharacter uc on c.id = uc.charId
where uc.id = :charId
LIMIT 1
"""
)
suspend fun getCharacterInfo(charId: Long): Character
} }
@@ -2,6 +2,7 @@ package com.github.nacabaro.vbhelper.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.github.nacabaro.vbhelper.daos.AdventureDao
import com.github.nacabaro.vbhelper.daos.CharacterDao import com.github.nacabaro.vbhelper.daos.CharacterDao
import com.github.nacabaro.vbhelper.daos.DexDao import com.github.nacabaro.vbhelper.daos.DexDao
import com.github.nacabaro.vbhelper.daos.DiMDao import com.github.nacabaro.vbhelper.daos.DiMDao
@@ -10,12 +11,12 @@ import com.github.nacabaro.vbhelper.daos.UserCharacterDao
import com.github.nacabaro.vbhelper.domain.characters.Character import com.github.nacabaro.vbhelper.domain.characters.Character
import com.github.nacabaro.vbhelper.domain.characters.Card import com.github.nacabaro.vbhelper.domain.characters.Card
import com.github.nacabaro.vbhelper.domain.Sprites import com.github.nacabaro.vbhelper.domain.Sprites
import com.github.nacabaro.vbhelper.domain.characters.Adventure
import com.github.nacabaro.vbhelper.domain.characters.Dex import com.github.nacabaro.vbhelper.domain.characters.Dex
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.domain.items.Items import com.github.nacabaro.vbhelper.domain.items.Items
import com.github.nacabaro.vbhelper.domain.items.UserItems
@Database( @Database(
version = 1, version = 1,
@@ -28,7 +29,7 @@ import com.github.nacabaro.vbhelper.domain.items.UserItems
TransformationHistory::class, TransformationHistory::class,
Dex::class, Dex::class,
Items::class, Items::class,
UserItems::class Adventure::class
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -37,4 +38,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun userCharacterDao(): UserCharacterDao abstract fun userCharacterDao(): UserCharacterDao
abstract fun dexDao(): DexDao abstract fun dexDao(): DexDao
abstract fun itemDao(): ItemDao abstract fun itemDao(): ItemDao
abstract fun adventureDao(): AdventureDao
} }
@@ -0,0 +1,22 @@
package com.github.nacabaro.vbhelper.domain.characters
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
@Entity(
foreignKeys = [
ForeignKey(
entity = UserCharacter::class,
parentColumns = ["id"],
childColumns = ["characterId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Adventure(
@PrimaryKey val characterId: Long,
val originalDuration: Long,
val finishesAdventure: Long
)
@@ -3,6 +3,7 @@ package com.github.nacabaro.vbhelper.domain.characters
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.ForeignKey import androidx.room.ForeignKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
@Entity( @Entity(
foreignKeys = [ foreignKeys = [
@@ -14,13 +15,18 @@ import androidx.room.ForeignKey
) )
] ]
) )
/*
* Character represents a character on a DIM card. There should only be one of these per dimId
* and monIndex.
* TODO: Customs will mean this should be unique per cardName and monIndex
*/
data class Character ( data class Character (
@PrimaryKey(autoGenerate = true) val id: Long = 0, @PrimaryKey(autoGenerate = true) val id: Long = 0,
val dimId: Long, val dimId: Long,
val monIndex: Int, val monIndex: Int,
val name: ByteArray, val name: ByteArray,
val stage: Int, // These should be replaced with enums val stage: Int, // These should be replaced with enums
val attribute: Int, // This one too val attribute: NfcCharacter.Attribute, // This one too
val baseHp: Int, val baseHp: Int,
val baseBp: Int, val baseBp: Int,
val baseAp: Int, val baseAp: Int,
@@ -17,11 +17,12 @@ import com.github.nacabaro.vbhelper.domain.characters.Character
) )
] ]
) )
/**
* UserCharacter represents and instance of a character. The charId should map to a Character
*/
data class UserCharacter ( data class UserCharacter (
@PrimaryKey(autoGenerate = true) val id: Long = 0, @PrimaryKey(autoGenerate = true) val id: Long = 0,
var charId: Long, var charId: Long,
var stage: Int,
var attribute: NfcCharacter.Attribute,
var ageInDays: Int, var ageInDays: Int,
var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim
var mood: Int, var mood: Int,
@@ -10,5 +10,6 @@ data class Items(
val description: String, val description: String,
val itemIcon: Int, val itemIcon: Int,
val itemLength: Int, val itemLength: Int,
val price: Int val price: Int,
val quantity: Int
) )
@@ -1,20 +0,0 @@
package com.github.nacabaro.vbhelper.domain.items
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = Items::class,
parentColumns = ["id"],
childColumns = ["itemId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class UserItems(
@PrimaryKey val itemId: Long,
val quantity: Int,
)
@@ -27,7 +27,11 @@ object CharacterDtos {
val spriteIdle: ByteArray, val spriteIdle: ByteArray,
val spriteWidth: Int, val spriteWidth: Int,
val spriteHeight: Int, val spriteHeight: Int,
val isBemCard: Boolean val nameSprite: ByteArray,
val nameSpriteWidth: Int,
val nameSpriteHeight: Int,
val isBemCard: Boolean,
val isInAdventure: Boolean
) )
data class DiMInfo( data class DiMInfo(
@@ -51,4 +55,31 @@ object CharacterDtos {
val spriteHeight: Int, val spriteHeight: Int,
val discoveredOn: Long? val discoveredOn: Long?
) )
data class AdventureCharacterWithSprites(
var id: Long = 0,
var charId: Long,
var stage: Int,
var attribute: NfcCharacter.Attribute,
var ageInDays: Int,
var nextAdventureMissionStage: Int, // next adventure mission stage on the character's dim
var mood: Int,
var vitalPoints: Int,
var transformationCountdown: Int,
var injuryStatus: NfcCharacter.InjuryStatus,
var trophies: Int,
var currentPhaseBattlesWon: Int,
var currentPhaseBattlesLost: Int,
var totalBattlesWon: Int,
var totalBattlesLost: Int,
var activityLevel: Int,
var heartRateCurrent: Int,
var characterType: DeviceType,
val spriteIdle: ByteArray,
val spriteWidth: Int,
val spriteHeight: Int,
val isBemCard: Boolean,
val finishesAdventure: Long,
val originalTimeInMinutes: Long
)
} }
@@ -2,7 +2,7 @@ package com.github.nacabaro.vbhelper.dtos
object ItemDtos { object ItemDtos {
data class ItemsWithQuantities ( data class ItemsWithQuantities(
val id: Long, val id: Long,
val name: String, val name: String,
val description: String, val description: String,
@@ -11,4 +11,13 @@ object ItemDtos {
val price: Int, val price: Int,
val quantity: Int, val quantity: Int,
) )
data class PurchasedItem(
val itemId: Long,
val itemName: String,
val itemDescription: String,
val itemIcon: Int,
val itemLength: Int,
val itemAmount: Int
)
} }
@@ -16,21 +16,28 @@ import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreen import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreen
import com.github.nacabaro.vbhelper.screens.SpriteViewer import com.github.nacabaro.vbhelper.screens.SpriteViewer
import com.github.nacabaro.vbhelper.screens.StorageScreen import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreen
import com.github.nacabaro.vbhelper.screens.itemsScreen.ChooseCharacterScreen import com.github.nacabaro.vbhelper.screens.itemsScreen.ChooseCharacterScreen
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl import com.github.nacabaro.vbhelper.screens.settingsScreen.SettingsScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreen
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.settingsScreen.CreditsScreen
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl
data class AppNavigationHandlers( data class AppNavigationHandlers(
val settingsScreenController: SettingsScreenControllerImpl, val settingsScreenController: SettingsScreenControllerImpl,
val scanScreenController: ScanScreenControllerImpl, val scanScreenController: ScanScreenControllerImpl,
val itemsScreenController: ItemsScreenControllerImpl val itemsScreenController: ItemsScreenControllerImpl,
val adventureScreenController: AdventureScreenControllerImpl,
val storageScreenController: StorageScreenControllerImpl,
val homeScreenController: HomeScreenControllerImpl
) )
@Composable @Composable
fun AppNavigation( fun AppNavigation(
applicationNavigationHandlers: AppNavigationHandlers, applicationNavigationHandlers: AppNavigationHandlers,
onClickImportCard: () -> Unit,
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
@@ -50,12 +57,15 @@ fun AppNavigation(
} }
composable(NavigationItems.Home.route) { composable(NavigationItems.Home.route) {
HomeScreen( HomeScreen(
navController = navController navController = navController,
homeScreenController = applicationNavigationHandlers.homeScreenController
) )
} }
composable(NavigationItems.Storage.route) { composable(NavigationItems.Storage.route) {
StorageScreen( StorageScreen(
navController = navController navController = navController,
adventureScreenController = applicationNavigationHandlers.adventureScreenController,
storageScreenController = applicationNavigationHandlers.storageScreenController
) )
} }
composable(NavigationItems.Scan.route) { composable(NavigationItems.Scan.route) {
@@ -76,8 +86,7 @@ fun AppNavigation(
composable(NavigationItems.Settings.route) { composable(NavigationItems.Settings.route) {
SettingsScreen( SettingsScreen(
navController = navController, navController = navController,
settingsScreenController = applicationNavigationHandlers.settingsScreenController, settingsScreenController = applicationNavigationHandlers.settingsScreenController
onClickImportCard = onClickImportCard,
) )
} }
composable(NavigationItems.Viewer.route) { composable(NavigationItems.Viewer.route) {
@@ -110,6 +119,18 @@ fun AppNavigation(
) )
} }
} }
composable(NavigationItems.Adventure.route) {
AdventureScreen(
navController = navController,
storageScreenController = applicationNavigationHandlers
.adventureScreenController
)
}
composable(NavigationItems.Credits.route) {
CreditsScreen(
navController = navController
)
}
} }
} }
} }
@@ -19,6 +19,7 @@ fun BottomNavigationBar(navController: NavController) {
NavigationItems.Dex, NavigationItems.Dex,
NavigationItems.Storage, NavigationItems.Storage,
) )
NavigationBar { NavigationBar {
val currentBackStackEntry = navController.currentBackStackEntryAsState() val currentBackStackEntry = navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry.value?.destination?.route val currentRoute = currentBackStackEntry.value?.destination?.route
@@ -19,4 +19,6 @@ sealed class NavigationItems (
object MyItems : NavigationItems("MyItems", R.drawable.baseline_data_24, "My items") object MyItems : NavigationItems("MyItems", R.drawable.baseline_data_24, "My items")
object ItemsStore : NavigationItems("ItemsStore", R.drawable.baseline_data_24, "Items store") object ItemsStore : NavigationItems("ItemsStore", R.drawable.baseline_data_24, "Items store")
object ApplyItem : NavigationItems("ApplyItem/{itemId}", R.drawable.baseline_data_24, "Apply item") object ApplyItem : NavigationItems("ApplyItem/{itemId}", R.drawable.baseline_data_24, "Apply item")
object Adventure : NavigationItems("Adventure", R.drawable.baseline_fort_24, "Adventure")
object Credits : NavigationItems("Credits", R.drawable.baseline_data_24, "Credits")
} }
@@ -1,194 +0,0 @@
package com.github.nacabaro.vbhelper.screens
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun StorageScreen(
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val monList = remember { mutableStateOf<List<CharacterDtos.CharacterWithSprites>>(emptyList()) }
var selectedCharacter by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(storageRepository) {
coroutineScope.launch {
val characterList = storageRepository.getAllCharacters()
monList.value = characterList
}
}
Scaffold (
topBar = { TopBanner(text = "My characters") }
) { contentPadding ->
if (monList.value.isEmpty()) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
Text(
text = "Nothing to see here",
textAlign = TextAlign.Center,
modifier = Modifier
)
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical)
.padding(top = contentPadding.calculateTopPadding())
) {
items(monList.value) { index ->
CharacterEntry(
icon = BitmapData(
bitmap = index.spriteIdle,
width = index.spriteWidth,
height = index.spriteHeight
),
onClick = {
selectedCharacter = index.id
}
)
}
}
if (selectedCharacter != null) {
StorageDialog(
characterId = selectedCharacter!!,
onDismissRequest = { selectedCharacter = null },
onClickSetActive = {
coroutineScope.launch {
withContext(Dispatchers.IO) {
storageRepository.setActiveCharacter(selectedCharacter!!)
selectedCharacter = null
}
navController.navigate(NavigationItems.Home.route)
}
},
onSendToBracelet = {
navController.navigate(
NavigationItems.Scan.route.replace(
"{characterId}",
selectedCharacter.toString()
)
)
}
)
}
}
}
@Composable
fun StorageDialog(
characterId: Long,
onDismissRequest: () -> Unit,
onSendToBracelet: () -> Unit,
onClickSetActive: () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val character = remember { mutableStateOf<UserCharacter?>(null) }
LaunchedEffect(storageRepository) {
coroutineScope.launch {
character.value = storageRepository.getSingleCharacter(characterId)
}
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card(
shape = RoundedCornerShape(16.dp)
) {
Column (
modifier = Modifier
.padding(16.dp)
) {
if (character.value != null) {
Text(
text = character.value?.toString() ?: "Loading...",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(8.dp)
)
}
Row (
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
Button(
onClick = onSendToBracelet
) {
Text(text = "Send to bracelet")
}
Button(
onClick = onClickSetActive
) {
Text(text = "Set active")
}
Button(
onClick = onDismissRequest
) {
Text(text = "Close")
}
}
}
}
}
}
@@ -0,0 +1,68 @@
package com.github.nacabaro.vbhelper.screens.adventureScreen
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
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.material3.Text
import androidx.compose.material3.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getBitmap
import java.util.Locale
@Composable
fun AdventureEntry(
icon: BitmapData,
timeLeft: Long,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val bitmap = remember (icon.bitmap) { icon.getBitmap() }
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (icon.width * 4 / density).dp
Card(
onClick = onClick,
modifier = modifier
.padding(8.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier
.padding(8.dp)
.height(96.dp)
) {
Image(
bitmap = imageBitmap,
contentDescription = null,
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
)
Text(
text = when {
timeLeft < 0 -> "Adventure finished"
else -> "Time left: ${formatSeconds(timeLeft)}"
}
)
}
}
}
fun formatSeconds(totalSeconds: Long): String {
val hours = totalSeconds / 3600
val minutes = (totalSeconds % 3600) / 60
val seconds = totalSeconds % 60
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds)
}
@@ -0,0 +1,145 @@
package com.github.nacabaro.vbhelper.screens.adventureScreen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.screens.itemsScreen.ObtainedItemDialog
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.Instant
@Composable
fun AdventureScreen(
navController: NavController,
storageScreenController: AdventureScreenControllerImpl
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val database = application.container.db
val storageRepository = StorageRepository(database)
val characterList = remember {
mutableStateOf<List<CharacterDtos.AdventureCharacterWithSprites>>(emptyList())
}
var obtainedItem by remember {
mutableStateOf<ItemDtos.PurchasedItem?>(null)
}
val currentTime by produceState(initialValue = Instant.now().epochSecond) {
while (true) {
value = Instant.now().epochSecond
delay(1000)
}
}
var cancelAdventureDialog by remember {
mutableStateOf<CharacterDtos.AdventureCharacterWithSprites?>(null)
}
LaunchedEffect(storageRepository) {
coroutineScope.launch {
characterList.value = storageRepository
.getAdventureCharacters()
}
}
Scaffold(
topBar = {
TopBanner(
text = "Adventure",
onBackClick = {
navController.popBackStack()
}
)
}
) { contentPadding ->
if (characterList.value.isEmpty()) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
Text(text = "Nothing to see here")
}
} else {
LazyColumn(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
items(characterList.value) {
AdventureEntry(
icon = BitmapData(
bitmap = it.spriteIdle,
width = it.spriteWidth,
height = it.spriteHeight
),
timeLeft = it.finishesAdventure - currentTime,
onClick = {
if (it.finishesAdventure < currentTime) {
storageScreenController
.getItemFromAdventure(it.id) { adventureResult ->
obtainedItem = adventureResult
}
} else {
cancelAdventureDialog = it
}
}
)
}
}
}
}
if (obtainedItem != null) {
ObtainedItemDialog(
obtainedItem = obtainedItem!!,
onClickDismiss = {
obtainedItem = null
}
)
}
if (cancelAdventureDialog != null) {
CancelAdventureDialog(
characterSprite = BitmapData(
bitmap = cancelAdventureDialog!!.spriteIdle,
width = cancelAdventureDialog!!.spriteWidth,
height = cancelAdventureDialog!!.spriteHeight
),
onDismissRequest = {
cancelAdventureDialog = null
},
onClickConfirm = {
storageScreenController.cancelAdventure(cancelAdventureDialog!!.id) {
navController.navigate(NavigationItems.Storage.route)
}
cancelAdventureDialog = null
}
)
}
}
@@ -0,0 +1,9 @@
package com.github.nacabaro.vbhelper.screens.adventureScreen
import com.github.nacabaro.vbhelper.dtos.ItemDtos
interface AdventureScreenController {
fun sendCharacterToAdventure(characterId: Long, timeInMinutes: Long)
fun getItemFromAdventure(characterId: Long, onResult: (ItemDtos.PurchasedItem) -> Unit)
fun cancelAdventure(characterId: Long, onResult: () -> Unit)
}
@@ -0,0 +1,100 @@
package com.github.nacabaro.vbhelper.screens.adventureScreen
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
import kotlin.random.Random
class AdventureScreenControllerImpl(
private val componentActivity: ComponentActivity,
) : AdventureScreenController {
private val application = componentActivity.applicationContext as VBHelper
private val database = application.container.db
override fun sendCharacterToAdventure(characterId: Long, timeInMinutes: Long) {
val finishesAdventureAt = timeInMinutes * 60
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
val characterData = database
.userCharacterDao()
.getCharacter(characterId)
if (characterData.isActive) {
database
.userCharacterDao()
.clearActiveCharacter()
}
database
.adventureDao()
.insertNewAdventure(characterId, timeInMinutes, finishesAdventureAt)
}
}
override fun getItemFromAdventure(
characterId: Long,
onResult: (ItemDtos.PurchasedItem) -> Unit
) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
database
.adventureDao()
.deleteAdventure(characterId)
val generatedItem = generateItem(characterId)
onResult(generatedItem)
}
}
override fun cancelAdventure(characterId: Long, onResult: () -> Unit) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
database
.adventureDao()
.deleteAdventure(characterId)
componentActivity
.runOnUiThread {
Toast.makeText(
componentActivity,
"Adventure canceled",
Toast.LENGTH_SHORT
).show()
onResult()
}
}
}
private suspend fun generateItem(characterId: Long): ItemDtos.PurchasedItem {
val character = database
.userCharacterDao()
.getCharacterInfo(characterId)
val randomItem = database
.itemDao()
.getAllItems()
.random()
val random = ((Random.nextFloat() * character.stage) + 3).roundToInt()
database
.itemDao()
.purchaseItem(
itemId = randomItem.id,
itemAmount = random
)
return ItemDtos.PurchasedItem(
itemId = randomItem.id,
itemAmount = random,
itemName = randomItem.name,
itemIcon = randomItem.itemIcon,
itemLength = randomItem.itemLength,
itemDescription = randomItem.description
)
}
}
@@ -0,0 +1,73 @@
package com.github.nacabaro.vbhelper.screens.adventureScreen
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
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.getBitmap
@Composable
fun CancelAdventureDialog(
characterSprite: BitmapData,
onDismissRequest: () -> Unit,
onClickConfirm: () -> Unit
) {
val bitmap = remember (characterSprite) { characterSprite.getBitmap() }
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (characterSprite.width * 4 / density).dp
Dialog(
onDismissRequest = onDismissRequest
) {
Card {
Column(
modifier = Modifier
.padding(16.dp)
) {
Row {
Image(
bitmap = imageBitmap,
contentDescription = null,
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
)
Text(
text = "Are you sure you want to cancel this character's adventure?"
)
}
Row(
modifier = Modifier
.padding(8.dp)
) {
Button(
onClick = onClickConfirm
) {
Text(text = "Confirm")
}
Spacer(modifier = Modifier.padding(4.dp))
Button(
onClick = onDismissRequest
) {
Text(text = "Dismiss")
}
}
}
}
}
}
@@ -15,7 +15,7 @@ import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.CharacterEntry import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.ItemDisplay import com.github.nacabaro.vbhelper.components.ItemDisplay
import com.github.nacabaro.vbhelper.components.TransformationHistoryCard import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
import com.github.nacabaro.vbhelper.components.getIconResource import com.github.nacabaro.vbhelper.screens.itemsScreen.getIconResource
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
@@ -16,7 +16,7 @@ import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.components.CharacterEntry import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.ItemDisplay import com.github.nacabaro.vbhelper.components.ItemDisplay
import com.github.nacabaro.vbhelper.components.TransformationHistoryCard import com.github.nacabaro.vbhelper.components.TransformationHistoryCard
import com.github.nacabaro.vbhelper.components.getIconResource import com.github.nacabaro.vbhelper.screens.itemsScreen.getIconResource
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.dtos.CharacterDtos import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
@@ -0,0 +1,46 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@Composable
fun BetaWarning(
onDismissRequest: () -> Unit
) {
Dialog(
onDismissRequest = onDismissRequest
) {
Card {
Column (
modifier = Modifier
.padding(16.dp)
) {
Text(
text = "This application is currently in alpha and it is not complete. Do not use to store important characters for you, as any future updates might delete all your characters. Sorry for the inconvenience!"
)
Spacer(modifier = Modifier.padding(8.dp))
Text(
text = "Also, this application does not work yet with the original VB."
)
Spacer(modifier = Modifier.padding(8.dp))
Text(
text = "Thank you for your understanding and patience. Sincerely, the dev team."
)
Spacer(modifier = Modifier.padding(8.dp))
Button(
onClick = onDismissRequest
) {
Text(text = "Dismiss")
}
}
}
}
}
@@ -1,19 +1,27 @@
package com.github.nacabaro.vbhelper.screens.homeScreens package com.github.nacabaro.vbhelper.screens.homeScreens
import android.util.Log
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.navigation.NavController import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.di.VBHelper
@@ -28,7 +36,8 @@ import kotlinx.coroutines.withContext
@Composable @Composable
fun HomeScreen( fun HomeScreen(
navController: NavController navController: NavController,
homeScreenController: HomeScreenControllerImpl
) { ) {
val application = LocalContext.current.applicationContext as VBHelper val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db) val storageRepository = StorageRepository(application.container.db)
@@ -36,6 +45,8 @@ fun HomeScreen(
val transformationHistory = remember { mutableStateOf<List<CharacterDtos.TransformationHistory>?>(null) } val transformationHistory = remember { mutableStateOf<List<CharacterDtos.TransformationHistory>?>(null) }
val beData = remember { mutableStateOf<BECharacterData?>(null) } val beData = remember { mutableStateOf<BECharacterData?>(null) }
val vbData = remember { mutableStateOf<VBCharacterData?>(null) } val vbData = remember { mutableStateOf<VBCharacterData?>(null) }
var adventureMissionsFinished by rememberSaveable { mutableStateOf(false) }
var betaWarning by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(storageRepository, activeMon) { LaunchedEffect(storageRepository, activeMon) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -47,6 +58,13 @@ fun HomeScreen(
} }
} }
LaunchedEffect(true) {
homeScreenController
.didAdventureMissionsFinish {
adventureMissionsFinished = it
}
}
Scaffold ( Scaffold (
topBar = { topBar = {
TopBanner( TopBanner(
@@ -72,7 +90,6 @@ fun HomeScreen(
} }
} else { } else {
if (activeMon.value!!.isBemCard) { if (activeMon.value!!.isBemCard) {
Log.d("HomeScreen", "BEDeviceBEm")
BEBEmHomeScreen( BEBEmHomeScreen(
activeMon = activeMon.value!!, activeMon = activeMon.value!!,
beData = beData.value!!, beData = beData.value!!,
@@ -80,7 +97,6 @@ fun HomeScreen(
contentPadding = contentPadding contentPadding = contentPadding
) )
} else if (!activeMon.value!!.isBemCard && activeMon.value!!.characterType == DeviceType.BEDevice) { } else if (!activeMon.value!!.isBemCard && activeMon.value!!.characterType == DeviceType.BEDevice) {
Log.d("HomeScreen", "BEDevice")
BEDiMHomeScreen( BEDiMHomeScreen(
activeMon = activeMon.value!!, activeMon = activeMon.value!!,
beData = beData.value!!, beData = beData.value!!,
@@ -97,6 +113,40 @@ fun HomeScreen(
} }
} }
} }
if (adventureMissionsFinished) {
Dialog(
onDismissRequest = { adventureMissionsFinished = false },
) {
Card {
Column(
modifier = Modifier
.padding(16.dp)
) {
Text(
text = "One of your characters has finished their adventure mission!",
textAlign = TextAlign.Center
)
Button(
onClick = {
adventureMissionsFinished = false
},
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Text(text = "Dismiss")
}
}
}
}
}
if (betaWarning) {
BetaWarning {
betaWarning = false
}
}
} }
@@ -0,0 +1,5 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
interface HomeScreenController {
fun didAdventureMissionsFinish(onCompletion: (Boolean) -> Unit)
}
@@ -0,0 +1,29 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.di.VBHelper
import kotlinx.coroutines.launch
import java.time.Instant
class HomeScreenControllerImpl(
private val componentActivity: ComponentActivity,
): HomeScreenController {
private val application = componentActivity.applicationContext as VBHelper
private val database = application.container.db
override fun didAdventureMissionsFinish(onCompletion: (Boolean) -> Unit) {
componentActivity.lifecycleScope.launch {
val currentTime = Instant.now().epochSecond
val adventureCharacters = database
.adventureDao()
.getAdventureCharacters()
val finishedAdventureCharacters = adventureCharacters.filter { character ->
character.finishesAdventure <= currentTime
}
onCompletion(finishedAdventureCharacters.isNotEmpty())
}
}
}
@@ -1,4 +1,4 @@
package com.github.nacabaro.vbhelper.components package com.github.nacabaro.vbhelper.screens.itemsScreen
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -25,7 +25,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.github.nacabaro.vbhelper.R import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.screens.itemsScreen.ItemsScreenControllerImpl
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
@Composable @Composable
@@ -41,13 +40,13 @@ fun ItemElement(
.aspectRatio(1f) .aspectRatio(1f)
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
// Background image (full size)
Icon( Icon(
painter = painterResource(id = itemIcon), painter = painterResource(id = itemIcon),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(96.dp) .size(96.dp)
.align(Alignment.Center) .align(Alignment.Center)
.padding(16.dp)
) )
Icon( Icon(
painter = painterResource(id = lengthIcon), painter = painterResource(id = lengthIcon),
@@ -55,9 +54,8 @@ fun ItemElement(
tint = MaterialTheme.colorScheme.surfaceTint, tint = MaterialTheme.colorScheme.surfaceTint,
modifier = Modifier modifier = Modifier
.size(48.dp) // Set the size of the overlay image .size(48.dp) // Set the size of the overlay image
.padding(4.dp
)
.align(Alignment.TopStart) // Align to the top end (top-right corner) .align(Alignment.TopStart) // Align to the top end (top-right corner)
.padding(8.dp)
) )
} }
} }
@@ -124,7 +122,10 @@ fun ItemDialog(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodyMedium.fontSize, fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily, fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
text = description text = description,
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
) )
Text( Text(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@@ -133,7 +134,7 @@ fun ItemDialog(
text = "You have $amount of this item", text = "You have $amount of this item",
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(8.dp)
) )
Row ( Row (
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@@ -107,7 +107,7 @@ class ItemsScreenControllerImpl (
private fun getItem(itemId: Long): ItemDtos.ItemsWithQuantities { private fun getItem(itemId: Long): ItemDtos.ItemsWithQuantities {
return database return database
.itemDao() .itemDao()
.getUserItem(itemId) .getItem(itemId)
} }
private fun consumeItem(itemId: Long) { private fun consumeItem(itemId: Long) {
@@ -15,10 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.ItemDialog
import com.github.nacabaro.vbhelper.components.ItemElement
import com.github.nacabaro.vbhelper.components.getIconResource
import com.github.nacabaro.vbhelper.components.getLengthResource
import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.ItemDtos import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.source.ItemsRepository import com.github.nacabaro.vbhelper.source.ItemsRepository
@@ -19,10 +19,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.ItemDialog
import com.github.nacabaro.vbhelper.components.ItemElement
import com.github.nacabaro.vbhelper.components.getIconResource
import com.github.nacabaro.vbhelper.components.getLengthResource
import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.ItemDtos import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems import com.github.nacabaro.vbhelper.navigation.NavigationItems
@@ -0,0 +1,100 @@
package com.github.nacabaro.vbhelper.screens.itemsScreen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.dtos.ItemDtos
@Composable
fun ObtainedItemDialog(
obtainedItem: ItemDtos.PurchasedItem,
onClickDismiss: () -> Unit
) {
Dialog(
onDismissRequest = onClickDismiss
) {
Card {
Column(
modifier = Modifier
.padding(16.dp)
) {
Column (
modifier = Modifier
.padding(16.dp)
) {
Row {
Box(modifier = Modifier) {
Icon(
painter = painterResource(id = getIconResource(obtainedItem.itemIcon)),
contentDescription = null,
modifier = Modifier
.size(96.dp)
.align(Alignment.Center)
)
Icon(
painter = painterResource(id = getLengthResource(obtainedItem.itemLength)),
contentDescription = null,
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier
.size(64.dp)
.align(Alignment.BottomEnd)
)
}
Column (
modifier = Modifier
.padding(16.dp)
) {
Text(
fontSize = MaterialTheme.typography.titleLarge.fontSize,
text = obtainedItem.itemName,
modifier = Modifier
.fillMaxWidth()
)
}
}
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
text = obtainedItem.itemDescription,
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
)
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
text = "You have obtained ${obtainedItem.itemAmount} of this item",
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
)
Button(
onClick = onClickDismiss,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = "Dismiss")
}
}
}
}
}
}
@@ -11,20 +11,26 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vbnfc.TagCommunicator 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.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener 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.getCryptographicTransformerMap
import com.github.nacabaro.vbhelper.source.isMissingSecrets import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets import com.github.nacabaro.vbhelper.source.proto.Secrets
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.GregorianCalendar
class ScanScreenControllerImpl( class ScanScreenControllerImpl(
override val secretsFlow: Flow<Secrets>, override val secretsFlow: Flow<Secrets>,
private val nfcHandler: (NfcCharacter)->String, private val componentActivity: ComponentActivity,
private val context: ComponentActivity,
private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit, private val registerActivityLifecycleListener: (String, ActivityLifecycleListener)->Unit,
private val unregisterActivityLifecycleListener: (String)->Unit, private val unregisterActivityLifecycleListener: (String)->Unit,
): ScanScreenController { ): ScanScreenController {
@@ -32,9 +38,9 @@ class ScanScreenControllerImpl(
private val nfcAdapter: NfcAdapter private val nfcAdapter: NfcAdapter
init { init {
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(context) val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(componentActivity)
if (maybeNfcAdapter == null) { 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 nfcAdapter = maybeNfcAdapter
checkSecrets() checkSecrets()
@@ -43,7 +49,7 @@ class ScanScreenControllerImpl(
override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) { override fun onClickRead(secrets: Secrets, onComplete: ()->Unit) {
handleTag(secrets) { tagCommunicator -> handleTag(secrets) { tagCommunicator ->
val character = tagCommunicator.receiveCharacter() val character = tagCommunicator.receiveCharacter()
val resultMessage = nfcHandler(character) val resultMessage = addCharacterScannedIntoDatabase(character)
onComplete.invoke() onComplete.invoke()
resultMessage resultMessage
} }
@@ -51,7 +57,7 @@ class ScanScreenControllerImpl(
override fun cancelRead() { override fun cancelRead() {
if(nfcAdapter.isEnabled) { if(nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(context) nfcAdapter.disableReaderMode(componentActivity)
} }
} }
@@ -74,7 +80,7 @@ class ScanScreenControllerImpl(
val options = Bundle() val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast // Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250) 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 options
) )
} }
@@ -85,26 +91,26 @@ class ScanScreenControllerImpl(
return { tag-> return { tag->
val nfcData = NfcA.get(tag) val nfcData = NfcA.get(tag)
if (nfcData == null) { if (nfcData == null) {
context.runOnUiThread { componentActivity.runOnUiThread {
Toast.makeText(context, "Tag detected is not VB", Toast.LENGTH_SHORT).show() Toast.makeText(componentActivity, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
} }
} }
nfcData.connect() nfcData.connect()
nfcData.use { nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap()) val tagCommunicator = TagCommunicator.getInstance(nfcData, secrets.getCryptographicTransformerMap())
val successText = handlerFunc(tagCommunicator) val successText = handlerFunc(tagCommunicator)
context.runOnUiThread { componentActivity.runOnUiThread {
Toast.makeText(context, successText, Toast.LENGTH_SHORT).show() Toast.makeText(componentActivity, successText, Toast.LENGTH_SHORT).show()
} }
} }
} }
} }
private fun checkSecrets() { private fun checkSecrets() {
context.lifecycleScope.launch(Dispatchers.IO) { componentActivity.lifecycleScope.launch(Dispatchers.IO) {
if(secretsFlow.stateIn(context.lifecycleScope).value.isMissingSecrets()) { if(secretsFlow.stateIn(componentActivity.lifecycleScope).value.isMissingSecrets()) {
context.runOnUiThread { componentActivity.runOnUiThread {
Toast.makeText(context, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show() Toast.makeText(componentActivity, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
} }
} }
} }
@@ -141,7 +147,105 @@ class ScanScreenControllerImpl(
// EXTRACTED DIRECTLY FROM EXAMPLE APP // EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() { private fun showWirelessSettings() {
Toast.makeText(context, "NFC must be enabled", Toast.LENGTH_SHORT).show() Toast.makeText(componentActivity, "NFC must be enabled", Toast.LENGTH_SHORT).show()
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) 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,
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!"
} }
} }
@@ -0,0 +1,40 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
@Composable
fun CreditsScreen(
navController: NavController
) {
Scaffold (
topBar = {
TopBanner(
text = "Credits",
onBackClick = {
navController.popBackStack()
}
)
},
modifier = Modifier
.fillMaxSize()
) { contentPadding ->
Column (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
SettingsSection("Reverse engineering")
SettingsEntry(title = "cyanic", description = "Reversed the firmware and helped us during development.") { }
SettingsSection("Application development")
SettingsEntry(title = "cfogrady", description = "Developed vb-lib-nfc and part of this application.") { }
SettingsEntry(title = "nacabaro", description = "Developed this application.") { }
SettingsEntry(title = "lightheel", description = "Developing the battling part for this application, including server. Still in the works.") { }
}
}
}
@@ -18,12 +18,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.NavigationItems
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
navController: NavController, navController: NavController,
settingsScreenController: SettingsScreenControllerImpl, settingsScreenController: SettingsScreenControllerImpl,
onClickImportCard: () -> Unit
) { ) {
Scaffold ( Scaffold (
topBar = { topBar = {
@@ -47,6 +47,16 @@ fun SettingsScreen(
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") { SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
settingsScreenController.onClickImportApk() settingsScreenController.onClickImportApk()
} }
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import 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") {
navController.navigate(NavigationItems.Credits.route)
}
SettingsEntry(title = "About", description = "About") { }
SettingsSection("Data management") SettingsSection("Data management")
SettingsEntry(title = "Export data", description = "Export application database") { SettingsEntry(title = "Export data", description = "Export application database") {
settingsScreenController.onClickOpenDirectory() settingsScreenController.onClickOpenDirectory()
@@ -54,12 +64,6 @@ fun SettingsScreen(
SettingsEntry(title = "Import data", description = "Import application database") { SettingsEntry(title = "Import data", description = "Import application database") {
settingsScreenController.onClickImportDatabase() settingsScreenController.onClickImportDatabase()
} }
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import DiM card", description = "Import DiM/BEm card file", onClick = onClickImportCard)
SettingsEntry(title = "Rename DiM/BEm", description = "Set card name") { }
SettingsSection("About and credits")
SettingsEntry(title = "Credits", description = "Credits") { }
SettingsEntry(title = "About", description = "About") { }
} }
} }
} }
@@ -4,4 +4,5 @@ interface SettingsScreenController {
fun onClickOpenDirectory() fun onClickOpenDirectory()
fun onClickImportDatabase() fun onClickImportDatabase()
fun onClickImportApk() fun onClickImportApk()
fun onClickImportCard()
} }
@@ -9,7 +9,14 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts 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.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper 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.ApkSecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsImporter import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository import com.github.nacabaro.vbhelper.source.SecretsRepository
@@ -27,9 +34,11 @@ class SettingsScreenControllerImpl(
private val filePickerLauncher: ActivityResultLauncher<String> private val filePickerLauncher: ActivityResultLauncher<String>
private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>> private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
private val filePickerApk: ActivityResultLauncher<Array<String>> private val filePickerApk: ActivityResultLauncher<Array<String>>
private val filePickerCard: ActivityResultLauncher<Array<String>>
private val secretsImporter: SecretsImporter = ApkSecretsImporter() private val secretsImporter: SecretsImporter = ApkSecretsImporter()
private val application = context.applicationContext as VBHelper private val application = context.applicationContext as VBHelper
private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
private val database: AppDatabase = application.container.db
init { init {
filePickerLauncher = context.registerForActivityResult( filePickerLauncher = context.registerForActivityResult(
@@ -68,6 +77,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() { override fun onClickOpenDirectory() {
@@ -82,6 +103,96 @@ class SettingsScreenControllerImpl(
filePickerApk.launch(arrayOf("*/*")) 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 -> 54
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 = NfcCharacter.Attribute.entries[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) { private fun exportDatabase(destinationUri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) { context.lifecycleScope.launch(Dispatchers.IO) {
try { try {
@@ -0,0 +1,117 @@
package com.github.nacabaro.vbhelper.screens.storageScreen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.R
fun getAdventureTime(time: Int): String {
return when (time) {
360 -> "6 hours"
720 -> "12 hours"
1440 -> "24 hours"
else -> "Unknown"
}
}
@Composable
fun StorageAdventureTimeDialog(
onClickSendToAdventure: (time: Long) -> Unit,
onDismissRequest: () -> Unit
) {
val times = arrayOf(360, 720, 1440)
var expanded by remember { mutableStateOf(false) }
var itemPosition by remember { mutableIntStateOf(-1) }
Dialog(
onDismissRequest = onDismissRequest
) {
Card {
Column(
modifier = Modifier
.padding(16.dp)
) {
Box(
modifier = Modifier
.padding(16.dp)
) {
Row (
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.width(256.dp)
.clickable(true) {
expanded = true
}
) {
Text(
text = when (itemPosition) {
-1 -> "Choose time"
else -> getAdventureTime(times[itemPosition])
}
)
Icon(
painter = painterResource(R.drawable.baseline_single_arrow_down),
contentDescription = "Show more"
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(256.dp)
) {
times.forEach { time ->
DropdownMenuItem(
text = { Text(getAdventureTime(time)) },
onClick = {
itemPosition = times.indexOf(time)
expanded = false
}
)
}
}
}
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = {
if (itemPosition != -1) {
onClickSendToAdventure(times[itemPosition].toLong())
onDismissRequest()
}
}
) {
Text(text = "Send on adventure")
}
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = onDismissRequest
) {
Text(text = "Dismiss")
}
}
}
}
}
@@ -0,0 +1,163 @@
package com.github.nacabaro.vbhelper.screens.storageScreen
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getBitmap
import kotlinx.coroutines.launch
@Composable
fun StorageDialog(
characterId: Long,
onDismissRequest: () -> Unit,
onSendToBracelet: () -> Unit,
onClickSetActive: () -> Unit,
onClickSendToAdventure: (time: Long) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val character = remember { mutableStateOf<CharacterDtos.CharacterWithSprites?>(null) }
val characterSprite = remember { mutableStateOf<BitmapData?>(null) }
val characterName = remember { mutableStateOf<BitmapData?>(null) }
var onSendToAdventureClicked by remember { mutableStateOf(false) }
LaunchedEffect(storageRepository) {
coroutineScope.launch {
character.value = storageRepository.getSingleCharacter(characterId)
characterSprite.value = BitmapData(
bitmap = character.value!!.spriteIdle,
width = character.value!!.spriteWidth,
height = character.value!!.spriteHeight
)
characterName.value = BitmapData(
bitmap = character.value!!.nameSprite,
width = character.value!!.nameSpriteWidth,
height = character.value!!.nameSpriteHeight
)
}
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card(
shape = RoundedCornerShape(16.dp)
) {
Column (
modifier = Modifier
.padding(16.dp)
) {
if (character.value != null &&
characterSprite.value != null &&
characterName.value != null
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
val bitmap = remember (characterSprite.value!!) { characterSprite.value!!.getBitmap() }
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (characterSprite.value!!.width * 4 / density).dp
Image(
bitmap = imageBitmap,
contentDescription = "Character image",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
)
val nameBitmap = remember (characterName.value!!) { characterName.value!!.getBitmap() }
val nameImageBitmap = remember(nameBitmap) { nameBitmap.asImageBitmap() }
val nameDpSize = (characterName.value!!.width * 4 / density).dp
Image(
bitmap = nameImageBitmap,
contentDescription = "Character image",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(nameDpSize)
)
}
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
) {
Button(
onClick = onSendToBracelet,
modifier = Modifier
.weight(1f)
) {
Text(text = "Send to watch")
}
Spacer(
modifier = Modifier
.padding(4.dp)
)
Button(
onClick = onClickSetActive,
) {
Text(text = "Set active")
}
}
Button(
onClick = {
onSendToAdventureClicked = true
},
modifier = Modifier
.fillMaxWidth()
) {
Text(text = "Send on adventure")
}
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = onDismissRequest
) {
Text(text = "Close")
}
}
}
}
if (onSendToAdventureClicked) {
StorageAdventureTimeDialog(
onClickSendToAdventure = { time ->
onClickSendToAdventure(time)
},
onDismissRequest = { onSendToAdventureClicked = false }
)
}
}
@@ -0,0 +1,145 @@
package com.github.nacabaro.vbhelper.screens.storageScreen
import android.widget.Toast
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.launch
@Composable
fun StorageScreen(
navController: NavController,
storageScreenController: StorageScreenControllerImpl,
adventureScreenController: AdventureScreenControllerImpl
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val monList = remember { mutableStateOf<List<CharacterDtos.CharacterWithSprites>>(emptyList()) }
var selectedCharacter by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(storageRepository, selectedCharacter) {
coroutineScope.launch {
val characterList = storageRepository.getAllCharacters()
monList.value = characterList
}
}
Scaffold (
topBar = {
TopBanner(
text = "My characters",
onAdventureClick = {
navController.navigate(NavigationItems.Adventure.route)
}
)
}
) { contentPadding ->
if (monList.value.isEmpty()) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
Text(
text = "Nothing to see here",
textAlign = TextAlign.Center,
modifier = Modifier
)
}
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical)
.padding(top = contentPadding.calculateTopPadding())
) {
items(monList.value) { index ->
CharacterEntry(
icon = BitmapData(
bitmap = index.spriteIdle,
width = index.spriteWidth,
height = index.spriteHeight
),
onClick = {
if (!index.isInAdventure) {
selectedCharacter = index.id
} else {
Toast.makeText(
application,
"This character is in an adventure",
Toast.LENGTH_SHORT
).show()
navController.navigate(
NavigationItems.Adventure.route
)
}
},
)
}
}
}
if (selectedCharacter != null) {
StorageDialog(
characterId = selectedCharacter!!,
onDismissRequest = { selectedCharacter = null },
onClickSetActive = {
storageScreenController
.setActive(selectedCharacter!!) {
selectedCharacter = null
navController.navigate(NavigationItems.Home.route)
}
},
onSendToBracelet = {
navController.navigate(
NavigationItems.Scan.route.replace(
"{characterId}",
selectedCharacter.toString()
)
)
},
onClickSendToAdventure = { time ->
adventureScreenController
.sendCharacterToAdventure(
characterId = selectedCharacter!!,
timeInMinutes = time
)
selectedCharacter = null
}
)
}
}
}
@@ -0,0 +1,5 @@
package com.github.nacabaro.vbhelper.screens.storageScreen
interface StorageScreenController {
fun setActive(characterId: Long, onCompletion: () -> Unit)
}
@@ -0,0 +1,31 @@
package com.github.nacabaro.vbhelper.screens.storageScreen
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
class StorageScreenControllerImpl(
private val componentActivity: ComponentActivity
): StorageScreenController {
private val application = componentActivity.applicationContext as VBHelper
private val database = application.container.db
override fun setActive(characterId: Long, onCompletion: () -> Unit) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
database.userCharacterDao().clearActiveCharacter()
database.userCharacterDao().setActiveCharacter(characterId)
componentActivity.runOnUiThread {
Toast.makeText(
componentActivity,
"Active character updated!",
Toast.LENGTH_SHORT
).show()
onCompletion()
}
}
}
}
@@ -2,8 +2,6 @@ package com.github.nacabaro.vbhelper.source
import com.github.nacabaro.vbhelper.database.AppDatabase import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos import com.github.nacabaro.vbhelper.dtos.CharacterDtos
class StorageRepository ( class StorageRepository (
@@ -13,8 +11,8 @@ class StorageRepository (
return db.userCharacterDao().getAllCharacters() return db.userCharacterDao().getAllCharacters()
} }
suspend fun getSingleCharacter(id: Long): UserCharacter { suspend fun getSingleCharacter(id: Long): CharacterDtos.CharacterWithSprites {
return db.userCharacterDao().getCharacter(id) return db.userCharacterDao().getCharacterWithSprites(id)
} }
suspend fun getCharacterBeData(id: Long): BECharacterData { suspend fun getCharacterBeData(id: Long): BECharacterData {
@@ -37,8 +35,7 @@ class StorageRepository (
return db.userCharacterDao().deleteCharacterById(id) return db.userCharacterDao().deleteCharacterById(id)
} }
fun setActiveCharacter(id: Long) { suspend fun getAdventureCharacters(): List<CharacterDtos.AdventureCharacterWithSprites> {
db.userCharacterDao().clearActiveCharacter() return db.adventureDao().getAdventureCharacters()
return db.userCharacterDao().setActiveCharacter(id)
} }
} }
@@ -0,0 +1,117 @@
package com.github.nacabaro.vbhelper.widget
import android.content.Context
import android.graphics.Bitmap
import androidx.compose.material3.MaterialTheme
import androidx.glance.GlanceTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.glance.background
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.size
import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.Text
import androidx.glance.unit.ColorProvider
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getBitmap
/*
RANT: Why did they have to create the components from Jetpack Compose
with a different API for Glance? Now there are things I use in Jetpack Compose
like the filterQuality, which isn't here.
*/
val CURRENT_IMAGE_KEY = booleanPreferencesKey("current_image")
class CharacterDisplayWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val application = context.applicationContext as VBHelper
val storageRepository = application.container.db
val currentCharacter = storageRepository
.userCharacterDao()
.getActiveCharacter()
val multiplier = 4
provideContent {
var bitmapIdle1 by remember { mutableStateOf<Bitmap?>(null) }
var bitmapIdle2 by remember { mutableStateOf<Bitmap?>(null) }
var dpSize by remember { mutableStateOf(0.dp) }
if (currentCharacter != null) {
val currentCharacterBitmapIdle1 = BitmapData(
currentCharacter.spriteIdle,
currentCharacter.spriteWidth,
currentCharacter.spriteHeight
)
val currentCharacterBitmapIdle2 = BitmapData(
currentCharacter.spriteIdle2,
currentCharacter.spriteWidth,
currentCharacter.spriteHeight
)
bitmapIdle1 = currentCharacterBitmapIdle1.getBitmap()
bitmapIdle2 = currentCharacterBitmapIdle2.getBitmap()
val density: Float = application.resources.displayMetrics.density
dpSize = (currentCharacterBitmapIdle1.width * multiplier / density).dp
}
WidgetContent(
imageBitmapFrame1 = bitmapIdle1,
imageBitmapFrame2 = bitmapIdle2,
dpSize = dpSize
)
}
}
@Composable
private fun WidgetContent(
imageBitmapFrame1: Bitmap?,
imageBitmapFrame2: Bitmap?,
dpSize: Dp
) {
GlanceTheme {
Column (
horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
verticalAlignment = Alignment.Vertical.CenterVertically,
modifier = GlanceModifier
.fillMaxSize()
.background(ColorProvider(MaterialTheme.colorScheme.background))
) {
if (imageBitmapFrame1 != null) {
Image(
provider = ImageProvider(
bitmap = imageBitmapFrame1
),
contentDescription = "Character",
modifier = GlanceModifier
.size(dpSize)
)
} else {
Text(
text = "No character selected"
)
}
}
}
}
}
@@ -0,0 +1,3 @@
package com.github.nacabaro.vbhelper.widget
import androidx.glance.state.PreferencesGlanceStateDefinition
@@ -0,0 +1,8 @@
package com.github.nacabaro.vbhelper.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class CharacterDisplayWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = CharacterDisplayWidget()
}
@@ -0,0 +1,35 @@
package com.github.nacabaro.vbhelper.widget
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
class CharacterDisplayWidgetWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// Get all widget IDs for your ImageSwapWidget
val glanceIds = GlanceAppWidgetManager(applicationContext)
.getGlanceIds(CharacterDisplayWidget::class.java)
glanceIds.forEach { glanceId ->
// Update the widget's state (toggle the boolean)
updateAppWidgetState(
context = applicationContext,
glanceId = glanceId
) { prefs ->
val currentImageIsImage1 = prefs[CURRENT_IMAGE_KEY] ?: true
prefs[CURRENT_IMAGE_KEY] = !currentImageIsImage1 // Toggle the boolean
}
// Trigger the widget to redraw itself with the new state
CharacterDisplayWidget().update(applicationContext, glanceId)
}
// Indicate that the work was successful
return Result.success()
}
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M40,840v-160l80,-80v-240l-80,-80v-160h80v80h80v-80h80v80h80v-80h80v160l-80,80v40h240v-40l-80,-80v-160h80v80h80v-80h80v80h80v-80h80v160l-80,80v240l80,80v160L560,840v-120q0,-33 -23.5,-56.5T480,640q-33,0 -56.5,23.5T400,720v120L40,840ZM120,760h200v-40q0,-66 47,-113t113,-47q66,0 113,47t47,113v40h200v-47l-80,-80v-306l47,-47L633,280l47,47v153L280,480v-153l47,-47L153,280l47,47v306l-80,80v47ZM480,520Z"
android:fillColor="#000000"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="40dp"
android:minHeight="20dp"
android:minResizeWidth="20dp"
android:previewImage="@drawable/baseline_image_24"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="300000"
android:widgetCategory="home_screen" />