Update: changes and new files from local development

This commit is contained in:
Taveon Nelson 2026-04-18 07:24:06 -05:00
parent 3f29d725ea
commit 5273bae7b6
17 changed files with 2346 additions and 47 deletions

1782
app/lint-baseline.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,15 @@
android:theme="@style/Theme.VBHelper" android:theme="@style/Theme.VBHelper"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -45,6 +54,11 @@
<data android:scheme="http" android:host="localhost" android:port="8080" android:pathPrefix="/authenticate" /> <data android:scheme="http" android:host="localhost" android:port="8080" android:pathPrefix="/authenticate" />
<data android:scheme="http" android:host="127.0.0.1" android:port="8080" android:pathPrefix="/authenticate" /> <data android:scheme="http" android:host="127.0.0.1" android:port="8080" android:pathPrefix="/authenticate" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/x-vitalwear-character" />
</intent-filter>
</activity> </activity>
</application> </application>

View File

@ -1,11 +1,16 @@
package com.github.nacabaro.vbhelper package com.github.nacabaro.vbhelper
import android.content.Intent
import android.net.Uri
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.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vitalwear.protos.Character
import com.github.nacabaro.vbhelper.navigation.AppNavigation import com.github.nacabaro.vbhelper.navigation.AppNavigation
import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers import com.github.nacabaro.vbhelper.navigation.AppNavigationHandlers
@ -17,7 +22,10 @@ import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenContr
import com.github.nacabaro.vbhelper.screens.cardScreen.CardScreenControllerImpl import com.github.nacabaro.vbhelper.screens.cardScreen.CardScreenControllerImpl
import com.github.nacabaro.vbhelper.screens.spriteViewer.SpriteViewerControllerImpl import com.github.nacabaro.vbhelper.screens.spriteViewer.SpriteViewerControllerImpl
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl
import com.github.nacabaro.vbhelper.source.VitalWearCharacterImporter
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -70,6 +78,7 @@ class MainActivity : ComponentActivity() {
} }
Log.i("MainActivity", "Activity onCreated") Log.i("MainActivity", "Activity onCreated")
handleImportIntent(intent)
} }
override fun onPause() { override fun onPause() {
@ -88,6 +97,58 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleImportIntent(intent)
}
private fun handleImportIntent(intent: Intent?) {
val importUri = extractVitalWearImportUri(intent) ?: return
val application = applicationContext as VBHelper
lifecycleScope.launch(Dispatchers.IO) {
val result = runCatching {
contentResolver.openInputStream(importUri)?.use { inputStream ->
val character = Character.parseFrom(inputStream)
VitalWearCharacterImporter(application.container.db).importCharacter(character)
} ?: VitalWearCharacterImporter.ImportResult(
success = false,
message = "VitalWear import file could not be opened."
)
}.getOrElse {
VitalWearCharacterImporter.ImportResult(
success = false,
message = "VitalWear import failed: ${it.message ?: "Unknown error"}"
)
}
runOnUiThread {
Toast.makeText(this@MainActivity, result.message, Toast.LENGTH_LONG).show()
}
}
}
private fun extractVitalWearImportUri(intent: Intent?): Uri? {
if (intent == null) {
return null
}
val isVitalWearImport = intent.type == VITALWEAR_CHARACTER_MIME
if (!isVitalWearImport) {
return null
}
return when (intent.action) {
Intent.ACTION_SEND -> {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
Intent.ACTION_VIEW -> intent.data
else -> null
}
}
@Composable @Composable
private fun MainApplication( private fun MainApplication(
scanScreenController: ScanScreenControllerImpl, scanScreenController: ScanScreenControllerImpl,
@ -112,4 +173,8 @@ class MainActivity : ComponentActivity() {
) )
) )
} }
companion object {
private const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
}
} }

View File

