mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-07-30 00:31:54 +00:00
Update: changes and new files from local development
This commit is contained in:
parent
3f29d725ea
commit
5273bae7b6
1782
app/lint-baseline.xml
Normal file
1782
app/lint-baseline.xml
Normal file
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,15 @@
|
||||
android:theme="@style/Theme.VBHelper"
|
||||
android:usesCleartextTraffic="true"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -45,7 +54,12 @@
|
||||
<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" />
|
||||
</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>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
package com.github.nacabaro.vbhelper
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.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.di.VBHelper
|
||||
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.spriteViewer.SpriteViewerControllerImpl
|
||||
import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl
|
||||
import com.github.nacabaro.vbhelper.source.VitalWearCharacterImporter
|
||||
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@ -70,6 +78,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
Log.i("MainActivity", "Activity onCreated")
|
||||
handleImportIntent(intent)
|
||||
}
|
||||
|
||||
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
|
||||
private fun MainApplication(
|
||||
scanScreenController: ScanScreenControllerImpl,
|
||||
@ -112,4 +173,8 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VITALWEAR_CHARACTER_MIME = "application/x-vitalwear-character"
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,9 @@ interface CardDao {
|
||||
@Query("SELECT * FROM Card WHERE id = :id")
|
||||
fun getCardById(id: Long): Card?
|
||||
|
||||
@Query("SELECT * FROM Card WHERE LOWER(name) = LOWER(:name) LIMIT 1")
|
||||
fun getCardByName(name: String): Card?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT ca.*
|
||||
@ -30,6 +33,18 @@ interface CardDao {
|
||||
)
|
||||
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")
|
||||
suspend fun renameCard(id: Int, newName: String)
|
||||
|
||||
@ -46,4 +61,4 @@ interface CardDao {
|
||||
WHERE cc.id = :charaId
|
||||
""")
|
||||
fun getCardIconByCharaId(charaId: Long): Flow<CardDtos.CardIcon>
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ interface CardProgressDao {
|
||||
)
|
||||
fun getCardProgress(cardId: Long): Flow<Int>
|
||||
|
||||
@Query("SELECT currentStage FROM CardProgress WHERE cardId = :cardId LIMIT 1")
|
||||
fun getCardProgressSync(cardId: Long): Int?
|
||||
|
||||
@Insert
|
||||
fun insertCardProgress(cardProgress: CardProgress)
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,6 +55,22 @@ interface UserCharacterDao {
|
||||
)
|
||||
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(
|
||||
"""
|
||||
SELECT
|
||||
@ -234,4 +250,4 @@ interface UserCharacterDao {
|
||||
"""
|
||||
)
|
||||
suspend fun getVBDimCharacters(): List<CharacterDtos.CharacterWithSprites>
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,13 @@ object CharacterDtos {
|
||||
val transformationDate: Long
|
||||
)
|
||||
|
||||
data class TransformationHistoryExport(
|
||||
val stageId: Long,
|
||||
val monIndex: Int,
|
||||
val cardName: String,
|
||||
val transformationDate: Long
|
||||
)
|
||||
|
||||
data class CardCharaProgress(
|
||||
val id: Long,
|
||||
val spriteIdle: ByteArray,
|
||||
@ -125,4 +132,4 @@ object CharacterDtos {
|
||||
val discoveredOn: Long?,
|
||||
val fusionAttribute: NfcCharacter.Attribute
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.github.nacabaro.vbhelper.screens.homeScreens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.dtos.CardDtos
|
||||
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 kotlinx.coroutines.flow.flowOf
|
||||
import kotlin.collections.emptyList
|
||||
@ -136,42 +139,66 @@ fun HomeScreen(
|
||||
Text(text = stringResource(R.string.adventure_empty_state))
|
||||
}
|
||||
} else {
|
||||
val cardIcon = BitmapData(
|
||||
bitmap = cardIconData!!.cardIcon,
|
||||
width = cardIconData!!.cardIconWidth,
|
||||
height = cardIconData!!.cardIconHeight
|
||||
)
|
||||
Column(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) {
|
||||
val cardIcon = BitmapData(
|
||||
bitmap = cardIconData!!.cardIcon,
|
||||
width = cardIconData!!.cardIconWidth,
|
||||
height = cardIconData!!.cardIconHeight
|
||||
)
|
||||
|
||||
if (activeMon!!.isBemCard && beData != null) {
|
||||
BEBEmHomeScreen(
|
||||
activeMon = activeMon!!,
|
||||
beData = beData!!,
|
||||
transformationHistory = transformationHistory,
|
||||
contentPadding = contentPadding,
|
||||
cardIcon = cardIcon
|
||||
)
|
||||
} else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) {
|
||||
BEDiMHomeScreen(
|
||||
activeMon = activeMon!!,
|
||||
beData = beData!!,
|
||||
transformationHistory = transformationHistory,
|
||||
contentPadding = contentPadding,
|
||||
cardIcon = cardIcon
|
||||
)
|
||||
} else if (vbData != null) {
|
||||
VBDiMHomeScreen(
|
||||
activeMon = activeMon!!,
|
||||
vbData = vbData!!,
|
||||
transformationHistory = transformationHistory,
|
||||
contentPadding = contentPadding,
|
||||
specialMissions = vbSpecialMissions,
|
||||
homeScreenController = homeScreenController,
|
||||
onClickCollect = { item, currency ->
|
||||
collectedItem = item
|
||||
collectedCurrency = currency
|
||||
if (activeMon!!.isBemCard && beData != null) {
|
||||
BEBEmHomeScreen(
|
||||
activeMon = activeMon!!,
|
||||
beData = beData!!,
|
||||
transformationHistory = transformationHistory,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
cardIcon = cardIcon
|
||||
)
|
||||
} else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) {
|
||||
BEDiMHomeScreen(
|
||||
activeMon = activeMon!!,
|
||||
beData = beData!!,
|
||||
transformationHistory = transformationHistory,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
cardIcon = cardIcon
|
||||
)
|
||||
} else if (vbData != null) {
|
||||
VBDiMHomeScreen(
|
||||
activeMon = activeMon!!,
|
||||
vbData = vbData!!,
|
||||
transformationHistory = transformationHistory,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
specialMissions = vbSpecialMissions,
|
||||
homeScreenController = homeScreenController,
|
||||
onClickCollect = { item, currency ->
|
||||
collectedItem = item
|
||||
collectedCurrency = currency
|
||||
},
|
||||
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()
|
||||
}
|
||||
},
|
||||
cardIcon = cardIcon
|
||||
)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(text = "Send to VitalWear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -221,5 +248,3 @@ fun HomeScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -42,6 +42,11 @@ fun ChooseConnectOption(
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
Text(
|
||||
text = "Bandai Toys = Vital Bracelet, Vital Hero, BE Bracelet",
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
ScanButton(
|
||||
text = stringResource(R.string.scan_vb_to_app),
|
||||
disabled = onClickRead == null,
|
||||
|
||||
@ -43,6 +43,7 @@ fun StorageDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onClickDelete: () -> Unit,
|
||||
onSendToBracelet: () -> Unit,
|
||||
onSendToVitalWear: () -> Unit,
|
||||
onClickSetActive: () -> Unit,
|
||||
onClickSendToAdventure: (time: Long) -> Unit
|
||||
) {
|
||||
@ -136,6 +137,13 @@ fun StorageDialog(
|
||||
Text(text = stringResource(R.string.storage_set_active))
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onSendToVitalWear,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(R.string.storage_send_to_vitalwear))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
onSendToAdventureClicked = true
|
||||
@ -171,4 +179,4 @@ fun StorageDialog(
|
||||
onDismissRequest = { onSendToAdventureClicked = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ import com.github.nacabaro.vbhelper.di.VBHelper
|
||||
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.source.VitalWearCharacterExporter
|
||||
import com.github.nacabaro.vbhelper.utils.BitmapData
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 ->
|
||||
adventureScreenController
|
||||
.sendCharacterToAdventure(
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,14 @@
|
||||
package com.github.nacabaro.vbhelper.utils
|
||||
|
||||
enum class DeviceType {
|
||||
VBDevice,
|
||||
BEDevice
|
||||
VBDevice, // Bandai Vital Bracelet
|
||||
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
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -48,8 +48,8 @@
|
||||
</string>
|
||||
|
||||
<string name="scan_title">Scan a Vital Bracelet</string>
|
||||
<string name="scan_vb_to_app">Vital Bracelet to App</string>
|
||||
<string name="scan_app_to_vb">App to Vital Bracelet</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 (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_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_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_send_on_adventure">Send on adventure</string>
|
||||
<string name="storage_delete_character">Delete character</string>
|
||||
@ -226,4 +227,4 @@
|
||||
<string name="home_special_mission_delete_dismiss">Dismiss</string>
|
||||
<string name="home_special_mission_delete_remove">Remove</string>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
6
app/src/main/res/xml/provider_paths.xml
Normal file
6
app/src/main/res/xml/provider_paths.xml
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user