Compare commits

..
Author SHA1 Message Date
Nacho fd76c46f39 Not much
- Imported card backgrounds (not necessary but future proofing just in case)
- Readded the sprite viewer just to verify everything is working as intended, will probably get rid of it again? or maybe I should keep it since it holds a copy of all the sprites in the card...
- TODO: Remove old unused tables
2025-09-14 02:39:52 +02:00
Nacho e85e1f45bf More name changes 2025-09-14 01:09:33 +02:00
Nacho 630cccefe4 Increased AGP version and changed some terminology 2025-09-14 00:49:17 +02:00
113 changed files with 1411 additions and 8775 deletions
-7
View File
@@ -10,10 +10,3 @@ app/src/main/res/values/keys.xml
app/src/test/resources/com/github/nacabaro/vbhelper/source/com.bandai.vitalbraceletarena.apk
app/src/test/resources/com/github/nacabaro/vbhelper/source/classes.dex
app/src/main/java/com/github/nacabaro/vbhelper/battle/Battle_Sprites_Reference/
app/src/main/assets/battle_sprites
app/src/main/assets/extracted_audio
API-ACR122USAM-2.01.pdf
+35 -66
View File
@@ -1,69 +1,38 @@
# VBHelper
Application to interact with the Vital series, VB, VH, VBC and VBBE.
## Developer Setup
## Current state of the project
Right now the project is still under development, and until further notice, any database updates will result in having to erase application data.
This document will be updated once the application does not need any more database resets.
## Features
As of now, the project allows you to read characters, view characters stats, and send them back to your watch.
You can also apply items to the characters read, such as special missions, or change timers, and store characters in the storage section.
You also earn new items every time an item such as a special mission (VB only) or a character completes an in-app adventure mission.
App also comes with a dex that will update every time a new character is added, and allows you to see evolution requirements and current adventure stage in the watch.
## How to set up
1. Download the latest version for VB Arena APK from a trustworthy source. If your download is a standalone APK, continue to step 2. Otherwise, if your download is an XAPK, do the following:
1. Using your phone file manager, rename the XAPK file to ZIP, and extract its contents. You can also do this with any other device, such as Windows, macOS and Linux.
2. Once the files are extracted, look for an APK called `com.bandai.vitalbraceletarena.apk`. Copy it somewhere else, you will need it.
2. Install an APK release for VB Helper. You will find the releases [here](http://github.com/nacabaro/vbhelper/releases). Download the latest release and install its APK.
Note, in the current stage of the project, you will have to delete the old application from your device. If the app keeps crashing after installing, clear application data and storage.
3. Import secrets in the app. These secrets will allow the app to talk to the watch. On the main screen, click on the gear icon, then `Import secrets`.
You will be prompted to choose a file. Choose the APK file that was previously obtained.
4. Import cards. Due to copyright laws, we cannot offer the characters and sprites themselves in the application. In order to import the cards do the following.
1. Using your own DiM/BEm cards, dump the cards to your device. You can get an in-depth tutorial in [here](http://mrblinky.net/digimon/vb/dimcardtool/dimcardtool.html). You can download the dump tool from [here](http://mrblinky.net/digimon/vb/dimcardtool/)
2. Once installed the tool and drivers, open the tool, connect your DiM/BEm reader hardware to yout computer and click on Read card.
3. Transfer the resulting file to your mobile device. You can put them anywhere, as long as they are accessible. My recommendation is to put them under a folder called `Cards` in your `Internal storage` or `SD Card`
4. In the app, click on import card. Next choose the BIN file corresponding to the card you want to import.
**Note: if you do not import the card, whenever you attempt to read a character from th watch, the character you read will get deleted.**
5. App will now be ready to be used.
## Planned features
- Online battles, undegoing development by `lightheel`.
- VitalWear compatibility, undergoing development by `cfogrady`.
- Support for multiple languages, not yet started.
- Database backup/restore.
## Credits
- `cyanic` for helping us understand more about the VB connection protocol.
- `cfogrady` for making both [`VB-DIM-Reader`](https://github.com/cfogrady/VB-DIM-Reader) and [`lib-vb-nfc`](https://github.com/cfogrady/lib-vb-nfc)
- `lightheel` for working on the online component in the application, both server and battle client.
- `shvstrz` for the app icon.
1. Clone vb-nfc-reader (https://github.com/cfogrady/lib-vb-nfc)
2. Run vb-nfc-reader/publishToMavenLocal gradle task in the lib-vb-nfc project.
3. Clone vb-dim-reader (https://github.com/cfogrady/vb-dim-reader)
4. Run publishToMavenLocal gradle task in the vb-dim-reader project.
5. Create res/values/keys.xml within the app module.
6. Populate with:
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="password1">beHmacKey1</string>
<string name="password2">beHmacKey2</string>
<string name="decryptionKey">aesKey</string>
<integer-array name="substitutionArray">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
</integer-array>
</resources>
```
7. Replace the values in the keys.xml file with those extracted from the original app.
8. Run
+10 -37
View File
@@ -1,23 +1,21 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.3.0"
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
id("com.google.protobuf")
}
android {
namespace = "com.github.nacabaro.vbhelper"
compileSdk = 36
compileSdk = 35
defaultConfig {
applicationId = "com.github.nacabaro.vbhelper"
minSdk = 28
targetSdk = 36
targetSdk = 35
versionCode = 1
versionName = "Alpha 0.6.3"
versionName = "Alpha 0.6.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -35,7 +33,9 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
@@ -45,12 +45,6 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.27.0"
@@ -74,13 +68,10 @@ dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.vb.nfc.reader)
implementation(libs.dim.reader)
ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.navigation.compose)
implementation(libs.material)
implementation(libs.protobuf.javalite)
implementation(libs.androidx.compose.material)
implementation("androidx.room:room-ktx:2.6.1")
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
@@ -89,33 +80,15 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.material.icons.extended)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
testImplementation(libs.junit)
ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation("androidx.navigation:navigation-compose:2.7.0")
implementation("com.google.android.material:material:1.2.0")
implementation(libs.protobuf.javalite)
implementation("androidx.compose.material:material")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
// HTTP request logging
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
}
-19
View File
@@ -4,11 +4,6 @@
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application
android:name=".di.VBHelper"
@@ -20,7 +15,6 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VBHelper"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
@@ -32,19 +26,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="vbhelper" android:host="auth" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<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>
</activity>
</application>
@@ -1,14 +0,0 @@
package com.github.nacabaro.vbhelper.battle
data class APIBattleCharacter(
val name: String,
val namekey: String,
val charaId: String,
val stage: Int,
val attribute: Int,
val baseHp: Int,
val currentHp: Int,
val baseBp: Float,
val baseAp: Float,
val displayName: String? = null
)
@@ -1,82 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
@Composable
fun AnimatedSpriteImage(
characterId: String,
animationType: DigimonAnimationType = DigimonAnimationType.IDLE,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
reloadMappings: Boolean = false,
animationOffset: Long = 0L // New parameter for offsetting animation timing
) {
val context = LocalContext.current
val spriteManager = remember { IndividualSpriteManager(context) }
// Calculate frame offset based on animation offset
// 750ms is the idle animation duration, so we calculate how many frames to offset
val frameOffset = if (animationOffset > 0L) {
// Convert time offset to frame offset (2 frames per cycle, 750ms per frame)
((animationOffset / 750L) * 2).toInt()
} else {
0
}
val animationStateMachine = remember { DigimonAnimationStateMachine(characterId, context, frameOffset, animationOffset) }
val coroutineScope = rememberCoroutineScope()
var bitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
// Reload mappings when reloadMappings parameter changes
LaunchedEffect(reloadMappings) {
if (reloadMappings) {
animationStateMachine.reloadMappings()
}
}
// Start the animation when the component is first created
LaunchedEffect(characterId) {
coroutineScope.launch {
animationStateMachine.playIdleAnimation()
}
}
// Change animation when animationType changes
LaunchedEffect(animationType) {
coroutineScope.launch {
if (animationType == DigimonAnimationType.IDLE) {
animationStateMachine.playIdleAnimation()
} else {
animationStateMachine.playAnimation(animationType)
}
}
}
// Update sprite when animation state changes
LaunchedEffect(animationStateMachine.currentFrameNumber) {
val frameNumber = animationStateMachine.getCurrentFrame()
bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber)
if (bitmap == null) {
println("Failed to load animated sprite frame: $frameNumber for character: $characterId")
}
}
bitmap?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Animated Sprite: $characterId - ${animationStateMachine.currentAnimation}",
modifier = modifier,
contentScale = contentScale
)
}
}
@@ -1,385 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.State
class ArenaBattleSystem {
companion object {
private const val TAG = "ArenaBattleSystem"
}
// Attack phases: 0=Idle, 1=Player attack on player screen, 2=Player attack on opponent screen,
// 3=Opponent attack on opponent screen, 4=Opponent attack on player screen
private var _attackPhase by mutableStateOf(0)
val attackPhase: Int get() = _attackPhase
private var _attackProgress by mutableStateOf(0f)
val attackProgress: Float get() = _attackProgress
private var _isPlayerAttacking by mutableStateOf(false)
val isPlayerAttacking: Boolean get() = _isPlayerAttacking
private var _attackIsHit by mutableStateOf(false)
val attackIsHit: Boolean get() = _attackIsHit
private var _isAttackButtonEnabled by mutableStateOf(true)
val isAttackButtonEnabled: Boolean get() = _isAttackButtonEnabled
private var _currentView by mutableStateOf(0)
val currentView: Int get() = _currentView
private var _playerHP by mutableStateOf(100f)
val playerHP: Float get() = _playerHP
private var _opponentHP by mutableStateOf(100f)
val opponentHP: Float get() = _opponentHP
private var _isBattleOver by mutableStateOf(false)
val isBattleOver: Boolean get() = _isBattleOver
private var _critBarProgress by mutableStateOf(0)
val critBarProgress: Int get() = _critBarProgress
// Dodge animation states
private var _isDodging by mutableStateOf(false)
val isDodging: Boolean get() = _isDodging
private var _dodgeProgress by mutableStateOf(0f)
val dodgeProgress: Float get() = _dodgeProgress
private var _dodgeDirection by mutableStateOf(1f) // 1f = up, -1f = down
val dodgeDirection: Float get() = _dodgeDirection
private var _isHit by mutableStateOf(false)
val isHit: Boolean get() = _isHit
private var _hitProgress by mutableStateOf(0f)
val hitProgress: Float get() = _hitProgress
// Separate states for player and opponent
private var _isPlayerDodging by mutableStateOf(false)
val isPlayerDodging: Boolean get() = _isPlayerDodging
private var _isOpponentDodging by mutableStateOf(false)
val isOpponentDodging: Boolean get() = _isOpponentDodging
// Separate dodge progress and direction for player and opponent
private var _playerDodgeProgress by mutableStateOf(0f)
val playerDodgeProgress: Float get() = _playerDodgeProgress
private var _playerDodgeDirection by mutableStateOf(1f)
val playerDodgeDirection: Float get() = _playerDodgeDirection
private var _opponentDodgeProgress by mutableStateOf(0f)
val opponentDodgeProgress: Float get() = _opponentDodgeProgress
private var _opponentDodgeDirection by mutableStateOf(1f)
val opponentDodgeDirection: Float get() = _opponentDodgeDirection
private var _isPlayerHit by mutableStateOf(false)
val isPlayerHit: Boolean get() = _isPlayerHit
private var _isOpponentHit by mutableStateOf(false)
val isOpponentHit: Boolean get() = _isOpponentHit
// Delayed hit states for SLEEP animation timing
private var _isPlayerHitDelayed by mutableStateOf(false)
val isPlayerHitDelayed: Boolean get() = _isPlayerHitDelayed
private var _isOpponentHitDelayed by mutableStateOf(false)
val isOpponentHitDelayed: Boolean get() = _isOpponentHitDelayed
// Delayed shake states for shake animation timing
private var _isPlayerShakeDelayed by mutableStateOf(false)
val isPlayerShakeDelayed: Boolean get() = _isPlayerShakeDelayed
private var _isOpponentShakeDelayed by mutableStateOf(false)
val isOpponentShakeDelayed: Boolean get() = _isOpponentShakeDelayed
// Counter-attack tracking
private var _shouldCounterAttack by mutableStateOf(false)
val shouldCounterAttack: Boolean get() = _shouldCounterAttack
private var _counterAttackIsHit by mutableStateOf(false)
val counterAttackIsHit: Boolean get() = _counterAttackIsHit
// Separate tracking for opponent attack result
private var _opponentAttackIsHit by mutableStateOf(false)
val opponentAttackIsHit: Boolean get() = _opponentAttackIsHit
fun startPlayerAttack() {
_attackPhase = 1
_attackProgress = 0f
_isPlayerAttacking = true
_isAttackButtonEnabled = false
_currentView = 0
}
fun startOpponentAttack() {
_attackPhase = 3
_attackProgress = 0f
_isPlayerAttacking = false
_currentView = 1
}
fun advanceAttackPhase() {
_attackPhase++
_attackProgress = 0f
}
fun setAttackProgress(progress: Float) {
_attackProgress = progress
}
fun setAttackHitState(isHit: Boolean) {
_attackIsHit = isHit
}
fun switchToView(view: Int) {
_currentView = view
}
fun enableAttackButton() {
_isAttackButtonEnabled = true
}
fun applyDamage(isPlayer: Boolean, damage: Float) {
if (isPlayer) {
_playerHP = (_playerHP - damage).coerceAtLeast(0f)
} else {
_opponentHP = (_opponentHP - damage).coerceAtLeast(0f)
}
}
fun updateHPFromAPI(playerHP: Float, opponentHP: Float) {
_playerHP = playerHP
_opponentHP = opponentHP
}
fun initializeHP(playerHP: Float, opponentHP: Float) {
_playerHP = playerHP
_opponentHP = opponentHP
}
fun completeAttackAnimation(playerDamage: Float = 0f, opponentDamage: Float = 0f) {
if (playerDamage > 0f) {
applyDamage(true, playerDamage)
}
if (opponentDamage > 0f) {
applyDamage(false, opponentDamage)
}
}
fun resetAttackState() {
_attackPhase = 0
_attackProgress = 0f
_isPlayerAttacking = false
_attackIsHit = false
_currentView = 0
_isDodging = false
_dodgeProgress = 0f
_dodgeDirection = 1f
_isHit = false
_hitProgress = 0f
_isPlayerDodging = false
_isOpponentDodging = false
_playerDodgeProgress = 0f
_playerDodgeDirection = 1f
_opponentDodgeProgress = 0f
_opponentDodgeDirection = 1f
_isPlayerHit = false
_isOpponentHit = false
_isPlayerHitDelayed = false
_isOpponentHitDelayed = false
_isPlayerShakeDelayed = false
_isOpponentShakeDelayed = false
_shouldCounterAttack = false
_counterAttackIsHit = false
_opponentAttackIsHit = false
}
fun checkBattleOver(): Boolean {
return _playerHP <= 0f || _opponentHP <= 0f
}
fun endBattle() {
_isBattleOver = true
}
fun updateCritBarProgress(progress: Int) {
_critBarProgress = progress
//Log.d(TAG, "Updated crit bar progress: $progress")
}
// Dodge animation methods
fun startDodge() {
_isDodging = true
_dodgeProgress = 0f
_dodgeDirection = 1f // Start moving up
}
fun setDodgeProgress(progress: Float) {
_dodgeProgress = progress
}
fun setDodgeDirection(direction: Float) {
_dodgeDirection = direction
}
fun endDodge() {
_isDodging = false
_dodgeProgress = 0f
}
// Hit animation methods
fun startHit() {
_isHit = true
_hitProgress = 0f
}
fun setHitProgress(progress: Float) {
_hitProgress = progress
}
fun endHit() {
_isHit = false
_hitProgress = 0f
}
// Player-specific dodge methods
fun startPlayerDodge() {
_isPlayerDodging = true
_playerDodgeProgress = 0f
_playerDodgeDirection = 1f
}
fun endPlayerDodge() {
_isPlayerDodging = false
_playerDodgeProgress = 0f
}
fun setPlayerDodgeProgress(progress: Float) {
_playerDodgeProgress = progress
}
fun setPlayerDodgeDirection(direction: Float) {
_playerDodgeDirection = direction
}
// Opponent-specific dodge methods
fun startOpponentDodge() {
_isOpponentDodging = true
_opponentDodgeProgress = 0f
_opponentDodgeDirection = 1f
}
fun endOpponentDodge() {
_isOpponentDodging = false
_opponentDodgeProgress = 0f
}
fun setOpponentDodgeProgress(progress: Float) {
_opponentDodgeProgress = progress
}
fun setOpponentDodgeDirection(direction: Float) {
_opponentDodgeDirection = direction
}
// Player-specific hit methods
fun startPlayerHit() {
_isPlayerHit = true
_hitProgress = 0f
}
fun startPlayerHitDelayed() {
_isPlayerHitDelayed = true
}
fun endPlayerHit() {
_isPlayerHit = false
_hitProgress = 0f
}
fun endPlayerHitDelayed() {
_isPlayerHitDelayed = false
}
// Opponent-specific hit methods
fun startOpponentHit() {
_isOpponentHit = true
_hitProgress = 0f
}
fun startOpponentHitDelayed() {
_isOpponentHitDelayed = true
}
fun endOpponentHit() {
_isOpponentHit = false
_hitProgress = 0f
}
fun endOpponentHitDelayed() {
_isOpponentHitDelayed = false
}
// Delayed shake methods
fun startPlayerShakeDelayed() {
_isPlayerShakeDelayed = true
}
fun endPlayerShakeDelayed() {
_isPlayerShakeDelayed = false
}
fun startOpponentShakeDelayed() {
_isOpponentShakeDelayed = true
}
fun endOpponentShakeDelayed() {
_isOpponentShakeDelayed = false
}
// Combined method to handle attack result
fun handleAttackResult(isHit: Boolean) {
_attackIsHit = isHit
if (isHit) {
// Player attack hit - opponent gets hit
startOpponentHit()
} else {
// Player attack missed - opponent dodges
startOpponentDodge()
}
}
// Method to handle opponent attack result
fun handleOpponentAttackResult(isHit: Boolean) {
_opponentAttackIsHit = isHit
if (isHit) {
// Opponent attack hit - player gets hit
startPlayerHit()
} else {
// Opponent attack missed - player dodges
startPlayerDodge()
}
}
// Counter-attack methods
fun setupCounterAttack(isHit: Boolean) {
_shouldCounterAttack = true
_counterAttackIsHit = isHit
}
fun startCounterAttack() {
_attackPhase = 3
_attackProgress = 0f
_isPlayerAttacking = false
_currentView = 1
_opponentAttackIsHit = _counterAttackIsHit
}
}
@@ -1,151 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import com.google.gson.Gson
import java.io.File
data class CharacterData(
val name: String,
val charaId: String,
val smalefilename: String,
val laugeFileName: String
)
data class CharacterDataResponse(
val name: String,
val type: String,
val source_file: String,
val collection: String,
val unity_collection_id: String,
val relative_path: String,
val all_attributes: CharacterDataAttributes
)
data class CharacterDataAttributes(
val DataList: List<String>
)
class AttackSpriteManager(private val context: Context) {
private val gson = Gson()
private val characterDataCache = mutableMapOf<String, CharacterData>()
// Get the external storage directory for attack sprites
private fun getAttackTexturesBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites/extracted_atksprites")
}
fun getAttackSprite(characterId: String, isLarge: Boolean = false): Bitmap? {
println("AttackSpriteManager: Getting attack sprite for characterId=$characterId, isLarge=$isLarge")
try {
// Get character data
val characterData = getCharacterData(characterId) ?: return null
// Determine which attack file to use
val attackFileName = if (isLarge) {
characterData.laugeFileName
} else {
characterData.smalefilename
}
// Skip if no attack file
if (attackFileName == "0") {
println("AttackSpriteManager: Skipping attack file (filename is '0')")
return null
}
// Load the attack sprite from external storage
val attackFile = File(getAttackTexturesBaseDir(), "$attackFileName.png")
return if (attackFile.exists()) {
val bitmap = BitmapFactory.decodeFile(attackFile.absolutePath)
bitmap
} else {
println("AttackSpriteManager: Attack file does not exist")
null
}
} catch (e: Exception) {
println("AttackSpriteManager: Exception occurred: ${e.message}")
e.printStackTrace()
return null
}
}
private fun getCharacterData(characterId: String): CharacterData? {
// Check cache first
if (characterDataCache.containsKey(characterId)) {
return characterDataCache[characterId]
}
try {
// Load character data from JSON file in external storage
val externalDir = android.os.Environment.getExternalStorageDirectory()
val characterDataFile = File(externalDir, "VBHelper/battle_sprites/extracted_digimon_stats/character_data/CharacterData.json")
if (!characterDataFile.exists()) {
println("AttackSpriteManager: Character data file does not exist, using default data")
// For now, return a default character data
val characterData = CharacterData(
name = characterId,
charaId = characterId,
smalefilename = "atk_s_02", // Default small attack
laugeFileName = "atk_l_04" // Default large attack
)
characterDataCache[characterId] = characterData
return characterData
}
val jsonContent = characterDataFile.readText()
// Parse the JSON response
val response = gson.fromJson(jsonContent, CharacterDataResponse::class.java)
// Search through the DataList for the matching characterId
for (characterString in response.all_attributes.DataList) {
// Extract charaId from the string format: "<UnknownObject<Character> id=0, charaId='dim000_mon03', ...>"
val charaIdMatch = Regex("charaId='([^']+)'").find(characterString)
if (charaIdMatch != null) {
val foundCharaId = charaIdMatch.groupValues[1]
if (foundCharaId == characterId) {
// Extract smalefilename and laugeFileName
val smallFileMatch = Regex("smalefilename='([^']+)'").find(characterString)
val largeFileMatch = Regex("laugeFileName='([^']+)'").find(characterString)
val smallFileName = smallFileMatch?.groupValues?.get(1) ?: "0"
val largeFileName = largeFileMatch?.groupValues?.get(1) ?: "0"
val characterData = CharacterData(
name = characterId,
charaId = characterId,
smalefilename = smallFileName,
laugeFileName = largeFileName
)
characterDataCache[characterId] = characterData
return characterData
}
}
}
// If character not found, return default data
println("AttackSpriteManager: Character not found in JSON, using default data")
val characterData = CharacterData(
name = characterId,
charaId = characterId,
smalefilename = "atk_s_02", // Default small attack
laugeFileName = "atk_l_04" // Default large attack
)
characterDataCache[characterId] = characterData
return characterData
} catch (e: Exception) {
println("AttackSpriteManager: Exception in getCharacterData: ${e.message}")
e.printStackTrace()
return null
}
}
}
@@ -1,31 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import okhttp3.Interceptor
import okhttp3.Response
/**
* OkHttp interceptor that adds Authorization header to API requests.
* Skips adding header for auth endpoints.
*/
class AuthInterceptor(private val token: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// Skip adding auth header for auth endpoints
if (originalRequest.url.encodedPath.startsWith("/api/auth")) {
return chain.proceed(originalRequest)
}
// Add authentication header for game endpoints
// Use X-Session-Token header (preferred) or Authorization: Bearer
val authenticatedRequest = originalRequest.newBuilder()
.header("X-Session-Token", token)
.build()
// Debug: Log which header is being used (first few chars of token for security)
val tokenPreview = if (token.length > 8) "${token.take(4)}...${token.takeLast(4)}" else "***"
println("AuthInterceptor: Adding X-Session-Token header (token: $tokenPreview)")
return chain.proceed(authenticatedRequest)
}
}
@@ -1,14 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface AuthService {
@POST("api/auth/validate")
fun validate(@Body request: AuthenticateRequest): Call<AuthenticateResponse>
@POST("api/auth/login")
fun login(@Body request: AuthenticateRequest): Call<AuthenticateResponse>
}
@@ -1,6 +0,0 @@
package com.github.nacabaro.vbhelper.battle
data class AuthenticateRequest(
val userToken: String
)
@@ -1,21 +0,0 @@
package com.github.nacabaro.vbhelper.battle
data class AdditionalInfo(
val avatar: String? = null,
val id: Long? = null,
val name: String? = null,
val status: String? = null
)
data class UserInfo(
val userId: String? = null,
val additionalInfo: AdditionalInfo? = null
)
data class AuthenticateResponse(
val success: Boolean,
val message: String? = null,
val userInfo: UserInfo? = null,
val sessionToken: String? = null
)
@@ -1,16 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.github.nacabaro.vbhelper.source.AuthRepository
private const val BATTLE_AUTH_PREFERENCES_NAME = "battle_auth_preferences"
val Context.battleAuthStore: androidx.datastore.core.DataStore<Preferences> by preferencesDataStore(
name = BATTLE_AUTH_PREFERENCES_NAME
)
class BattleAuthContainer(private val context: Context) {
val authRepository: AuthRepository = AuthRepository(context.battleAuthStore)
}
@@ -1,181 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import com.google.gson.Gson
import java.io.File
data class SpriteMapping(
val atlas_name: String,
val atlas_file: String,
val texture: TextureInfo,
val sprites: List<String>
)
data class TextureInfo(
val name: String,
val file: String,
val path_id: Long
)
data class SpriteData(
val name: String,
val atlas_name: String,
val m_Name: String,
val texture_rect: TextureRect
)
data class TextureRect(
val height: Float,
val width: Float,
val x: Float,
val y: Float
)
class BattleSpriteManager(private val context: Context) {
private val gson = Gson()
private val spriteCache = mutableMapOf<String, Bitmap>()
// Get the external storage directory for sprite files
private fun getSpriteBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites/extracted_assets")
}
fun loadSprite(spriteName: String, atlasName: String): Bitmap? {
val cacheKey = "${spriteName}_${atlasName}"
// Check cache first
if (spriteCache.containsKey(cacheKey)) {
return spriteCache[cacheKey]
}
// Debug: Check if base directory exists
val spriteBaseDir = getSpriteBaseDir()
if (!spriteBaseDir.exists()) {
println("Sprite base directory does not exist: ${spriteBaseDir.absolutePath}")
return null
}
println("Sprite base directory exists: ${spriteBaseDir.absolutePath}")
println("Available directories: ${spriteBaseDir.listFiles()?.map { it.name }}")
try {
// Load the PNG texture file directly using the atlas name
val textureFile = File(spriteBaseDir, "extracted_textures/${atlasName}.png")
if (!textureFile.exists()) {
println("Texture file not found: ${textureFile.absolutePath}")
return null
}
val fullBitmap = BitmapFactory.decodeFile(textureFile.absolutePath)
if (fullBitmap == null) {
println("Failed to decode texture file: ${textureFile.absolutePath}")
return null
}
// Load the specific sprite data file
val spriteDataFile = File(spriteBaseDir, "sprites/${spriteName}.json")
if (!spriteDataFile.exists()) {
println("Sprite data file not found: ${spriteDataFile.absolutePath}")
return null
}
val spriteDataJson = spriteDataFile.readText()
val spriteData = gson.fromJson(spriteDataJson, SpriteData::class.java)
// Debug: Print sprite coordinates
println("Sprite coordinates: x=${spriteData.texture_rect.x}, y=${spriteData.texture_rect.y}, width=${spriteData.texture_rect.width}, height=${spriteData.texture_rect.height}")
println("Texture dimensions: width=${fullBitmap.width}, height=${fullBitmap.height}")
// Calculate the correct Y coordinate (inverted coordinate system)
val correctedY = fullBitmap.height - spriteData.texture_rect.y.toInt() - spriteData.texture_rect.height.toInt()
// Extract the sprite from the atlas using texture_rect coordinates
val spriteBitmap = Bitmap.createBitmap(
fullBitmap,
spriteData.texture_rect.x.toInt(),
correctedY,
spriteData.texture_rect.width.toInt(),
spriteData.texture_rect.height.toInt()
)
// Ensure the bitmap is not scaled and has proper quality
val finalBitmap = if (spriteBitmap.width != spriteData.texture_rect.width.toInt() ||
spriteBitmap.height != spriteData.texture_rect.height.toInt()) {
// If the bitmap was scaled during creation, create a new one with exact dimensions
Bitmap.createScaledBitmap(spriteBitmap,
spriteData.texture_rect.width.toInt(),
spriteData.texture_rect.height.toInt(),
false) // false = no filtering/interpolation
} else {
spriteBitmap
}
println("Extracted sprite dimensions: ${finalBitmap.width}x${finalBitmap.height}")
// Cache the result
spriteCache[cacheKey] = finalBitmap
return finalBitmap
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun clearCache() {
spriteCache.clear()
}
// Helper method to get available sprites for an atlas
fun getAvailableSprites(atlasName: String): List<String> {
try {
val spritesDir = File(getSpriteBaseDir(), "sprites")
if (!spritesDir.exists()) {
return emptyList()
}
val spriteFiles = spritesDir.listFiles { file ->
file.name.startsWith("${atlasName}_sprite_") && file.name.endsWith(".json")
} ?: emptyArray()
return spriteFiles.map { file ->
// Extract sprite number from filename (e.g., "dim000_mon01_sprite_00.json" -> "00")
val spriteNumber = file.name.substringAfter("_sprite_").substringBefore(".json")
spriteNumber
}.sorted()
} catch (e: Exception) {
e.printStackTrace()
return emptyList()
}
}
// Helper method to get available atlases
fun getAvailableAtlases(): List<String> {
try {
val texturesDir = File(getSpriteBaseDir(), "extracted_textures")
if (!texturesDir.exists()) {
return emptyList()
}
val textureFiles = texturesDir.listFiles { file ->
file.name.endsWith(".png")
} ?: emptyArray()
return textureFiles.map { file ->
// Extract atlas name from filename (e.g., "dim000_mon01.png" -> "dim000_mon01")
file.name.substringBefore(".png")
}.sorted()
} catch (e: Exception) {
e.printStackTrace()
return emptyList()
}
}
}
@@ -1,167 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.delay
import android.content.Context
import java.io.File
enum class DigimonAnimationType {
IDLE,
IDLE2,
WALK,
WALK2,
RUN,
RUN2,
WORKOUT,
WORKOUT2,
HAPPY,
SLEEP,
ATTACK,
FLEE
}
data class AnimationState(
val type: DigimonAnimationType,
val frameNumber: Int, // 1-12 for individual PNG files
val duration: Long = 100L, // Duration in milliseconds
val loop: Boolean = true
)
class DigimonAnimationStateMachine(
private val characterId: String,
private val context: Context,
private val initialFrameOffset: Int = 0, // New parameter for offsetting the starting frame
private val timingOffset: Long = 0L // New parameter for offsetting the timing
) {
var currentAnimation by mutableStateOf<DigimonAnimationType>(DigimonAnimationType.IDLE)
private set
var currentFrameNumber by mutableStateOf(1)
private set
var isPlaying by mutableStateOf(false)
private set
// Direct mapping of frame numbers (1-12) to animation types
// This is based on the standard Digimon sprite frame order
private val frameToAnimationType = mapOf(
1 to DigimonAnimationType.IDLE,
2 to DigimonAnimationType.IDLE2,
3 to DigimonAnimationType.WALK,
4 to DigimonAnimationType.WALK2,
5 to DigimonAnimationType.RUN,
6 to DigimonAnimationType.RUN2,
7 to DigimonAnimationType.WORKOUT,
8 to DigimonAnimationType.WORKOUT2,
9 to DigimonAnimationType.HAPPY,
10 to DigimonAnimationType.SLEEP,
11 to DigimonAnimationType.ATTACK,
12 to DigimonAnimationType.FLEE
)
// Reverse mapping for getting frame numbers for each animation type
private val animationTypeToFrames = frameToAnimationType.entries.groupBy({ it.value }, { it.key })
// Animation durations for each type
private val animationDurations = mapOf(
DigimonAnimationType.IDLE to 750L,
DigimonAnimationType.IDLE2 to 750L,
DigimonAnimationType.WALK to 200L,
DigimonAnimationType.WALK2 to 200L,
DigimonAnimationType.RUN to 150L,
DigimonAnimationType.RUN2 to 150L,
DigimonAnimationType.WORKOUT to 300L,
DigimonAnimationType.WORKOUT2 to 300L,
DigimonAnimationType.HAPPY to 400L,
DigimonAnimationType.SLEEP to 1500L,
DigimonAnimationType.ATTACK to 650L,
DigimonAnimationType.FLEE to 150L
)
/*
init {
println("Initialized DigimonAnimationStateMachine for character: $characterId with frame offset: $initialFrameOffset, timing offset: $timingOffset")
println("Available animation types: ${animationTypeToFrames.keys}")
}
*/
suspend fun playAnimation(animationType: DigimonAnimationType) {
if (currentAnimation == animationType && isPlaying) {
return // Already playing this animation
}
currentAnimation = animationType
isPlaying = true
val frameNumbers = animationTypeToFrames[animationType] ?: listOf(1)
val duration = animationDurations[animationType] ?: 100L
// For non-looping animations like ATTACK, play once and return to IDLE
if (animationType == DigimonAnimationType.ATTACK) {
currentFrameNumber = frameNumbers.firstOrNull() ?: 1
delay(duration)
playAnimation(DigimonAnimationType.IDLE)
} else {
// For looping animations, cycle through frames
var frameIndex = 0
while (isPlaying && currentAnimation == animationType) {
val frameNumber = frameNumbers[frameIndex % frameNumbers.size]
currentFrameNumber = frameNumber
delay(duration)
frameIndex++
}
}
}
// Special method for idle animation that cycles between IDLE and IDLE2
suspend fun playIdleAnimation() {
if (currentAnimation == DigimonAnimationType.IDLE && isPlaying) {
return // Already playing idle animation
}
currentAnimation = DigimonAnimationType.IDLE
isPlaying = true
val idleFrames = animationTypeToFrames[DigimonAnimationType.IDLE] ?: listOf(1)
val idle2Frames = animationTypeToFrames[DigimonAnimationType.IDLE2] ?: listOf(2)
// Combine frames for cycling idle animation
val combinedFrames = (idleFrames + idle2Frames).distinct()
val duration = animationDurations[DigimonAnimationType.IDLE] ?: 500L
// Apply initial timing offset
if (timingOffset > 0L) {
delay(timingOffset)
}
// Cycle through idle frames, starting from the offset
var frameIndex = initialFrameOffset
while (isPlaying && currentAnimation == DigimonAnimationType.IDLE) {
val frameNumber = combinedFrames[frameIndex % combinedFrames.size]
currentFrameNumber = frameNumber
delay(duration)
frameIndex++
}
}
fun stopAnimation() {
isPlaying = false
}
fun getCurrentFrame(): Int {
return currentFrameNumber
}
fun getCurrentCharacterId(): String {
return characterId
}
// Method to reload mappings (useful for testing)
fun reloadMappings() {
println("Reloading mappings for character: $characterId")
// No need to reload since we use direct frame mapping
}
}
@@ -1,120 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun HitEffectOverlay(
isVisible: Boolean,
modifier: Modifier = Modifier,
isPlayerScreen: Boolean = false,
onAnimationComplete: () -> Unit = {}
) {
if (!isVisible) return
val context = LocalContext.current
val configuration = LocalConfiguration.current
val isLandscapeMode = configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
val hitEffectManager = remember { HitEffectSpriteManager(context) }
val coroutineScope = rememberCoroutineScope()
var currentFrame by remember { mutableStateOf(0) }
var currentSprite by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
var animationProgress by remember { mutableStateOf(0f) }
var scale by remember { mutableStateOf(0.5f) }
var alpha by remember { mutableStateOf(1f) }
LaunchedEffect(isVisible) {
if (isVisible) {
// Add delay before starting hit effect animation
delay(400) // Increased from 200ms to 400ms delay before hit effect appears
// Randomly choose between hit_01, hit_02, and hit_02_white
val hitSpriteName = when (kotlin.random.Random.nextInt(3)) {
0 -> "hit_01"
1 -> "hit_02"
else -> "hit_02_white"
}
currentSprite = hitEffectManager.loadHitSprite(hitSpriteName)
if (currentSprite != null) {
// Animate the hit effect
animationProgress = 0f
scale = 0.5f
alpha = 1f
// Scale up animation - slowed down
while (scale < 1.2f) {
scale += 0.05f // Reduced from 0.1f to 0.05f
delay(32) // Increased from 16ms to 32ms
}
// Hold for a moment - increased duration
delay(300) // Increased from 100ms to 300ms
// Fade out - slowed down
while (alpha > 0f) {
alpha -= 0.03f // Reduced from 0.05f to 0.03f
delay(32) // Increased from 16ms to 32ms
}
println("DEBUG: Hit effect animation completed")
onAnimationComplete()
} else {
println("DEBUG: Failed to load hit sprite")
onAnimationComplete()
}
}
}
currentSprite?.let { sprite ->
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
bitmap = sprite.asImageBitmap(),
contentDescription = "Hit Effect",
modifier = Modifier
.size((sprite.width * scale).dp, (sprite.height * scale).dp)
.offset(
x = if (isPlayerScreen) {
// On player screen, position further to the left
if (isLandscapeMode) {
// In landscape mode, move even further left for player screen
(-sprite.width * scale / 2 - 300).dp
} else {
// In portrait mode, use original positioning
(-sprite.width * scale / 2 - 100).dp
}
} else {
// On enemy screen, position further to the right
if (isLandscapeMode) {
// In landscape mode, move even further right for enemy screen
(-sprite.width * scale / 2 + 350).dp
} else {
// In portrait mode, use original positioning
(-sprite.width * scale / 2 + 150).dp
}
},
y = (-sprite.height * scale / 2 + 40).dp // Position lower on screen (was -60, now +40)
),
contentScale = ContentScale.Fit
)
}
}
}
@@ -1,172 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Rect
import android.os.Environment
import java.io.File
class HitEffectSpriteManager(private val context: Context) {
private val spriteCache = mutableMapOf<String, Bitmap>()
// Get the external storage directory for hit effect sprites
private fun getHitSpritesDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites/extracted_hit_sprites")
}
/**
* Load a hit sprite (hit_01.png, hit_02.png, hit_02_white.png)
* @param spriteName The sprite name (e.g., "hit_01", "hit_02", "hit_02_white")
* @return Bitmap of the hit sprite, or null if not found
*/
fun loadHitSprite(spriteName: String): Bitmap? {
val cacheKey = "hit_$spriteName"
// Check cache first
if (spriteCache.containsKey(cacheKey)) {
return spriteCache[cacheKey]
}
try {
val hitSpritesDir = getHitSpritesDir()
val spriteFile = File(hitSpritesDir, "$spriteName.png")
if (!spriteFile.exists()) {
println("Hit sprite file not found: ${spriteFile.absolutePath}")
return null
}
val bitmap = BitmapFactory.decodeFile(spriteFile.absolutePath)
if (bitmap == null) {
println("Failed to decode hit sprite file: ${spriteFile.absolutePath}")
return null
}
// Cache the result
spriteCache[cacheKey] = bitmap
return bitmap
} catch (e: Exception) {
println("Error loading hit sprite: ${e.message}")
e.printStackTrace()
return null
}
}
/**
* Load a damage effect sprite from spritesheet
* @param spritesheetName The spritesheet name (e.g., "dmg_ef1", "dmg_ef2")
* @param frameIndex The frame index (0-3 for dmg_ef1 and dmg_ef2, 0 for dmg_ef3)
* @return Bitmap of the damage effect frame, or null if not found
*/
fun loadDamageEffectSprite(spritesheetName: String, frameIndex: Int = 0): Bitmap? {
val cacheKey = "dmg_${spritesheetName}_frame_${frameIndex}"
// Check cache first
if (spriteCache.containsKey(cacheKey)) {
return spriteCache[cacheKey]
}
try {
val spritesheetFile = File(getHitSpritesDir(), "$spritesheetName.png")
if (!spritesheetFile.exists()) {
println("Damage effect spritesheet not found: ${spritesheetFile.absolutePath}")
return null
}
val spritesheet = BitmapFactory.decodeFile(spritesheetFile.absolutePath)
if (spritesheet == null) {
println("Failed to decode damage effect spritesheet: ${spritesheetFile.absolutePath}")
return null
}
// Extract frame from spritesheet
val frameBitmap = when (spritesheetName) {
"dmg_ef1", "dmg_ef2" -> {
// These are 2x2 spritesheets (4 frames)
val frameWidth = spritesheet.width / 2
val frameHeight = spritesheet.height / 2
val row = frameIndex / 2
val col = frameIndex % 2
val x = col * frameWidth
val y = row * frameHeight
Bitmap.createBitmap(spritesheet, x, y, frameWidth, frameHeight)
}
"dmg_ef3" -> {
// This is a single sprite
spritesheet
}
else -> {
println("Unknown spritesheet name: $spritesheetName")
return null
}
}
println("Successfully loaded damage effect frame: $spritesheetName frame $frameIndex (${frameBitmap.width}x${frameBitmap.height})")
// Cache the result
spriteCache[cacheKey] = frameBitmap
return frameBitmap
} catch (e: Exception) {
println("Error loading damage effect sprite: ${e.message}")
e.printStackTrace()
return null
}
}
/**
* Get all available hit sprites
* @return List of hit sprite names (without .png extension)
*/
fun getAvailableHitSprites(): List<String> {
val hitSpritesDir = getHitSpritesDir()
if (!hitSpritesDir.exists()) {
return emptyList()
}
return hitSpritesDir.listFiles { file ->
file.name.startsWith("hit_") && file.name.endsWith(".png")
}?.map { file ->
file.name.substringBefore(".png")
}?.sorted() ?: emptyList()
}
/**
* Get available damage effect spritesheet names
* @return List of available damage effect spritesheet names
*/
fun getAvailableDamageEffectSpritesheets(): List<String> {
try {
if (!getHitSpritesDir().exists()) {
return emptyList()
}
val dmgFiles = getHitSpritesDir().listFiles { file ->
file.name.startsWith("dmg_ef") && file.name.endsWith(".png")
} ?: emptyArray()
return dmgFiles.map { file ->
file.name.substringBefore(".png")
}.sorted()
} catch (e: Exception) {
println("Error getting available damage effect spritesheets: ${e.message}")
e.printStackTrace()
return emptyList()
}
}
/**
* Clear the sprite cache
*/
fun clearCache() {
spriteCache.clear()
}
}
@@ -1,132 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import java.io.File
class IndividualSpriteManager(private val context: Context) {
private val spriteCache = mutableMapOf<String, Bitmap>()
// Get the external storage directory for sprite files
private fun getSpriteBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites/extracted_assets/sprites")
}
/**
* Load a specific sprite frame for a character
* @param characterId The character ID (e.g., "dim012_mon03")
* @param frameNumber The frame number (1-12)
* @return Bitmap of the sprite frame, or null if not found
*/
fun loadSpriteFrame(characterId: String, frameNumber: Int): Bitmap? {
val cacheKey = "${characterId}_frame_${frameNumber}"
// Check cache first
if (spriteCache.containsKey(cacheKey)) {
return spriteCache[cacheKey]
}
// Debug: Check if base directory exists
val spriteBaseDir = getSpriteBaseDir()
if (!spriteBaseDir.exists()) {
println("Sprite base directory does not exist: ${spriteBaseDir.absolutePath}")
return null
}
try {
// Construct the sprite file path
val spriteFileName = "${characterId}_${String.format("%02d", frameNumber)}.png"
val spriteFile = File(spriteBaseDir, "$characterId/$spriteFileName")
if (!spriteFile.exists()) {
println("Sprite file not found: ${spriteFile.absolutePath}")
return null
}
// Load the PNG file directly
val bitmap = BitmapFactory.decodeFile(spriteFile.absolutePath)
if (bitmap == null) {
println("Failed to decode sprite file: ${spriteFile.absolutePath}")
return null
}
// Cache the result
spriteCache[cacheKey] = bitmap
return bitmap
} catch (e: Exception) {
println("Error loading sprite frame: ${e.message}")
e.printStackTrace()
return null
}
}
/**
* Get all available sprite frames for a character
* @param characterId The character ID
* @return List of frame numbers (1-12) that exist for this character
*/
fun getAvailableFrames(characterId: String): List<Int> {
val spriteBaseDir = getSpriteBaseDir()
val characterDir = File(spriteBaseDir, characterId)
if (!characterDir.exists()) {
return emptyList()
}
val spriteFiles = characterDir.listFiles { file ->
file.name.startsWith("${characterId}_") && file.name.endsWith(".png")
} ?: emptyArray()
return spriteFiles.mapNotNull { file ->
val fileName = file.name
val frameMatch = Regex("${characterId}_(\\d{2})\\.png").find(fileName)
frameMatch?.groupValues?.get(1)?.toIntOrNull()
}.sorted()
}
/**
* Get all available character IDs
* @return List of character IDs that have sprite directories
*/
fun getAvailableCharacters(): List<String> {
val spriteBaseDir = getSpriteBaseDir()
if (!spriteBaseDir.exists()) {
return emptyList()
}
return spriteBaseDir.listFiles { file ->
file.isDirectory && file.listFiles()?.any { it.name.endsWith(".png") } == true
}?.map { it.name }?.sorted() ?: emptyList()
}
/**
* Clear the sprite cache
*/
fun clearCache() {
spriteCache.clear()
}
/**
* Check if a character has sprite files
* @param characterId The character ID to check
* @return true if the character has sprite files, false otherwise
*/
fun hasCharacterSprites(characterId: String): Boolean {
val characterDir = File(getSpriteBaseDir(), characterId)
if (!characterDir.exists()) {
return false
}
val spriteFiles = characterDir.listFiles { file ->
file.name.startsWith("${characterId}_") && file.name.endsWith(".png")
} ?: emptyArray()
return spriteFiles.isNotEmpty()
}
}
@@ -1,13 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface OpponentService {
@GET("api/opponents")
// This method returns a Call object with a generic
// type of DataModel, which represents
// the data model for the response.
fun getopponents(@Query("stage") stage: String): Call<OpponentsDataModel>
}
@@ -1,5 +0,0 @@
package com.github.nacabaro.vbhelper.battle
data class OpponentsDataModel (
val opponentsList: ArrayList<APIBattleCharacter>
):java.io.Serializable
@@ -1,16 +0,0 @@
package com.github.nacabaro.vbhelper.battle
data class PVPDataModel (
val status: String,
val state: Int,
val currentRound: Int,
val playerHP: Int,
val opponentHP: Int,
val playerAttackHit: Boolean,
val playerAttackDamage: Int,
val opponentAttackDamage: Int,
val winner: String,
val opponentCharaId: String? = null, // Server provides opponent's charaId from the match
val playerMaxHP: Int? = null, // Server should provide max HP for resumed matches
val opponentMaxHP: Int? = null // Server should provide max HP for resumed matches
):java.io.Serializable
@@ -1,22 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface PVPService {
@GET("api/pvp")
// This method returns a Call object with a generic
// type of DataModel, which represents
// the data model for the response.
fun getwinner(
@Query("apiStage") apiStage: Int,
@Query("playerID") playerID: Long,
@Query("playerDigi") playerDigi: String,
@Query("playerStage") playerStage: Int,
@Query("critBar") critBar: Int,
@Query("opponentDigi") opponentDigi: String,
@Query("opponentStage") opponentStage: Int,
@Query("action") action: String? = null // Optional: "quit" or "rejoin"
): Call<PVPDataModel>
}
@@ -1,377 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import retrofit2.Retrofit
import android.widget.Toast
import retrofit2.*
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.github.nacabaro.vbhelper.battle.BattleAuthContainer
class RetrofitHelper {
/**
* Creates an OkHttpClient with authentication interceptor for game endpoints.
* Requires a non-null, non-empty token.
*/
private fun createAuthenticatedClient(token: String): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(token))
.addInterceptor(loggingInterceptor)
.build()
}
/**
* Gets the session token from AuthRepository for API calls.
* Falls back to nacatech token if session token is not available (backward compatibility).
*/
private fun getAuthToken(context: Context): String? {
return try {
val authContainer = BattleAuthContainer(context)
runBlocking {
// Prefer session token, fall back to nacatech token for backward compatibility
val sessionToken = authContainer.authRepository.sessionToken.first()
if (!sessionToken.isNullOrEmpty()) {
println("RetrofitHelper: Using sessionToken for API call")
sessionToken
} else {
// Fallback to nacatech token (slower, but works)
val nacatechToken = authContainer.authRepository.authToken.first()
if (!nacatechToken.isNullOrEmpty()) {
println("RetrofitHelper: No sessionToken found, falling back to nacatechToken")
}
nacatechToken
}
}
} catch (e: Exception) {
println("RetrofitHelper: Error getting auth token: ${e.message}")
null
}
}
/**
* Creates a Retrofit instance with authentication for game endpoints.
*/
private fun createAuthenticatedRetrofit(context: Context): Retrofit? {
val token = getAuthToken(context)
if (token.isNullOrEmpty()) {
println("RetrofitHelper: No auth token available")
return null
}
val client = createAuthenticatedClient(token)
return Retrofit.Builder()
.baseUrl("http://battle.io-void.com:8080/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
/**
* Handles HTTP error responses (401, 403, 429).
* For 401/403, clears authentication state to trigger re-authentication.
*/
private fun handleErrorResponse(context: Context, response: Response<*>, errorMessage: String) {
when (response.code()) {
401 -> {
println("RetrofitHelper: Authentication failed (401) - token may be expired")
clearAuthAndNotify(context, "Authentication failed. Please log in again.")
}
403 -> {
println("RetrofitHelper: Access forbidden (403) - token may be expired or invalid")
// 403 could mean expired token, so clear auth state to trigger re-authentication
clearAuthAndNotify(context, "Session expired. Please log in again.")
}
429 -> {
println("RetrofitHelper: Rate limit exceeded (429)")
Toast.makeText(context, "Too many requests. Please wait a moment.", Toast.LENGTH_SHORT).show()
}
else -> {
println("RetrofitHelper: API error (${response.code()}): $errorMessage")
Toast.makeText(context, "Request failed: ${response.code()}", Toast.LENGTH_SHORT).show()
}
}
}
/**
* Clears authentication state and shows a message.
* This will trigger BattlesScreen to detect the auth state change and open the login page.
*/
private fun clearAuthAndNotify(context: Context, message: String) {
try {
val authContainer = BattleAuthContainer(context)
CoroutineScope(Dispatchers.IO).launch {
authContainer.authRepository.logout()
println("RetrofitHelper: Cleared authentication state due to expired/invalid token")
}
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
} catch (e: Exception) {
println("RetrofitHelper: Error clearing auth state: ${e.message}")
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
fun getOpponents(context: Context, stage: String, callback: (OpponentsDataModel) -> Unit) {
//println("RetrofitHelper: Starting API call for stage: $stage")
try {
// Create an authenticated Retrofit instance
val retrofit = createAuthenticatedRetrofit(context)
if (retrofit == null) {
println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
return
}
// Create an ApiService instance from the Retrofit instance.
val service: OpponentService = retrofit.create<OpponentService>(OpponentService::class.java)
//println("RetrofitHelper: Service created")
// Call the getopponents() method of the ApiService
// to make an API request.
val call: Call<OpponentsDataModel> = service.getopponents(stage)
//println("RetrofitHelper: API call created, enqueueing...")
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<OpponentsDataModel> {
override fun onFailure(call: Call<OpponentsDataModel>, t: Throwable) {
println("RetrofitHelper: API call failed: ${t.message}")
t.printStackTrace()
Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<OpponentsDataModel>, response: Response<OpponentsDataModel>) {
println("RetrofitHelper: API response received - Code: ${response.code()}")
println("RetrofitHelper: Response body: ${response.body()}")
if(response.isSuccessful){
//println("RetrofitHelper: Response successful, calling callback")
val opponentsList: OpponentsDataModel = response.body() as OpponentsDataModel
callback(opponentsList)
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: Response not successful - Error: $errorBody")
handleErrorResponse(context, response, errorBody ?: "Unknown error")
}
}
})
} catch (e: Exception) {
println("RetrofitHelper: Exception in getOpponents: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
/*
fun getCombatWinner(context: Context, stage: String, callback: (CombatDataModel) -> Unit) {
// Create a Retrofit instance with the base URL and
// a GsonConverterFactory for parsing the response.
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://battle.io-void.com:8080/").addConverterFactory(
GsonConverterFactory.create()).build()
// Create an ApiService instance from the Retrofit instance.
val service: CombatService = retrofit.create<CombatService>(CombatService::class.java)
// Call the getwinner() method of the ApiService
// to make an API request.
val call: Call<CombatDataModel> = service.getwinner(stage)
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<CombatDataModel> {
// This is an anonymous inner class that implements the Callback interface.
override fun onFailure(call: Call<CombatDataModel>, t: Throwable) {
// This method is called when the API request fails.
Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<CombatDataModel>, response: Response<CombatDataModel>) {
// This method is called when the API response is received successfully.
if(response.isSuccessful){
// If the response is successful, parse the
// response body to a DataModel object.
val winner: CombatDataModel = response.body() as CombatDataModel
// Call the callback function with the DataModel
// object as a parameter.
callback(winner)
}
}
})
}
fun getBattleWinner(context: Context, playerDigi: String, playerStage: Int, opponentDigi: String, opponentStage: Int, callback: (BattleDataModel) -> Unit) {
// Create a Retrofit instance with the base URL and
// a GsonConverterFactory for parsing the response.
val retrofit: Retrofit = Retrofit.Builder().baseUrl("http://battle.io-void.com:8080/").addConverterFactory(
GsonConverterFactory.create()).build()
// Create an ApiService instance from the Retrofit instance.
val service: BattleService = retrofit.create<BattleService>(BattleService::class.java)
// Call the getwinner() method of the ApiService
// to make an API request.
val call: Call<BattleDataModel> = service.getwinner(playerDigi, playerStage, opponentDigi, opponentStage)
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<BattleDataModel> {
// This is an anonymous inner class that implements the Callback interface.
override fun onFailure(call: Call<BattleDataModel>, t: Throwable) {
// This method is called when the API request fails.
Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<BattleDataModel>, response: Response<BattleDataModel>) {
// This method is called when the API response is received successfully.
if(response.isSuccessful){
// If the response is successful, parse the
// response body to a DataModel object.
val winner: BattleDataModel = response.body() as BattleDataModel
// Call the callback function with the DataModel
// object as a parameter.
callback(winner)
}
}
})
}
*/
fun getPVPWinner(context: Context, apiStage: Int, playerID: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, callback: (PVPDataModel) -> Unit) {
getPVPWinner(context, apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, null, callback)
}
fun getPVPWinner(context: Context, apiStage: Int, playerID: Long, playerDigi: String, playerStage: Int, critBar: Int, opponentDigi: String, opponentStage: Int, action: String?, callback: (PVPDataModel) -> Unit) {
try {
// Create an authenticated Retrofit instance
val retrofit = createAuthenticatedRetrofit(context)
if (retrofit == null) {
println("RetrofitHelper: Cannot create authenticated Retrofit - no token available")
Toast.makeText(context, "Authentication required. Please log in.", Toast.LENGTH_SHORT).show()
return
}
// Create an ApiService instance from the Retrofit instance.
val service: PVPService = retrofit.create<PVPService>(PVPService::class.java)
// Call the getwinner() method of the ApiService
// to make an API request.
val call: Call<PVPDataModel> = service.getwinner(apiStage, playerID, playerDigi, playerStage, critBar, opponentDigi, opponentStage, action)
// Use the enqueue() method of the Call object to
// make an asynchronous API request.
call.enqueue(object : Callback<PVPDataModel> {
// This is an anonymous inner class that implements the Callback interface.
override fun onFailure(call: Call<PVPDataModel>, t: Throwable) {
// This method is called when the API request fails.
println("RetrofitHelper: PVP API call failed: ${t.message}")
t.printStackTrace()
Toast.makeText(context, "Request Fail", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<PVPDataModel>, response: Response<PVPDataModel>) {
// This method is called when the API response is received successfully.
println("RetrofitHelper: PVP API response received - Code: ${response.code()}")
if(response.isSuccessful){
// If the response is successful, parse the
// response body to a DataModel object.
val apiResults: PVPDataModel = response.body() as PVPDataModel
// Call the callback function with the DataModel
// object as a parameter.
callback(apiResults)
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: PVP API response not successful - Code: ${response.code()}, Error: $errorBody")
handleErrorResponse(context, response, errorBody ?: "Unknown error")
}
}
})
} catch (e: Exception) {
println("RetrofitHelper: Exception in getPVPWinner: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Request failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
fun authenticate(context: Context, token: String, callback: (AuthenticateResponse) -> Unit) {
//println("RetrofitHelper: Starting validate API call with token: $token")
if (token.isEmpty()) {
println("RetrofitHelper: ERROR - Token is empty!")
Toast.makeText(context, "Authentication failed: Token is empty", Toast.LENGTH_SHORT).show()
return
}
try {
// Add logging interceptor to see the actual HTTP request
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("http://battle.io-void.com:8080/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service: AuthService = retrofit.create<AuthService>(AuthService::class.java)
val request = AuthenticateRequest(userToken = token)
// Use login endpoint instead of validate to get sessionToken
val call: Call<AuthenticateResponse> = service.login(request)
call.enqueue(object : Callback<AuthenticateResponse> {
override fun onFailure(call: Call<AuthenticateResponse>, t: Throwable) {
println("RetrofitHelper: Validate API call failed: ${t.message}")
t.printStackTrace()
Toast.makeText(context, "Authentication failed: ${t.message}", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<AuthenticateResponse>, response: Response<AuthenticateResponse>) {
if (response.isSuccessful) {
val authResponse: AuthenticateResponse? = response.body()
if (authResponse != null) {
callback(authResponse)
} else {
println("RetrofitHelper: Validation failed: Invalid response body")
Toast.makeText(context, "Authentication failed: Invalid response", Toast.LENGTH_SHORT).show()
}
} else {
val errorBody = response.errorBody()?.string()
println("RetrofitHelper: Validate response not successful - Code: ${response.code()}, Error: $errorBody")
Toast.makeText(context, "Authentication failed: ${response.code()}", Toast.LENGTH_SHORT).show()
}
}
})
} catch (e: Exception) {
println("RetrofitHelper: Exception in validate: ${e.message}")
e.printStackTrace()
Toast.makeText(context, "Authentication failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
@@ -1,328 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.content.Context
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.FileInputStream
class SpriteFileManager(private val context: Context) {
// Get the external storage directory where files are already located
fun getExternalSpriteBaseDir(): File {
val externalDir = android.os.Environment.getExternalStorageDirectory()
return File(externalDir, "VBHelper/battle_sprites")
}
// Get the internal storage directory for sprite files
private fun getInternalSpriteBaseDir(): File {
return File(context.filesDir, "battle_sprites")
}
fun copySpriteFilesToInternalStorage() {
try {
println("Starting sprite file copy process from external storage to internal storage...")
val externalDir = getExternalSpriteBaseDir()
val internalDir = getInternalSpriteBaseDir()
// Check if external directory exists
if (!externalDir.exists()) {
println("External sprite directory does not exist: ${externalDir.absolutePath}")
return
}
println("External sprite directory exists: ${externalDir.absolutePath}")
println("Copying to internal storage: ${internalDir.absolutePath}")
// Create internal directory if it doesn't exist
if (!internalDir.exists()) {
val created = internalDir.mkdirs()
println("Created internal sprite directory: $created")
}
// Copy all subdirectories from external to internal storage
val externalFiles = externalDir.listFiles()
if (externalFiles != null) {
println("Found ${externalFiles.size} items in external directory")
externalFiles.forEach { item ->
val targetItem = File(internalDir, item.name)
if (item.isDirectory) {
println("Copying directory: ${item.name}")
copyDirectory(item, targetItem)
} else {
println("Copying file: ${item.name}")
copyFile(item, targetItem)
}
}
}
println("Sprite files copied successfully to internal storage: ${internalDir.absolutePath}")
} catch (e: Exception) {
println("Error copying sprite files to internal storage: ${e.message}")
e.printStackTrace()
}
}
fun copySpriteFilesToExternalStorage() {
try {
println("Starting sprite file copy process to external storage...")
// Debug: List what's in the assets directory
val assetManager = context.assets
val battleSpritesFiles = assetManager.list("battle_sprites")
println("battle_sprites directory in assets contains: ${battleSpritesFiles?.joinToString(", ")}")
val extractedAssetsFiles = assetManager.list("battle_sprites/extracted_assets")
println("battle_sprites/extracted_assets directory in assets contains: ${extractedAssetsFiles?.joinToString(", ")}")
// Check specifically for extracted_atksprites in assets (now directly under battle_sprites)
val atkspritesInAssets = assetManager.list("battle_sprites/extracted_atksprites")
println("extracted_atksprites in assets contains: ${atkspritesInAssets?.size ?: 0} files")
if (atkspritesInAssets != null && atkspritesInAssets.isNotEmpty()) {
println("First few attack files in assets: ${atkspritesInAssets.take(5).joinToString(", ")}")
}
// Check for extracted_battlebgs in assets (now directly under battle_sprites)
val battlebgsInAssets = assetManager.list("battle_sprites/extracted_battlebgs")
println("extracted_battlebgs in assets contains: ${battlebgsInAssets?.size ?: 0} files")
if (battlebgsInAssets != null && battlebgsInAssets.isNotEmpty()) {
println("First few battle background files in assets: ${battlebgsInAssets.take(5).joinToString(", ")}")
}
// Try to list all possible subdirectories in battle_sprites
println("Checking all possible subdirectories in battle_sprites...")
battleSpritesFiles?.forEach { subdir ->
try {
val subdirFiles = assetManager.list("battle_sprites/$subdir")
println(" $subdir contains: ${subdirFiles?.size ?: 0} files")
if (subdirFiles != null && subdirFiles.isNotEmpty()) {
println(" First few files: ${subdirFiles.take(3).joinToString(", ")}")
}
} catch (e: Exception) {
println(" Error listing $subdir: ${e.message}")
}
}
// Create the base directory for battle_sprites in external storage
val battleSpritesDir = getExternalSpriteBaseDir()
if (!battleSpritesDir.exists()) {
battleSpritesDir.mkdirs()
println("Created battle_sprites directory in external storage: ${battleSpritesDir.absolutePath}")
} else {
println("battle_sprites directory already exists in external storage: ${battleSpritesDir.absolutePath}")
}
// Copy all subdirectories from battle_sprites assets to external storage
println("Copying all battle_sprites subdirectories to external storage...")
battleSpritesFiles?.forEach { subdir ->
val sourcePath = "battle_sprites/$subdir"
val targetDir = File(battleSpritesDir, subdir)
println("Copying $sourcePath to ${targetDir.absolutePath}")
copyAssetDirectory(sourcePath, targetDir)
}
println("Sprite files copied successfully to external storage: ${battleSpritesDir.absolutePath}")
// Verify that attack sprites were copied
val atkspritesDir = File(battleSpritesDir, "extracted_atksprites")
if (atkspritesDir.exists()) {
val attackFiles = atkspritesDir.listFiles()
println("Attack sprites directory exists with ${attackFiles?.size ?: 0} files")
if (attackFiles != null && attackFiles.isNotEmpty()) {
println("First few attack files: ${attackFiles.take(5).map { it.name }}")
}
} else {
println("WARNING: extracted_atksprites directory does not exist!")
// List what's actually in the battle_sprites directory
val battleSpritesContents = battleSpritesDir.listFiles()
println("battle_sprites directory contains: ${battleSpritesContents?.map { it.name }?.joinToString(", ")}")
}
// Verify that battle backgrounds were copied
val battlebgsDir = File(battleSpritesDir, "extracted_battlebgs")
if (battlebgsDir.exists()) {
val bgFiles = battlebgsDir.listFiles()
println("Battle backgrounds directory exists with ${bgFiles?.size ?: 0} files")
if (bgFiles != null && bgFiles.isNotEmpty()) {
println("First few battle background files: ${bgFiles.take(5).map { it.name }}")
}
} else {
println("WARNING: extracted_battlebgs directory does not exist!")
}
} catch (e: Exception) {
println("Error copying sprite files to external storage: ${e.message}")
e.printStackTrace()
}
}
private fun copyAssetDirectory(assetPath: String, targetDir: File) {
try {
val assetManager = context.assets
val files = assetManager.list(assetPath) ?: return
println("Copying asset directory: $assetPath (${files.size} items)")
println("Files found: ${files.joinToString(", ")}")
for (file in files) {
val assetFilePath = if (assetPath.isEmpty()) file else "$assetPath/$file"
val targetFile = File(targetDir, file)
// Create subdirectories if needed
if (targetFile.parentFile != null && !targetFile.parentFile!!.exists()) {
targetFile.parentFile!!.mkdirs()
}
// Check if it's a directory by trying to list its contents
try {
val subFiles = assetManager.list(assetFilePath)
if (subFiles != null && subFiles.isNotEmpty()) {
// It's a directory, create it and copy contents
println("Copying subdirectory: $assetFilePath (${subFiles.size} files)")
if (!targetFile.exists()) {
targetFile.mkdirs()
}
copyAssetDirectory(assetFilePath, targetFile)
} else {
// It's a file, copy it
copyAssetFile(assetFilePath, targetFile)
}
} catch (e: Exception) {
// If we can't list contents, it's probably a file
println("Treating $assetFilePath as file (could not list contents)")
copyAssetFile(assetFilePath, targetFile)
}
}
// Special handling for extracted_atksprites - try to copy it directly if it wasn't found
if (assetPath == "battle_sprites/extracted_assets") {
println("Special handling: Checking for extracted_atksprites directory...")
try {
val atkspritesFiles = assetManager.list("battle_sprites/extracted_assets/extracted_atksprites")
if (atkspritesFiles != null && atkspritesFiles.isNotEmpty()) {
println("Found extracted_atksprites with ${atkspritesFiles.size} files")
val atkspritesDir = File(targetDir, "extracted_atksprites")
if (!atkspritesDir.exists()) {
atkspritesDir.mkdirs()
}
copyAssetDirectory("battle_sprites/extracted_assets/extracted_atksprites", atkspritesDir)
} else {
println("extracted_atksprites directory not found in assets")
}
} catch (e: Exception) {
println("Error checking extracted_atksprites: ${e.message}")
}
}
} catch (e: Exception) {
println("Error copying asset directory $assetPath: ${e.message}")
e.printStackTrace()
}
}
private fun copyDirectory(sourceDir: File, targetDir: File) {
if (!targetDir.exists()) {
targetDir.mkdirs()
}
val files = sourceDir.listFiles()
if (files != null) {
files.forEach { file ->
val targetFile = File(targetDir, file.name)
if (file.isDirectory) {
copyDirectory(file, targetFile)
} else {
copyFile(file, targetFile)
}
}
}
}
private fun copyFile(sourceFile: File, targetFile: File) {
try {
val inputStream = FileInputStream(sourceFile)
val outputStream = FileOutputStream(targetFile)
inputStream.copyTo(outputStream)
inputStream.close()
outputStream.close()
println("Copied: ${sourceFile.name} -> ${targetFile.absolutePath}")
} catch (e: IOException) {
println("Error copying file ${sourceFile.name}: ${e.message}")
}
}
private fun copyAssetFile(assetPath: String, targetFile: File) {
try {
val inputStream = context.assets.open(assetPath)
val outputStream = FileOutputStream(targetFile)
inputStream.copyTo(outputStream)
inputStream.close()
outputStream.close()
println("Copied: $assetPath -> ${targetFile.absolutePath}")
} catch (e: IOException) {
println("Error copying asset file $assetPath: ${e.message}")
}
}
fun checkSpriteFilesExist(): Boolean {
val battleSpritesDir = getExternalSpriteBaseDir()
val extractedAssetsDir = File(battleSpritesDir, "extracted_assets")
val extractedStatsDir = File(battleSpritesDir, "extracted_digimon_stats")
val atkspritesDir = File(battleSpritesDir, "extracted_atksprites")
val battlebgsDir = File(battleSpritesDir, "extracted_battlebgs")
val battleSpritesExist = battleSpritesDir.exists() && battleSpritesDir.listFiles()?.isNotEmpty() == true
val assetsExist = extractedAssetsDir.exists() && extractedAssetsDir.listFiles()?.isNotEmpty() == true
val statsExist = extractedStatsDir.exists() && extractedStatsDir.listFiles()?.isNotEmpty() == true
val atkspritesExist = atkspritesDir.exists() && atkspritesDir.listFiles()?.isNotEmpty() == true
val battlebgsExist = battlebgsDir.exists() && battlebgsDir.listFiles()?.isNotEmpty() == true
/*
println("Checking sprite files exist in external storage:")
println(" battle_sprites exists: $battleSpritesExist")
println(" extracted_assets exists: $assetsExist")
println(" extracted_digimon_stats exists: $statsExist")
println(" extracted_atksprites exists: $atkspritesExist")
println(" extracted_battlebgs exists: $battlebgsExist")
*/
return battleSpritesExist && assetsExist && statsExist && atkspritesExist && battlebgsExist
}
fun clearSpriteFiles() {
try {
val battleSpritesDir = getInternalSpriteBaseDir()
if (battleSpritesDir.exists()) {
deleteDirectory(battleSpritesDir)
println("Cleared battle_sprites directory from internal storage")
}
} catch (e: Exception) {
println("Error clearing sprite files: ${e.message}")
e.printStackTrace()
}
}
private fun deleteDirectory(directory: File) {
if (directory.exists()) {
val files = directory.listFiles()
if (files != null) {
for (file in files) {
if (file.isDirectory) {
deleteDirectory(file)
} else {
file.delete()
}
}
}
directory.delete()
}
}
}
@@ -1,38 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@Composable
fun SpriteImage(
characterId: String,
frameNumber: Int,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit
) {
val context = LocalContext.current
val spriteManager = remember { IndividualSpriteManager(context) }
var bitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
LaunchedEffect(characterId, frameNumber) {
println("Loading sprite frame: $frameNumber for character: $characterId")
bitmap = spriteManager.loadSpriteFrame(characterId, frameNumber)
if (bitmap == null) {
println("Failed to load sprite frame: $frameNumber for character: $characterId")
}
}
bitmap?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Sprite: $characterId frame $frameNumber",
modifier = modifier,
contentScale = contentScale
)
}
}
@@ -1,45 +0,0 @@
package com.github.nacabaro.vbhelper.battle
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import com.github.nacabaro.vbhelper.battle.AttackSpriteManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun AttackSpriteImage(
characterId: String,
isLarge: Boolean = false,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit
) {
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
LaunchedEffect(characterId, isLarge) {
//println("AttackSpriteImage: Loading attack sprite for characterId=$characterId, isLarge=$isLarge")
coroutineScope.launch {
val attackSpriteManager = AttackSpriteManager(context)
val loadedBitmap = withContext(Dispatchers.IO) {
attackSpriteManager.getAttackSprite(characterId, isLarge)
}
bitmap = loadedBitmap
}
}
bitmap?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Attack Sprite",
modifier = modifier,
contentScale = contentScale
)
}
}
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -36,27 +35,20 @@ import com.github.cfogrady.vbnfc.vb.SpecialMission
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
import com.github.nacabaro.vbhelper.utils.getObscuredBitmap
import androidx.compose.ui.res.stringResource
@Composable
fun CharacterEntry(
icon: BitmapData,
modifier: Modifier = Modifier,
cardIcon: BitmapData? = null,
obscure: Boolean = false,
disabled: Boolean = false,
shape: Shape = MaterialTheme.shapes.medium,
multiplier: Int = 4,
cardColors: CardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
),
onClick: () -> Unit = { }
) {
val bitmap = remember (icon.bitmap) {
if(obscure) icon.getObscuredBitmap() else icon.getBitmap()
}
val iconSizeMultiplier = 3
val imageBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val density: Float = LocalContext.current.resources.displayMetrics.density
val dpSize = (icon.width * multiplier / density).dp
@@ -69,8 +61,7 @@ fun CharacterEntry(
},
modifier = modifier
.aspectRatio(1f)
.padding(8.dp),
colors = cardColors
.padding(8.dp)
) {
Box(
contentAlignment = Alignment.BottomCenter,
@@ -88,24 +79,7 @@ fun CharacterEntry(
},
modifier = Modifier
.size(dpSize)
.align(Alignment.BottomCenter)
)
if (cardIcon != null) {
val bitmap = remember (icon.bitmap) { cardIcon.getBitmap() }
val iconBitmap = remember(bitmap) { bitmap.asImageBitmap() }
val dpSize = (icon.width * iconSizeMultiplier /density).dp
Image(
bitmap = iconBitmap,
contentDescription = "Card icon",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
.align(Alignment.BottomEnd)
.padding(8.dp)
)
}
}
}
}
@@ -152,27 +126,14 @@ fun ItemDisplay(
fun SpecialMissionsEntry(
specialMission: SpecialMissions,
modifier: Modifier = Modifier,
onClickMission: (Long) -> Unit = { },
onClickCollect: (Long) -> Unit = { }
onClickCard: () -> Unit = { },
) {
val textValue = when (specialMission.missionType) {
SpecialMission.Type.NONE -> stringResource(R.string.special_mission_none)
SpecialMission.Type.STEPS -> stringResource(
R.string.special_mission_steps,
specialMission.goal
)
SpecialMission.Type.BATTLES -> stringResource(
R.string.special_mission_battles,
specialMission.goal
)
SpecialMission.Type.WINS -> stringResource(
R.string.special_mission_wins,
specialMission.goal
)
SpecialMission.Type.VITALS -> stringResource(
R.string.special_mission_vitals,
specialMission.goal
)
SpecialMission.Type.NONE -> "No mission selected"
SpecialMission.Type.STEPS -> "Walk ${specialMission.goal} steps"
SpecialMission.Type.BATTLES -> "Battle ${specialMission.goal} times"
SpecialMission.Type.WINS -> "Win ${specialMission.goal} battles"
SpecialMission.Type.VITALS -> "Earn ${specialMission.goal} vitals"
}
val progress = if (specialMission.status == SpecialMission.Status.COMPLETED) {
@@ -183,22 +144,10 @@ fun SpecialMissionsEntry(
val completion = when (specialMission.missionType) {
SpecialMission.Type.NONE -> ""
SpecialMission.Type.STEPS -> stringResource(
R.string.special_mission_steps_progress,
progress
)
SpecialMission.Type.BATTLES -> stringResource(
R.string.special_mission_battles_progress,
progress
)
SpecialMission.Type.WINS -> stringResource(
R.string.special_mission_wins_progress,
progress
)
SpecialMission.Type.VITALS -> stringResource(
R.string.special_mission_vitals_progress,
progress
)
SpecialMission.Type.STEPS -> "Walked $progress steps"
SpecialMission.Type.BATTLES -> "Battled $progress times"
SpecialMission.Type.WINS -> "Won $progress battles"
SpecialMission.Type.VITALS -> "Earned $progress vitals"
}
val icon = when (specialMission.missionType) {
@@ -220,12 +169,10 @@ fun SpecialMissionsEntry(
Card(
modifier = modifier,
shape = androidx.compose.material.MaterialTheme.shapes.small,
onClick = if (specialMission.status == SpecialMission.Status.COMPLETED) {
{ onClickCollect(specialMission.id) }
} else if (specialMission.status == SpecialMission.Status.UNAVAILABLE) {
{ }
onClick = if (specialMission.status == SpecialMission.Status.COMPLETED || specialMission.status == SpecialMission.Status.FAILED) {
onClickCard
} else {
{ onClickMission(specialMission.id) }
{ }
},
colors = CardDefaults.cardColors(
containerColor = color
@@ -241,7 +188,7 @@ fun SpecialMissionsEntry(
) {
Icon(
painter = painterResource(icon),
contentDescription = stringResource(R.string.special_mission_icon_content_description),
contentDescription = "Vitals",
modifier = Modifier
.fillMaxHeight()
.padding(16.dp)
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
@@ -38,7 +37,7 @@ interface AdventureDao {
JOIN Adventure a ON uc.id = a.characterId
"""
)
fun getAdventureCharacters(): Flow<List<CharacterDtos.AdventureCharacterWithSprites>>
suspend fun getAdventureCharacters(): List<CharacterDtos.AdventureCharacterWithSprites>
@Query("""
DELETE FROM Adventure
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.CardDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface CardAdventureDao {
@@ -53,7 +52,7 @@ interface CardAdventureDao {
WHERE
cc.cardId = :cardId
""")
fun getAdventureForCard(
suspend fun getAdventureForCard(
cardId: Long
): Flow<List<CardDtos.CardAdventureWithSprites>>
): List<CardDtos.CardAdventureWithSprites>
}
@@ -0,0 +1,24 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.card.CardBackground
@Dao
interface CardBackgroundDao {
@Insert
suspend fun insertCardBackground(vararg cardBackground: CardBackground)
@Query("""
SELECT * FROM CardBackground
WHERE cardId = :cardId
""")
suspend fun getCardBackground(cardId: Long): CardBackground
@Query("""
SELECT * FROM CardBackground
""")
suspend fun getBackgrounds(): List<CardBackground>
}
@@ -5,8 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.dtos.CardDtos
import kotlinx.coroutines.flow.Flow
import com.github.nacabaro.vbhelper.domain.card.CardSprites
@Dao
interface CardDao {
@@ -28,7 +27,7 @@ interface CardDao {
WHERE uc.id = :id
"""
)
fun getCardByCharacterId(id: Long): Flow<Card>
suspend fun getCardByCharacterId(id: Long): Card
@Query("UPDATE Card SET name = :newName WHERE id = :id")
suspend fun renameCard(id: Int, newName: String)
@@ -36,14 +35,10 @@ interface CardDao {
@Query("DELETE FROM Card WHERE id = :id")
suspend fun deleteCard(id: Long)
@Query("""
SELECT
c.logo as cardIcon,
c.logoWidth as cardIconWidth,
c.logoHeight as cardIconHeight
FROM Card c
JOIN CardCharacter cc ON cc.cardId = c.id
WHERE cc.id = :charaId
""")
fun getCardIconByCharaId(charaId: Long): Flow<CardDtos.CardIcon>
@Insert
suspend fun insertSprites(vararg sprites: CardSprites)
@Query("SELECT * FROM CardSprites")
suspend fun getSprites(): List<CardSprites>
}
@@ -2,47 +2,33 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface CardFusionsDao {
@Query("""
@Query(
"""
INSERT INTO
CardFusions (
fromCharaId,
attribute,
toCharaId
toVirusFusion,
toDataFusion,
toVaccineFusion,
toFreeFusion
)
SELECT
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :fromCharaId),
:attribute,
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaId)
""")
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdVirus),
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdData),
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdVaccine),
(SELECT id FROM CardCharacter WHERE cardId = :cardId AND charaIndex = :toCharaIdFree)
"""
)
suspend fun insertNewFusion(
cardId: Long,
fromCharaId: Int,
attribute: NfcCharacter.Attribute,
toCharaId: Int
toCharaIdVirus: Int,
toCharaIdData: Int,
toCharaIdVaccine: Int,
toCharaIdFree: Int
)
@Query("""
SELECT
cf.toCharaId as charaId,
cf.fromCharaId as fromCharaId,
s.spriteIdle1 as spriteIdle,
cc.attribute as attribute,
s.width as spriteWidth,
s.height as spriteHeight,
d.discoveredOn as discoveredOn,
cf.attribute as fusionAttribute
FROM CardFusions cf
JOIN CardCharacter cc ON cc.id = cf.toCharaId
JOIN Sprite s ON s.id = cc.id
LEFT JOIN Dex d ON d.id = cc.id
WHERE cf.fromCharaId = :charaId
ORDER BY cc.charaIndex
""")
fun getFusionsForCharacter(charaId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>>
}
@@ -1,29 +1,17 @@
package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface CardProgressDao {
@Query("""
UPDATE CardProgress
SET
currentStage = :currentStage,
unlocked = :unlocked
WHERE cardId = :cardId AND
currentStage < :currentStage
""")
fun updateCardProgress(currentStage: Int, cardId: Long, unlocked: Boolean)
@Upsert
fun updateDimProgress(vararg cardProgresses: CardProgress)
@Query(
"SELECT currentStage FROM CardProgress WHERE cardId = :cardId"
)
fun getCardProgress(cardId: Long): Flow<Int>
@Insert
fun insertCardProgress(cardProgress: CardProgress)
fun getCardProgress(cardId: Long): Int
}
@@ -6,7 +6,6 @@ import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface CharacterDao {
@@ -25,12 +24,10 @@ interface CharacterDao {
d.cardId as cardId,
c.charaIndex as charId,
c.stage as stage,
c.attribute as attribute,
cp.currentStage as currentStage
c.attribute as attribute
FROM CardCharacter c
JOIN UserCharacter uc ON c.id = uc.charId
JOIN Card d ON c.cardId = d.id
JOIN CardProgress cp ON d.id = cp.cardId
WHERE c.id = :charId
"""
)
@@ -38,7 +35,7 @@ interface CharacterDao {
@Query(
"""
INSERT INTO PossibleTransformations (charaId, requiredVitals, requiredTrophies, requiredBattles, requiredWinRate, changeTimerHours, requiredAdventureLevelCompleted, toCharaId)
INSERT INTO CardTransformations (charaId, requiredVitals, requiredTrophies, requiredBattles, requiredWinRate, changeTimerHours, requiredAdventureLevelCompleted, toCharaId)
SELECT
(SELECT id FROM CardCharacter WHERE charaIndex = :fromChraraIndex AND cardId = :cardId),
:requiredVitals,
@@ -78,13 +75,13 @@ interface CharacterDao {
pt.changeTimerHours as changeTimerHours,
pt.requiredAdventureLevelCompleted as requiredAdventureLevelCompleted
FROM
PossibleTransformations pt
CardTransformations pt
JOIN CardCharacter c on pt.toCharaId = c.id
JOIN Sprite s ON s.id = c.spriteId
LEFT JOIN Dex d ON d.id = pt.toCharaId
WHERE
pt.charaId = :characterId
c.cardId = :cardId
"""
)
fun getEvolutionRequirementsForCard(characterId: Long): Flow<List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>>
suspend fun getEvolutionRequirementsForCard(cardId: Long): List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>
}
@@ -4,7 +4,6 @@ import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface DexDao {
@@ -41,7 +40,7 @@ interface DexDao {
WHERE c.cardId = :cardId
"""
)
fun getSingleCardProgress(cardId: Long): Flow<List<CharacterDtos.CardCharaProgress>>
suspend fun getSingleCardProgress(cardId: Long): List<CharacterDtos.CardCharaProgress>
@Query(
"""
@@ -56,5 +55,5 @@ interface DexDao {
FROM Card c
"""
)
fun getCardsWithProgress(): Flow<List<CardDtos.CardProgress>>
suspend fun getCardsWithProgress(): List<CardDtos.CardProgress>
}
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface ItemDao {
@@ -14,7 +13,7 @@ interface ItemDao {
ORDER BY Items.itemIcon ASC
"""
)
fun getAllItems(): Flow<List<ItemDtos.ItemsWithQuantities>>
suspend fun getAllItems(): List<ItemDtos.ItemsWithQuantities>
@Query(
"""
@@ -23,7 +22,7 @@ interface ItemDao {
WHERE quantity > 0
"""
)
fun getAllUserItems(): Flow<List<ItemDtos.ItemsWithQuantities>>
suspend fun getAllUserItems(): List<ItemDtos.ItemsWithQuantities>
@Query(
"""
@@ -2,8 +2,6 @@ package com.github.nacabaro.vbhelper.daos
import androidx.room.Dao
import androidx.room.Query
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
import kotlinx.coroutines.flow.Flow
@Dao
interface SpecialMissionDao {
@@ -14,11 +12,4 @@ interface SpecialMissionDao {
WHERE id = :id
""")
suspend fun clearSpecialMission(id: Long)
@Query("""
SELECT *
FROM SpecialMissions
WHERE id = :id
""")
fun getSpecialMission(id: Long): Flow<SpecialMissions>
}
@@ -13,7 +13,6 @@ import com.github.nacabaro.vbhelper.domain.device_data.TransformationHistory
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.domain.device_data.VitalsHistory
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
@Dao
interface UserCharacterDao {
@@ -53,7 +52,7 @@ interface UserCharacterDao {
WHERE monId = :monId
"""
)
fun getTransformationHistory(monId: Long): Flow<List<CharacterDtos.TransformationHistory>>
suspend fun getTransformationHistory(monId: Long): List<CharacterDtos.TransformationHistory>?
@Query(
"""
@@ -69,8 +68,7 @@ interface UserCharacterDao {
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId = uc.id as isInAdventure,
uc.isActive as active
a.characterId = uc.id as isInAdventure
FROM UserCharacter uc
JOIN CardCharacter c ON uc.charId = c.id
JOIN Card d ON d.id = c.cardId
@@ -78,7 +76,7 @@ interface UserCharacterDao {
LEFT JOIN Adventure a ON a.characterId = uc.id
"""
)
fun getAllCharacters(): Flow<List<CharacterDtos.CharacterWithSprites>>
suspend fun getAllCharacters(): List<CharacterDtos.CharacterWithSprites>
@Query(
"""
@@ -94,8 +92,7 @@ interface UserCharacterDao {
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId = uc.id as isInAdventure,
uc.isActive as active
a.characterId = uc.id as isInAdventure
FROM UserCharacter uc
JOIN CardCharacter c ON uc.charId = c.id
JOIN Card d ON c.cardId = d.id
@@ -110,13 +107,13 @@ interface UserCharacterDao {
suspend fun getCharacter(id: Long): UserCharacter
@Query("SELECT * FROM BECharacterData WHERE id = :id")
fun getBeData(id: Long): Flow<BECharacterData>
suspend fun getBeData(id: Long): BECharacterData
@Query("SELECT * FROM VBCharacterData WHERE id = :id")
fun getVbData(id: Long): Flow<VBCharacterData>
suspend fun getVbData(id: Long): VBCharacterData
@Query("SELECT * FROM SpecialMissions WHERE characterId = :id")
fun getSpecialMissions(id: Long): Flow<List<SpecialMissions>>
suspend fun getSpecialMissions(id: Long): List<SpecialMissions>
@Query(
"""
@@ -132,8 +129,7 @@ interface UserCharacterDao {
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId as isInAdventure,
uc.isActive as active
a.characterId as isInAdventure
FROM UserCharacter uc
JOIN CardCharacter c ON uc.charId = c.id
JOIN Card d ON c.cardId = d.id
@@ -143,7 +139,7 @@ interface UserCharacterDao {
LIMIT 1
"""
)
fun getActiveCharacter(): Flow<CharacterDtos.CharacterWithSprites?>
suspend fun getActiveCharacter(): CharacterDtos.CharacterWithSprites?
@Query("DELETE FROM UserCharacter WHERE id = :id")
fun deleteCharacterById(id: Long)
@@ -197,8 +193,7 @@ interface UserCharacterDao {
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId = uc.id as isInAdventure,
uc.isActive as active
a.characterId = uc.id as isInAdventure
FROM UserCharacter uc
JOIN CardCharacter c ON uc.charId = c.id
JOIN Card d ON d.id = c.cardId
@@ -223,8 +218,7 @@ interface UserCharacterDao {
c.nameWidth as nameSpriteWidth,
c.nameHeight as nameSpriteHeight,
d.isBEm as isBemCard,
a.characterId = uc.id as isInAdventure,
uc.isActive as active
a.characterId = uc.id as isInAdventure
FROM UserCharacter uc
JOIN CardCharacter c ON uc.charId = c.id
JOIN Card d ON d.id = c.cardId
@@ -4,6 +4,7 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.nacabaro.vbhelper.daos.AdventureDao
import com.github.nacabaro.vbhelper.daos.CardAdventureDao
import com.github.nacabaro.vbhelper.daos.CardBackgroundDao
import com.github.nacabaro.vbhelper.daos.CharacterDao
import com.github.nacabaro.vbhelper.daos.DexDao
import com.github.nacabaro.vbhelper.daos.CardDao
@@ -13,13 +14,14 @@ import com.github.nacabaro.vbhelper.daos.ItemDao
import com.github.nacabaro.vbhelper.daos.SpecialMissionDao
import com.github.nacabaro.vbhelper.daos.SpriteDao
import com.github.nacabaro.vbhelper.daos.UserCharacterDao
import com.github.nacabaro.vbhelper.domain.card.Background
import com.github.nacabaro.vbhelper.domain.card.CardBackground
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.domain.card.CardAdventure
import com.github.nacabaro.vbhelper.domain.card.CardFusions
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.domain.card.PossibleTransformations
import com.github.nacabaro.vbhelper.domain.card.CardSprites
import com.github.nacabaro.vbhelper.domain.card.CardTransformations
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import com.github.nacabaro.vbhelper.domain.characters.Adventure
import com.github.nacabaro.vbhelper.domain.characters.Dex
@@ -39,6 +41,7 @@ import com.github.nacabaro.vbhelper.domain.items.Items
CardCharacter::class,
CardAdventure::class,
CardFusions::class,
CardSprites::class,
Sprite::class,
UserCharacter::class,
BECharacterData::class,
@@ -49,8 +52,8 @@ import com.github.nacabaro.vbhelper.domain.items.Items
Dex::class,
Items::class,
Adventure::class,
Background::class,
PossibleTransformations::class
CardBackground::class,
CardTransformations::class
]
)
abstract class AppDatabase : RoomDatabase() {
@@ -65,4 +68,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun specialMissionDao(): SpecialMissionDao
abstract fun cardAdventureDao(): CardAdventureDao
abstract fun cardFusionsDao(): CardFusionsDao
abstract fun cardBackgroundDao(): CardBackgroundDao
}
@@ -1,11 +1,9 @@
package com.github.nacabaro.vbhelper.di
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
interface AppContainer {
val db: AppDatabase
val dataStoreSecretsRepository: DataStoreSecretsRepository
val currencyRepository: CurrencyRepository
}
@@ -1,27 +1,20 @@
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import androidx.room.Room
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.AppContainer
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
import com.github.nacabaro.vbhelper.source.SecretsSerializer
import com.github.nacabaro.vbhelper.source.proto.Secrets
private const val SECRETS_DATA_STORE_NAME = "secrets.pb"
private const val USER_PREFERENCES_NAME = "user_preferences"
val Context.secretsStore: DataStore<Secrets> by dataStore(
fileName = SECRETS_DATA_STORE_NAME,
serializer = SecretsSerializer
)
val Context.currencyStore: DataStore<Preferences> by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
class DefaultAppContainer(private val context: Context) : AppContainer {
override val db: AppDatabase by lazy {
@@ -36,6 +29,5 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
override val currencyRepository = CurrencyRepository(context.currencyStore)
}
@@ -1,30 +0,0 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = Character::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Character::class,
parentColumns = ["id"],
childColumns = ["nextMon"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Evolutions(
@PrimaryKey val monId: Int,
@PrimaryKey val nextMon: Int,
val trophies: Int,
val vitals: Int,
val totalBattles: Int,
val winRate: Int // Does not need to be a floating point
)
@@ -14,8 +14,8 @@ import androidx.room.PrimaryKey
)
]
)
data class Background (
@PrimaryKey(autoGenerate = true) val id: Long,
data class CardBackground (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val cardId: Long,
val background: ByteArray,
val backgroundWidth: Int,
@@ -3,7 +3,6 @@ package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.github.cfogrady.vbnfc.data.NfcCharacter
@Entity(
foreignKeys = [
@@ -16,7 +15,25 @@ import com.github.cfogrady.vbnfc.data.NfcCharacter
ForeignKey(
entity = CardCharacter::class,
parentColumns = ["id"],
childColumns = ["toCharaId"],
childColumns = ["toVirusFusion"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = CardCharacter::class,
parentColumns = ["id"],
childColumns = ["toDataFusion"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = CardCharacter::class,
parentColumns = ["id"],
childColumns = ["toVaccineFusion"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = CardCharacter::class,
parentColumns = ["id"],
childColumns = ["toFreeFusion"],
onDelete = ForeignKey.CASCADE
)
]
@@ -24,6 +41,8 @@ import com.github.cfogrady.vbnfc.data.NfcCharacter
data class CardFusions(
@PrimaryKey(autoGenerate = true) val id: Long,
val fromCharaId: Long,
val attribute: NfcCharacter.Attribute,
val toCharaId: Long
val toVirusFusion: Long?,
val toDataFusion: Long?,
val toVaccineFusion: Long?,
val toFreeFusion: Long?
)
@@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.domain.card
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class CardSprites(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val sprite: ByteArray,
val width: Int,
val height: Int
)
@@ -20,7 +20,7 @@ import androidx.room.PrimaryKey
)
]
)
data class PossibleTransformations (
data class CardTransformations (
@PrimaryKey(autoGenerate = true) val id: Long = 0,
var charaId: Long,
val requiredVitals: Int,
@@ -24,10 +24,4 @@ object CardDtos {
val characterHp: Int,
val steps: Int,
)
data class CardIcon (
val cardIcon: ByteArray,
val cardIconWidth: Int,
val cardIconHeight: Int
)
}
@@ -31,16 +31,14 @@ object CharacterDtos {
val nameSpriteWidth: Int,
val nameSpriteHeight: Int,
val isBemCard: Boolean,
val isInAdventure: Boolean,
val active: Boolean
val isInAdventure: Boolean
)
data class CardCharacterInfo(
val cardId: Long,
val charId: Int,
val stage: Int,
val attribute: NfcCharacter.Attribute,
val currentStage: Int
val attribute: NfcCharacter.Attribute
)
data class TransformationHistory(
@@ -115,14 +113,4 @@ object CharacterDtos {
val changeTimerHours: Int,
val requiredAdventureLevelCompleted: Int
)
data class FusionsWithSpritesAndObtained(
val charaId: Long,
val fromCharaId: Long,
val spriteIdle: ByteArray,
val spriteWidth: Int,
val spriteHeight: Int,
val discoveredOn: Long?,
val fusionAttribute: NfcCharacter.Attribute
)
}
@@ -1,12 +1,13 @@
package com.github.nacabaro.vbhelper.navigation
import android.util.Log
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -98,14 +99,20 @@ fun AppNavigation(
composable(NavigationItems.Scan.route) {
val characterIdString = it.arguments?.getString("characterId")
var characterId by remember { mutableStateOf(characterIdString?.toLongOrNull()) }
Log.d("ScanScreen", "characterId: $characterId")
val launchedFromHomeScreen = (characterIdString?.toLongOrNull() == null)
if (characterId == null) {
val context = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(context.container.db)
val characterData by storageRepository.getActiveCharacter().collectAsState(null)
if (characterData != null) {
characterId = characterData!!.id
LaunchedEffect(characterId) {
if (characterId == null) {
val characterData = storageRepository.getActiveCharacter()
if (characterData != null) {
characterId = characterData.id
}
}
}
}
@@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.ui.res.stringResource
@Composable
fun BottomNavigationBar(navController: NavController) {
@@ -26,8 +26,8 @@ fun BottomNavigationBar(navController: NavController) {
items.forEach { item ->
NavigationBarItem (
icon = { Icon(painter = painterResource(item.icon), contentDescription = stringResource(item.label)) },
label = { Text(text = stringResource(item.label)) },
icon = { Icon(painter = painterResource(item.icon), contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
@@ -1,101 +1,25 @@
package com.github.nacabaro.vbhelper.navigation
import com.github.nacabaro.vbhelper.R
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
sealed class NavigationItems(
val route: String,
@DrawableRes val icon: Int,
@StringRes val label: Int
sealed class NavigationItems (
var route: String,
var icon: Int,
var label: String
) {
object Scan : NavigationItems(
"Scan/{characterId}",
R.drawable.baseline_nfc_24,
R.string.nav_scan
)
object Battles : NavigationItems(
"Battle",
R.drawable.baseline_swords_24,
R.string.nav_battle
)
object Home : NavigationItems(
"Home",
R.drawable.baseline_cottage_24,
R.string.nav_home
)
object Dex : NavigationItems(
"Dex",
R.drawable.baseline_menu_book_24,
R.string.nav_dex
)
object CardAdventure : NavigationItems(
"CardAdventure/{cardId}",
R.drawable.baseline_fort_24,
R.string.nav_card_adventure
)
object Storage : NavigationItems(
"Storage",
R.drawable.baseline_catching_pokemon_24,
R.string.nav_storage
)
object Settings : NavigationItems(
"Settings",
R.drawable.baseline_settings_24,
R.string.nav_settings
)
object Viewer : NavigationItems(
"Viewer",
R.drawable.baseline_image_24,
R.string.nav_viewer
)
object CardView : NavigationItems(
"Card/{cardId}",
R.drawable.baseline_image_24,
R.string.nav_card
)
object Items : NavigationItems(
"Items",
R.drawable.baseline_data_24,
R.string.nav_items
)
object MyItems : NavigationItems(
"MyItems",
R.drawable.baseline_data_24,
R.string.nav_my_items
)
object ItemsStore : NavigationItems(
"ItemsStore",
R.drawable.baseline_data_24,
R.string.nav_items_store
)
object ApplyItem : NavigationItems(
"ApplyItem/{itemId}",
R.drawable.baseline_data_24,
R.string.nav_apply_item
)
object Adventure : NavigationItems(
"Adventure",
R.drawable.baseline_fort_24,
R.string.nav_adventure
)
object Credits : NavigationItems(
"Credits",
R.drawable.baseline_data_24,
R.string.nav_credits
)
object Scan : NavigationItems("Scan/{characterId}", R.drawable.baseline_nfc_24, "Scan")
object Battles : NavigationItems("Battle", R.drawable.baseline_swords_24, "Battle")
object Home : NavigationItems("Home", R.drawable.baseline_cottage_24, "Home")
object Dex : NavigationItems("Dex", R.drawable.baseline_menu_book_24, "Dex")
object CardAdventure : NavigationItems("CardAdventure/{cardId}", R.drawable.baseline_fort_24, "Card adventure")
object Storage : NavigationItems("Storage", R.drawable.baseline_catching_pokemon_24, "Storage")
object Settings : NavigationItems("Settings", R.drawable.baseline_settings_24, "Settings")
object Viewer : NavigationItems("Viewer", R.drawable.baseline_image_24, "Viewer")
object CardView : NavigationItems("Card/{cardId}", R.drawable.baseline_image_24, "Card")
object Items : NavigationItems("Items", R.drawable.baseline_data_24, "Items")
object MyItems : NavigationItems("MyItems", R.drawable.baseline_data_24, "My items")
object ItemsStore : NavigationItems("ItemsStore", R.drawable.baseline_data_24, "Items store")
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")
}
File diff suppressed because it is too large Load Diff
@@ -9,16 +9,16 @@ 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.collectAsState
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.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.screens.itemsScreen.ObtainedItemDialog
import com.github.nacabaro.vbhelper.components.TopBanner
@@ -29,25 +29,24 @@ 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
import com.github.nacabaro.vbhelper.R
@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 by storageRepository.getAdventureCharacters().collectAsState(emptyList())
val characterList = remember {
mutableStateOf<List<CharacterDtos.AdventureCharacterWithSprites>>(emptyList())
}
var obtainedItem by remember {
mutableStateOf<ItemDtos.PurchasedItem?>(null)
}
var obtainedCurrency by remember {
mutableStateOf(0)
}
val currentTime by produceState(initialValue = Instant.now().epochSecond) {
while (true) {
@@ -60,17 +59,24 @@ fun AdventureScreen(
mutableStateOf<CharacterDtos.AdventureCharacterWithSprites?>(null)
}
LaunchedEffect(storageRepository) {
coroutineScope.launch {
characterList.value = storageRepository
.getAdventureCharacters()
}
}
Scaffold(
topBar = {
TopBanner(
text = stringResource(R.string.adventure_title),
text = "Adventure",
onBackClick = {
navController.popBackStack()
}
)
}
) { contentPadding ->
if (characterList.isEmpty()) {
if (characterList.value.isEmpty()) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
@@ -78,14 +84,14 @@ fun AdventureScreen(
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
) {
Text(text = stringResource(R.string.adventure_empty_state))
Text(text = "Nothing to see here")
}
} else {
LazyColumn(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
items(characterList) {
items(characterList.value) {
AdventureEntry(
icon = BitmapData(
bitmap = it.spriteIdle,
@@ -96,9 +102,8 @@ fun AdventureScreen(
onClick = {
if (it.finishesAdventure < currentTime) {
storageScreenController
.getItemFromAdventure(it.id) { adventureResult, generatedCurrency ->
.getItemFromAdventure(it.id) { adventureResult ->
obtainedItem = adventureResult
obtainedCurrency = generatedCurrency
}
} else {
cancelAdventureDialog = it
@@ -113,7 +118,6 @@ fun AdventureScreen(
if (obtainedItem != null) {
ObtainedItemDialog(
obtainedItem = obtainedItem!!,
obtainedCurrency = obtainedCurrency,
onClickDismiss = {
obtainedItem = null
}
@@ -4,6 +4,6 @@ import com.github.nacabaro.vbhelper.dtos.ItemDtos
interface AdventureScreenController {
fun sendCharacterToAdventure(characterId: Long, timeInMinutes: Long)
fun getItemFromAdventure(characterId: Long, onResult: (ItemDtos.PurchasedItem, Int) -> Unit)
fun getItemFromAdventure(characterId: Long, onResult: (ItemDtos.PurchasedItem) -> Unit)
fun cancelAdventure(characterId: Long, onResult: () -> Unit)
}
@@ -6,7 +6,6 @@ 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.flow.first
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
import kotlin.random.Random
@@ -38,29 +37,19 @@ class AdventureScreenControllerImpl(
override fun getItemFromAdventure(
characterId: Long,
onResult: (ItemDtos.PurchasedItem, Int) -> Unit
onResult: (ItemDtos.PurchasedItem) -> Unit
) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
database
.adventureDao()
.deleteAdventure(characterId)
val generatedCurrency = generateRandomCurrency()
val generatedItem = generateItem(characterId)
onResult(generatedItem, generatedCurrency)
onResult(generatedItem)
}
}
private suspend fun generateRandomCurrency(): Int {
val currentValue = application.container.currencyRepository.currencyValue.first()
val random = (2..6).random() * 1000
application.container.currencyRepository.setCurrencyValue(currentValue + random)
return random
}
override fun cancelAdventure(characterId: Long, onResult: () -> Unit) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
database
@@ -88,7 +77,6 @@ class AdventureScreenControllerImpl(
val randomItem = database
.itemDao()
.getAllItems()
.first()
.random()
val random = ((Random.nextFloat() * character.stage) + 3).roundToInt()
@@ -6,14 +6,15 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.dtos.CardDtos
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun CardAdventureScreen(
@@ -21,17 +22,25 @@ fun CardAdventureScreen(
cardScreenController: CardScreenControllerImpl,
cardId: Long
) {
val cardAdventureMissions by cardScreenController
.getCardAdventureMissions(cardId)
.collectAsState(emptyList())
val currentCardAdventure by cardScreenController
.getCardProgress(cardId)
.collectAsState(0)
val cardAdventureMissions = remember { mutableStateOf(emptyList<CardDtos.CardAdventureWithSprites>()) }
var currentCardAdventure = remember { 0 }
LaunchedEffect(cardId) {
withContext(Dispatchers.IO) {
cardAdventureMissions.value =
cardScreenController
.getCardAdventureMissions(cardId)
currentCardAdventure =
cardScreenController
.getCardProgress(cardId)
}
}
Scaffold (
topBar = {
TopBanner(
text = stringResource(R.string.card_adventure_missions_title),
text = "Adventure missions",
onBackClick = {
navController.popBackStack()
}
@@ -43,10 +52,10 @@ fun CardAdventureScreen(
.padding(top = contentPadding.calculateTopPadding())
.verticalScroll(state = rememberScrollState())
) {
cardAdventureMissions.mapIndexed { index, it ->
cardAdventureMissions.value.mapIndexed { index, it ->
CardAdventureEntry(
cardAdventureEntry = it,
obscure = index > currentCardAdventure - 1
obscure = index > currentCardAdventure
)
}
}
@@ -24,9 +24,6 @@ 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 androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
@Composable
fun CardEntry(
@@ -78,11 +75,7 @@ fun CardEntry(
modifier = Modifier
)
Text(
text = stringResource(
R.string.card_entry_characters_obtained,
obtainedCharacters,
totalCharacters
),
text = "$obtainedCharacters of $totalCharacters characters obtained",
fontFamily = MaterialTheme.typography.labelSmall.fontFamily,
fontSize = MaterialTheme.typography.labelSmall.fontSize,
modifier = Modifier
@@ -98,7 +91,7 @@ fun CardEntry(
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = stringResource(R.string.card_entry_edit)
contentDescription = "Edit"
)
}
IconButton(
@@ -106,7 +99,7 @@ fun CardEntry(
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.card_entry_delete)
contentDescription = "Delete"
)
}
}
@@ -1,13 +1,10 @@
package com.github.nacabaro.vbhelper.screens.cardScreen
import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
interface CardScreenController {
fun renameCard(cardId: Long, newName: String, onRenamed: (String) -> Unit)
fun deleteCard(cardId: Long, onDeleted: () -> Unit)
fun getCardAdventureMissions(cardId: Long): Flow<List<CardDtos.CardAdventureWithSprites>>
fun getCardProgress(cardId: Long): Flow<Int>
fun getFusionsForCharacters(characterId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>>
suspend fun getCardAdventureMissions(cardId: Long): List<CardDtos.CardAdventureWithSprites>
suspend fun getCardProgress(cardId: Long): Int
}
@@ -4,8 +4,6 @@ import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CardDtos
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
class CardScreenControllerImpl(
@@ -34,22 +32,15 @@ class CardScreenControllerImpl(
}
}
override fun getCardAdventureMissions(cardId: Long): Flow<List<CardDtos.CardAdventureWithSprites>> {
override suspend fun getCardAdventureMissions(cardId: Long): List<CardDtos.CardAdventureWithSprites> {
return database
.cardAdventureDao()
.getAdventureForCard(cardId)
}
override fun getCardProgress(cardId: Long): Flow<Int> {
override suspend fun getCardProgress(cardId: Long): Int {
return database
.cardProgressDao()
.getCardProgress(cardId)
}
override fun getFusionsForCharacters(characterId: Long): Flow<List<CharacterDtos.FusionsWithSpritesAndObtained>> {
return database
.cardFusionsDao()
.getFusionsForCharacter(characterId)
}
}
@@ -5,12 +5,11 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.components.CharacterEntry
@@ -20,25 +19,36 @@ import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.cardScreen.dialogs.DexCharaDetailsDialog
import com.github.nacabaro.vbhelper.source.DexRepository
import com.github.nacabaro.vbhelper.R
import kotlinx.coroutines.launch
@Composable
fun CardViewScreen(
navController: NavController,
cardId: Long
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val dexRepository = DexRepository(application.container.db)
val characterList by dexRepository.getCharactersByCardId(cardId).collectAsState(emptyList())
val characterList = remember { mutableStateOf<List<CharacterDtos.CardCharaProgress>>(emptyList()) }
val cardPossibleTransformations = remember { mutableStateOf<List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>>(emptyList()) }
val selectedCharacter = remember { mutableStateOf<CharacterDtos.CardCharaProgress?>(null) }
LaunchedEffect(dexRepository) {
coroutineScope.launch {
val newCharacterList = dexRepository.getCharactersByCardId(cardId)
characterList.value = newCharacterList
val newCardPossibleTransformations = dexRepository.getCardPossibleTransformations(cardId)
cardPossibleTransformations.value = newCardPossibleTransformations
}
}
Scaffold (
topBar = {
TopBanner(
text = stringResource(R.string.card_view_discovered_characters),
text = "Discovered characters",
onBackClick = {
navController.popBackStack()
},
@@ -60,7 +70,7 @@ fun CardViewScreen(
columns = GridCells.Fixed(3),
contentPadding = contentPadding
) {
items(characterList) { character ->
items(characterList.value) { character ->
CharacterEntry(
onClick = {
selectedCharacter.value = character
@@ -78,6 +88,7 @@ fun CardViewScreen(
if (selectedCharacter.value != null) {
DexCharaDetailsDialog(
currentChara = selectedCharacter.value!!,
possibleTransformations = cardPossibleTransformations.value,
obscure = selectedCharacter.value!!.discoveredOn == null,
onClickClose = {
selectedCharacter.value = null
@@ -7,10 +7,11 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -24,17 +25,17 @@ import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.cardScreen.dialogs.CardDeleteDialog
import com.github.nacabaro.vbhelper.screens.cardScreen.dialogs.CardRenameDialog
import com.github.nacabaro.vbhelper.source.DexRepository
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
import kotlinx.coroutines.launch
@Composable
fun CardsScreen(
navController: NavController,
cardScreenController: CardScreenControllerImpl
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val dexRepository = DexRepository(application.container.db)
val cardList by dexRepository.getAllDims().collectAsState(emptyList())
val cardList = remember { mutableStateOf<List<CardDtos.CardProgress>>(emptyList()) }
val selectedCard = remember { mutableStateOf<CardDtos.CardProgress?>(null) }
var clickedDelete by remember { mutableStateOf(false) }
@@ -42,10 +43,17 @@ fun CardsScreen(
var modifyCards by remember { mutableStateOf(false) }
LaunchedEffect(dexRepository) {
coroutineScope.launch {
val newDimList = dexRepository.getAllDims()
cardList.value = newDimList
}
}
Scaffold (
topBar = {
TopBanner(
text = stringResource(R.string.cards_my_cards_title),
text = "My cards",
onModifyClick = {
modifyCards = !modifyCards
}
@@ -56,7 +64,7 @@ fun CardsScreen(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
items(cardList) {
items(cardList.value) {
CardEntry(
name = it.cardName,
logo = BitmapData(
@@ -16,11 +16,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
@@ -28,36 +23,22 @@ import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.source.DexRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getImageBitmap
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
@Composable
fun DexCharaDetailsDialog(
currentChara: CharacterDtos.CardCharaProgress,
possibleTransformations: List<CharacterDtos.EvolutionRequirementsWithSpritesAndObtained>,
obscure: Boolean,
onClickClose: () -> Unit
) {
val nameMultiplier = 3
val charaMultiplier = 4
val application = LocalContext.current.applicationContext as VBHelper
val database = application.container.db
val dexRepository = DexRepository(database)
var showFusions by remember { mutableStateOf(false) }
val currentCharaPossibleTransformations by dexRepository
.getCharacterPossibleTransformations(currentChara.id)
.collectAsState(emptyList())
val currentCharaPossibleFusions by dexRepository
.getCharacterPossibleFusions(currentChara.id)
.collectAsState(emptyList())
val currentCharaPossibleTransformations = possibleTransformations.filter { it.fromCharaId == currentChara.id }
val romanNumeralsStage = when (currentChara.stage) {
1 -> "II"
@@ -120,7 +101,7 @@ fun DexCharaDetailsDialog(
) {
Image(
bitmap = charaImageBitmapData.imageBitmap,
contentDescription = stringResource(R.string.dex_chara_icon_description),
contentDescription = "Icon",
modifier = Modifier
.size(charaImageBitmapData.dpWidth)
.padding(8.dp),
@@ -139,7 +120,7 @@ fun DexCharaDetailsDialog(
Column {
Image(
bitmap = nameImageBitmapData.imageBitmap,
contentDescription = stringResource(R.string.dex_chara_name_icon_description),
contentDescription = "Icon",
modifier = Modifier
.width(nameImageBitmapData.dpWidth)
.height(nameImageBitmapData.dpHeight),
@@ -148,28 +129,17 @@ fun DexCharaDetailsDialog(
Spacer(modifier = Modifier.padding(4.dp))
if (currentChara.baseHp != 65535) {
Text(
text = stringResource(
R.string.dex_chara_stats,
currentChara.baseHp,
currentChara.baseBp,
currentChara.baseAp
)
)
Text(
text = stringResource(
R.string.dex_chara_stage_attribute,
romanNumeralsStage,
currentChara.attribute.toString().substring(0, 2)
)
text = "HP: ${currentChara.baseHp}, BP: ${currentChara.baseBp}, AP: ${currentChara.baseAp}"
)
Text(text = "Stg: ${romanNumeralsStage}, Atr: ${currentChara.attribute.toString().substring(0, 2)}")
}
}
} else {
Column {
Text(stringResource(R.string.dex_chara_unknown_name))
Text(text = "????????????????")
Spacer(modifier = Modifier.padding(4.dp))
Text(stringResource(R.string.dex_chara_stage_attribute_unknown))
Text(stringResource(R.string.dex_chara_stats_unknown))
Text(text = "Stg: -, Atr: -")
Text(text = "HP: -, BP: -, AP: -")
}
}
}
@@ -210,7 +180,7 @@ fun DexCharaDetailsDialog(
) {
Image(
bitmap = selectedCharaImageBitmap.imageBitmap,
contentDescription = stringResource(R.string.dex_chara_icon_description),
contentDescription = "Icon",
modifier = Modifier
.size(selectedCharaImageBitmap.dpWidth)
.padding(8.dp),
@@ -226,62 +196,20 @@ fun DexCharaDetailsDialog(
.padding(16.dp)
)
Column {
Text(
text = stringResource(
R.string.dex_chara_requirements,
it.requiredTrophies,
it.requiredBattles,
it.requiredVitals,
it.requiredWinRate,
it.changeTimerHours
)
)
Text(
text = stringResource(
R.string.dex_chara_adventure_level,
it.requiredAdventureLevelCompleted + 1
)
)
Text("Tr: ${it.requiredTrophies}; Bt: ${it.requiredBattles}; Vr: ${it.requiredVitals}; Wr: ${it.requiredWinRate}%; Ct: ${it.changeTimerHours}h")
Text("AdvLvl ${it.requiredAdventureLevelCompleted + 1}")
}
}
}
}
}
Row {
if (currentCharaPossibleFusions.isNotEmpty()) {
Button(
onClick = {
showFusions = true
}
) {
Text(stringResource(R.string.dex_chara_fusions_button))
}
}
Spacer(
modifier = Modifier
.padding(4.dp)
)
Button(
onClick = onClickClose
) {
Text(stringResource(R.string.dex_chara_close_button))
}
Button(
onClick = onClickClose
) {
Text("Close")
}
}
}
}
if (showFusions) {
DexCharaFusionsDialog(
currentChara = currentChara,
currentCharaPossibleFusions = currentCharaPossibleFusions,
onClickDismiss = {
showFusions = false
},
obscure = obscure
)
}
}
@@ -1,191 +0,0 @@
package com.github.nacabaro.vbhelper.screens.cardScreen.dialogs
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getImageBitmap
@Composable
fun DexCharaFusionsDialog(
currentChara: CharacterDtos.CardCharaProgress,
currentCharaPossibleFusions: List<CharacterDtos.FusionsWithSpritesAndObtained>,
obscure: Boolean,
onClickDismiss: () -> Unit,
) {
val nameMultiplier = 3
val charaMultiplier = 4
val charaBitmapData = BitmapData(
bitmap = currentChara.spriteIdle,
width = currentChara.spriteWidth,
height = currentChara.spriteHeight
)
val charaImageBitmapData = charaBitmapData.getImageBitmap(
context = LocalContext.current,
multiplier = charaMultiplier,
obscure = obscure
)
val nameBitmapData = BitmapData(
bitmap = currentChara.nameSprite,
width = currentChara.nameSpriteWidth,
height = currentChara.nameSpriteHeight
)
val nameImageBitmapData = nameBitmapData.getImageBitmap(
context = LocalContext.current,
multiplier = nameMultiplier,
obscure = obscure
)
Dialog(
onDismissRequest = onClickDismiss,
) {
Card(
modifier = Modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(16.dp)
) {
Row {
Card (
colors = CardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledContentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
)
)
) {
Image(
bitmap = charaImageBitmapData.imageBitmap,
contentDescription = "Icon",
modifier = Modifier
.size(charaImageBitmapData.dpWidth)
.padding(8.dp),
colorFilter = when (obscure) {
true -> ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
false -> null
},
filterQuality = FilterQuality.None
)
}
Spacer(
modifier = Modifier
.padding(16.dp)
)
if (!obscure) {
Column {
Image(
bitmap = nameImageBitmapData.imageBitmap,
contentDescription = "Icon",
modifier = Modifier
.width(nameImageBitmapData.dpWidth)
.height(nameImageBitmapData.dpHeight),
filterQuality = FilterQuality.None
)
}
} else {
Column {
Text(text = "????????????????")
}
}
}
Spacer(modifier = Modifier.padding(16.dp))
Column {
currentCharaPossibleFusions.map {
val selectedCharaBitmap = BitmapData(
bitmap = it.spriteIdle,
width = it.spriteWidth,
height = it.spriteHeight
)
val selectedCharaImageBitmap = selectedCharaBitmap.getImageBitmap(
context = LocalContext.current,
multiplier = 4,
obscure = it.discoveredOn == null
)
Card (
modifier = Modifier
.padding(vertical = 8.dp)
) {
Row (
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
) {
Card (
colors = CardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledContentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
)
)
) {
Image(
bitmap = selectedCharaImageBitmap.imageBitmap,
contentDescription = "Icon",
modifier = Modifier
.size(selectedCharaImageBitmap.dpWidth)
.padding(8.dp),
colorFilter = when (it.discoveredOn == null) {
true -> ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
false -> null
},
filterQuality = FilterQuality.None
)
}
Spacer(
modifier = Modifier
.padding(16.dp)
)
Column {
Text("Combine with ${it.fusionAttribute}")
}
}
}
}
}
Button(
onClick = onClickDismiss
) {
Text("Close")
}
}
}
}
}
@@ -8,10 +8,8 @@ import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.R
@Composable
fun BetaWarning(
@@ -21,26 +19,26 @@ fun BetaWarning(
onDismissRequest = onDismissRequest
) {
Card {
Column(
Column (
modifier = Modifier
.padding(16.dp)
) {
Text(
text = stringResource(R.string.beta_warning_message_main)
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 = stringResource(R.string.beta_warning_message_compatibility)
text = "Application should work now with the original VB and the VH."
)
Spacer(modifier = Modifier.padding(8.dp))
Text(
text = stringResource(R.string.beta_warning_message_thanks)
text = "Thank you for your understanding and patience. Sincerely, the dev team."
)
Spacer(modifier = Modifier.padding(8.dp))
Button(
onClick = onDismissRequest
) {
Text(text = stringResource(R.string.beta_warning_button_dismiss))
Text(text = "Dismiss")
}
}
}
@@ -1,5 +1,6 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -11,7 +12,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -20,7 +20,6 @@ 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -29,7 +28,9 @@ import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.utils.DeviceType
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
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.screens.homeScreens.screens.BEBEmHomeScreen
@@ -37,12 +38,8 @@ import com.github.nacabaro.vbhelper.screens.homeScreens.screens.BEDiMHomeScreen
import com.github.nacabaro.vbhelper.screens.homeScreens.screens.VBDiMHomeScreen
import com.github.nacabaro.vbhelper.screens.itemsScreen.ObtainedItemDialog
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.utils.BitmapData
import kotlinx.coroutines.flow.flowOf
import kotlin.collections.emptyList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun HomeScreen(
@@ -51,59 +48,28 @@ fun HomeScreen(
) {
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val cardRepository = CardRepository(application.container.db)
val activeMon by storageRepository
.getActiveCharacter()
.collectAsState(initial = null)
val cardIconData by (
activeMon
?.let { chara ->
cardRepository.getCardIconByCharaId(chara.charId)
}
?: flowOf<CardDtos.CardIcon?>(null)
).collectAsState(initial = null)
val transformationHistory by (
activeMon
?.let { chara ->
storageRepository.getTransformationHistory(chara.id)
}
?: flowOf(emptyList())
).collectAsState(initial = emptyList())
val vbSpecialMissions by (
activeMon
?.takeIf { it.characterType == DeviceType.VBDevice }
?.let { chara ->
storageRepository.getSpecialMissions(chara.id)
}
?: flowOf(emptyList())
).collectAsState(initial = emptyList())
val vbData by (
activeMon
?.takeIf { it.characterType == DeviceType.VBDevice }
?.let { chara ->
storageRepository.getCharacterVbData(chara.id)
}
?: flowOf<VBCharacterData?>(null)
).collectAsState(initial = null)
val beData by (
activeMon
?.takeIf { it.characterType == DeviceType.BEDevice }
?.let { chara ->
storageRepository.getCharacterBeData(chara.id)
}
?: flowOf<BECharacterData?>(null)
).collectAsState(initial = null)
val activeMon = remember { mutableStateOf<CharacterDtos.CharacterWithSprites?>(null) }
val transformationHistory = remember { mutableStateOf<List<CharacterDtos.TransformationHistory>?>(null) }
val beData = remember { mutableStateOf<BECharacterData?>(null) }
val vbData = remember { mutableStateOf<VBCharacterData?>(null) }
val vbSpecialMissions = remember { mutableStateOf<List<SpecialMissions>>(emptyList()) }
var adventureMissionsFinished by rememberSaveable { mutableStateOf(false) }
var betaWarning by rememberSaveable { mutableStateOf(true) }
var collectedItem by remember { mutableStateOf<ItemDtos.PurchasedItem?>(null) }
var collectedCurrency by remember { mutableStateOf<Int?>(null) }
LaunchedEffect(storageRepository, activeMon, collectedItem) {
withContext(Dispatchers.IO) {
activeMon.value = storageRepository.getActiveCharacter()
if (activeMon.value != null && activeMon.value!!.characterType == DeviceType.BEDevice) {
beData.value = storageRepository.getCharacterBeData(activeMon.value!!.id)
transformationHistory.value = storageRepository.getTransformationHistory(activeMon.value!!.id)
} else if (activeMon.value != null && activeMon.value!!.characterType == DeviceType.VBDevice) {
vbData.value = storageRepository.getCharacterVbData(activeMon.value!!.id)
vbSpecialMissions.value = storageRepository.getSpecialMissions(activeMon.value!!.id)
transformationHistory.value = storageRepository.getTransformationHistory(activeMon.value!!.id)
}
}
}
LaunchedEffect(true) {
homeScreenController
@@ -115,7 +81,7 @@ fun HomeScreen(
Scaffold (
topBar = {
TopBanner(
text = stringResource(R.string.home_title),
text = "VB Helper",
onScanClick = {
navController.navigate(NavigationItems.Scan.route)
},
@@ -125,7 +91,8 @@ fun HomeScreen(
)
}
) { contentPadding ->
if (activeMon == null || (beData == null && vbData == null) || cardIconData == null || transformationHistory.isEmpty()) {
if (activeMon.value == null || (beData.value == null && vbData.value == null) || transformationHistory.value == null) {
Log.d("TetTet", "Something is null")
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
@@ -133,44 +100,35 @@ fun HomeScreen(
.fillMaxSize()
.padding(top = contentPadding.calculateTopPadding())
) {
Text(text = stringResource(R.string.adventure_empty_state))
Text(text = "Nothing to see here")
}
} else {
val cardIcon = BitmapData(
bitmap = cardIconData!!.cardIcon,
width = cardIconData!!.cardIconWidth,
height = cardIconData!!.cardIconHeight
)
if (activeMon!!.isBemCard && beData != null) {
Log.d("TetTet", "Something is not null")
if (activeMon.value!!.isBemCard) {
BEBEmHomeScreen(
activeMon = activeMon!!,
beData = beData!!,
transformationHistory = transformationHistory,
contentPadding = contentPadding,
cardIcon = cardIcon
activeMon = activeMon.value!!,
beData = beData.value!!,
transformationHistory = transformationHistory.value!!,
contentPadding = contentPadding
)
} else if (!activeMon!!.isBemCard && activeMon!!.characterType == DeviceType.BEDevice && beData != null) {
} else if (!activeMon.value!!.isBemCard && activeMon.value!!.characterType == DeviceType.BEDevice) {
BEDiMHomeScreen(
activeMon = activeMon!!,
beData = beData!!,
transformationHistory = transformationHistory,
contentPadding = contentPadding,
cardIcon = cardIcon
activeMon = activeMon.value!!,
beData = beData.value!!,
transformationHistory = transformationHistory.value!!,
contentPadding = contentPadding
)
} else if (vbData != null) {
} else {
VBDiMHomeScreen(
activeMon = activeMon!!,
vbData = vbData!!,
transformationHistory = transformationHistory,
activeMon = activeMon.value!!,
vbData = vbData.value!!,
transformationHistory = transformationHistory.value!!,
contentPadding = contentPadding,
specialMissions = vbSpecialMissions,
specialMissions = vbSpecialMissions.value,
homeScreenController = homeScreenController,
onClickCollect = { item, currency ->
onClickCollect = { item ->
collectedItem = item
collectedCurrency = currency
},
cardIcon = cardIcon
}
)
}
}
@@ -179,10 +137,8 @@ fun HomeScreen(
if (collectedItem != null) {
ObtainedItemDialog(
obtainedItem = collectedItem!!,
obtainedCurrency = collectedCurrency!!,
onClickDismiss = {
collectedItem = null
collectedCurrency = null
}
)
}
@@ -197,7 +153,7 @@ fun HomeScreen(
.padding(16.dp)
) {
Text(
text = stringResource(R.string.home_adventure_mission_finished),
text = "One of your characters has finished their adventure mission!",
textAlign = TextAlign.Center
)
Button(
@@ -208,7 +164,7 @@ fun HomeScreen(
.padding(8.dp)
.fillMaxWidth()
) {
Text(text = stringResource(R.string.beta_warning_button_dismiss))
Text(text = "Dismiss")
}
}
}
@@ -1,8 +1,9 @@
package com.github.nacabaro.vbhelper.screens.homeScreens
import com.github.cfogrady.vbnfc.vb.SpecialMission
import com.github.nacabaro.vbhelper.dtos.ItemDtos
interface HomeScreenController {
fun didAdventureMissionsFinish(onCompletion: (Boolean) -> Unit)
fun clearSpecialMission(missionId: Long, onCleared: (ItemDtos.PurchasedItem?, Int?) -> Unit)
fun clearSpecialMission(missionId: Long, missionCompletion: SpecialMission.Status, onCleared: (ItemDtos.PurchasedItem?) -> Unit)
}
@@ -5,7 +5,6 @@ import androidx.lifecycle.lifecycleScope
import com.github.cfogrady.vbnfc.vb.SpecialMission
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.time.Instant
import kotlin.math.roundToInt
@@ -23,7 +22,6 @@ class HomeScreenControllerImpl(
val adventureCharacters = database
.adventureDao()
.getAdventureCharacters()
.first()
val finishedAdventureCharacters = adventureCharacters.filter { character ->
character.finishesAdventure <= currentTime
@@ -33,22 +31,16 @@ class HomeScreenControllerImpl(
}
}
override fun clearSpecialMission(missionId: Long, onCleared: (ItemDtos.PurchasedItem?, Int?) -> Unit) {
override fun clearSpecialMission(missionId: Long, missionCompletion: SpecialMission.Status, onCleared: (ItemDtos.PurchasedItem?) -> Unit) {
componentActivity.lifecycleScope.launch {
val missionStatus = database
.specialMissionDao()
.getSpecialMission(missionId)
.first()
database
.specialMissionDao()
.clearSpecialMission(missionId)
if (missionStatus.status == SpecialMission.Status.COMPLETED) {
if (missionCompletion == SpecialMission.Status.COMPLETED) {
val randomItem = database
.itemDao()
.getAllItems()
.first()
.random()
val randomItemAmount = (Random.nextFloat() * 5).roundToInt()
@@ -70,14 +62,11 @@ class HomeScreenControllerImpl(
itemType = randomItem.itemType
)
val randomAmount = (2..6).random() * 1000
val currentCurrency = application.container.currencyRepository.currencyValue.first()
application.container.currencyRepository.setCurrencyValue(currentCurrency + randomAmount)
onCleared(purchasedItem, randomAmount)
onCleared(purchasedItem)
} else {
onCleared(null, null)
onCleared(null)
}
}
}
}
@@ -1,56 +0,0 @@
package com.github.nacabaro.vbhelper.screens.homeScreens.dialogs
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.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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.github.nacabaro.vbhelper.R
@Composable
fun DeleteSpecialMissionDialog(
onClickDismiss: () -> Unit,
onClickDelete: () -> Unit
) {
Dialog(
onDismissRequest = onClickDismiss
) {
Card {
Column(
modifier = Modifier
.padding(16.dp)
) {
Text(
text = stringResource(R.string.home_special_mission_delete_main),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.padding(8.dp))
Row {
Button(
onClick = onClickDismiss,
modifier = Modifier
.padding(8.dp)
) {
Text(text = stringResource(R.string.home_special_mission_delete_dismiss))
}
Button(
onClick = onClickDelete,
modifier = Modifier
.padding(8.dp)
) {
Text(text = stringResource(R.string.home_special_mission_delete_remove))
}
}
}
}
}
}
@@ -26,7 +26,6 @@ import java.util.Locale
fun BEBEmHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
beData: BECharacterData,
cardIcon: BitmapData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
) {
@@ -47,7 +46,6 @@ fun BEBEmHomeScreen(
),
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
cardIcon = cardIcon,
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
@@ -26,7 +26,6 @@ import kotlin.text.format
@Composable
fun BEDiMHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
cardIcon: BitmapData,
beData: BECharacterData,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues
@@ -46,7 +45,6 @@ fun BEDiMHomeScreen(
width = activeMon.spriteWidth,
height = activeMon.spriteHeight
),
cardIcon = cardIcon,
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = Modifier
@@ -10,10 +10,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
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.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -29,23 +25,17 @@ import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.screens.homeScreens.HomeScreenControllerImpl
import com.github.nacabaro.vbhelper.utils.BitmapData
import java.util.Locale
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.screens.homeScreens.dialogs.DeleteSpecialMissionDialog
@Composable
fun VBDiMHomeScreen(
activeMon: CharacterDtos.CharacterWithSprites,
cardIcon: BitmapData,
vbData: VBCharacterData,
specialMissions: List<SpecialMissions>,
homeScreenController: HomeScreenControllerImpl,
transformationHistory: List<CharacterDtos.TransformationHistory>,
contentPadding: PaddingValues,
onClickCollect: (ItemDtos.PurchasedItem?, Int?) -> Unit
onClickCollect: (ItemDtos.PurchasedItem?) -> Unit
) {
var selectedSpecialMissionId by remember { mutableStateOf<Long>(-1) }
Column(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
@@ -61,7 +51,6 @@ fun VBDiMHomeScreen(
width = activeMon.spriteWidth,
height = activeMon.spriteHeight
),
cardIcon = cardIcon,
multiplier = 8,
shape = androidx.compose.material.MaterialTheme.shapes.small,
modifier = Modifier
@@ -76,7 +65,7 @@ fun VBDiMHomeScreen(
ItemDisplay(
icon = R.drawable.baseline_vitals_24,
textValue = activeMon.vitalPoints.toString(),
definition = stringResource(R.string.home_vbdim_vitals),
definition = "Vitals",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
@@ -85,7 +74,7 @@ fun VBDiMHomeScreen(
ItemDisplay(
icon = R.drawable.baseline_trophy_24,
textValue = activeMon.trophies.toString(),
definition = stringResource(R.string.home_vbdim_trophies),
definition = "Trophies",
modifier = Modifier
.weight(0.5f)
.aspectRatio(1f)
@@ -100,7 +89,7 @@ fun VBDiMHomeScreen(
ItemDisplay(
icon = R.drawable.baseline_mood_24,
textValue = activeMon.mood.toString(),
definition = stringResource(R.string.home_vbdim_mood),
definition = "Mood",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
@@ -113,7 +102,7 @@ fun VBDiMHomeScreen(
0 -> "${activeMon.transformationCountdown} m"
else -> "$transformationCountdownInHours h"
},
definition = stringResource(R.string.home_vbdim_next_timer),
definition = "Next timer",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
@@ -133,7 +122,7 @@ fun VBDiMHomeScreen(
) + " %" // Specify locale
}
},
definition = stringResource(R.string.home_vbdim_total_battle_win),
definition = "Total battle win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
@@ -153,7 +142,7 @@ fun VBDiMHomeScreen(
) + " %" // Specify locale
}
},
definition = stringResource(R.string.home_vbdim_current_phase_win),
definition = "Current phase win %",
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
@@ -176,7 +165,7 @@ fun VBDiMHomeScreen(
.padding(16.dp)
) {
Text(
text = stringResource(R.string.home_vbdim_special_missions),
text = "Special missions",
fontSize = 24.sp
)
}
@@ -190,28 +179,11 @@ fun VBDiMHomeScreen(
modifier = Modifier
.weight(1f)
.padding(8.dp),
onClickMission = { missionId ->
selectedSpecialMissionId = missionId
},
onClickCollect = {
homeScreenController
.clearSpecialMission(selectedSpecialMissionId, onClickCollect)
}
)
) {
homeScreenController
.clearSpecialMission(mission.id, mission.status, onClickCollect)
}
}
}
}
if (selectedSpecialMissionId.toInt() != -1) {
DeleteSpecialMissionDialog(
onClickDismiss = {
selectedSpecialMissionId = -1
},
onClickDelete = {
homeScreenController
.clearSpecialMission(selectedSpecialMissionId, onClickCollect)
selectedSpecialMissionId = -1
}
)
}
}
@@ -15,7 +15,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.CharacterEntry
import com.github.nacabaro.vbhelper.components.TopBanner
@@ -25,9 +24,8 @@ import com.github.nacabaro.vbhelper.dtos.CharacterDtos
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import com.github.nacabaro.vbhelper.R
@Composable
fun ChooseCharacterScreen(
@@ -37,7 +35,7 @@ fun ChooseCharacterScreen(
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db, )
val storageRepository = StorageRepository(application.container.db)
val characterList = remember {
mutableStateOf<List<CharacterDtos.CharacterWithSprites>>(emptyList())
}
@@ -59,7 +57,7 @@ fun ChooseCharacterScreen(
characterList.value = storageRepository.getVBCharacters()
}
else -> {
characterList.value = storageRepository.getAllCharacters().first()
characterList.value = storageRepository.getAllCharacters()
}
}
}
@@ -70,7 +68,7 @@ fun ChooseCharacterScreen(
itemsScreenController.applyItem(itemId, selectedCharacter!!) {
Toast.makeText(
application.applicationContext,
application.getString(R.string.choose_character_item_applied),
"Item applied!",
Toast.LENGTH_SHORT
).show()
navController.popBackStack()
@@ -81,7 +79,7 @@ fun ChooseCharacterScreen(
Scaffold(
topBar = {
TopBanner(
text = stringResource(R.string.choose_character_title),
text = "Choose character",
onBackClick = {
navController.popBackStack()
}
@@ -1,173 +0,0 @@
package com.github.nacabaro.vbhelper.screens.itemsScreen
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.Spacer
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.domain.items.ItemType
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
@Composable
fun ItemDialog(
item: ItemDtos.ItemsWithQuantities,
onClickCancel: () -> Unit,
onClickUse: (() -> Unit)? = null,
onClickPurchase: (() -> Unit)? = null,
) {
Dialog(
onDismissRequest = onClickCancel,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card (
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column (
modifier = Modifier
.padding(16.dp)
) {
Row {
Box(modifier = Modifier) {
// Background image (full size)
Icon(
painter = painterResource(id = getIconResource(item.itemIcon)),
contentDescription = null,
modifier = Modifier
.size(96.dp)
.align(Alignment.Center)
)
Icon(
painter = painterResource(id = getLengthResource(item.itemLength)),
contentDescription = null,
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier
.size(64.dp) // Set the size of the overlay image
.align(Alignment.BottomEnd) // Align to the top end (top-right corner)
)
}
Column (
modifier = Modifier
.padding(16.dp)
) {
Text(
fontSize = MaterialTheme.typography.titleLarge.fontSize,
text = item.name,
modifier = Modifier
.fillMaxWidth()
)
}
}
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
text = item.description,
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
)
if (onClickPurchase != null) {
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
text = stringResource(
R.string.item_dialog_costs_credits,
item.price
),
modifier = Modifier
.fillMaxWidth()
)
}
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
text = stringResource(
R.string.item_dialog_you_have_quantity,
item.quantity
),
modifier = Modifier
.fillMaxWidth()
)
Row (
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
) {
if (onClickUse != null) {
Button(
onClick = onClickUse
) {
Text(stringResource(R.string.item_dialog_use))
}
}
if (onClickPurchase != null) {
Button(
onClick = onClickPurchase
) {
Text(stringResource(R.string.item_dialog_purchase))
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
onClick = onClickCancel
) {
Text(stringResource(R.string.item_dialog_cancel))
}
}
}
}
}
}
@Composable
@Preview(showBackground = true)
fun PreviewItemDialog() {
VBHelperTheme {
ItemDialog(
item = ItemDtos.ItemsWithQuantities(
name = "AP Training x3 (60 min)",
description = "Boosts AP during training (for 60 minutes)",
itemIcon = R.drawable.baseline_attack_24,
itemLength = R.drawable.baseline_60_min_timer,
quantity = 19,
id = 1,
price = 500,
itemType = ItemType.BEITEM
),
onClickUse = { },
onClickCancel = { }
)
}
}
@@ -1,24 +1,36 @@
package com.github.nacabaro.vbhelper.screens.itemsScreen
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.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
@Composable
fun ItemElement(
item: ItemDtos.ItemsWithQuantities,
itemIcon: Int,
lengthIcon: Int,
modifier: Modifier = Modifier,
onClick: (() -> Unit) = { }
) {
@@ -29,7 +41,7 @@ fun ItemElement(
) {
Box(modifier = Modifier.fillMaxSize()) {
Icon(
painter = painterResource(id = getIconResource(item.itemIcon)),
painter = painterResource(id = itemIcon),
contentDescription = null,
modifier = Modifier
.size(96.dp)
@@ -37,14 +49,164 @@ fun ItemElement(
.padding(16.dp)
)
Icon(
painter = painterResource(id = getLengthResource(item.itemLength)),
painter = painterResource(id = lengthIcon),
contentDescription = null,
tint = MaterialTheme.colorScheme.surfaceTint,
modifier = Modifier
.size(48.dp)
.align(Alignment.TopStart)
.size(48.dp) // Set the size of the overlay image
.align(Alignment.TopStart) // Align to the top end (top-right corner)
.padding(8.dp)
)
}
}
}
@Composable
fun ItemDialog(
name: String,
description: String,
itemIcon: Int,
lengthIcon: Int,
amount: Int,
onClickUse: () -> Unit,
onClickCancel: () -> Unit
) {
Dialog(
onDismissRequest = onClickCancel,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card (
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column (
modifier = Modifier
.padding(16.dp)
) {
Row {
Box(modifier = Modifier) {
// Background image (full size)
Icon(
painter = painterResource(id = itemIcon),
contentDescription = null,
modifier = Modifier
.size(96.dp)
.align(Alignment.Center)
)
Icon(
painter = painterResource(id = lengthIcon),
contentDescription = null,
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier
.size(64.dp) // Set the size of the overlay image
.align(Alignment.BottomEnd) // Align to the top end (top-right corner)
)
}
Column (
modifier = Modifier
.padding(16.dp)
) {
Text(
fontSize = MaterialTheme.typography.titleLarge.fontSize,
text = name,
modifier = Modifier
.fillMaxWidth()
)
}
}
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
text = description,
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
)
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
text = "You have $amount of this item",
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
Row (
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
) {
Button(
onClick = onClickUse
) {
Text("Use item")
}
Spacer(modifier = Modifier.size(8.dp))
Button(
onClick = onClickCancel
) {
Text("Cancel")
}
}
}
}
}
}
fun getIconResource(index: Int): Int {
return when (index) {
ItemsScreenControllerImpl.ItemTypes.PPTraining.id -> R.drawable.baseline_agility_24
ItemsScreenControllerImpl.ItemTypes.APTraining.id -> R.drawable.baseline_attack_24
ItemsScreenControllerImpl.ItemTypes.HPTraining.id -> R.drawable.baseline_shield_24
ItemsScreenControllerImpl.ItemTypes.BPTraining.id -> R.drawable.baseline_trophy_24
ItemsScreenControllerImpl.ItemTypes.AllTraining.id -> R.drawable.baseline_arrow_up_24
6 -> R.drawable.baseline_timer_24
7 -> R.drawable.baseline_rank_24
8 -> R.drawable.baseline_vitals_24
else -> R.drawable.baseline_question_mark_24
}
}
fun getLengthResource(index: Int): Int {
return when (index) {
15 -> R.drawable.baseline_15_min_timer
30 -> R.drawable.baseline_30_min_timer
60 -> R.drawable.baseline_60_min_timer
-60 -> R.drawable.baseline_60_min_timer
300 -> R.drawable.baseline_5_hour_timer
600 -> R.drawable.baseline_10_hour_timer
-720 -> R.drawable.baseline_12_hour_timer
-1440 -> R.drawable.baseline_24_hour_timer
6000 -> R.drawable.baseline_reset_24
1000 -> R.drawable.baseline_single_arrow_up
2500 -> R.drawable.baseline_double_arrow_up
5000 -> R.drawable.baseline_triple_arrow_up
9999 -> R.drawable.baseline_health_24
-500 -> R.drawable.baseline_single_arrow_down
-1000 -> R.drawable.baseline_double_arrow_down
-2500 -> R.drawable.baseline_triple_arrow_down
-9999 -> R.drawable.baseline_reset_24
else -> R.drawable.baseline_question_mark_24
}
}
@Composable
@Preview(showBackground = true)
fun PreviewItemDialog() {
VBHelperTheme {
ItemDialog(
name = "AP Training x3 (60 min)",
description = "Boosts AP during training (for 60 minutes)",
itemIcon = R.drawable.baseline_attack_24,
lengthIcon = R.drawable.baseline_60_min_timer,
onClickUse = { },
onClickCancel = { },
amount = 19
)
}
}
@@ -1,40 +0,0 @@
package com.github.nacabaro.vbhelper.screens.itemsScreen
import com.github.nacabaro.vbhelper.R
fun getIconResource(index: Int): Int {
return when (index) {
ItemsScreenControllerImpl.ItemTypes.PPTraining.id -> R.drawable.baseline_agility_24
ItemsScreenControllerImpl.ItemTypes.APTraining.id -> R.drawable.baseline_attack_24
ItemsScreenControllerImpl.ItemTypes.HPTraining.id -> R.drawable.baseline_shield_24
ItemsScreenControllerImpl.ItemTypes.BPTraining.id -> R.drawable.baseline_trophy_24
ItemsScreenControllerImpl.ItemTypes.AllTraining.id -> R.drawable.baseline_arrow_up_24
6 -> R.drawable.baseline_timer_24
7 -> R.drawable.baseline_rank_24
8 -> R.drawable.baseline_vitals_24
else -> R.drawable.baseline_question_mark_24
}
}
fun getLengthResource(index: Int): Int {
return when (index) {
15 -> R.drawable.baseline_15_min_timer
30 -> R.drawable.baseline_30_min_timer
60 -> R.drawable.baseline_60_min_timer
-60 -> R.drawable.baseline_60_min_timer
300 -> R.drawable.baseline_5_hour_timer
600 -> R.drawable.baseline_10_hour_timer
-720 -> R.drawable.baseline_12_hour_timer
-1440 -> R.drawable.baseline_24_hour_timer
6000 -> R.drawable.baseline_reset_24
1000 -> R.drawable.baseline_single_arrow_up
2500 -> R.drawable.baseline_double_arrow_up
5000 -> R.drawable.baseline_triple_arrow_up
9999 -> R.drawable.baseline_health_24
-500 -> R.drawable.baseline_single_arrow_down
-1000 -> R.drawable.baseline_double_arrow_down
-2500 -> R.drawable.baseline_triple_arrow_down
-9999 -> R.drawable.baseline_reset_24
else -> R.drawable.baseline_question_mark_24
}
}
@@ -17,9 +17,6 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
@Composable
fun ItemsScreen(
@@ -33,14 +30,14 @@ fun ItemsScreen(
Scaffold(
topBar = {
Column {
TopBanner(text = stringResource(R.string.items_title))
TopBanner("Items")
TabRow(
selectedTabIndex = selectedTabItem,
modifier = Modifier
) {
items.forEachIndexed { index, item ->
Tab(
text = { Text(text = stringResource(item.label)) },
text = { Text(item.label) },
selected = selectedTabItem == index,
onClick = { selectedTabItem = index }
)
@@ -11,8 +11,6 @@ import com.github.nacabaro.vbhelper.domain.device_data.VBCharacterData
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.utils.DeviceType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -54,16 +52,9 @@ class ItemsScreenControllerImpl (
var vbCharacterData: VBCharacterData? = null
if (characterData.characterType == DeviceType.BEDevice) {
beCharacterData = database
.userCharacterDao()
.getBeData(characterId)
.firstOrNull()
beCharacterData = database.userCharacterDao().getBeData(characterId)
} else if (characterData.characterType == DeviceType.VBDevice) {
vbCharacterData = database
.userCharacterDao()
.getVbData(characterId)
.firstOrNull()
vbCharacterData = database.userCharacterDao().getVbData(characterId)
}
if (
@@ -162,35 +153,33 @@ class ItemsScreenControllerImpl (
ItemTypes.Win4.id -> 4
else -> 0
}
val specialMissionSlot = when (itemIcon) {
ItemTypes.Step8k.id -> 0
ItemTypes.Step4k.id -> 0
ItemTypes.Vitals1000.id -> 1
ItemTypes.Vitals250.id -> 1
ItemTypes.Battle20.id -> 2
ItemTypes.Battle5.id -> 2
ItemTypes.Win10.id -> 3
ItemTypes.Win4.id -> 3
else -> 0
}
val availableSpecialMissions = database
.userCharacterDao()
.getSpecialMissions(characterId)
.first()
var newSpecialMission = availableSpecialMissions[specialMissionSlot]
newSpecialMission = SpecialMissions(
id = newSpecialMission.id,
characterId = newSpecialMission.characterId,
var firstUnavailableMissionSlot: Long = 0
var watchId = 0
for ((index, mission) in availableSpecialMissions.withIndex()) {
if (
mission.status == SpecialMission.Status.UNAVAILABLE
) {
firstUnavailableMissionSlot = mission.id
watchId = index + 1
}
}
val newSpecialMission = SpecialMissions(
id = firstUnavailableMissionSlot,
characterId = characterId,
missionType = specialMissionType,
goal = specialMissionGoal,
watchId = newSpecialMission.watchId,
progress = 0,
status = SpecialMission.Status.AVAILABLE,
timeElapsedInMinutes = 0,
timeLimitInMinutes = itemLength,
missionType = specialMissionType
watchId = watchId,
status = SpecialMission.Status.AVAILABLE,
progress = 0,
timeElapsedInMinutes = 0
)
database
@@ -1,37 +1,25 @@
package com.github.nacabaro.vbhelper.screens.itemsScreen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.source.CurrencyRepository
import com.github.nacabaro.vbhelper.source.ItemsRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import com.github.nacabaro.vbhelper.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun ItemsStore(
@@ -39,102 +27,45 @@ fun ItemsStore(
) {
val application = LocalContext.current.applicationContext as VBHelper
val itemsRepository = ItemsRepository(application.container.db)
val myItems by itemsRepository.getAllItems().collectAsState(emptyList())
val myItems = remember { mutableStateOf(emptyList<ItemDtos.ItemsWithQuantities>()) }
var selectedElementIndex by remember { mutableStateOf<Int?>(null) }
val currencyRepository = application.container.currencyRepository
val currentCurrency = currencyRepository.currencyValue.collectAsState(0)
val scope = rememberCoroutineScope()
if (myItems.isEmpty()) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(stringResource(R.string.items_no_items))
LaunchedEffect(itemsRepository) {
withContext(Dispatchers.IO) {
myItems.value = itemsRepository.getAllItems()
}
}
if (myItems.value.isEmpty()) {
Text("No items")
} else {
Column() {
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Text(
text = stringResource(
R.string.items_store_credits,
currentCurrency.value
),
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
) {
items(myItems.value) { index ->
ItemElement(
itemIcon = getIconResource(index.itemIcon),
lengthIcon = getLengthResource(index.itemLength),
modifier = Modifier
.padding(8.dp)
.padding(8.dp),
onClick = {
selectedElementIndex = myItems.value.indexOf(index)
}
)
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
) {
items(myItems) { index ->
ItemElement(
item = index,
modifier = Modifier
.padding(8.dp),
onClick = {
selectedElementIndex = myItems.indexOf(index)
}
)
}
}
if (selectedElementIndex != null) {
ItemDialog(
name = myItems.value[selectedElementIndex!!].name,
description = myItems.value[selectedElementIndex!!].description,
itemIcon = getIconResource(myItems.value[selectedElementIndex!!].itemIcon),
lengthIcon = getLengthResource(myItems.value[selectedElementIndex!!].itemLength),
amount = myItems.value[selectedElementIndex!!].quantity,
onClickUse = { },
onClickCancel = { selectedElementIndex = null }
)
}
}
if (selectedElementIndex != null) {
ItemDialog(
item = myItems[selectedElementIndex!!],
onClickPurchase = {
scope.launch {
Toast.makeText(
application.applicationContext,
application.getString(
purchaseItem(
application.container.db,
myItems[selectedElementIndex!!],
currencyRepository
)
),
Toast.LENGTH_SHORT
).show()
}
},
onClickCancel = { selectedElementIndex = null }
)
}
}
suspend fun purchaseItem(
db: AppDatabase,
item: ItemDtos.ItemsWithQuantities,
currencyRepository: CurrencyRepository
): Int {
return if (currencyRepository.currencyValue.first() < item.price) {
R.string.items_not_enough_credits
} else {
db
.itemDao()
.purchaseItem(
item.id,
1
)
currencyRepository
.setCurrencyValue(
currencyRepository.currencyValue.first() - item.price
)
R.string.items_purchase_success
}
}
@@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -17,14 +17,14 @@ 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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.dtos.ItemDtos
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.source.ItemsRepository
import com.github.nacabaro.vbhelper.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun MyItems(
@@ -32,30 +32,36 @@ fun MyItems(
) {
val application = LocalContext.current.applicationContext as VBHelper
val itemsRepository = ItemsRepository(application.container.db)
val myItems by itemsRepository.getUserItems().collectAsState(emptyList())
val myItems = remember { mutableStateOf(emptyList<ItemDtos.ItemsWithQuantities>()) }
var selectedElementIndex by remember { mutableStateOf<Int?>(null) }
if (myItems.isEmpty()) {
LaunchedEffect(itemsRepository) {
withContext(Dispatchers.IO) {
myItems.value = itemsRepository.getUserItems()
}
}
if (myItems.value.isEmpty()) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(stringResource(R.string.items_no_items))
Text("No items")
}
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
) {
items(myItems) { index ->
items(myItems.value) { index ->
ItemElement(
item = index,
itemIcon = getIconResource(index.itemIcon),
lengthIcon = getLengthResource(index.itemLength),
modifier = Modifier
.padding(8.dp),
onClick = {
selectedElementIndex = myItems.indexOf(index)
selectedElementIndex = myItems.value.indexOf(index)
}
)
}
@@ -63,7 +69,11 @@ fun MyItems(
if (selectedElementIndex != null) {
ItemDialog(
item = myItems[selectedElementIndex!!],
name = myItems.value[selectedElementIndex!!].name,
description = myItems.value[selectedElementIndex!!].description,
itemIcon = getIconResource(myItems.value[selectedElementIndex!!].itemIcon),
lengthIcon = getLengthResource(myItems.value[selectedElementIndex!!].itemLength),
amount = myItems.value[selectedElementIndex!!].quantity,
onClickUse = {
navController
.navigate(
@@ -71,7 +81,7 @@ fun MyItems(
.ApplyItem.route
.replace(
"{itemId}",
myItems[selectedElementIndex!!].id.toString()
myItems.value[selectedElementIndex!!].id.toString()
)
)
selectedElementIndex = null
@@ -19,13 +19,10 @@ 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
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
@Composable
fun ObtainedItemDialog(
obtainedItem: ItemDtos.PurchasedItem,
obtainedCurrency: Int,
onClickDismiss: () -> Unit
) {
Dialog(
@@ -84,32 +81,17 @@ fun ObtainedItemDialog(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
text = stringResource(
R.string.obtained_item_you_have,
obtainedItem.itemAmount
),
text = "You have obtained ${obtainedItem.itemAmount} of this item",
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp)
)
Text(
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
text = stringResource(
R.string.obtained_item_you_also_got_credits,
obtainedCurrency
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp)
.padding(5.dp)
)
Button(
onClick = onClickDismiss,
modifier = Modifier
.fillMaxWidth()
) {
Text(text = stringResource(R.string.obtained_item_dismiss))
Text(text = "Dismiss")
}
}
}
@@ -1,80 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
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.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.R
@Composable
fun ChooseConnectOption(
onClickRead: (() -> Unit)? = null,
onClickWrite: (() -> Unit)? = null,
navController: NavController
) {
Scaffold(
topBar = {
TopBanner(
text = stringResource(R.string.scan_title),
onBackClick = {
navController.popBackStack()
}
)
}
) { contentPadding ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
ScanButton(
text = stringResource(R.string.scan_vb_to_app),
disabled = onClickRead == null,
onClick = onClickRead?: { },
)
Spacer(modifier = Modifier.height(16.dp))
ScanButton(
text = stringResource(R.string.scan_app_to_vb),
disabled = onClickWrite == null,
onClick = onClickWrite?: { },
)
}
}
}
@Composable
fun ScanButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = !disabled,
) {
Text(
text = text,
fontSize = 16.sp,
modifier = Modifier
.padding(4.dp)
)
}
}
@@ -1,4 +1,4 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
package com.github.nacabaro.vbhelper.screens.scanScreen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -10,22 +10,17 @@ 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.stringResource
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.R
@Composable
fun ActionScreen(
fun ReadingCharacterScreen(
topBannerText: String,
onClickCancel: () -> Unit,
) {
Scaffold (
topBar = {
TopBanner(
text = topBannerText,
onBackClick = onClickCancel
)
TopBanner(topBannerText)
}
) { innerPadding ->
Column (
@@ -35,12 +30,12 @@ fun ActionScreen(
.padding(innerPadding)
.fillMaxSize()
) {
Text(stringResource(R.string.action_place_near_reader))
Text("Place your Vital Bracelet near the reader...")
Button(
onClick = onClickCancel,
modifier = Modifier.padding(16.dp)
) {
Text(stringResource(R.string.action_cancel))
Text("Cancel")
}
}
}
@@ -1,31 +1,44 @@
package com.github.nacabaro.vbhelper.screens.scanScreen
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.screens.scanScreen.screens.ReadingScreen
import com.github.nacabaro.vbhelper.screens.scanScreen.screens.WritingScreen
import com.github.nacabaro.vbhelper.screens.cardScreen.ChooseCard
import com.github.nacabaro.vbhelper.source.StorageRepository
import com.github.nacabaro.vbhelper.source.isMissingSecrets
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import com.github.nacabaro.vbhelper.R
const val SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER = "SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER"
@@ -42,6 +55,8 @@ fun ScanScreen(
val storageRepository = StorageRepository(application.container.db)
var nfcCharacter by remember { mutableStateOf<NfcCharacter?>(null) }
var cardsRead by remember { mutableStateOf<List<Card>?>(null) }
val context = LocalContext.current
LaunchedEffect(storageRepository) {
@@ -58,33 +73,143 @@ fun ScanScreen(
}
}
var writingScreen by remember { mutableStateOf(false) }
var readingScreen by remember { mutableStateOf(false) }
var writingScreen by remember { mutableStateOf(false) }
var cardSelectScreen by remember { mutableStateOf(false) }
var isDoneReadingCharacter by remember { mutableStateOf(false) }
var isDoneSendingCard by remember { mutableStateOf(false) }
var isDoneWritingCharacter by remember { mutableStateOf(false) }
if (writingScreen && nfcCharacter != null && characterId != null) {
WritingScreen(
scanScreenController = scanScreenController,
nfcCharacter = nfcCharacter!!,
characterId = characterId,
onComplete = {
writingScreen = false
navController.navigate(NavigationItems.Home.route)
},
onCancel = {
writingScreen = false
navController.navigate(NavigationItems.Home.route)
DisposableEffect(readingScreen) {
if(readingScreen) {
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object: ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
scanScreenController.onClickRead(
secrets = secrets!!,
onComplete = {
isDoneReadingCharacter = true
},
onMultipleCards = { cards ->
cardsRead = cards
readingScreen = false
cardSelectScreen = true
isDoneReadingCharacter = true
}
)
}
}
)
scanScreenController.onClickRead(
secrets = secrets!!,
onComplete = {
isDoneReadingCharacter = true
},
onMultipleCards = { cards ->
cardsRead = cards
readingScreen = false
cardSelectScreen = true
isDoneReadingCharacter = true
}
)
}
onDispose {
if(readingScreen) {
scanScreenController.unregisterActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
)
scanScreenController.cancelRead()
}
)
} else if (readingScreen) {
ReadingScreen(
scanScreenController = scanScreenController,
onCancel = {
readingScreen = false
navController.navigate(NavigationItems.Home.route)
},
onComplete = {
readingScreen = false
navController.navigate(NavigationItems.Home.route)
}
}
DisposableEffect(writingScreen, isDoneSendingCard) {
if (writingScreen) {
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object : ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter!!) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter!!) {
isDoneWritingCharacter = true
}
}
}
}
)
}
if (secrets != null && nfcCharacter != null) {
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter!!) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter!!) {
isDoneWritingCharacter = true
}
}
}
onDispose {
if(writingScreen) {
scanScreenController.unregisterActivityLifecycleListener(SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER)
scanScreenController.cancelRead()
}
}
}
if (isDoneReadingCharacter && !cardSelectScreen) {
readingScreen = false
navController.navigate(NavigationItems.Home.route)
} else if (isDoneSendingCard && isDoneWritingCharacter) {
writingScreen = false
navController.navigate(NavigationItems.Home.route)
LaunchedEffect(storageRepository) {
withContext(Dispatchers.IO) {
storageRepository
.deleteCharacter(characterId!!)
}
}
}
if (readingScreen) {
ReadingCharacterScreen("Reading character") {
readingScreen = false
scanScreenController.cancelRead()
}
} else if (writingScreen) {
if (!isDoneSendingCard) {
ReadingCharacterScreen("Sending card") {
writingScreen = false
scanScreenController.cancelRead()
}
} else if (!isDoneWritingCharacter) {
ReadingCharacterScreen("Writing character") {
isDoneSendingCard = false
writingScreen = false
scanScreenController.cancelRead()
}
}
} else if (cardSelectScreen) {
ChooseCard(
cards = cardsRead!!,
onCardSelected = { card ->
cardSelectScreen = false
scanScreenController.flushCharacter(card.id)
}
)
} else {
@@ -94,9 +219,9 @@ fun ScanScreen(
else -> {
{
if(secrets == null) {
Toast.makeText(context, context.getString(R.string.scan_secrets_not_initialized), Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
} else if(secrets?.isMissingSecrets() == true) {
Toast.makeText(context, context.getString(R.string.scan_secrets_not_imported), Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
} else {
readingScreen = true // kicks off nfc adapter in DisposableEffect
}
@@ -108,9 +233,9 @@ fun ScanScreen(
else -> {
{
if(secrets == null) {
Toast.makeText(context, context.getString(R.string.scan_secrets_not_initialized), Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Secrets is not yet initialized. Try again.", Toast.LENGTH_SHORT).show()
} else if(secrets?.isMissingSecrets() == true) {
Toast.makeText(context, context.getString(R.string.scan_secrets_not_imported), Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Secrets not yet imported. Go to Settings and Import APK", Toast.LENGTH_SHORT).show()
} else {
writingScreen = true // kicks off nfc adapter in DisposableEffect
}
@@ -122,8 +247,66 @@ fun ScanScreen(
}
}
@Composable
fun ChooseConnectOption(
onClickRead: (() -> Unit)? = null,
onClickWrite: (() -> Unit)? = null,
navController: NavController
) {
Scaffold(
topBar = {
TopBanner(
text = "Scan a Vital Bracelet",
onBackClick = {
navController.popBackStack()
}
)
}
) { contentPadding ->
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
ScanButton(
text = "Vital Bracelet to App",
disabled = onClickRead == null,
onClick = onClickRead?: { },
)
Spacer(modifier = Modifier.height(16.dp))
ScanButton(
text = "App to Vital Bracelet",
disabled = onClickWrite == null,
onClick = onClickWrite?: { },
)
}
}
}
@Composable
fun ScanButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
disabled: Boolean = false,
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = !disabled,
) {
Text(
text = text,
fontSize = 16.sp,
modifier = Modifier
.padding(4.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun ScanScreenPreview() {
@@ -25,7 +25,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import com.github.nacabaro.vbhelper.R
class ScanScreenControllerImpl(
override val secretsFlow: Flow<Secrets>,
@@ -39,7 +38,7 @@ class ScanScreenControllerImpl(
init {
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(componentActivity)
if (maybeNfcAdapter == null) {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_no_nfc_on_device), Toast.LENGTH_SHORT).show()
Toast.makeText(componentActivity, "No NFC on device!", Toast.LENGTH_SHORT).show()
}
nfcAdapter = maybeNfcAdapter
checkSecrets()
@@ -95,7 +94,7 @@ class ScanScreenControllerImpl(
val nfcData = NfcA.get(tag)
if (nfcData == null) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_tag_not_vb), Toast.LENGTH_SHORT).show()
Toast.makeText(componentActivity, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
@@ -113,7 +112,7 @@ class ScanScreenControllerImpl(
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
if(secretsFlow.stateIn(componentActivity.lifecycleScope).value.isMissingSecrets()) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_missing_secrets), Toast.LENGTH_SHORT).show()
Toast.makeText(componentActivity, "Missing Secrets. Go to settings and import Vital Arena APK", Toast.LENGTH_SHORT).show()
}
}
}
@@ -136,10 +135,10 @@ class ScanScreenControllerImpl(
tagCommunicator.sendCharacter(castNfcCharacter)
}
onComplete.invoke()
componentActivity.getString(R.string.scan_sent_character_success)
"Sent character successfully!"
} catch (e: Throwable) {
Log.e("TAG", e.stackTraceToString())
componentActivity.getString(R.string.scan_error_generic)
"Whoops"
}
}
}
@@ -152,13 +151,13 @@ class ScanScreenControllerImpl(
handleTag(secrets) { tagCommunicator ->
tagCommunicator.prepareDIMForCharacter(nfcCharacter.dimId)
onComplete.invoke()
componentActivity.getString(R.string.scan_sent_dim_success)
"Sent DIM successfully!"
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
Toast.makeText(componentActivity, componentActivity.getString(R.string.scan_nfc_must_be_enabled), Toast.LENGTH_SHORT).show()
Toast.makeText(componentActivity, "NFC must be enabled", Toast.LENGTH_SHORT).show()
componentActivity.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
@@ -1,11 +1,13 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.converters
import android.util.Log
import androidx.activity.ComponentActivity
import com.github.cfogrady.vbnfc.be.BENfcCharacter
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.vb.VBNfcCharacter
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.domain.device_data.BECharacterData
import com.github.nacabaro.vbhelper.domain.device_data.SpecialMissions
import com.github.nacabaro.vbhelper.domain.device_data.UserCharacter
@@ -150,13 +152,15 @@ class FromNfcConverter (
nfcCharacter: NfcCharacter,
cardData: Card
) {
val currentCardProgress = CardProgress(
cardId = cardData.id,
currentStage = nfcCharacter.nextAdventureMissionStage.toInt(),
unlocked = nfcCharacter.nextAdventureMissionStage.toInt() > cardData.stageCount
)
database
.cardProgressDao()
.updateCardProgress(
currentStage = nfcCharacter.nextAdventureMissionStage.toInt(),
cardId = cardData.id,
unlocked = nfcCharacter.nextAdventureMissionStage.toInt() > cardData.stageCount,
)
.updateDimProgress(currentCardProgress)
}
@@ -248,11 +252,10 @@ class FromNfcConverter (
) {
val vitalsHistoryWatch = nfcCharacter.vitalHistory
val vitalsHistory = vitalsHistoryWatch.map { historyElement ->
val year = if (historyElement.year.toInt() !in 2021 .. 2035) 0 else historyElement.year.toInt()
Log.d("VitalsHistory", "${historyElement.year.toInt()} ${historyElement.month.toInt()} ${historyElement.day.toInt()}")
VitalsHistory(
charId = characterId,
year = year,
year = historyElement.year.toInt(),
month = historyElement.month.toInt(),
day = historyElement.day.toInt(),
vitalPoints = historyElement.vitalsGained.toInt()
@@ -14,7 +14,6 @@ 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.utils.DeviceType
import kotlinx.coroutines.flow.first
import java.util.Date
class ToNfcConverter(
@@ -39,10 +38,14 @@ class ToNfcConverter(
.characterDao()
.getCharacterInfo(userCharacter.charId)
val currentCardStage = database
.cardProgressDao()
.getCardProgress(characterInfo.cardId)
return if (userCharacter.characterType == DeviceType.BEDevice)
nfcToBENfc(characterId, characterInfo, userCharacter)
nfcToBENfc(characterId, characterInfo, currentCardStage, userCharacter)
else
nfcToVBNfc(characterId, characterInfo, userCharacter)
nfcToVBNfc(characterId, characterInfo, currentCardStage, userCharacter)
}
@@ -50,12 +53,12 @@ class ToNfcConverter(
private suspend fun nfcToVBNfc(
characterId: Long,
characterInfo: CharacterDtos.CardCharacterInfo,
currentCardStage: Int,
userCharacter: UserCharacter
): VBNfcCharacter {
val vbData = database
.userCharacterDao()
.getVbData(characterId)
.first()
val paddedTransformationArray = generateTransformationHistory(characterId, 9)
@@ -67,7 +70,7 @@ class ToNfcConverter(
stage = characterInfo.stage.toByte(),
attribute = characterInfo.attribute,
ageInDays = userCharacter.ageInDays.toByte(),
nextAdventureMissionStage = characterInfo.currentStage.toByte(),
nextAdventureMissionStage = currentCardStage.toByte(),
mood = userCharacter.mood.toByte(),
vitalPoints = userCharacter.vitalPoints.toUShort(),
transformationCountdownInMinutes = userCharacter.transformationCountdown.toUShort(),
@@ -98,7 +101,6 @@ class ToNfcConverter(
val cardData = database
.cardDao()
.getCardByCharacterId(userCharacter.id)
.first()
val appReserved = Array<UShort>(3) {
0u
@@ -117,7 +119,6 @@ class ToNfcConverter(
val specialMissions = database
.userCharacterDao()
.getSpecialMissions(characterId)
.first()
val watchSpecialMissions = specialMissions.map {
SpecialMission(
@@ -172,12 +173,12 @@ class ToNfcConverter(
private suspend fun nfcToBENfc(
characterId: Long,
characterInfo: CharacterDtos.CardCharacterInfo,
currentCardStage: Int,
userCharacter: UserCharacter
): BENfcCharacter {
val beData = database
.userCharacterDao()
.getBeData(characterId)
.first()
val paddedTransformationArray = generateTransformationHistory(characterId)
@@ -187,7 +188,7 @@ class ToNfcConverter(
stage = characterInfo.stage.toByte(),
attribute = characterInfo.attribute,
ageInDays = userCharacter.ageInDays.toByte(),
nextAdventureMissionStage = characterInfo.currentStage.toByte(),
nextAdventureMissionStage = currentCardStage.toByte(),
mood = userCharacter.mood.toByte(),
vitalPoints = userCharacter.vitalPoints.toUShort(),
itemEffectMentalStateValue = beData.itemEffectMentalStateValue.toByte(),
@@ -240,8 +241,7 @@ class ToNfcConverter(
): Array<NfcCharacter.Transformation> {
val transformationHistory = database
.userCharacterDao()
.getTransformationHistory(characterId)
.first()
.getTransformationHistory(characterId)!!
.map {
val date = Date(it.transformationDate)
val calendar = android.icu.util.GregorianCalendar(TimeZone.getTimeZone("UTC"))
@@ -1,61 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
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.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.R
@Composable
fun ReadCharacterScreen(
onClickCancel: () -> Unit,
onClickConfirm: () -> Unit
) {
Scaffold(
topBar = {
TopBanner(
text = stringResource(R.string.read_character_title),
onBackClick = onClickCancel
)
}
) { innerPadding ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
Text(
text = stringResource(R.string.read_character_prepare_device),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.read_character_go_to_connect),
textAlign = TextAlign.Center
)
Spacer(
modifier = Modifier.padding(8.dp)
)
Button(
onClick = onClickConfirm,
) {
Text(stringResource(R.string.read_character_confirm))
}
}
}
}
@@ -1,112 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.screens.cardScreen.ChooseCard
import com.github.nacabaro.vbhelper.screens.scanScreen.SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenController
import com.github.nacabaro.vbhelper.R
@Composable
fun ReadingScreen(
scanScreenController: ScanScreenController,
onCancel: () -> Unit,
onComplete: () -> Unit
) {
val secrets by scanScreenController.secretsFlow.collectAsState(null)
var cardsRead by remember { mutableStateOf<List<Card>?>(null) }
var readingScreen by remember { mutableStateOf(false) }
var isDoneReadingCharacter by remember { mutableStateOf(false) }
var cardSelectScreen by remember { mutableStateOf(false) }
DisposableEffect(readingScreen) {
if(readingScreen) {
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object: ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
scanScreenController.onClickRead(
secrets = secrets!!,
onComplete = {
isDoneReadingCharacter = true
},
onMultipleCards = { cards ->
cardsRead = cards
readingScreen = false
cardSelectScreen = true
isDoneReadingCharacter = true
}
)
}
}
)
scanScreenController.onClickRead(
secrets = secrets!!,
onComplete = {
isDoneReadingCharacter = true
},
onMultipleCards = { cards ->
cardsRead = cards
readingScreen = false
cardSelectScreen = true
isDoneReadingCharacter = true
}
)
}
onDispose {
if(readingScreen) {
scanScreenController.unregisterActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
)
scanScreenController.cancelRead()
}
}
}
if (isDoneReadingCharacter && !cardSelectScreen) {
readingScreen = false
onComplete()
}
if (!readingScreen) {
ReadCharacterScreen(
onClickConfirm = {
readingScreen = true
},
onClickCancel = {
onCancel()
}
)
}
if (readingScreen) {
ActionScreen(topBannerText = stringResource(R.string.reading_character_title),) {
readingScreen = false
scanScreenController.cancelRead()
onCancel()
}
} else if (cardSelectScreen) {
ChooseCard(
cards = cardsRead!!,
onCardSelected = { card ->
cardSelectScreen = false
scanScreenController.flushCharacter(card.id)
}
)
}
}
@@ -1,142 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.source.ScanRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getImageBitmap
import com.github.nacabaro.vbhelper.R
@Composable
fun WriteCardScreen(
characterId: Long,
onClickCancel: () -> Unit,
onClickConfirm: () -> Unit
) {
val application = LocalContext.current.applicationContext as VBHelper
val database = application.container.db
val scanRepository = ScanRepository(database)
val cardDetails by scanRepository.getCardDetails(characterId).collectAsState(Card(
id = 0,
cardId = 0,
name = "",
logo = byteArrayOf(),
logoHeight = 0,
logoWidth = 0,
stageCount = 0,
isBEm = false
))
Scaffold(
topBar = {
TopBanner(
text = stringResource(R.string.write_card_title),
onBackClick = onClickCancel
)
}
) { innerPadding ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
Card (
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row (
modifier = Modifier.padding(16.dp),
){
if (cardDetails.logoHeight > 0 && cardDetails.logoWidth > 0) {
val charaBitmapData = BitmapData(
bitmap = cardDetails.logo,
width = cardDetails.logoWidth,
height = cardDetails.logoHeight
)
val charaImageBitmapData = charaBitmapData.getImageBitmap(
context = LocalContext.current,
multiplier = 4,
obscure = false
)
Card (
colors = CardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledContentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
)
)
) {
Image(
bitmap = charaImageBitmapData.imageBitmap,
contentDescription = stringResource(
R.string.write_card_icon_description
),
modifier = Modifier
.size(charaImageBitmapData.dpWidth)
.padding(8.dp),
filterQuality = FilterQuality.None
)
}
}
Spacer(
modifier = Modifier.width(8.dp)
)
Column {
Text(stringResource(R.string.write_card_device_ready))
Text(
stringResource(
R.string.write_card_required_card,
cardDetails.name
)
)
}
}
}
Button(
onClick = onClickConfirm,
) {
Text(stringResource(R.string.write_card_confirm))
}
}
}
}
@@ -1,138 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.source.ScanRepository
import com.github.nacabaro.vbhelper.utils.BitmapData
import com.github.nacabaro.vbhelper.utils.getImageBitmap
import com.github.nacabaro.vbhelper.R
@Composable
fun WriteCharacterScreen(
characterId: Long,
onClickCancel: () -> Unit,
onClickConfirm: () -> Unit
) {
val application = LocalContext.current.applicationContext as VBHelper
val database = application.container.db
val scanRepository = ScanRepository(database)
val cardDetails by scanRepository.getCardDetails(characterId).collectAsState(Card(
id = 0,
cardId = 0,
name = "",
logo = byteArrayOf(),
logoHeight = 0,
logoWidth = 0,
stageCount = 0,
isBEm = false
))
Scaffold(
topBar = {
TopBanner(
text = stringResource(R.string.write_character_title),
onBackClick = onClickCancel
)
}
) { innerPadding ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
Card (
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row (
modifier = Modifier.padding(16.dp),
){
if (cardDetails.logoHeight > 0 && cardDetails.logoWidth > 0) {
val charaBitmapData = BitmapData(
bitmap = cardDetails.logo,
width = cardDetails.logoWidth,
height = cardDetails.logoHeight
)
val charaImageBitmapData = charaBitmapData.getImageBitmap(
context = LocalContext.current,
multiplier = 4,
obscure = false
)
Card (
colors = CardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
disabledContentColor = MaterialTheme.colorScheme.contentColorFor(
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
)
)
) {
Image(
bitmap = charaImageBitmapData.imageBitmap,
contentDescription = stringResource(R.string.write_character_icon_description),
modifier = Modifier
.size(charaImageBitmapData.dpWidth)
.padding(8.dp),
filterQuality = FilterQuality.None
)
}
}
Spacer(
modifier = Modifier.width(8.dp)
)
Column {
Text(stringResource(R.string.write_character_success))
Text(stringResource(R.string.write_character_wait_ready))
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onClickConfirm,
) {
Text(stringResource(R.string.write_character_confirm))
}
}
}
}
@@ -1,142 +0,0 @@
package com.github.nacabaro.vbhelper.screens.scanScreen.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.ActivityLifecycleListener
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.screens.scanScreen.SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
import com.github.nacabaro.vbhelper.screens.scanScreen.ScanScreenController
import com.github.nacabaro.vbhelper.source.StorageRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
@Composable
fun WritingScreen(
scanScreenController: ScanScreenController,
characterId: Long,
nfcCharacter: NfcCharacter,
onComplete: () -> Unit,
onCancel: () -> Unit,
) {
val secrets by scanScreenController.secretsFlow.collectAsState(null)
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
var writing by remember { mutableStateOf(false) }
var writingScreen by remember { mutableStateOf(false) }
var writingConfirmScreen by remember { mutableStateOf(false) }
var isDoneSendingCard by remember { mutableStateOf(false) }
var isDoneWritingCharacter by remember { mutableStateOf(false) }
DisposableEffect(writing) {
if (writing) {
scanScreenController.registerActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER,
object : ActivityLifecycleListener {
override fun onPause() {
scanScreenController.cancelRead()
}
override fun onResume() {
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter) {
isDoneWritingCharacter = true
}
}
}
}
)
if (secrets != null) {
if (!isDoneSendingCard) {
scanScreenController.onClickCheckCard(secrets!!, nfcCharacter) {
isDoneSendingCard = true
}
} else if (!isDoneWritingCharacter) {
scanScreenController.onClickWrite(secrets!!, nfcCharacter) {
isDoneWritingCharacter = true
}
}
}
}
onDispose {
if (writing) {
scanScreenController.unregisterActivityLifecycleListener(
SCAN_SCREEN_ACTIVITY_LIFECYCLE_LISTENER
)
scanScreenController.cancelRead()
}
}
}
if (!writingScreen) {
writing = false
WriteCardScreen (
characterId = characterId,
onClickCancel = {
scanScreenController.cancelRead()
onCancel()
},
onClickConfirm = {
writingScreen = true
}
)
} else if (!isDoneSendingCard) {
writing = true
ActionScreen( topBannerText = stringResource(R.string.sending_card_title)) {
scanScreenController.cancelRead()
onCancel()
}
} else if (!writingConfirmScreen) {
writing = false
WriteCharacterScreen (
characterId = characterId,
onClickCancel = {
scanScreenController.cancelRead()
onCancel()
},
onClickConfirm = {
writingConfirmScreen = true
}
)
} else if (!isDoneWritingCharacter) {
writing = true
ActionScreen(topBannerText = stringResource(R.string.writing_character_action_title)) {
isDoneSendingCard = false
scanScreenController.cancelRead()
onCancel()
}
}
var completedWriting by remember { mutableStateOf(false) }
LaunchedEffect(isDoneSendingCard, isDoneWritingCharacter) {
withContext(Dispatchers.IO) {
if (isDoneSendingCard && isDoneWritingCharacter) {
storageRepository
.deleteCharacter(characterId)
completedWriting = true
}
}
}
if (completedWriting) {
onComplete()
}
}
@@ -6,10 +6,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.R
import com.github.nacabaro.vbhelper.navigation.NavigationItems
@Composable
fun CreditsScreen(
@@ -18,9 +17,12 @@ fun CreditsScreen(
Scaffold (
topBar = {
TopBanner(
text = stringResource(R.string.credits_title),
text = "Credits",
onBackClick = {
navController.popBackStack()
},
onGearClick = {
navController.navigate(NavigationItems.Viewer.route)
}
)
},
@@ -31,13 +33,13 @@ fun CreditsScreen(
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
) {
SettingsSection(stringResource(R.string.credits_section_reverse_engineering))
SettingsEntry(title = "cyanic", description = stringResource(R.string.credits_cyanic_description)) { }
SettingsSection(stringResource(R.string.credits_section_app_development))
SettingsEntry(title = "cfogrady", description = stringResource(R.string.credits_cfogrady_description)) { }
SettingsEntry(title = "nacabaro", description = stringResource(R.string.credits_nacabaro_description)) { }
SettingsEntry(title = "lightheel", description = stringResource(R.string.credits_lightheel_description)) { }
SettingsEntry(title = "shvstrz", description = stringResource(R.string.credits_shvstrz_description)) { }
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.") { }
SettingsEntry(title = "shvstrz", description = "Designing the app icon in SVG.") { }
}
}
}
@@ -1,7 +1,5 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -15,28 +13,22 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.github.nacabaro.vbhelper.components.TopBanner
import com.github.nacabaro.vbhelper.navigation.NavigationItems
import com.github.nacabaro.vbhelper.R
@Composable
fun SettingsScreen(
navController: NavController,
settingsScreenController: SettingsScreenControllerImpl,
) {
val context = LocalContext.current
Scaffold(
Scaffold (
topBar = {
TopBanner(
text = stringResource(R.string.settings_title),
text = "Settings",
onBackClick = {
navController.popBackStack()
}
@@ -45,57 +37,31 @@ fun SettingsScreen(
modifier = Modifier
.fillMaxSize()
) { contentPadding ->
Column(
Column (
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SettingsSection(title = stringResource(R.string.settings_section_nfc))
SettingsEntry(
title = stringResource(R.string.settings_import_apk_title),
description = stringResource(R.string.settings_import_apk_desc)
) {
SettingsSection("NFC Communication")
SettingsEntry(title = "Import APK", description = "Import Secrets From Vital Arean 2.1.0 APK") {
settingsScreenController.onClickImportApk()
}
SettingsSection(title = stringResource(R.string.settings_section_dim_bem))
SettingsEntry(
title = stringResource(R.string.settings_import_card_title),
description = stringResource(R.string.settings_import_card_desc)
) {
SettingsSection("DiM/BEm management")
SettingsEntry(title = "Import card", description = "Import DiM/BEm card file") {
settingsScreenController.onClickImportCard()
}
SettingsSection(title = stringResource(R.string.settings_section_about))
SettingsEntry(
title = stringResource(R.string.settings_credits_title),
description = stringResource(R.string.settings_credits_desc)
) {
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 = stringResource(R.string.settings_about_title),
description = stringResource(R.string.settings_about_desc)
) {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://github.com/nacabaro/vbhelper/")
)
context.startActivity(browserIntent)
}
SettingsSection(title = stringResource(R.string.settings_section_data))
SettingsEntry(
title = stringResource(R.string.settings_export_data_title),
description = stringResource(R.string.settings_export_data_desc)
) {
SettingsEntry(title = "About", description = "About") { }
SettingsSection("Data management")
SettingsEntry(title = "Export data", description = "Export application database") {
settingsScreenController.onClickOpenDirectory()
}
SettingsEntry(
title = stringResource(R.string.settings_import_data_title),
description = stringResource(R.string.settings_import_data_desc)
) {
SettingsEntry(title = "Import data", description = "Import application database") {
settingsScreenController.onClickImportDatabase()
}
}
@@ -127,9 +93,9 @@ fun SettingsEntry(
fun SettingsSection(
title: String
) {
Box(
Box (
modifier = Modifier
.padding(start = 16.dp, top = 16.dp, bottom = 4.dp)
.padding(start = 16.dp)
) {
Text(
text = title,
@@ -138,4 +104,4 @@ fun SettingsSection(
color = MaterialTheme.colorScheme.primary
)
}
}
}
@@ -3,24 +3,38 @@ package com.github.nacabaro.vbhelper.screens.settingsScreen
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.github.cfogrady.vb.dim.card.BemCard
import com.github.cfogrady.vb.dim.card.DimCard
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.CardImportController
import com.github.nacabaro.vbhelper.screens.settingsScreen.controllers.DatabaseManagementController
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.domain.card.CardBackground
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.CardSprites
import com.github.nacabaro.vbhelper.source.ApkSecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsImporter
import com.github.nacabaro.vbhelper.source.SecretsRepository
import com.github.nacabaro.vbhelper.source.proto.Secrets
import kotlinx.coroutines.Dispatchers
import java.io.File
import java.io.InputStream
import java.io.OutputStream
class SettingsScreenControllerImpl(
private val context: ComponentActivity,
): SettingsScreenController {
private val roomDbName = "internalDb"
private val filePickerLauncher: ActivityResultLauncher<String>
private val filePickerOpenerLauncher: ActivityResultLauncher<Array<String>>
private val filePickerApk: ActivityResultLauncher<Array<String>>
@@ -29,17 +43,13 @@ class SettingsScreenControllerImpl(
private val application = context.applicationContext as VBHelper
private val secretsRepository: SecretsRepository = application.container.dataStoreSecretsRepository
private val database: AppDatabase = application.container.db
private val databaseManagementController = DatabaseManagementController(
componentActivity = context,
application = application
)
init {
filePickerLauncher = context.registerForActivityResult(
ActivityResultContracts.CreateDocument("application/octet-stream")
) { uri ->
if (uri != null) {
databaseManagementController.exportDatabase(uri)
exportDatabase(uri)
} else {
context.runOnUiThread {
Toast.makeText(context, "No destination selected", Toast.LENGTH_SHORT)
@@ -52,7 +62,7 @@ class SettingsScreenControllerImpl(
ActivityResultContracts.OpenDocument()
) { uri ->
if (uri != null) {
databaseManagementController.importDatabase(uri)
importDatabase(uri)
} else {
context.runOnUiThread {
Toast.makeText(context, "No source selected", Toast.LENGTH_SHORT).show()
@@ -101,14 +111,324 @@ class SettingsScreenControllerImpl(
filePickerCard.launch(arrayOf("*/*"))
}
private suspend fun importEvoData(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
for (index in 0 until card.transformationRequirements.transformationEntries.size) {
val evo = card.transformationRequirements.transformationEntries[index]
var transformationTimerHours: Int
var unlockAdventureLevel: Int
if (card is BemCard) {
transformationTimerHours = card
.transformationRequirements
.transformationEntries[index]
.minutesUntilTransformation / 60
unlockAdventureLevel = if (
card
.transformationRequirements
.transformationEntries[index]
.requiredCompletedAdventureLevel == 65535
) {
0
} else {
card
.transformationRequirements
.transformationEntries[index]
.requiredCompletedAdventureLevel
}
} else {
transformationTimerHours = (card as DimCard)
.transformationRequirements
.transformationEntries[index]
.hoursUntilEvolution
unlockAdventureLevel = if (
card
.adventureLevels
.levels
.last()
.bossCharacterIndex == card.transformationRequirements.transformationEntries[index].toCharacterIndex
) {
14
/*
Magic number incoming!!
In the case of DiMCards, stage 15 is the one that unlocks the locked character.
We know it is a locked character if the last adventure level's boss character index
is the current index. If it is, we add stage 15 complete as a requirement for transformation.
*/
} else {
0
/*
Another magic number...
The rest of the characters are not locked.
*/
}
}
database
.characterDao()
.insertPossibleTransformation(
cardId = cardId,
fromChraraIndex = evo.fromCharacterIndex,
toChraraIndex = evo.toCharacterIndex,
requiredVitals = evo.requiredVitalValues,
requiredTrophies = evo.requiredTrophies,
requiredBattles = evo.requiredBattles,
requiredWinRate = evo.requiredWinRatio,
requiredAdventureLevelCompleted = unlockAdventureLevel,
changeTimerHours = transformationTimerHours
)
}
}
private suspend fun importCharacterData(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
var spriteCounter = when (card is BemCard) {
true -> 54
false -> 10
}
val domainCharacters = mutableListOf<CardCharacter>()
val characters = card
.characterStats
.characterEntries
for (index in 0 until characters.size) {
var domainSprite: Sprite?
if (index < 2 && card is DimCard) {
domainSprite = Sprite(
width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
spriteWalk1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteWalk2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteRun1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteRun2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteTrain1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteTrain2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteHappy = card.spriteData.sprites[spriteCounter + 4].pixelData,
spriteSleep = card.spriteData.sprites[spriteCounter + 5].pixelData,
spriteAttack = card.spriteData.sprites[spriteCounter + 2].pixelData,
spriteDodge = card.spriteData.sprites[spriteCounter + 3].pixelData,
)
} else {
domainSprite = Sprite(
width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
spriteWalk1 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteWalk2 = card.spriteData.sprites[spriteCounter + 4].pixelData,
spriteRun1 = card.spriteData.sprites[spriteCounter + 5].pixelData,
spriteRun2 = card.spriteData.sprites[spriteCounter + 6].pixelData,
spriteTrain1 = card.spriteData.sprites[spriteCounter + 7].pixelData,
spriteTrain2 = card.spriteData.sprites[spriteCounter + 8].pixelData,
spriteHappy = card.spriteData.sprites[spriteCounter + 9].pixelData,
spriteSleep = card.spriteData.sprites[spriteCounter + 10].pixelData,
spriteAttack = card.spriteData.sprites[spriteCounter + 11].pixelData,
spriteDodge = card.spriteData.sprites[spriteCounter + 12].pixelData
)
}
val spriteId = database
.spriteDao()
.insertSprite(domainSprite)
domainCharacters.add(
CardCharacter(
cardId = cardId,
spriteId = spriteId,
charaIndex = index,
nameSprite = card.spriteData.sprites[spriteCounter].pixelData,
stage = characters[index].stage,
attribute = NfcCharacter.Attribute.entries[characters[index].attribute],
baseHp = characters[index].hp,
baseBp = characters[index].dp,
baseAp = characters[index].ap,
nameWidth = card.spriteData.sprites[spriteCounter].spriteDimensions.width,
nameHeight = card.spriteData.sprites[spriteCounter].spriteDimensions.height
)
)
spriteCounter += if (card is BemCard) {
14
} else {
when (index) {
0 -> 6
1 -> 7
else -> 14
}
}
}
database
.characterDao()
.insertCharacter(*domainCharacters.toTypedArray())
}
private suspend fun importAdventureMissions(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
Log.d("importAdventureMissions", "Importing adventure missions")
if (card is BemCard) {
card.adventureLevels.levels.forEach {
database
.cardAdventureDao()
.insertNewAdventure(
cardId = cardId,
characterId = it.bossCharacterIndex,
steps = it.steps,
bossAp = it.bossAp,
bossHp = it.bossHp,
bossDp = it.bossDp,
bossBp = it.bossBp
)
}
} else if (card is DimCard) {
card.adventureLevels.levels.map {
database
.cardAdventureDao()
.insertNewAdventure(
cardId = cardId,
characterId = it.bossCharacterIndex,
steps = it.steps,
bossAp = it.bossAp,
bossHp = it.bossHp,
bossDp = it.bossDp,
bossBp = null
)
}
}
}
private suspend fun importCardFusions(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
Log.d("importCardFusions", "Importing card fusions")
if (card is DimCard) {
card
.attributeFusions
.entries
.forEach {
database
.cardFusionsDao()
.insertNewFusion(
cardId = cardId,
fromCharaId = it.characterIndex,
toCharaIdVirus = it.attribute1Fusion,
toCharaIdData = it.attribute2Fusion,
toCharaIdVaccine = it.attribute3Fusion,
toCharaIdFree = it.attribute4Fusion
)
}
}
}
private fun updateCardProgress(
cardId: Long,
) {
val cardProgress = CardProgress(
cardId = cardId,
currentStage = 0,
unlocked = false
)
database
.cardProgressDao()
.updateDimProgress(cardProgress)
}
private suspend fun importBackgrounds(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
val backgrounds = mutableListOf<CardBackground>()
if (card is DimCard) {
backgrounds.add(
CardBackground(
cardId = cardId,
background = card.spriteData.sprites[1].pixelData,
backgroundWidth = card.spriteData.sprites[1].width,
backgroundHeight = card.spriteData.sprites[1].height
)
)
} else if (card is BemCard) {
val possibleBackgroundsIds = listOf(1, 10, 30, 31, 32, 33)
for (id in possibleBackgroundsIds) {
backgrounds.add(
CardBackground(
cardId = cardId,
background = card.spriteData.sprites[id].pixelData,
backgroundWidth = card.spriteData.sprites[id].width,
backgroundHeight = card.spriteData.sprites[id].height
)
)
}
}
database
.cardBackgroundDao()
.insertCardBackground(*backgrounds.toTypedArray())
}
private fun importCard(uri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
val contentResolver = context.contentResolver
val inputStream = contentResolver.openInputStream(uri)
inputStream.use { fileReader ->
val cardImportController = CardImportController(database)
cardImportController.importCard(fileReader)
val dimReader = DimReader()
val card = dimReader.readCard(fileReader, false)
val cardModel = Card(
cardId = card.header.dimId,
logo = card.spriteData.sprites[0].pixelData,
name = card.spriteData.text,
stageCount = card.adventureLevels.levels.size,
logoHeight = card.spriteData.sprites[0].height,
logoWidth = card.spriteData.sprites[0].width,
isBEm = card is BemCard
)
val cardId = database
.cardDao()
.insertNewCard(cardModel)
updateCardProgress(cardId = cardId)
importCharacterData(cardId, card)
importEvoData(cardId, card)
importAdventureMissions(cardId, card)
importCardFusions(cardId, card)
importBackgrounds(cardId, card)
val cardSprites: List<CardSprites> = card.spriteData.sprites.map {
CardSprites(
sprite = it.pixelData,
width = it.width,
height = it.height
)
}
database
.cardDao()
.insertSprites(*cardSprites.toTypedArray())
}
inputStream?.close()
@@ -118,6 +438,100 @@ class SettingsScreenControllerImpl(
}
}
private fun exportDatabase(destinationUri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
try {
val dbFile = File(context.getDatabasePath(roomDbName).absolutePath)
if (!dbFile.exists()) {
throw IllegalStateException("Database file does not exist!")
}
application.container.db.close()
context.contentResolver.openOutputStream(destinationUri)?.use { outputStream ->
dbFile.inputStream().use { inputStream ->
copyFile(inputStream, outputStream)
}
} ?: throw IllegalArgumentException("Unable to open destination Uri for writing")
context.runOnUiThread {
Toast.makeText(context, "Database exported successfully!", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Closing application to avoid changes.", Toast.LENGTH_LONG).show()
context.finishAffinity()
}
} catch (e: Exception) {
Log.e("ScanScreenController", "Error exporting database $e")
context.runOnUiThread {
Toast.makeText(context, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
private fun importDatabase(sourceUri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
try {
if (!getFileNameFromUri(sourceUri)!!.endsWith(".vbhelper")) {
context.runOnUiThread {
Toast.makeText(context, "Invalid file format", Toast.LENGTH_SHORT).show()
}
return@launch
}
application.container.db.close()
val dbPath = context.getDatabasePath(roomDbName)
val shmFile = File(dbPath.parent, "$roomDbName-shm")
val walFile = File(dbPath.parent, "$roomDbName-wal")
// Delete existing database files
if (dbPath.exists()) dbPath.delete()
if (shmFile.exists()) shmFile.delete()
if (walFile.exists()) walFile.delete()
val dbFile = File(dbPath.absolutePath)
context.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
dbFile.outputStream().use { outputStream ->
copyFile(inputStream, outputStream)
}
} ?: throw IllegalArgumentException("Unable to open source Uri for reading")
context.runOnUiThread {
Toast.makeText(context, "Database imported successfully!", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Reopen the app to finish import process!", Toast.LENGTH_LONG).show()
context.finishAffinity()
}
} catch (e: Exception) {
Log.e("ScanScreenController", "Error importing database $e")
context.runOnUiThread {
Toast.makeText(context, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
private fun getFileNameFromUri(uri: Uri): String? {
var fileName: String? = null
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
fileName = it.getString(nameIndex)
}
}
return fileName
}
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
outputStream.flush()
}
private fun importApk(uri: Uri) {
context.lifecycleScope.launch(Dispatchers.IO) {
context.contentResolver.openInputStream(uri).use {
@@ -1,320 +0,0 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers
import android.util.Log
import com.github.cfogrady.vb.dim.card.BemCard
import com.github.cfogrady.vb.dim.card.DimCard
import com.github.cfogrady.vb.dim.card.DimReader
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.nacabaro.vbhelper.database.AppDatabase
import com.github.nacabaro.vbhelper.domain.card.Card
import com.github.nacabaro.vbhelper.domain.card.CardCharacter
import com.github.nacabaro.vbhelper.domain.card.CardProgress
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import java.io.InputStream
class CardImportController(
private val database: AppDatabase
) {
suspend fun importCard(
fileReader: InputStream?
) {
val dimReader = DimReader()
val card = dimReader.readCard(fileReader, false)
val cardModel = Card(
cardId = card.header.dimId,
logo = card.spriteData.sprites[0].pixelData,
name = card.spriteData.text,
stageCount = card.adventureLevels.levels.size,
logoHeight = card.spriteData.sprites[0].height,
logoWidth = card.spriteData.sprites[0].width,
isBEm = card is BemCard
)
val cardId = database
.cardDao()
.insertNewCard(cardModel)
updateCardProgress(cardId = cardId)
importCharacterData(cardId, card)
importEvoData(cardId, card)
importAdventureMissions(cardId, card)
importCardFusions(cardId, card)
}
private fun updateCardProgress(
cardId: Long,
) {
database
.cardProgressDao()
.insertCardProgress(
CardProgress(
cardId = cardId,
currentStage = 1,
unlocked = false
)
)
}
private suspend fun importCharacterData(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
var spriteCounter = when (card is BemCard) {
true -> 54
false -> 10
}
val domainCharacters = mutableListOf<CardCharacter>()
val characters = card
.characterStats
.characterEntries
for (index in 0 until characters.size) {
var domainSprite: Sprite?
if (index < 2 && card is DimCard) {
domainSprite = Sprite(
width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
spriteWalk1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteWalk2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteRun1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteRun2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteTrain1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteTrain2 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteHappy = card.spriteData.sprites[spriteCounter + 4].pixelData,
spriteSleep = card.spriteData.sprites[spriteCounter + 5].pixelData,
spriteAttack = card.spriteData.sprites[spriteCounter + 2].pixelData,
spriteDodge = card.spriteData.sprites[spriteCounter + 3].pixelData
)
} else {
domainSprite = Sprite(
width = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.width,
height = card.spriteData.sprites[spriteCounter + 1].spriteDimensions.height,
spriteIdle1 = card.spriteData.sprites[spriteCounter + 1].pixelData,
spriteIdle2 = card.spriteData.sprites[spriteCounter + 2].pixelData,
spriteWalk1 = card.spriteData.sprites[spriteCounter + 3].pixelData,
spriteWalk2 = card.spriteData.sprites[spriteCounter + 4].pixelData,
spriteRun1 = card.spriteData.sprites[spriteCounter + 5].pixelData,
spriteRun2 = card.spriteData.sprites[spriteCounter + 6].pixelData,
spriteTrain1 = card.spriteData.sprites[spriteCounter + 7].pixelData,
spriteTrain2 = card.spriteData.sprites[spriteCounter + 8].pixelData,
spriteHappy = card.spriteData.sprites[spriteCounter + 9].pixelData,
spriteSleep = card.spriteData.sprites[spriteCounter + 10].pixelData,
spriteAttack = card.spriteData.sprites[spriteCounter + 11].pixelData,
spriteDodge = card.spriteData.sprites[spriteCounter + 12].pixelData
)
}
val spriteId = database
.spriteDao()
.insertSprite(domainSprite)
domainCharacters.add(
CardCharacter(
cardId = cardId,
spriteId = spriteId,
charaIndex = index,
nameSprite = card.spriteData.sprites[spriteCounter].pixelData,
stage = characters[index].stage,
attribute = NfcCharacter.Attribute.entries[characters[index].attribute],
baseHp = characters[index].hp,
baseBp = characters[index].dp,
baseAp = characters[index].ap,
nameWidth = card.spriteData.sprites[spriteCounter].spriteDimensions.width,
nameHeight = card.spriteData.sprites[spriteCounter].spriteDimensions.height
)
)
spriteCounter += if (card is BemCard) {
14
} else {
when (index) {
0 -> 6
1 -> 7
else -> 14
}
}
}
database
.characterDao()
.insertCharacter(*domainCharacters.toTypedArray())
}
private suspend fun importAdventureMissions(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
Log.d("importAdventureMissions", "Importing adventure missions")
if (card is BemCard) {
card.adventureLevels.levels.forEach {
database
.cardAdventureDao()
.insertNewAdventure(
cardId = cardId,
characterId = it.bossCharacterIndex,
steps = it.steps,
bossAp = it.bossAp,
bossHp = it.bossHp,
bossDp = it.bossDp,
bossBp = it.bossBp
)
}
} else if (card is DimCard) {
card.adventureLevels.levels.map {
database
.cardAdventureDao()
.insertNewAdventure(
cardId = cardId,
characterId = it.bossCharacterIndex,
steps = it.steps,
bossAp = it.bossAp,
bossHp = it.bossHp,
bossDp = it.bossDp,
bossBp = null
)
}
}
}
private suspend fun importCardFusions(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
Log.d("importCardFusions", "Importing card fusions")
if (card is DimCard) {
card
.attributeFusions
.entries
.forEach {
Log.d("importCardFusions", "Importing fusion: ${it.attribute1Fusion}")
if (it.attribute1Fusion != 65535 && it.characterIndex != 65535) {
database
.cardFusionsDao()
.insertNewFusion(
cardId = cardId,
fromCharaId = it.characterIndex,
attribute = NfcCharacter.Attribute.Virus,
toCharaId = it.attribute1Fusion,
)
}
if (it.attribute2Fusion != 65535 && it.characterIndex != 65535) {
database
.cardFusionsDao()
.insertNewFusion(
cardId = cardId,
fromCharaId = it.characterIndex,
attribute = NfcCharacter.Attribute.Data,
toCharaId = it.attribute2Fusion,
)
}
if (it.attribute3Fusion != 65535 && it.characterIndex != 65535) {
database
.cardFusionsDao()
.insertNewFusion(
cardId = cardId,
fromCharaId = it.characterIndex,
attribute = NfcCharacter.Attribute.Vaccine,
toCharaId = it.attribute3Fusion,
)
}
if (it.attribute4Fusion != 65535 && it.characterIndex != 65535) {
database
.cardFusionsDao()
.insertNewFusion(
cardId = cardId,
fromCharaId = it.characterIndex,
attribute = NfcCharacter.Attribute.Free,
toCharaId = it.attribute4Fusion,
)
}
}
}
}
private suspend fun importEvoData(
cardId: Long,
card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *>
) {
for (index in 0 until card.transformationRequirements.transformationEntries.size) {
val evo = card.transformationRequirements.transformationEntries[index]
var transformationTimerHours: Int
var unlockAdventureLevel: Int
if (card is BemCard) {
transformationTimerHours = card
.transformationRequirements
.transformationEntries[index]
.minutesUntilTransformation / 60
unlockAdventureLevel = if (
card
.transformationRequirements
.transformationEntries[index]
.requiredCompletedAdventureLevel == 65535
) {
0
} else {
card
.transformationRequirements
.transformationEntries[index]
.requiredCompletedAdventureLevel
}
} else {
transformationTimerHours = (card as DimCard)
.transformationRequirements
.transformationEntries[index]
.hoursUntilEvolution
unlockAdventureLevel = if (
card
.adventureLevels
.levels
.last()
.bossCharacterIndex == card.transformationRequirements.transformationEntries[index].toCharacterIndex
) {
14
/*
Magic number incoming!!
In the case of DiMCards, stage 15 is the one that unlocks the locked character.
We know it is a locked character if the last adventure level's boss character index
is the current index. If it is, we add stage 15 complete as a requirement for transformation.
*/
} else {
0
/*
Another magic number...
The rest of the characters are not locked.
*/
}
}
database
.characterDao()
.insertPossibleTransformation(
cardId = cardId,
fromChraraIndex = evo.fromCharacterIndex,
toChraraIndex = evo.toCharacterIndex,
requiredVitals = evo.requiredVitalValues,
requiredTrophies = evo.requiredTrophies,
requiredBattles = evo.requiredBattles,
requiredWinRate = evo.requiredWinRatio,
requiredAdventureLevelCompleted = unlockAdventureLevel,
changeTimerHours = transformationTimerHours
)
}
}
}
@@ -1,115 +0,0 @@
package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.github.nacabaro.vbhelper.di.VBHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.InputStream
import java.io.OutputStream
class DatabaseManagementController(
val componentActivity: ComponentActivity,
val application: VBHelper
) {
private val roomDbName = "internalDb"
fun exportDatabase( destinationUri: Uri) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
try {
val dbFile = File(componentActivity.getDatabasePath(roomDbName).absolutePath)
if (!dbFile.exists()) {
throw IllegalStateException("Database file does not exist!")
}
application.container.db.close()
componentActivity.contentResolver.openOutputStream(destinationUri)?.use { outputStream ->
dbFile.inputStream().use { inputStream ->
copyFile(inputStream, outputStream)
}
} ?: throw IllegalArgumentException("Unable to open destination Uri for writing")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Database exported successfully!", Toast.LENGTH_SHORT).show()
Toast.makeText(componentActivity, "Closing application to avoid changes.", Toast.LENGTH_LONG).show()
componentActivity.finishAffinity()
}
} catch (e: Exception) {
Log.e("ScanScreenController", "Error exporting database $e")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
fun importDatabase(sourceUri: Uri) {
componentActivity.lifecycleScope.launch(Dispatchers.IO) {
try {
if (!getFileNameFromUri(sourceUri)!!.endsWith(".vbhelper")) {
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Invalid file format", Toast.LENGTH_SHORT).show()
}
return@launch
}
application.container.db.close()
val dbPath = componentActivity.getDatabasePath(roomDbName)
val shmFile = File(dbPath.parent, "$roomDbName-shm")
val walFile = File(dbPath.parent, "$roomDbName-wal")
// Delete existing database files
if (dbPath.exists()) dbPath.delete()
if (shmFile.exists()) shmFile.delete()
if (walFile.exists()) walFile.delete()
val dbFile = File(dbPath.absolutePath)
componentActivity.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
dbFile.outputStream().use { outputStream ->
copyFile(inputStream, outputStream)
}
} ?: throw IllegalArgumentException("Unable to open source Uri for reading")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Database imported successfully!", Toast.LENGTH_SHORT).show()
Toast.makeText(componentActivity, "Reopen the app to finish import process!", Toast.LENGTH_LONG).show()
componentActivity.finishAffinity()
}
} catch (e: Exception) {
Log.e("ScanScreenController", "Error importing database $e")
componentActivity.runOnUiThread {
Toast.makeText(componentActivity, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
outputStream.flush()
}
private fun getFileNameFromUri(uri: Uri): String? {
var fileName: String? = null
val cursor = componentActivity.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
fileName = it.getString(nameIndex)
}
}
return fileName
}
}
@@ -1,9 +1,9 @@
package com.github.nacabaro.vbhelper.screens.spriteViewer
import android.graphics.Bitmap
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import com.github.nacabaro.vbhelper.domain.card.CardBackground
interface SpriteViewerController {
suspend fun getAllSprites(): List<Sprite>
fun convertToBitmap(sprites: List<Sprite>): List<Bitmap>
suspend fun getAllSprites(): List<CardBackground>
fun convertToBitmap(sprites: List<CardBackground>): List<Bitmap>
}
@@ -1,65 +1,29 @@
package com.github.nacabaro.vbhelper.screens.spriteViewer
import android.graphics.Bitmap
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import com.github.nacabaro.vbhelper.di.VBHelper
import com.github.nacabaro.vbhelper.domain.characters.Sprite
import java.nio.ByteBuffer
import androidx.core.graphics.createBitmap
import com.github.nacabaro.vbhelper.domain.card.CardBackground
class SpriteViewerControllerImpl(
private val context: ComponentActivity
) : SpriteViewerController {
override suspend fun getAllSprites(): List<Sprite> {
override suspend fun getAllSprites(): List<CardBackground> {
val applicationContext = context.applicationContext as VBHelper
val db = applicationContext.container.db
val sprites = db.spriteDao().getAllSprites()
val sprites = db.cardBackgroundDao().getBackgrounds()
return sprites
}
// I don't like this, chief
override fun convertToBitmap(sprites: List<Sprite>): List<Bitmap> {
override fun convertToBitmap(sprites: List<CardBackground>): List<Bitmap> {
val bitmapList = mutableListOf<Bitmap>()
for (sprite in sprites) {
Log.d("SpriteViewer", "sprite: $sprite")
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteIdle1))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteIdle2))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteWalk1))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteWalk2))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteRun1))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteRun2))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteTrain1))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteTrain2))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteHappy))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteSleep))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteAttack))
})
bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteDodge))
bitmapList.add(createBitmap(sprite.backgroundWidth, sprite.backgroundHeight, Bitmap.Config.RGB_565).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(sprite.background))
})
}
@@ -33,15 +33,11 @@ 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
import androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
@Composable
fun StorageDialog(
characterId: Long,
onDismissRequest: () -> Unit,
onClickDelete: () -> Unit,
onSendToBracelet: () -> Unit,
onClickSetActive: () -> Unit,
onClickSendToAdventure: (time: Long) -> Unit
@@ -97,7 +93,7 @@ fun StorageDialog(
val dpSize = (characterSprite.value!!.width * 4 / density).dp
Image(
bitmap = imageBitmap,
contentDescription = stringResource(R.string.storage_character_image_description),
contentDescription = "Character image",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(dpSize)
@@ -107,7 +103,7 @@ fun StorageDialog(
val nameDpSize = (characterName.value!!.width * 4 / density).dp
Image(
bitmap = nameImageBitmap,
contentDescription = stringResource(R.string.storage_character_image_description),
contentDescription = "Character image",
filterQuality = FilterQuality.None,
modifier = Modifier
.size(nameDpSize)
@@ -124,7 +120,7 @@ fun StorageDialog(
modifier = Modifier
.weight(1f)
) {
Text(text = stringResource(R.string.storage_send_to_watch))
Text(text = "Send to watch")
}
Spacer(
modifier = Modifier
@@ -133,7 +129,7 @@ fun StorageDialog(
Button(
onClick = onClickSetActive,
) {
Text(text = stringResource(R.string.storage_set_active))
Text(text = "Set active")
}
}
Button(
@@ -143,21 +139,14 @@ fun StorageDialog(
modifier = Modifier
.fillMaxWidth()
) {
Text(text = stringResource(R.string.storage_send_on_adventure))
}
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = onClickDelete
) {
Text(text = stringResource(R.string.storage_delete_character))
Text(text = "Send on adventure")
}
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = onDismissRequest
) {
Text(text = stringResource(R.string.storage_close))
Text(text = "Close")
}
}
}
@@ -1,6 +1,5 @@
package com.github.nacabaro.vbhelper.screens.storageScreen
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
@@ -12,15 +11,14 @@ 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.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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
@@ -30,12 +28,12 @@ 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 androidx.compose.ui.res.stringResource
import com.github.nacabaro.vbhelper.R
import kotlinx.coroutines.launch
@Composable
@@ -44,23 +42,30 @@ fun StorageScreen(
storageScreenController: StorageScreenControllerImpl,
adventureScreenController: AdventureScreenControllerImpl
) {
val coroutineScope = rememberCoroutineScope()
val application = LocalContext.current.applicationContext as VBHelper
val storageRepository = StorageRepository(application.container.db)
val characterList by storageRepository.getAllCharacters().collectAsState(initial = emptyList())
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 = stringResource(R.string.storage_my_characters_title),
text = "My characters",
onAdventureClick = {
navController.navigate(NavigationItems.Adventure.route)
}
)
}
) { contentPadding ->
if (characterList.isEmpty()) {
if (monList.value.isEmpty()) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
@@ -69,7 +74,7 @@ fun StorageScreen(
.fillMaxSize()
) {
Text(
text = stringResource(R.string.storage_nothing_to_see_here),
text = "Nothing to see here",
textAlign = TextAlign.Center,
modifier = Modifier
)
@@ -81,7 +86,7 @@ fun StorageScreen(
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical)
.padding(top = contentPadding.calculateTopPadding())
) {
items(characterList) { index ->
items(monList.value) { index ->
CharacterEntry(
icon = BitmapData(
bitmap = index.spriteIdle,
@@ -94,7 +99,7 @@ fun StorageScreen(
} else {
Toast.makeText(
application,
application.getString(R.string.storage_in_adventure_toast),
"This character is in an adventure",
Toast.LENGTH_SHORT
).show()
navController.navigate(
@@ -102,16 +107,6 @@ fun StorageScreen(
)
}
},
cardColors = if (index.active) {
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
)
} else {
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
)
}
)
}
}
@@ -143,16 +138,6 @@ fun StorageScreen(
timeInMinutes = time
)
selectedCharacter = null
},
onClickDelete = {
storageScreenController
.deleteCharacter(
characterId = selectedCharacter!!,
onCompletion = {
Log.d("StorageScreen", "Character deleted")
}
)
selectedCharacter = null
}
)
}

Some files were not shown because too many files have changed in this diff Show More