@ -19,6 +19,9 @@ interface CardDao {
@Query("SELECT * FROM Card WHERE id = :id") @Query("SELECT * FROM Card WHERE id = :id")
fun getCardById(id: Long): Card? fun getCardById(id: Long): Card?
@Query("SELECT * FROM Card WHERE LOWER(name) = LOWER(:name) LIMIT 1")
fun getCardByName(name: String): Card?
@Query( @Query(
""" """
SELECT ca.* SELECT ca.*
@ -30,6 +33,18 @@ interface CardDao {
) )
fun getCardByCharacterId(id: Long): Flow<Card> fun getCardByCharacterId(id: Long): Flow<Card>
@Query(
"""
SELECT ca.*
FROM Card ca
JOIN CardCharacter ch ON ca.id = ch.cardId
JOIN UserCharacter uc ON ch.id = uc.charId
WHERE uc.id = :id
LIMIT 1
"""
)
fun getCardByCharacterIdSync(id: Long): Card?
@Query("UPDATE Card SET name = :newName WHERE id = :id") @Query("UPDATE Card SET name = :newName WHERE id = :id")
suspend fun renameCard(id: Int, newName: String) suspend fun renameCard(id: Int, newName: String)

View File

@ -24,6 +24,9 @@ interface CardProgressDao {
) )
fun getCardProgress(cardId: Long): Flow<Int> fun getCardProgress(cardId: Long): Flow<Int>
@Query("SELECT currentStage FROM CardProgress WHERE cardId = :cardId LIMIT 1")
fun getCardProgressSync(cardId: Long): Int?
@Insert @Insert
fun insertCardProgress(cardProgress: CardProgress) fun insertCardProgress(cardProgress: CardProgress)
} }

View File

@ -55,6 +55,22 @@ interface UserCharacterDao {
) )
fun getTransformationHistory(monId: Long): Flow<List<CharacterDtos.TransformationHistory>> fun getTransformationHistory(monId: Long): Flow<List<CharacterDtos.TransformationHistory>>
@Query(
"""
SELECT
t.stageId as stageId,
c.charaIndex AS monIndex,
ca.name as cardName,
t.transformationDate AS transformationDate
FROM TransformationHistory t
JOIN CardCharacter c ON c.id = t.stageId
JOIN Card ca ON ca.id = c.cardId
WHERE t.monId = :monId
ORDER BY t.transformationDate ASC, t.id ASC
"""
)
suspend fun getTransformationHistoryForExport(monId: Long): List<CharacterDtos.TransformationHistoryExport>
@Query( @Query(
""" """
SELECT SELECT

View File

@ -52,6 +52,13 @@ object CharacterDtos {
val transformationDate: Long val transformationDate: Long
) )
data class TransformationHistoryExport(
val stageId: Long,
val monIndex: Int,
val cardName: String,
val transformationDate: Long
)
data class CardCharaProgress( data class CardCharaProgress(
val id: Long, val id: Long,
val spriteIdle: ByteArray, val spriteIdle: ByteArray,

View File

@ -2,6 +2,7 @@ package com.github.nacabaro.vbhelper.screens.homeScreens
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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -40,6 +41,8 @@ import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.R import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.dtos.CardDtos import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.source.CardRepository import com.github.nacabaro.vbhelper.source.CardRepository
import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter
import android.widget.Toast
import com.github.nacabaro.vbhelper.utils.BitmapData import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlin.collections.emptyList import kotlin.collections.emptyList
@ -136,6 +139,7 @@ fun HomeScreen(
Text(text = stringResource(R.string.adventure_empty_state)) Text(text = stringResource(R.string.adventure_empty_state))
} }
} else { } else {
Column(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) {
val cardIcon = BitmapData( val cardIcon = BitmapData(
bitmap = cardIconData!!.cardIcon, bitmap = cardIconData!!.cardIcon,
width = cardIconData!!.cardIconWidth, width = cardIconData!!.cardIconWidth,
@ -147,7 +151,7 @@ fun HomeScreen(
activeMon = activeMon!!, activeMon = activeMon!!,
beData = beData!!, beData = beData!!,
transformationHistory = transformationHistory, transformationHistory = transformationHistory,
contentPadding = contentPadding, contentPadding = PaddingValues(0.dp),
cardIcon = cardIcon cardIcon = cardIcon
) )
} else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) { } else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) {
@ -155,7 +159,7 @@ fun HomeScreen(
activeMon = activeMon!!, activeMon = activeMon!!,
beData = beData!!, beData = beData!!,
transformationHistory = transformationHistory, transformationHistory = transformationHistory,
contentPadding = contentPadding, contentPadding = PaddingValues(0.dp),
cardIcon = cardIcon cardIcon = cardIcon
) )
} else if (vbData != null) { } else if (vbData != null) {
@ -163,7 +167,7 @@ fun HomeScreen(
activeMon = activeMon!!, activeMon = activeMon!!,
vbData = vbData!!, vbData = vbData!!,
transformationHistory = transformationHistory, transformationHistory = transformationHistory,
contentPadding = contentPadding, contentPadding = PaddingValues(0.dp),
specialMissions = vbSpecialMissions, specialMissions = vbSpecialMissions,
homeScreenController = homeScreenController, homeScreenController = homeScreenController,
onClickCollect = { item, currency -> onClickCollect = { item, currency ->
@ -173,6 +177,29 @@ fun HomeScreen(
cardIcon = cardIcon cardIcon = cardIcon
) )
} }
Button(
onClick = {
try {
val intent = VitalWearCharacterExporter(application, application.container.db)
.buildShareIntent(activeMon!!.id)
.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
application.startActivity(intent)
} catch (e: Exception) {
Toast.makeText(
application,
"Could not send character to VitalWear: ${e.message}",
Toast.LENGTH_LONG
).show()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Send to VitalWear")
}
}
} }
} }
@ -221,5 +248,3 @@ fun HomeScreen(
} }
} }
} }

