diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000000000000000000000000000000000..7643783a82f60b3b876fe58a9314fb50520df486 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <JetCodeStyleSettings> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> + </JetCodeStyleSettings> + <codeStyleSettings language="XML"> + <option name="FORCE_REARRANGE_MODE" value="1" /> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + </indentOptions> + <arrangement> + <rules> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:android</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:id</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>style</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>ANDROID_ATTRIBUTE_ORDER</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>.*</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + </rules> + </arrangement> + </codeStyleSettings> + <codeStyleSettings language="kotlin"> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> + </codeStyleSettings> + </code_scheme> +</component> \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000000000000000000000000000000000..79ee123c2b23e069e35ed634d687e17f731cc702 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="USE_PER_PROJECT_SETTINGS" value="true" /> + </state> +</component> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b8d4f77f878d2043557b31a9dcfbd579afc411d8..5aa79b62b958a17339e043920ab48f35299f96a7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,10 @@ <option name="filePathToZoomLevelMap"> <map> <entry key="app/src/main/res/layout/activity_main.xml" value="0.16510416666666666" /> + <entry key="app/src/main/res/layout/activity_profile.xml" value="0.165" /> <entry key="app/src/main/res/layout/recyclerview_main_menu_row.xml" value="0.1" /> + <entry key="app/src/main/res/layout/recyclerview_spell_row.xml" value="0.16510416666666666" /> + <entry key="app/src/main/res/menu/menu_main.xml" value="0.25" /> </map> </option> </component> diff --git a/app/build.gradle b/app/build.gradle index 252d348db7592362a1cb67b09e3b49390d99695b..373610d24cc2bbe198b03646234bb06d6fc3a449 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-kapt' } android { @@ -40,10 +41,21 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.0' implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' + def room_version = '2.3.0' + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + + def lifecycle_version = "2.4.0" + // LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + // Coroutine + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c10c1109b59974de2efb42eefe660ff8e39dbcd..2bde8d111bb2281ce73547ce1a3d2a9e4976cb45 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.flyinpancake.dndspells"> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="false" + android:requestLegacyExternalStorage="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.DndSpells"> + android:theme="@style/Theme.DndSpells" + android:name=".DndApplication"> + <activity + android:name=".ProfileActivity" + android:exported="false" /> <activity android:name=".MainActivity" android:exported="true"> diff --git a/app/src/main/java/com/flyinpancake/dndspells/DndApplication.kt b/app/src/main/java/com/flyinpancake/dndspells/DndApplication.kt new file mode 100644 index 0000000000000000000000000000000000000000..fec5a1fd18b00418f7ce5490781e61e7e00d9004 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/DndApplication.kt @@ -0,0 +1,23 @@ +package com.flyinpancake.dndspells + +import android.app.Application +import androidx.room.Room +import com.flyinpancake.dndspells.database.SpellDatabase + +class DndApplication: Application() { + + companion object { + lateinit var spellDatabase: SpellDatabase + private set + } + + override fun onCreate() { + spellDatabase = Room.databaseBuilder( + applicationContext, + SpellDatabase::class.java, + "spell_database", + ).fallbackToDestructiveMigration().build() + super.onCreate() + } + +} diff --git a/app/src/main/java/com/flyinpancake/dndspells/MainActivity.kt b/app/src/main/java/com/flyinpancake/dndspells/MainActivity.kt index 1a4f6e45e294a09dfa7f56e73587113e2389c6f7..6b7f42f7260a98ad68cf756b757e1bb096e15c05 100644 --- a/app/src/main/java/com/flyinpancake/dndspells/MainActivity.kt +++ b/app/src/main/java/com/flyinpancake/dndspells/MainActivity.kt @@ -1,35 +1,185 @@ package com.flyinpancake.dndspells -import androidx.appcompat.app.AppCompatActivity +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat import com.flyinpancake.dndspells.adapter.MainMenuRecyclerViewAdapter -import com.flyinpancake.dndspells.data.DndCharacter +import com.flyinpancake.dndspells.model.DndCharacter +import com.flyinpancake.dndspells.model.Spell +import com.flyinpancake.dndspells.model.SpellImporter import com.flyinpancake.dndspells.databinding.ActivityMainBinding +import com.flyinpancake.dndspells.databinding.RecyclerviewMainMenuRowBinding +import com.flyinpancake.dndspells.viewmodel.SpellViewModel +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.* +import java.io.InputStream + +class MainActivity : AppCompatActivity(), MainMenuRecyclerViewAdapter.SpellListItemClickListener { + + companion object { + var PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 100 + var FILE_PICKER_REQUEST_CODE = 123 + } -class MainActivity : AppCompatActivity() { - private lateinit var binding : ActivityMainBinding + private lateinit var binding: ActivityMainBinding private lateinit var recyclerViewAdapter: MainMenuRecyclerViewAdapter + private val spellViewModel = SpellViewModel() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(LayoutInflater.from(this)) setContentView(binding.root) + setSupportActionBar(binding.toolbar) + binding.toolbar.title = title + setupCharacterList() } private fun setupCharacterList() { val demoData = mutableListOf( DndCharacter("Klattic", 7, DndCharacter.DndClass.EldritchKnight, null), - DndCharacter("Ragrim", 6, DndCharacter.DndClass.Paladin, mutableListOf(2,5,1,3,6)) + DndCharacter("Ragrim", 6, DndCharacter.DndClass.Paladin, null), ) recyclerViewAdapter = MainMenuRecyclerViewAdapter() recyclerViewAdapter.addAll(demoData) + recyclerViewAdapter.itemClickListener = this binding.rvCharacterList.adapter = recyclerViewAdapter } + override fun onItemClick(character: DndCharacter, binding: RecyclerviewMainMenuRowBinding) { + val intent = Intent(this, ProfileActivity::class.java) + intent.putExtra(ProfileActivity.KEY_NAME, character.name) + val option = ActivityOptionsCompat.makeSceneTransitionAnimation( + this, + binding.tvCharacterName, + "character_name" + ) + startActivity(intent, option.toBundle()) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + + + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.import_xml -> { + handleReadFilesPermission() + } + } + return super.onOptionsItemSelected(item) + } + + private fun showRationaleDialog( + @StringRes title: Int = R.string.rationale_title, + @StringRes explanation: Int, + onPositiveButton: () -> Unit, + onNegativeButton: () -> Unit = this::finish + ) { + val alertDialog = AlertDialog.Builder(this) + .setTitle(title) + .setMessage(explanation) + .setCancelable(false) + .setPositiveButton(R.string.proceed) { dialog, _ -> + dialog.cancel() + onPositiveButton() + } + .setNegativeButton(R.string.cancel) { _, _ -> onNegativeButton() } + .create() + alertDialog.show() + } + + private fun handleReadFilesPermission() { + if (ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.READ_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + if (ActivityCompat.shouldShowRequestPermissionRationale( + this, + android.Manifest.permission.READ_EXTERNAL_STORAGE + ) + ) { + showRationaleDialog( + explanation = R.string.file_read_explanation, + onPositiveButton = { requestReadFilesystemPermission() } + ) + } else { + requestReadFilesystemPermission() + } + } else { + openSpellsFile() + } + } + + private fun openSpellsFile() { + // Open a file picker dialog + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .setType("*/*") + .addCategory(Intent.CATEGORY_OPENABLE) + startActivityForResult( + Intent.createChooser(intent, R.string.select_file.toString()), + FILE_PICKER_REQUEST_CODE + ) + } + + private fun loadSpellsFromFile(fis: InputStream, callback: (spellList: List<Spell>) -> Unit) { + val importer = SpellImporter() + val spellList = importer.importSpells(fis) + Log.d("DDS_DEBUG", "The spell list is ${spellList.size} long") + callback(spellList) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == FILE_PICKER_REQUEST_CODE && resultCode == RESULT_OK) { + data?.data?.let { uri -> GlobalScope.launch { read(this@MainActivity, uri) } } + } + } + + private suspend fun read(context: Context, source: Uri) = withContext(Dispatchers.IO) { + val resolver: ContentResolver = context.contentResolver + + resolver.openInputStream(source) + ?.let { fis -> loadSpellsFromFile(fis) { result -> onSpellsRead(result) } } + ?: throw IllegalStateException("could not open $source") + } + + private fun onSpellsRead(spellList: List<Spell>) { + Snackbar.make(binding.root, "Spells imported!", Snackbar.LENGTH_LONG).show() + Log.d("DDS_DEBUG", "Spells arrived!") + spellViewModel.nuke() + spellList.forEach { spell -> spellViewModel.insert(spell) } + } + + private fun requestReadFilesystemPermission() { + ActivityCompat.requestPermissions( + this, + arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSION_REQUEST_READ_EXTERNAL_STORAGE + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/flyinpancake/dndspells/ProfileActivity.kt b/app/src/main/java/com/flyinpancake/dndspells/ProfileActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d1882243c1d5830d5adfdf606691aedbf2683cd2 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/ProfileActivity.kt @@ -0,0 +1,43 @@ +package com.flyinpancake.dndspells + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.lifecycle.map +import androidx.recyclerview.widget.LinearLayoutManager +import com.flyinpancake.dndspells.adapter.ProfileSpellRecyclerViewAdapter +import com.flyinpancake.dndspells.model.Spell +import com.flyinpancake.dndspells.databinding.ActivityProfileBinding +import com.flyinpancake.dndspells.databinding.RecyclerviewSpellRowBinding +import com.flyinpancake.dndspells.viewmodel.SpellViewModel + +class ProfileActivity : AppCompatActivity(), ProfileSpellRecyclerViewAdapter.SpellListItemClickListener { + companion object { + const val KEY_NAME = "KEY_NAME" + } + private lateinit var binding : ActivityProfileBinding + private val spellListViewModel = SpellViewModel() + private val recyclerViewAdapter = ProfileSpellRecyclerViewAdapter() + override fun onCreate(savedInstanceState: Bundle?) { + binding = ActivityProfileBinding.inflate(this.layoutInflater) + super.onCreate(savedInstanceState) + setContentView(binding.root) + + binding.tvCharacterName.text = intent.getStringExtra(KEY_NAME)!! + + setupSpellList() + } + + + + private fun setupSpellList() { + + spellListViewModel.allSpells.map { spells -> recyclerViewAdapter.addAll(spells) } + + binding.rvSpellList.adapter = recyclerViewAdapter + binding.rvSpellList.layoutManager = LinearLayoutManager(this) + } + + override fun onItemClick(spell: Spell, binding: RecyclerviewSpellRowBinding) { + TODO("Open spell description") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flyinpancake/dndspells/adapter/MainMenuRecyclerViewAdapter.kt b/app/src/main/java/com/flyinpancake/dndspells/adapter/MainMenuRecyclerViewAdapter.kt index d776588de6c0c248f7534f620c98f4a0501871dd..210d22f55f9d7901c9c62babe513dc7f3ac38243 100644 --- a/app/src/main/java/com/flyinpancake/dndspells/adapter/MainMenuRecyclerViewAdapter.kt +++ b/app/src/main/java/com/flyinpancake/dndspells/adapter/MainMenuRecyclerViewAdapter.kt @@ -5,22 +5,37 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.flyinpancake.dndspells.databinding.RecyclerviewMainMenuRowBinding -import com.flyinpancake.dndspells.data.DndCharacter +import com.flyinpancake.dndspells.model.DndCharacter class MainMenuRecyclerViewAdapter : RecyclerView.Adapter<MainMenuRecyclerViewAdapter.MainMenuViewHolder>() { + var itemClickListener : SpellListItemClickListener? = null + + interface SpellListItemClickListener { + fun onItemClick(character: DndCharacter, binding: RecyclerviewMainMenuRowBinding) + } private val characters = mutableListOf<DndCharacter>() inner class MainMenuViewHolder(val binding: RecyclerviewMainMenuRowBinding) : - RecyclerView.ViewHolder(binding.root) + RecyclerView.ViewHolder(binding.root) { + + var character: DndCharacter? = null + + init { + itemView.setOnClickListener { + character?.let { character -> itemClickListener?.onItemClick(character, binding) } + } + } + } @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: MainMenuViewHolder, position: Int) { val character = characters[position] holder.binding.tvCharacterName.text = character.name - holder.binding.tvLevelAndClass.text = "Level ${character.level} ${character.dndClass.legibleName}" //FIXME Ask labvez + holder.binding.tvLevelAndClass.text = "Level ${character.level} ${character.dndClass.legibleName}" //FIXME Ask labvez about i18n + holder.character = character } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MainMenuViewHolder( RecyclerviewMainMenuRowBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/com/flyinpancake/dndspells/adapter/ProfileSpellRecyclerViewAdapter.kt b/app/src/main/java/com/flyinpancake/dndspells/adapter/ProfileSpellRecyclerViewAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..faead98c3c18a1c546c4f91f893142d28c649c9e --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/adapter/ProfileSpellRecyclerViewAdapter.kt @@ -0,0 +1,45 @@ +package com.flyinpancake.dndspells.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.flyinpancake.dndspells.model.Spell +import com.flyinpancake.dndspells.databinding.RecyclerviewSpellRowBinding + +class ProfileSpellRecyclerViewAdapter: RecyclerView.Adapter<ProfileSpellRecyclerViewAdapter.ViewHolder>() { + + val spells = mutableListOf<Spell>() + + inner class ViewHolder(val binding: RecyclerviewSpellRowBinding) : RecyclerView.ViewHolder(binding.root) { + val spell: Spell? = null + + init { + itemView.setOnClickListener { + spell?.let { spell -> itemClickListener?.onItemClick(spell, binding) } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + RecyclerviewSpellRowBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val spell = spells[position] + + holder.binding.SpellComponents.text = spell.components + holder.binding.SpellLevel.text = "${spell.level}" + holder.binding.SpellSchool.text = spell.school + holder.binding.tvSpellName.text = spell.name + } + + override fun getItemCount() = spells.size + + var itemClickListener: SpellListItemClickListener? = null + + interface SpellListItemClickListener { + fun onItemClick(spell: Spell, binding: RecyclerviewSpellRowBinding) + } + + fun addAll(spellList: List<Spell>) = spells.addAll(spellList) +} \ No newline at end of file diff --git a/app/src/main/java/com/flyinpancake/dndspells/database/RoomSpell.kt b/app/src/main/java/com/flyinpancake/dndspells/database/RoomSpell.kt new file mode 100644 index 0000000000000000000000000000000000000000..89320472b4b62af3e60e4d9bc509976bf4b7271b --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/database/RoomSpell.kt @@ -0,0 +1,22 @@ +package com.flyinpancake.dndspells.database + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + + +@Entity(tableName = "spells", indices = [Index(value = ["name"], unique = true)]) +data class RoomSpell( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val name: String, + val desc: String, + val level: Int, + val components: String, + val range: String, + val time: String, + val school: String, + val ritual: Boolean, + val duration: String, + val classes: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/flyinpancake/dndspells/database/SpellDao.kt b/app/src/main/java/com/flyinpancake/dndspells/database/SpellDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e1804194075543c760ae6f725d2691be347f545 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/database/SpellDao.kt @@ -0,0 +1,26 @@ +package com.flyinpancake.dndspells.database + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface SpellDao { + + @Insert + fun insertSpell(spell: RoomSpell) + + @Query("SELECT * FROM spells") + fun getAllSpells(): LiveData<List<RoomSpell>> + + @Query("SELECT * FROM spells WHERE id == :id") + fun getSpellById(id: Int): RoomSpell? + + @Query("SELECT * FROM spells WHERE name == :name") + fun getSpellByName(name: String): RoomSpell? + + @Delete + fun deleteSpell(spell: RoomSpell) + + @Query("DELETE FROM spells") + fun nukeSpells() +} diff --git a/app/src/main/java/com/flyinpancake/dndspells/database/SpellDatabase.kt b/app/src/main/java/com/flyinpancake/dndspells/database/SpellDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..b20de39c775cf2bf38c78e24151039b92afe7575 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/database/SpellDatabase.kt @@ -0,0 +1,15 @@ +package com.flyinpancake.dndspells.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + + +@Database( + version = 1, + exportSchema = false, + entities = [RoomSpell::class] +) +abstract class SpellDatabase: RoomDatabase() { + abstract fun spellDao(): SpellDao +} \ No newline at end of file diff --git a/app/src/main/java/com/flyinpancake/dndspells/data/DndCharacter.kt b/app/src/main/java/com/flyinpancake/dndspells/model/DndCharacter.kt similarity index 87% rename from app/src/main/java/com/flyinpancake/dndspells/data/DndCharacter.kt rename to app/src/main/java/com/flyinpancake/dndspells/model/DndCharacter.kt index 27afa9d3979bf8b97fb38d1a1606bda9635f5cf9..18cf83daff21f0641545948a81d7efdb02b39cc0 100644 --- a/app/src/main/java/com/flyinpancake/dndspells/data/DndCharacter.kt +++ b/app/src/main/java/com/flyinpancake/dndspells/model/DndCharacter.kt @@ -1,10 +1,10 @@ -package com.flyinpancake.dndspells.data +package com.flyinpancake.dndspells.model data class DndCharacter( val name: String, val level: Int, val dndClass: DndClass, - val spellIdList: List<Int>?, + val spellList: List<Spell>?, ) { enum class DndClass(val legibleName: String) { diff --git a/app/src/main/java/com/flyinpancake/dndspells/model/Spell.kt b/app/src/main/java/com/flyinpancake/dndspells/model/Spell.kt new file mode 100644 index 0000000000000000000000000000000000000000..ce35089846c1d8e56b17865d0f862931e4eb88b8 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/model/Spell.kt @@ -0,0 +1,14 @@ +package com.flyinpancake.dndspells.model + +data class Spell( + val name: String, + val desc: String, + val level: Int, + val components: String, + val range: String, + val time: String, + val school: String, + val ritual: Boolean, + val duration: String, + val classes: String, +) diff --git a/app/src/main/java/com/flyinpancake/dndspells/model/SpellImporter.kt b/app/src/main/java/com/flyinpancake/dndspells/model/SpellImporter.kt new file mode 100644 index 0000000000000000000000000000000000000000..77d940a2509ea6446858cd768428f5f626ca89b7 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/model/SpellImporter.kt @@ -0,0 +1,175 @@ +package com.flyinpancake.dndspells.model + +import android.util.Xml +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.io.InputStream + + +class SpellImporter { + fun importSpells(fis: InputStream): List<Spell> { + return parse(fis) + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun parse(inputStream: InputStream): List<Spell> { + inputStream.use { inputStream -> + val parser = Xml.newPullParser() + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) + parser.setInput(inputStream, null) + parser.nextTag() + return readCompendium(parser) + } + } + + private fun readCompendium(parser: XmlPullParser): List<Spell> { + val spells = mutableListOf<Spell>() + parser.require(XmlPullParser.START_TAG, null, "compendium") + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.eventType != XmlPullParser.START_TAG) + continue + if (parser.name == "spell") + spells.add(readSpell(parser)) + else + skip(parser) + } + + return spells + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun skip(parser: XmlPullParser) { + if (parser.eventType != XmlPullParser.START_TAG) { + throw IllegalStateException() + } + var depth = 1 + while (depth != 0) { + when (parser.next()) { + XmlPullParser.END_TAG -> depth-- + XmlPullParser.START_TAG -> depth++ + } + } + } + + + private fun readSpell(parser: XmlPullParser): Spell { + parser.require(XmlPullParser.START_TAG, null, "spell") + var name: String? = null + var desc = "" + var level: Int? = null + var components: String? = null + var range: String? = null + var time: String? = null + var school: String? = null + var ritual: Boolean? = null + var duration: String? = null + var classes: String? = null + + + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.eventType != XmlPullParser.START_TAG) + continue + when (parser.name) { + "name" -> name = readName(parser) + "level" -> level = readLevel(parser) + "school" -> school = readSchool(parser) + "ritual" -> ritual = readRitual(parser) + "time" -> time = readTime(parser) + "range" -> range = readRange(parser) + "components" -> components = readComponents(parser) + "duration" -> duration = readDuration(parser) + "classes" -> classes = readClasses(parser) + "text" -> desc += readText(parser) + else -> skip(parser) + } + } + + //maybe throw something if the XML is corrupt + + return Spell( + name!!, + desc, + level?: 0, + components?:"", + range?: "", + time?: "", + school ?: "", + ritual ?: false, + duration ?: "", + classes?: "Any" + ) + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readInsideTags(parser: XmlPullParser, tag: String): String { + parser.require(XmlPullParser.START_TAG, null, tag) + val text = readRawText(parser) + parser.require(XmlPullParser.END_TAG, null, tag) + return text + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readClasses(parser: XmlPullParser): String { + return readInsideTags(parser, "classes") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readText(parser: XmlPullParser): String { + var text = readInsideTags(parser, "text") + if (text.isEmpty()) + text = "\n" + return text + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readDuration(parser: XmlPullParser): String { + return readInsideTags(parser, "duration") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readComponents(parser: XmlPullParser): String { + return readInsideTags(parser, "components") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readRange(parser: XmlPullParser): String { + return readInsideTags(parser, "range") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readTime(parser: XmlPullParser): String { + return readInsideTags(parser, "time") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readRitual(parser: XmlPullParser): Boolean { + return readInsideTags(parser, "ritual") == "YES" + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readSchool(parser: XmlPullParser): String { + return readInsideTags(parser, "school") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readLevel(parser: XmlPullParser): Int { + return readInsideTags(parser, "level").toInt() + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readName(parser: XmlPullParser): String { + return readInsideTags(parser, "name") + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun readRawText(parser: XmlPullParser): String { + var result = "" + if (parser.next() == XmlPullParser.TEXT) { + result = parser.text + parser.nextTag() + } + return result + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/flyinpancake/dndspells/repository/SpellRepository.kt b/app/src/main/java/com/flyinpancake/dndspells/repository/SpellRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..a11bebcba600bb26bdd3ecac15c5af8b217c2173 --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/repository/SpellRepository.kt @@ -0,0 +1,58 @@ +package com.flyinpancake.dndspells.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.map +import com.flyinpancake.dndspells.database.RoomSpell +import com.flyinpancake.dndspells.database.SpellDao +import com.flyinpancake.dndspells.model.Spell +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SpellRepository(private val spellDao: SpellDao) { + fun getAllSpells(): LiveData<List<Spell>> { + return spellDao.getAllSpells().map { roomSpells -> + roomSpells.map { roomSpell -> roomSpell.toDomainModel() } + } + + } + + suspend fun insert(spell: Spell) = withContext(Dispatchers.IO) { + if (spellDao.getSpellByName(spell.name) == null) + spellDao.insertSpell(spell.toRoomModel()) + } + + suspend fun delete(spell: Spell) = withContext(Dispatchers.IO) { + val roomSpell = spellDao.getSpellByName(spell.name)?: return@withContext + spellDao.deleteSpell(roomSpell) + } + + suspend fun nuke() = withContext(Dispatchers.IO) { + spellDao.nukeSpells() + } + + suspend fun getAllSpellst() = withContext(Dispatchers.IO) { + + } +} + +private fun Spell.toRoomModel(): RoomSpell { + return RoomSpell( + name = name, + desc = desc, + classes = classes, + components = components, + duration = duration, + level = level, + range = range, + ritual = ritual, + school = school, + time = time + ) +} + + +private fun RoomSpell.toDomainModel(): Spell { + return Spell(name, desc, level, components, range, time, school, ritual, duration, classes) +} + + diff --git a/app/src/main/java/com/flyinpancake/dndspells/viewmodel/SpellViewModel.kt b/app/src/main/java/com/flyinpancake/dndspells/viewmodel/SpellViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..4df2f4f94fd226ea157426814a4983dea485189e --- /dev/null +++ b/app/src/main/java/com/flyinpancake/dndspells/viewmodel/SpellViewModel.kt @@ -0,0 +1,36 @@ +package com.flyinpancake.dndspells.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flyinpancake.dndspells.DndApplication +import com.flyinpancake.dndspells.model.Spell +import com.flyinpancake.dndspells.repository.SpellRepository +import kotlinx.coroutines.launch + +class SpellViewModel: ViewModel() { + private val repo: SpellRepository + + val allSpells: LiveData<List<Spell>> + + init{ + val spellDao = DndApplication.spellDatabase.spellDao() + repo = SpellRepository(spellDao) + allSpells = repo.getAllSpells() + } + + fun insert(spell: Spell) = viewModelScope.launch { + repo.insert(spell) + } + + fun delete(spell: Spell) = viewModelScope.launch { + repo.delete(spell) + } + + fun nuke() = viewModelScope.launch { + repo.nuke() + } + + + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 656df76a2d5e89c1ab433f5ad650a5def7f049a5..367b4f3779cb75b882922249f116c6b53d9936cc 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,6 +8,20 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/main_appbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:theme="@style/Theme.DndSpells.AppBarOverlay"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:popupTheme="@style/Theme.DndSpells.PopupOverlay"/> + + </com.google.android.material.appbar.AppBarLayout> + <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml new file mode 100644 index 0000000000000000000000000000000000000000..aedecc2e041970bb0cb195f8a451a724d820cc26 --- /dev/null +++ b/app/src/main/res/layout/activity_profile.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ProfileActivity"> + + <TextView + android:id="@+id/tvCharacterName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:text="Klattic" + android:textAppearance="@style/TextAppearance.AppCompat.Display1" + android:transitionName="character_name" + app:layout_constraintBottom_toBottomOf="@+id/imCharacterArt" + app:layout_constraintEnd_toStartOf="@+id/imCharacterArt" + app:layout_constraintHorizontal_bias="0.465" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/imCharacterArt" /> + + <ImageView + android:id="@+id/imCharacterArt" + android:transitionName="character_art" + + android:layout_width="106dp" + android:layout_height="104dp" + android:layout_marginTop="24dp" + android:layout_marginEnd="24dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:srcCompat="@drawable/dnd_logo" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/rvSpellList" + android:layout_width="409dp" + android:layout_height="601dp" + android:layout_marginStart="16dp" + android:layout_marginTop="24dp" + android:layout_marginEnd="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/imCharacterArt" /> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/recyclerview_main_menu_row.xml b/app/src/main/res/layout/recyclerview_main_menu_row.xml index bfc01c84282d252578407722b832c13b0b3cb8da..c58eb90c7f0ab3f27bc7e875ed63ae41e30bd9f9 100644 --- a/app/src/main/res/layout/recyclerview_main_menu_row.xml +++ b/app/src/main/res/layout/recyclerview_main_menu_row.xml @@ -13,7 +13,7 @@ android:layout_marginTop="16dp" android:layout_marginBottom="8dp" android:textSize="18sp" - + android:transitionName="character_name" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/tvLevelAndClass" app:layout_constraintStart_toStartOf="parent" @@ -36,6 +36,7 @@ android:layout_width="150dp" android:layout_height="0dp" android:layout_marginEnd="16dp" + android:transitionName="character_art" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/recyclerview_spell_row.xml b/app/src/main/res/layout/recyclerview_spell_row.xml new file mode 100644 index 0000000000000000000000000000000000000000..46f6a627fcfb133ba142a9a886420cfc1c7e27e0 --- /dev/null +++ b/app/src/main/res/layout/recyclerview_spell_row.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/tvSpellName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:text="Power word kill" + android:textSize="28sp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/SpellLevel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:text="Level 9" + android:textSize="28sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/SpellSchool" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginBottom="8dp" + android:text="school" + android:textSize="20sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/SpellComponents" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginBottom="8dp" + android:text="Components" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000000000000000000000000000000000000..d742344094735c2b51665ee083ff3875690387e9 --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<menu xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/import_xml" + android:title="@string/import_spells_xml"/> + +</menu> \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index adaf85e513cac9cb50743cb3c0530129627bb983..db628cc022133603a7bc9c7a2d200116c62799ce 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -12,5 +12,12 @@ <!-- Status bar color. --> <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> <!-- Customize your theme here. --> + <item name="android:windowContentTransitions">true</item> + <item name="windowNoTitle">true</item> + </style> + + <style name="Theme.DndSpells.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> + <style name="Theme.DndSpells.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> + </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 412e886f9186325406df93c73925d8775ccd04c1..fb65b8878ade903b009c18fc1f40cce012a9624d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,10 @@ <string name="app_name">DndSpells</string> <string name="select_your_character">Select Your Character</string> <string name="character_avatar_of">Character avatar of</string> + <string name="import_spells_xml">Import Spells.xml</string> + <string name="rationale_title">Fun Title</string> + <string name="proceed">Proceed</string> + <string name="cancel">Cancel</string> + <string name="file_read_explanation">App will no work</string> + <string name="select_file">Select an XML file</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 051230c93f19d3833f9d29ccd4da4b598b47ddec..48cb85cfb7abe327a86f1bb70f2835b0714f2c56 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -12,5 +12,12 @@ <!-- Status bar color. --> <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> <!-- Customize your theme here. --> + <item name="android:windowContentTransitions">true</item> + <item name="windowNoTitle">true</item> + </style> + <style name="Theme.DndSpells.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> + <style name="Theme.DndSpells.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> + + </resources> \ No newline at end of file diff --git a/build.gradle b/build.gradle index d4efb6264f9c88dd5627843dbfcd9841d3771c55..0022993c252fb0c43996da90aea6d1c67dd93d8c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:7.0.3" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31" + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files