diff --git a/Gemfile.lock b/Gemfile.lock
index 7ad7fbb7d..cbf714ee3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -83,7 +83,7 @@ GEM
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-properties (1.1.2)
java-properties
- fastlane-plugin-semantic_release (1.14.1)
+ fastlane-plugin-semantic_release (1.18.0)
fastlane-plugin-versioning_android (0.1.0)
gh_inspector (1.1.3)
google-api-client (0.38.0)
diff --git a/Habitica/build.gradle b/Habitica/build.gradle
index 5ce8fdf5a..c9e3bc90e 100644
--- a/Habitica/build.gradle
+++ b/Habitica/build.gradle
@@ -79,7 +79,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
- debugImplementation 'androidx.fragment:fragment-testing:1.5.2'
+ debugImplementation 'androidx.fragment:fragment-testing:1.5.3'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
@@ -109,18 +109,18 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
- implementation "androidx.fragment:fragment-ktx:1.5.2"
+ implementation "androidx.fragment:fragment-ktx:1.5.3"
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
- implementation "com.google.android.material:compose-theme-adapter:1.1.18"
+ implementation "com.google.android.material:compose-theme-adapter:1.1.19"
- implementation 'androidx.activity:activity-compose:1.5.1'
+ implementation 'androidx.activity:activity-compose:1.6.0'
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation 'com.willowtreeapps:signinwithapplebutton:0.3'
@@ -172,7 +172,7 @@ android {
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.3.1"
+ kotlinCompilerExtensionVersion = "1.3.2"
}
signingConfigs {
diff --git a/Habitica/res/layout/fragment_compose.xml b/Habitica/res/layout/fragment_compose.xml
new file mode 100644
index 000000000..3896c2c2f
--- /dev/null
+++ b/Habitica/res/layout/fragment_compose.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/Habitica/res/layout/fragment_compose_scrolling.xml b/Habitica/res/layout/fragment_compose_scrolling.xml
new file mode 100644
index 000000000..2418f8be3
--- /dev/null
+++ b/Habitica/res/layout/fragment_compose_scrolling.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/Habitica/res/values/strings.sidebar.xml b/Habitica/res/values/strings.sidebar.xml
index e6265a982..0f4224a3d 100644
--- a/Habitica/res/values/strings.sidebar.xml
+++ b/Habitica/res/values/strings.sidebar.xml
@@ -12,6 +12,7 @@
Challenges
Inventory
Avatar Customization
+ Avatar & Equipment
Equipment
Pets & Mounts
News
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaFirebaseMessagingService.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaFirebaseMessagingService.kt
index 32cc85184..93709ac1d 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaFirebaseMessagingService.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaFirebaseMessagingService.kt
@@ -23,9 +23,9 @@ class HabiticaFirebaseMessagingService : FirebaseMessagingService() {
PushNotificationManager.displayNotification(remoteMessage, applicationContext)
if (remoteMessage.data["identifier"]?.contains(PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY) == true) {
- if (this::userRepository.isInitialized) {
+ // if (this::userRepository.isInitialized) {
// userRepository.retrieveUser(true).subscribe({}, RxErrorHandler.handleEmptyError())
- }
+ // }
}
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
index 3b48f29cf..b39b10e16 100755
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
@@ -49,6 +49,7 @@ import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.android.habitica.ui.TutorialView
import com.habitrpg.android.habitica.ui.fragments.NavigationDrawerFragment
+import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.MainActivityViewModel
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel
import com.habitrpg.android.habitica.ui.views.AppHeaderView
@@ -212,7 +213,7 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
setupBottomnavigationLayoutListener()
binding.content.headerView.setContent {
- MdcTheme(setTextColors = true) {
+ HabiticaTheme {
AppHeaderView(viewModel.userViewModel)
}
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
index c8b3792a7..c370de647 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
@@ -29,11 +29,10 @@ import com.habitrpg.android.habitica.databinding.DrawerMainBinding
import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds
import com.habitrpg.android.habitica.extensions.getRemainingString
import com.habitrpg.android.habitica.extensions.getShortRemainingString
-import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.AppConfigManager
-import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.ExceptionHandler
+import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.WorldStateEvent
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion
@@ -368,10 +367,9 @@ class NavigationDrawerFragment : DialogFragment() {
items.add(HabiticaDrawerItem(R.id.timeTravelersShopFragment, SIDEBAR_SHOPS_TIMETRAVEL, context.getString(R.string.timeTravelers)))
items.add(HabiticaDrawerItem(0, SIDEBAR_INVENTORY, context.getString(R.string.sidebar_section_inventory), true))
+ items.add(HabiticaDrawerItem(R.id.avatarOverviewFragment, SIDEBAR_AVATAR, context.getString(R.string.sidebar_avatar_equipment)))
items.add(HabiticaDrawerItem(R.id.itemsFragment, SIDEBAR_ITEMS, context.getString(R.string.sidebar_items)))
- items.add(HabiticaDrawerItem(R.id.equipmentOverviewFragment, SIDEBAR_EQUIPMENT, context.getString(R.string.sidebar_equipment)))
items.add(HabiticaDrawerItem(R.id.stableFragment, SIDEBAR_STABLE, context.getString(R.string.sidebar_stable)))
- items.add(HabiticaDrawerItem(R.id.avatarOverviewFragment, SIDEBAR_AVATAR, context.getString(R.string.sidebar_avatar)))
items.add(HabiticaDrawerItem(R.id.gemPurchaseActivity, SIDEBAR_GEMS, context.getString(R.string.sidebar_gems)))
items.add(HabiticaDrawerItem(R.id.subscriptionPurchaseActivity, SIDEBAR_SUBSCRIPTION, context.getString(R.string.sidebar_subscription)))
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarEquipmentFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarEquipmentFragment.kt
deleted file mode 100644
index 21438f469..000000000
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarEquipmentFragment.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-package com.habitrpg.android.habitica.ui.fragments.inventory.customization
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-import com.habitrpg.android.habitica.R
-import com.habitrpg.android.habitica.components.UserComponent
-import com.habitrpg.android.habitica.data.InventoryRepository
-import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
-import com.habitrpg.android.habitica.helpers.ExceptionHandler
-import com.habitrpg.android.habitica.models.responses.UnlockResponse
-import com.habitrpg.android.habitica.models.user.User
-import com.habitrpg.android.habitica.ui.adapter.CustomizationEquipmentRecyclerViewAdapter
-import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
-import com.habitrpg.android.habitica.ui.helpers.MarginDecoration
-import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
-import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
-import io.reactivex.rxjava3.core.Flowable
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-class AvatarEquipmentFragment :
- BaseMainFragment(),
- SwipeRefreshLayout.OnRefreshListener {
-
- @Inject
- lateinit var inventoryRepository: InventoryRepository
- @Inject
- lateinit var userViewModel: MainUserViewModel
-
- override var binding: FragmentRefreshRecyclerviewBinding? = null
-
- override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRefreshRecyclerviewBinding {
- return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
- }
-
- var type: String? = null
- var category: String? = null
- private var activeEquipment: String? = null
-
- internal var adapter: CustomizationEquipmentRecyclerViewAdapter = CustomizationEquipmentRecyclerViewAdapter()
- internal var layoutManager: GridLayoutManager = GridLayoutManager(activity, 2)
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- showsBackButton = true
- compositeSubscription.add(
- adapter.getSelectCustomizationEvents()
- .flatMap { equipment ->
- val key = (if (equipment.key?.isNotBlank() != true) activeEquipment else equipment.key) ?: ""
- inventoryRepository.equip(if (userViewModel.user.value?.preferences?.costume == true) "costume" else "equipped", key)
- }
- .subscribe({ }, ExceptionHandler.rx())
- )
- compositeSubscription.add(
- adapter.getUnlockCustomizationEvents()
- .flatMap {
- Flowable.empty()
- }
- .subscribe({ }, ExceptionHandler.rx())
- )
- return super.onCreateView(inflater, container, savedInstanceState)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- showsBackButton = true
- super.onViewCreated(view, savedInstanceState)
- arguments?.let {
- val args = AvatarEquipmentFragmentArgs.fromBundle(it)
- type = args.type
- if (args.category.isNotEmpty()) {
- category = args.category
- }
- }
- binding?.refreshLayout?.setOnRefreshListener(this)
- setGridSpanCount(view.width)
- val layoutManager = GridLayoutManager(activity, 4)
- layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- return if (adapter.getItemViewType(position) == 0) {
- layoutManager.spanCount
- } else {
- 1
- }
- }
- }
- binding?.recyclerView?.layoutManager = layoutManager
- binding?.recyclerView?.addItemDecoration(MarginDecoration(context))
-
- binding?.recyclerView?.adapter = adapter
- binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
- this.loadEquipment()
-
- userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
- }
-
- override fun injectFragment(component: UserComponent) {
- component.inject(this)
- }
-
- private fun loadEquipment() {
- val type = this.type ?: return
- compositeSubscription.add(
- inventoryRepository.getEquipmentType(type, category ?: "").subscribe(
- {
- adapter.setEquipment(it)
- },
- ExceptionHandler.rx()
- )
- )
- }
-
- private fun setGridSpanCount(width: Int) {
- val itemWidth = context?.resources?.getDimension(R.dimen.customization_width) ?: 0F
- var spanCount = (width / itemWidth).toInt()
- if (spanCount == 0) {
- spanCount = 1
- }
- layoutManager.spanCount = spanCount
- }
-
- fun updateUser(user: User?) {
- this.updateActiveCustomization(user)
- this.adapter.gemBalance = user?.gemCount ?: 0
- adapter.notifyDataSetChanged()
- }
-
- private fun updateActiveCustomization(user: User?) {
- if (this.type == null || user?.preferences == null) {
- return
- }
- val outfit = if (user.preferences?.costume == true) user.items?.gear?.costume else user.items?.gear?.equipped
- val activeEquipment = when (this.type) {
- "headAccessory" -> outfit?.headAccessory
- "back" -> outfit?.back
- "eyewear" -> outfit?.eyeWear
- else -> ""
- }
- if (activeEquipment != null) {
- this.activeEquipment = activeEquipment
- this.adapter.activeEquipment = activeEquipment
- }
- }
-
- override fun onRefresh() {
- lifecycleScope.launch(ExceptionHandler.coroutine()) {
- userRepository.retrieveUser(true, true)
- binding?.refreshLayout?.isRefreshing = false
- }
- }
-}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt
index f40390f42..a5b74ab6e 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarOverviewFragment.kt
@@ -5,46 +5,68 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
+import androidx.compose.animation.AnimatedVisibility
+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.material.Switch
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
-import com.habitrpg.android.habitica.databinding.FragmentAvatarOverviewBinding
-import com.habitrpg.android.habitica.helpers.MainNavigationController
+import com.habitrpg.android.habitica.databinding.FragmentComposeScrollingBinding
import com.habitrpg.android.habitica.helpers.ExceptionHandler
-import com.habitrpg.android.habitica.models.user.User
+import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
+import com.habitrpg.android.habitica.ui.fragments.inventory.equipment.EquipmentOverviewFragmentDirections
+import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
+import com.habitrpg.android.habitica.ui.views.AvatarCustomizationOverviewView
+import com.habitrpg.android.habitica.ui.views.EquipmentOverviewView
import javax.inject.Inject
-class AvatarOverviewFragment : BaseMainFragment(), AdapterView.OnItemSelectedListener {
+class AvatarOverviewFragment : BaseMainFragment(),
+ AdapterView.OnItemSelectedListener {
@Inject
lateinit var userViewModel: MainUserViewModel
- override var binding: FragmentAvatarOverviewBinding? = null
+ override var binding: FragmentComposeScrollingBinding? = null
- override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentAvatarOverviewBinding {
- return FragmentAvatarOverviewBinding.inflate(inflater, container, false)
+ override fun createBinding(
+ inflater: LayoutInflater,
+ container: ViewGroup?
+ ): FragmentComposeScrollingBinding {
+ return FragmentComposeScrollingBinding.inflate(inflater, container, false)
}
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding?.avatarSizeSpinner?.onItemSelectedListener = this
-
- binding?.avatarShirtView?.setOnClickListener { displayCustomizationFragment("shirt", null) }
- binding?.avatarSkinView?.setOnClickListener { displayCustomizationFragment("skin", null) }
- binding?.avatarChairView?.setOnClickListener { displayCustomizationFragment("chair", null) }
- binding?.avatarGlassesView?.setOnClickListener { displayEquipmentFragment("eyewear", "glasses") }
- binding?.avatarAnimalEarsView?.setOnClickListener { displayEquipmentFragment("headAccessory", "animal") }
- binding?.avatarAnimalTailView?.setOnClickListener { displayEquipmentFragment("back", "animal") }
- binding?.avatarHeadbandView?.setOnClickListener { displayEquipmentFragment("headAccessory", "headband") }
- binding?.avatarHairColorView?.setOnClickListener { displayCustomizationFragment("hair", "color") }
- binding?.avatarHairBangsView?.setOnClickListener { displayCustomizationFragment("hair", "bangs") }
- binding?.avatarHairBaseView?.setOnClickListener { displayCustomizationFragment("hair", "base") }
- binding?.avatarAccentView?.setOnClickListener { displayCustomizationFragment("hair", "flower") }
- binding?.avatarHairBeardView?.setOnClickListener { displayCustomizationFragment("hair", "beard") }
- binding?.avatarHairMustacheView?.setOnClickListener { displayCustomizationFragment("hair", "mustache") }
- binding?.avatarBackgroundView?.setOnClickListener { displayCustomizationFragment("background", null) }
-
- userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = super.onCreateView(inflater, container, savedInstanceState)
+ binding?.composeView?.apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ HabiticaTheme {
+ AvatarOverviewView(userViewModel, { type, category ->
+ displayCustomizationFragment(type, category)
+ }, { type, equipped, isCostume ->
+ displayEquipmentFragment(type, equipped, isCostume)
+ })
+ }
+ }
+ }
+ return view
}
override fun injectFragment(component: UserComponent) {
@@ -52,46 +74,16 @@ class AvatarOverviewFragment : BaseMainFragment()
}
private fun displayCustomizationFragment(type: String, category: String?) {
- MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarDetail(type, category ?: ""))
+ MainNavigationController.navigate(
+ AvatarOverviewFragmentDirections.openAvatarDetail(
+ type,
+ category ?: ""
+ )
+ )
}
- private fun displayEquipmentFragment(type: String, category: String?) {
- MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
- }
-
- fun updateUser(user: User?) {
- this.setSize(user?.preferences?.size)
- setCustomizations(user)
- }
-
- private fun setCustomizations(user: User?) {
- if (user == null) return
- binding?.avatarShirtView?.customizationIdentifier = user.preferences?.size + "_shirt_" + user.preferences?.shirt
- binding?.avatarSkinView?.customizationIdentifier = "skin_" + user.preferences?.skin
- val chair = user.preferences?.chair
- binding?.avatarChairView?.customizationIdentifier = if (chair?.startsWith("handleless") == true) "chair_$chair" else chair
- binding?.avatarGlassesView?.equipmentIdentifier = user.equipped?.eyeWear
- binding?.avatarAnimalEarsView?.equipmentIdentifier = user.equipped?.headAccessory
- binding?.avatarHeadbandView?.equipmentIdentifier = user.equipped?.headAccessory
- binding?.avatarAnimalTailView?.equipmentIdentifier = user.equipped?.back
- binding?.avatarHairColorView?.customizationIdentifier = if (user.preferences?.hair?.color != null && user.preferences?.hair?.color != "") "hair_bangs_1_" + user.preferences?.hair?.color else ""
- binding?.avatarHairBangsView?.customizationIdentifier = if (user.preferences?.hair?.bangs != null && user.preferences?.hair?.bangs != 0) "hair_bangs_" + user.preferences?.hair?.bangs + "_" + user.preferences?.hair?.color else ""
- binding?.avatarHairBaseView?.customizationIdentifier = if (user.preferences?.hair?.base != null && user.preferences?.hair?.base != 0) "hair_base_" + user.preferences?.hair?.base + "_" + user.preferences?.hair?.color else ""
- binding?.avatarAccentView?.customizationIdentifier = if (user.preferences?.hair?.flower != null && user.preferences?.hair?.flower != 0) "hair_flower_" + user.preferences?.hair?.flower else ""
- binding?.avatarHairBeardView?.customizationIdentifier = if (user.preferences?.hair?.beard != null && user.preferences?.hair?.beard != 0) "hair_beard_" + user.preferences?.hair?.beard + "_" + user.preferences?.hair?.color else ""
- binding?.avatarHairMustacheView?.customizationIdentifier = if (user.preferences?.hair?.mustache != null && user.preferences?.hair?.mustache != 0) "hair_mustache_" + user.preferences?.hair?.mustache + "_" + user.preferences?.hair?.color else ""
- binding?.avatarBackgroundView?.customizationIdentifier = "background_" + user.preferences?.background
- }
-
- private fun setSize(size: String?) {
- if (size == null) {
- return
- }
- if (size == "slim") {
- binding?.avatarSizeSpinner?.setSelection(0, false)
- } else {
- binding?.avatarSizeSpinner?.setSelection(1, false)
- }
+ private fun displayEquipmentFragment(type: String, equipped: String?, isCostume: Boolean = false) {
+ MainNavigationController.navigate(EquipmentOverviewFragmentDirections.openEquipmentDetail(type, isCostume, equipped ?: ""))
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
@@ -103,5 +95,68 @@ class AvatarOverviewFragment : BaseMainFragment()
)
}
- override fun onNothingSelected(parent: AdapterView<*>) { /* no-on */ }
+ override fun onNothingSelected(parent: AdapterView<*>) { /* no-on */
+ }
}
+
+@Composable
+fun AvatarOverviewView(userViewModel: MainUserViewModel,
+ onCustomizationTap: (String, String?) -> Unit,
+ onEquipmentTap: (String, String?, Boolean) -> Unit
+ ) {
+ val user by userViewModel.user.observeAsState()
+ Column(
+ Modifier
+ .padding(horizontal = 8.dp)
+ .padding(bottom = 16.dp)) {
+ Row(Modifier.padding(horizontal = 12.dp, vertical = 15.dp)) {
+ Text(
+ stringResource(R.string.avatar_size),
+ style = HabiticaTheme.typography.subtitle2
+ )
+ }
+ AvatarCustomizationOverviewView(user?.preferences, onCustomizationTap)
+ Row(
+ Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 15.dp),
+ verticalAlignment = Alignment.CenterVertically) {
+ Text(stringResource(R.string.equipped), style = HabiticaTheme.typography.subtitle2)
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ stringResource(R.string.equip_automatically),
+ style = HabiticaTheme.typography.body2
+ )
+ Switch(checked = user?.preferences?.autoEquip == true, onCheckedChange = {
+ userViewModel.updateUser("preferences.autoEquip", it)
+ })
+ }
+ EquipmentOverviewView(user?.items?.gear?.equipped, { type, equipped ->
+ onEquipmentTap(type, equipped, false)
+ })
+ Row(
+ Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 15.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ stringResource(R.string.costume),
+ style = HabiticaTheme.typography.subtitle2
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ stringResource(R.string.wear_costume),
+ style = HabiticaTheme.typography.body2
+ )
+ Switch(checked = user?.preferences?.costume == true, onCheckedChange = {
+ userViewModel.updateUser("preferences.costume", it)
+ })
+ }
+ AnimatedVisibility(visible = user?.preferences?.costume == true) {
+ EquipmentOverviewView(user?.items?.gear?.costume, { type, equipped ->
+ onEquipmentTap(type, equipped, true)
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt
deleted file mode 100644
index 7925ca84e..000000000
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/equipment/EquipmentOverviewFragment.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.habitrpg.android.habitica.ui.fragments.inventory.equipment
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.viewModels
-import com.habitrpg.android.habitica.components.UserComponent
-import com.habitrpg.android.habitica.databinding.FragmentEquipmentOverviewBinding
-import com.habitrpg.android.habitica.helpers.MainNavigationController
-import com.habitrpg.android.habitica.models.user.Gear
-import com.habitrpg.android.habitica.models.user.Outfit
-import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
-import com.habitrpg.android.habitica.ui.viewmodels.inventory.equipment.EquipmentOverviewViewModel
-import com.habitrpg.android.habitica.ui.views.equipment.EquipmentOverviewView
-
-class EquipmentOverviewFragment : BaseMainFragment() {
-
- private val viewModel: EquipmentOverviewViewModel by viewModels()
-
- override var binding: FragmentEquipmentOverviewBinding? = null
-
- override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentEquipmentOverviewBinding {
- return FragmentEquipmentOverviewBinding.inflate(inflater, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding?.battlegearView?.onNavigate = { type, equipped ->
- displayEquipmentDetailList(type, equipped, false)
- }
- binding?.costumeView?.onNavigate = { type, equipped ->
- displayEquipmentDetailList(type, equipped, true)
- }
-
- binding?.autoEquipSwitch?.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked == viewModel.user.value?.preferences?.autoEquip) return@setOnCheckedChangeListener
- viewModel.updateUser("preferences.autoEquip", isChecked)
- }
- binding?.costumeSwitch?.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked == viewModel.user.value?.preferences?.costume) return@setOnCheckedChangeListener
- viewModel.updateUser("preferences.costume", isChecked)
- }
-
- viewModel.user.observe(viewLifecycleOwner) {
- it?.items?.gear?.let {
- updateGearData(it)
- }
- binding?.autoEquipSwitch?.isChecked = viewModel.usesAutoEquip
- binding?.costumeSwitch?.isChecked = viewModel.usesCostume
-
- binding?.costumeView?.isEnabled = viewModel.usesCostume
- }
- }
-
- override fun injectFragment(component: UserComponent) {
- component.inject(this)
- }
-
- private fun displayEquipmentDetailList(type: String, equipped: String?, isCostume: Boolean?) {
- MainNavigationController.navigate(EquipmentOverviewFragmentDirections.openEquipmentDetail(type, isCostume ?: false, equipped ?: ""))
- }
-
- private fun updateGearData(gear: Gear) {
- updateOutfit(binding?.battlegearView, gear.equipped)
- updateOutfit(binding?.costumeView, gear.costume)
- }
-
- private fun updateOutfit(view: EquipmentOverviewView?, outfit: Outfit?) {
- if (outfit?.weapon?.isNotEmpty() == true) {
- viewModel.getGear(outfit.weapon) {
- if (it.isValid) {
- view?.updateData(outfit, it.twoHanded)
- }
- }
- } else {
- view?.updateData(outfit)
- }
- }
-}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt
new file mode 100644
index 000000000..a82ff67d1
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt
@@ -0,0 +1,111 @@
+package com.habitrpg.android.habitica.ui.theme
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.google.android.material.composethemeadapter.createMdcTheme
+
+@Composable
+fun HabiticaTheme(
+ content: @Composable () -> Unit
+) {
+ val context = LocalContext.current
+ val layoutDirection = LocalLayoutDirection.current
+ val (colors, typography, shapes) = createMdcTheme(
+ context = context,
+ layoutDirection = layoutDirection,
+ setTextColors = true
+ )
+ MaterialTheme(
+ colors = colors ?: MaterialTheme.colors,
+ typography = Typography(
+ defaultFontFamily = FontFamily.Default,
+ h1 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 20.sp,
+ letterSpacing = (0.05).sp
+ ),
+ h2 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 28.sp,
+ letterSpacing = (0.05).sp
+ ),
+ subtitle1 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ ),
+ subtitle2 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ letterSpacing = 0.1.sp
+ ),
+ body1 = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ letterSpacing = 0.35.sp,
+ lineHeight = 16.sp
+ ),
+ body2 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ letterSpacing = 0.2.sp,
+ lineHeight = 16.sp
+ ),
+ button = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ letterSpacing = 1.25.sp
+ ),
+ caption = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp
+ ),
+ overline = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 10.sp,
+ letterSpacing = 1.5.sp
+ )
+ ),
+ shapes = shapes ?: MaterialTheme.shapes,
+ content = content
+ )
+}
+
+val Typography.caption1
+ get() = caption
+val Typography.caption2
+get() = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ letterSpacing = 0.4.sp
+)
+val Typography.caption3
+ get() = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ letterSpacing = 0.3.sp,
+ lineHeight = 14.sp
+ )
+val Typography.caption4
+ get() = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ letterSpacing = 0.35.sp
+ )
+val Typography.subtitle3
+ get() = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ letterSpacing = 0.15.sp
+ )
+object HabiticaTheme {
+ val typography: Typography
+ @Composable
+ get() = MaterialTheme.typography
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentOverviewView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentOverviewView.kt
new file mode 100644
index 000000000..ce37fef3b
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/EquipmentOverviewView.kt
@@ -0,0 +1,217 @@
+package com.habitrpg.android.habitica.ui.views
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.colorResource
+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 com.habitrpg.android.habitica.R
+import com.habitrpg.android.habitica.models.user.Outfit
+import com.habitrpg.android.habitica.models.user.Preferences
+import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
+import com.habitrpg.android.habitica.ui.theme.caption2
+
+@Composable
+fun OverviewItem(
+ text: String,
+ iconName: String?,
+ modifier: Modifier = Modifier,
+ isTwoHanded: Boolean = false
+) {
+ val hasIcon = iconName?.isNotBlank() == true
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier
+ .width(70.dp)
+ ) {
+ Box(
+ Modifier
+ .size(70.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(colorResource(if (hasIcon) R.color.gray_700 else R.color.gray_10)),
+ contentAlignment = Alignment.Center
+ ) {
+ if (isTwoHanded) {
+ Image(painterResource(R.drawable.equipment_two_handed), null)
+ } else if (hasIcon) {
+ PixelArtView(
+ imageName = iconName, Modifier
+ .size(70.dp)
+ )
+ } else {
+ Image(painterResource(R.drawable.equipment_nothing_equipped), null)
+ }
+ }
+ Text(
+ text,
+ style = HabiticaTheme.typography.caption2,
+ color = colorResource(R.color.gray_400),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ }
+}
+
+@Composable
+fun EquipmentOverviewView(
+ outfit: Outfit?,
+ onEquipmentTap: (String, String?) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(18.dp),
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(6.dp))
+ .background(colorResource(R.color.gray_50))
+ .padding(12.dp)
+ ) {
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ OverviewItem(stringResource(R.string.outfit_weapon), outfit?.weapon.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("weapon", null)
+ })
+ OverviewItem(stringResource(R.string.outfit_shield), outfit?.shield.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("shield", null)
+ })
+ OverviewItem(stringResource(R.string.outfit_head), outfit?.head.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("head", null)
+ })
+ OverviewItem(stringResource(R.string.outfit_armor), outfit?.armor.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("armor", null)
+ })
+ }
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ OverviewItem(
+ stringResource(R.string.outfit_headAccessory),
+ outfit?.headAccessory.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("headAccessory", null)
+ })
+ OverviewItem(stringResource(R.string.outfit_body), outfit?.body.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("body", null)
+ })
+ OverviewItem(stringResource(R.string.outfit_back), outfit?.back.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("back", null)
+ })
+ OverviewItem(
+ stringResource(R.string.outfit_eyewear),
+ outfit?.eyeWear.let { "shop_$it" }, Modifier.clickable {
+ onEquipmentTap("eyewear", null)
+ })
+ }
+ }
+}
+
+@Composable
+fun AvatarCustomizationOverviewView(
+ preferences: Preferences?,
+ onCustomizationTap: (String, String?) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(18.dp),
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(6.dp))
+ .background(colorResource(R.color.gray_50))
+ .padding(12.dp)
+ ) {
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ OverviewItem(
+ stringResource(R.string.avatar_shirt),
+ preferences?.shirt.let { "${preferences?.size}_shirt$it" }, Modifier.clickable {
+ onCustomizationTap("shirt", null)
+ })
+ OverviewItem(
+ stringResource(R.string.avatar_skin),
+ preferences?.skin.let { "skin_$it" },
+ Modifier.clickable {
+ onCustomizationTap("skin", null)
+ })
+ OverviewItem(
+ stringResource(R.string.avatar_hair_color),
+ if (preferences?.hair?.color != null && preferences.hair?.color != "") "hair_bangs_1_" + preferences.hair?.color else "",
+ Modifier.clickable {
+ onCustomizationTap("hair", "color")
+ }
+ )
+ OverviewItem(
+ stringResource(R.string.avatar_hair_bangs),
+ if (preferences?.hair?.bangs != null && preferences.hair?.bangs != 0) "hair_bangs_" + preferences.hair?.bangs + "_" + preferences.hair?.color else "",
+ Modifier.clickable {
+ onCustomizationTap("hair", "bangs")
+ }
+ )
+ }
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ OverviewItem(
+ stringResource(R.string.avatar_style),
+ if (preferences?.hair?.base != null && preferences.hair?.base != 0) "hair_base_" + preferences.hair?.base + "_" + preferences.hair?.color else "",
+ Modifier.clickable {
+ onCustomizationTap("hair", "base")
+ }
+ )
+ OverviewItem(
+ stringResource(R.string.avatar_mustache),
+ if (preferences?.hair?.mustache != null && preferences.hair?.mustache != 0) "hair_mustache_" + preferences.hair?.mustache + "_" + preferences.hair?.color else "",
+ Modifier.clickable {
+ onCustomizationTap("hair", "mustache")
+ }
+ )
+ OverviewItem(
+ stringResource(R.string.avatar_beard),
+ if (preferences?.hair?.beard != null && preferences.hair?.beard != 0) "hair_beard_" + preferences.hair?.beard + "_" + preferences.hair?.color else "",
+ Modifier.clickable {
+ onCustomizationTap("hair", "beard")
+ }
+ )
+ OverviewItem(
+ stringResource(R.string.avatar_flower),
+ if (preferences?.hair?.flower != null && preferences.hair?.flower != 0) "hair_flower_" + preferences.hair?.flower else "",
+ Modifier.clickable {
+ onCustomizationTap("hair", "flower")
+ }
+ )
+ }
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ OverviewItem(
+ stringResource(R.string.avatar_wheelchair),
+ preferences?.chair?.let { if (it.startsWith("handleless")) "chair_$it" else it })
+ OverviewItem(
+ stringResource(R.string.avatar_background),
+ preferences?.background.let { "background_$it" })
+ Box(Modifier.size(70.dp))
+ Box(Modifier.size(70.dp))
+ }
+ }
+}
+
+@Preview
+@Composable
+fun EquipmentOverviewItemPreview() {
+ Column(Modifier.width(320.dp)) {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ OverviewItem("Main-Hand", "shop_weapon_warrior_1")
+ OverviewItem("Off-Hand", null, isTwoHanded = true)
+ OverviewItem("Armor", null)
+ }
+ EquipmentOverviewView(null, { _, _ -> })
+ AvatarCustomizationOverviewView(null, { _, _ -> })
+ }
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PixelArtView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PixelArtView.kt
new file mode 100644
index 000000000..d9f95f89c
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/PixelArtView.kt
@@ -0,0 +1,28 @@
+package com.habitrpg.android.habitica.ui.views
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.habitrpg.common.habitica.extensions.loadImage
+import com.habitrpg.common.habitica.views.PixelArtView
+
+@Composable
+fun PixelArtView(
+ imageName: String?,
+ modifier: Modifier = Modifier,
+ imageFormat: String? = null
+) {
+ AndroidView(
+ modifier = modifier, // Occupy the max size in the Compose UI tree
+ factory = { context ->
+ PixelArtView(context)
+ },
+ update = { view ->
+ if (imageName != null) {
+ view.loadImage(imageName, imageFormat)
+ } else {
+ view.bitmap = null
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 41ced8bc4..0518c1c63 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,7 @@ buildscript {
coroutines_version = '1.6.4'
daggerhilt_version = '2.42'
firebase_bom = '30.2.0'
- kotlin_version = '1.7.10'
+ kotlin_version = '1.7.20'
lifecycle_version = '2.5.1'
markwon_version = '4.6.2'
moshi_version = '1.13.0'