View File

@ -42,6 +42,11 @@ fun ChooseConnectOption(
.fillMaxSize() .fillMaxSize()
.padding(contentPadding) .padding(contentPadding)
) { ) {
Text(
text = "Bandai Toys = Vital Bracelet, Vital Hero, BE Bracelet",
fontSize = 14.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
ScanButton( ScanButton(
text = stringResource(R.string.scan_vb_to_app), text = stringResource(R.string.scan_vb_to_app),
disabled = onClickRead == null, disabled = onClickRead == null,

View File

@ -43,6 +43,7 @@ fun StorageDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onClickDelete: () -> Unit, onClickDelete: () -> Unit,
onSendToBracelet: () -> Unit, onSendToBracelet: () -> Unit,
onSendToVitalWear: () -> Unit,
onClickSetActive: () -> Unit, onClickSetActive: () -> Unit,
onClickSendToAdventure: (time: Long) -> Unit onClickSendToAdventure: (time: Long) -> Unit
) { ) {
@ -136,6 +137,13 @@ fun StorageDialog(
Text(text = stringResource(R.string.storage_set_active)) Text(text = stringResource(R.string.storage_set_active))
} }
} }
Button(
onClick = onSendToVitalWear,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = stringResource(R.string.storage_send_to_vitalwear))
}
Button( Button(
onClick = { onClick = {
onSendToAdventureClicked = true onSendToAdventureClicked = true

View File

@ -33,6 +33,7 @@ import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.navigation.NavigationItems import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenControllerImpl
import com.github.nacabaro.vbhelper.source.StorageRepository import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.source.VitalWearCharacterExporter
import com.github.nacabaro.vbhelper.utils.BitmapData import com.github.nacabaro.vbhelper.utils.BitmapData
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R import com.github.nacabaro.vbhelper.R
@ -136,6 +137,21 @@ fun StorageScreen(
) )
) )
}, },
onSendToVitalWear = {
try {
val intent = VitalWearCharacterExporter(application, application.container.db)
.buildShareIntent(selectedCharacter!!)
.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
application.startActivity(intent)
selectedCharacter = null
} catch (e: Exception) {
Toast.makeText(
application,
"Could not send character to VitalWear: ${e.message}",
Toast.LENGTH_LONG
).show()
}
},
onClickSendToAdventure = { time -> onClickSendToAdventure = { time ->
adventureScreenController adventureScreenController
.sendCharacterToAdventure( .sendCharacterToAdventure(

View File

@ -0,0 +1,114 @@
package com.github.nacabaro.vbhelper.source
import android.content.ClipData
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import com.github.cfogrady.vitalwear.protos.Character
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import java.io.File
class VitalWearCharacterExporter(
private val context: Context,
private val database: AppDatabase
) {
fun buildShareIntent(characterId: Long): Intent {
return runBlocking {
val characterWithSprites = database.userCharacterDao().getCharacterWithSprites(characterId)
val userCharacter = database.userCharacterDao().getCharacter(characterId)
val card = database.cardDao().getCardByCharacterIdSync(characterId)
?: error("Card not found for character $characterId")
val cardProgress = database.cardProgressDao().getCardProgressSync(card.id) ?: 0
val proto = Character.newBuilder()
.setCardId(card.cardId)
.setCardName(card.name)
.setCharacterStats(
Character.CharacterStats.newBuilder()
.setSlotId(database.userCharacterDao().getCharacterInfo(characterId).charaIndex)
.setVitals(characterWithSprites.vitalPoints)
.setTrainingTimeRemainingInSeconds(resolveTrainingSeconds(characterId, userCharacter.characterType))
.setTimeUntilNextTransformation(characterWithSprites.transformationCountdown.toLong() * 60L)
.setTrainedBp(resolveTrainedBp(characterId, userCharacter.characterType))
.setTrainedHp(resolveTrainedHp(characterId, userCharacter.characterType))
.setTrainedAp(resolveTrainedAp(characterId, userCharacter.characterType))
.setTrainedPp(characterWithSprites.trophies)
.setInjured(characterWithSprites.injuryStatus.name.lowercase().contains("inj"))
.setAccumulatedDailyInjuries(0)
.setTotalBattles(characterWithSprites.totalBattlesWon + characterWithSprites.totalBattlesLost)
.setCurrentPhaseBattles(characterWithSprites.currentPhaseBattlesWon + characterWithSprites.currentPhaseBattlesLost)
.setTotalWins(characterWithSprites.totalBattlesWon)
.setCurrentPhaseWins(characterWithSprites.currentPhaseBattlesWon)
.setMood(characterWithSprites.mood)
.build()
)
.setSettings(
Character.Settings.newBuilder()
.setTrainingInBackground(false)
.setAllowedBattles(Character.Settings.AllowedBattles.CARD_ONLY)
.build()
)
.putMaxAdventureCompletedByCard(card.name, (cardProgress - 1).coerceAtLeast(0))
.addAllTransformationHistory(
database.userCharacterDao().getTransformationHistoryForExport(characterId).map {
Character.TransformationEvent.newBuilder()
.setCardName(it.cardName)
.setPhase(0)
.setSlotId(it.monIndex)
.build()
}
)
.build()
val exportDir = File(context.cacheDir, "exports").apply { mkdirs() }
val exportFile = File(exportDir, "vbhelper_character_$characterId.vitalwear")
exportFile.writeBytes(proto.toByteArray())
val exportUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", exportFile)
Intent(Intent.ACTION_SEND).apply {
`package` = "com.github.cfogrady.vitalwear"
type = VITALWEAR_CHARACTER_MIME
putExtra(Intent.EXTRA_STREAM, exportUri)
clipData = ClipData.newRawUri("", exportUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
}
private fun resolveTrainingSeconds(characterId: Long, deviceType: DeviceType): Long {
if (deviceType != DeviceType.BEDevice) {
return 0L
}
return database.userCharacterDao().getBeData(characterId).valueOrNull()?.remainingTrainingTimeInMinutes?.toLong()?.times(60L) ?: 0L
}
private fun resolveTrainedBp(characterId: Long, deviceType: DeviceType): Int {
return if (deviceType == DeviceType.BEDevice) {
database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingBp ?: 0
} else 0
}
private fun resolveTrainedHp(characterId: Long, deviceType: DeviceType): Int {
return if (deviceType == DeviceType.BEDevice) {
database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingHp ?: 0
} else 0
}
private fun resolveTrainedAp(characterId: Long, deviceType: DeviceType): Int {
return if (deviceType == DeviceType.BEDevice) {
database.userCharacterDao().getBeData(characterId).valueOrNull()?.trainingAp ?: 0
} else 0
}
private fun <T> kotlinx.coroutines.flow.Flow<T>.valueOrNull(): T? {
return runBlocking {
firstOrNull()
}
}
companion object {
const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
}
}

View File

@ -0,0 +1,163 @@
package com.github.nacabaro.vbhelper.source
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vitalwear.protos.Character
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlin.math.max
class VitalWearCharacterImporter(
private val database: AppDatabase
) {
data class ImportResult(
val success: Boolean,
val message: String
)
fun importCharacter(character: Character): ImportResult {
val importedCard = resolveCard(character)
?: return ImportResult(
success = false,
message = "Matching card not found in VBHelper. Import that card first."
)
val slotId = character.characterStats.slotId
val cardCharacter = runCatching {
database.characterDao().getCharacterByMonIndex(slotId, importedCard.id)
}.getOrNull() ?: return ImportResult(
success = false,
message = "Character slot $slotId was not found on card ${importedCard.name}."
)
database.userCharacterDao().clearActiveCharacter()
val totalBattles = character.characterStats.totalBattles
val totalWins = character.characterStats.totalWins.coerceAtMost(totalBattles)
val currentPhaseBattles = character.characterStats.currentPhaseBattles
val currentPhaseWins = character.characterStats.currentPhaseWins.coerceAtMost(currentPhaseBattles)
val userCharacterId = database.userCharacterDao().insertCharacterData(
UserCharacter(
charId = cardCharacter.id,
ageInDays = max(character.transformationHistoryCount - 1, 0),
mood = character.characterStats.mood,
vitalPoints = character.characterStats.vitals,
transformationCountdown = secondsToMinutes(character.characterStats.timeUntilNextTransformation),
injuryStatus = resolveInjuryStatus(character.characterStats.injured),
trophies = character.characterStats.trainedPp,
currentPhaseBattlesWon = currentPhaseWins,
currentPhaseBattlesLost = (currentPhaseBattles - currentPhaseWins).coerceAtLeast(0),
totalBattlesWon = totalWins,
totalBattlesLost = (totalBattles - totalWins).coerceAtLeast(0),
activityLevel = 0,
heartRateCurrent = 0,
characterType = DeviceType.BEDevice,
isActive = true
)
)
database.userCharacterDao().insertBECharacterData(
BECharacterData(
id = userCharacterId,
trainingHp = character.characterStats.trainedHp,
trainingAp = character.characterStats.trainedAp,
trainingBp = character.characterStats.trainedBp,
remainingTrainingTimeInMinutes = secondsToMinutes(character.characterStats.trainingTimeRemainingInSeconds),
itemEffectMentalStateValue = 0,
itemEffectMentalStateMinutesRemaining = 0,
itemEffectActivityLevelValue = 0,
itemEffectActivityLevelMinutesRemaining = 0,
itemEffectVitalPointsChangeValue = 0,
itemEffectVitalPointsChangeMinutesRemaining = 0,
abilityRarity = resolveDefaultAbilityRarity(),
abilityType = 0,
abilityBranch = 0,
abilityReset = 0,
rank = 0,
itemType = 0,
itemMultiplier = 0,
itemRemainingTime = 0,
otp0 = "",
otp1 = "",
minorVersion = 0,
majorVersion = 0
)
)
val now = System.currentTimeMillis()
database.dexDao().insertCharacter(slotId, importedCard.id, now)
for (transformation in character.transformationHistoryList) {
val transformationCard = resolveCard(transformation.cardName, character.cardId)
if (transformationCard != null) {
database.userCharacterDao().insertTransformation(
userCharacterId,
transformation.slotId,
transformationCard.id,
now
)
database.dexDao().insertCharacter(transformation.slotId, transformationCard.id, now)
}
}
for ((cardName, maxAdventureCompleted) in character.maxAdventureCompletedByCardMap) {
val adventureCard = resolveCard(cardName, null) ?: continue
val currentStage = (maxAdventureCompleted + 1).coerceAtLeast(0)
database.cardProgressDao().updateCardProgress(
currentStage = currentStage,
cardId = adventureCard.id,
unlocked = currentStage > adventureCard.stageCount
)
}
return ImportResult(
success = true,
message = "Imported ${importedCard.name} slot $slotId from VitalWear."
)
}
private fun resolveCard(character: Character) = resolveCard(character.cardName, character.cardId)
private fun resolveCard(cardName: String?, cardId: Int?): com.github.nacabaro.vbhelper.domain.card.Card? {
if (!cardName.isNullOrBlank()) {
database.cardDao().getCardByName(cardName)?.let { return it }
}
if (cardId != null) {
val matches = database.cardDao().getCardByCardId(cardId)
if (matches.size == 1) {
return matches.first()
}
}
return null
}
private fun secondsToMinutes(seconds: Long): Int {
if (seconds <= 0L) {
return 0
}
return (seconds / 60L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
}
private fun resolveInjuryStatus(injured: Boolean): NfcCharacter.InjuryStatus {
val statuses = enumValues<NfcCharacter.InjuryStatus>()
if (!injured) {
return statuses.firstOrNull {
val normalized = it.name.lowercase()
normalized.contains("none") || normalized.contains("normal") || normalized.contains("healthy")
} ?: statuses.first()
}
return statuses.firstOrNull {
val normalized = it.name.lowercase()
normalized.contains("inj")
} ?: statuses.last()
}
private fun resolveDefaultAbilityRarity(): NfcCharacter.AbilityRarity {
return enumValues<NfcCharacter.AbilityRarity>().first()
}
}

View File

@ -1,6 +1,14 @@
package com.github.nacabaro.vbhelper.utils package com.github.nacabaro.vbhelper.utils
enum class DeviceType { enum class DeviceType {
VBDevice, VBDevice, // Bandai Vital Bracelet
BEDevice BEDevice, // Bandai BE Bracelet
VitalWear // WearOS VitalWear app
}
fun detectDeviceType(deviceName: String): DeviceType = when {
deviceName.startsWith("VitalWear", ignoreCase = true) || deviceName.startsWith("VW-", ignoreCase = true) -> DeviceType.VitalWear
deviceName.startsWith("BE-", ignoreCase = true) -> DeviceType.BEDevice
deviceName.startsWith("VB-", ignoreCase = true) -> DeviceType.VBDevice
else -> DeviceType.VBDevice // Default/fallback, can be refined
} }

View File

@ -0,0 +1,51 @@
syntax = "proto3";
option java_package = "com.github.cfogrady.vitalwear.protos";
option java_multiple_files = true;
message Character {
string card_name = 1;
int32 card_id = 2;
message CharacterStats {
int32 slot_id = 1;
int32 vitals = 2;
int64 training_time_remaining_in_seconds = 3;
int64 time_until_next_transformation = 4;
int32 trained_bp = 5;
int32 trained_hp = 6;
int32 trained_ap = 7;
int32 trained_pp = 8;
bool injured = 9;
int32 accumulated_daily_injuries = 10;
int32 total_battles = 11;
int32 current_phase_battles = 12;
int32 total_wins = 13;
int32 current_phase_wins = 14;
int32 mood = 15;
}
CharacterStats character_stats = 3;
message Settings {
bool training_in_background = 1;
enum AllowedBattles {
CARD_ONLY = 0;
ALL_FRANCHISE = 1;
ALL_FRANCHISE_AND_DIM = 2;
ALL = 3;
}
AllowedBattles allowed_battles = 2;
optional int32 assumed_franchise = 3;
}
Settings settings = 4;
message TransformationEvent {
string card_name = 1;
int32 phase = 2;
int32 slot_id = 3;
}
repeated TransformationEvent transformation_history = 5;
map<string, int32> max_adventure_completed_by_card = 6;
}

View File

@ -48,8 +48,8 @@
</string> </string>
<string name="scan_title">Scan a Vital Bracelet</string> <string name="scan_title">Scan a Vital Bracelet</string>
<string name="scan_vb_to_app">Vital Bracelet to App</string> <string name="scan_vb_to_app">Vital Bracelet to App (Receive Digimon from Bandai Toys: Vital Bracelet, Vital Hero, BE Bracelet)</string>
<string name="scan_app_to_vb">App to Vital Bracelet</string> <string name="scan_app_to_vb">App to Vital Bracelet (Send Digimon from VBHelper to Bandai Toys: Vital Bracelet, Vital Hero, BE Bracelet)</string>
<string name="scan_no_nfc_on_device">No NFC on device!</string> <string name="scan_no_nfc_on_device">No NFC on device!</string>
<string name="scan_tag_not_vb">Tag detected is not VB</string> <string name="scan_tag_not_vb">Tag detected is not VB</string>
@ -199,6 +199,7 @@
<string name="storage_character_image_description">Character image</string> <string name="storage_character_image_description">Character image</string>
<string name="storage_send_to_watch">Send to watch</string> <string name="storage_send_to_watch">Send to watch</string>
<string name="storage_send_to_vitalwear">Send to VitalWear</string>
<string name="storage_set_active">Set active</string> <string name="storage_set_active">Set active</string>
<string name="storage_send_on_adventure">Send on adventure</string> <string name="storage_send_on_adventure">Send on adventure</string>
<string name="storage_delete_character">Delete character</string> <string name="storage_delete_character">Delete character</string>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path
name="exports"
path="exports" />
</paths>