Merge avatar and equipment screens

This commit is contained in:
Phillip Thelen 2022-10-07 13:22:56 +02:00
parent 8fe9de1223
commit 5ae7f9bbb7
15 changed files with 506 additions and 320 deletions

View file

@ -83,7 +83,7 @@ GEM
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-properties (1.1.2) fastlane-plugin-properties (1.1.2)
java-properties java-properties
fastlane-plugin-semantic_release (1.14.1) fastlane-plugin-semantic_release (1.18.0)
fastlane-plugin-versioning_android (0.1.0) fastlane-plugin-versioning_android (0.1.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-api-client (0.38.0) google-api-client (0.38.0)

View file

@ -79,7 +79,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules: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.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
@ -109,18 +109,18 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-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 "androidx.paging:paging-runtime-ktx:3.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.animation:animation:$compose_version" implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$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' implementation 'com.willowtreeapps:signinwithapplebutton:0.3'
@ -172,7 +172,7 @@ android {
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.3.1" kotlinCompilerExtensionVersion = "1.3.2"
} }
signingConfigs { signingConfigs {

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>

View file

@ -12,6 +12,7 @@
<string name="sidebar_challenges">Challenges</string> <string name="sidebar_challenges">Challenges</string>
<string name="sidebar_section_inventory">Inventory</string> <string name="sidebar_section_inventory">Inventory</string>
<string name="sidebar_avatar">Avatar Customization</string> <string name="sidebar_avatar">Avatar Customization</string>
<string name="sidebar_avatar_equipment">Avatar &amp; Equipment</string>
<string name="sidebar_equipment">Equipment</string> <string name="sidebar_equipment">Equipment</string>
<string name="sidebar_stable">Pets &amp; Mounts</string> <string name="sidebar_stable">Pets &amp; Mounts</string>
<string name="sidebar_news">News</string> <string name="sidebar_news">News</string>

View file

@ -23,9 +23,9 @@ class HabiticaFirebaseMessagingService : FirebaseMessagingService() {
PushNotificationManager.displayNotification(remoteMessage, applicationContext) PushNotificationManager.displayNotification(remoteMessage, applicationContext)
if (remoteMessage.data["identifier"]?.contains(PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY) == true) { 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()) // userRepository.retrieveUser(true).subscribe({}, RxErrorHandler.handleEmptyError())
} // }
} }
} }

View file

@ -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.models.user.UserQuestStatus
import com.habitrpg.android.habitica.ui.TutorialView import com.habitrpg.android.habitica.ui.TutorialView
import com.habitrpg.android.habitica.ui.fragments.NavigationDrawerFragment 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.MainActivityViewModel
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel
import com.habitrpg.android.habitica.ui.views.AppHeaderView import com.habitrpg.android.habitica.ui.views.AppHeaderView
@ -212,7 +213,7 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
setupBottomnavigationLayoutListener() setupBottomnavigationLayoutListener()
binding.content.headerView.setContent { binding.content.headerView.setContent {
MdcTheme(setTextColors = true) { HabiticaTheme {
AppHeaderView(viewModel.userViewModel) AppHeaderView(viewModel.userViewModel)
} }
} }

View file

@ -29,11 +29,10 @@ import com.habitrpg.android.habitica.databinding.DrawerMainBinding
import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds
import com.habitrpg.android.habitica.extensions.getRemainingString import com.habitrpg.android.habitica.extensions.getRemainingString
import com.habitrpg.android.habitica.extensions.getShortRemainingString 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.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.AppConfigManager 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.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.WorldStateEvent import com.habitrpg.android.habitica.models.WorldStateEvent
import com.habitrpg.android.habitica.models.inventory.Item import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion 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(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(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.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.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.gemPurchaseActivity, SIDEBAR_GEMS, context.getString(R.string.sidebar_gems)))
items.add(HabiticaDrawerItem(R.id.subscriptionPurchaseActivity, SIDEBAR_SUBSCRIPTION, context.getString(R.string.sidebar_subscription))) items.add(HabiticaDrawerItem(R.id.subscriptionPurchaseActivity, SIDEBAR_SUBSCRIPTION, context.getString(R.string.sidebar_subscription)))

View file

@ -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<FragmentRefreshRecyclerviewBinding>(),
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<UnlockResponse> {
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
}
}
}

View file

@ -5,46 +5,68 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView 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.components.UserComponent
import com.habitrpg.android.habitica.databinding.FragmentAvatarOverviewBinding import com.habitrpg.android.habitica.databinding.FragmentComposeScrollingBinding
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.ExceptionHandler 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.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.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.AvatarCustomizationOverviewView
import com.habitrpg.android.habitica.ui.views.EquipmentOverviewView
import javax.inject.Inject import javax.inject.Inject
class AvatarOverviewFragment : BaseMainFragment<FragmentAvatarOverviewBinding>(), AdapterView.OnItemSelectedListener { class AvatarOverviewFragment : BaseMainFragment<FragmentComposeScrollingBinding>(),
AdapterView.OnItemSelectedListener {
@Inject @Inject
lateinit var userViewModel: MainUserViewModel lateinit var userViewModel: MainUserViewModel
override var binding: FragmentAvatarOverviewBinding? = null override var binding: FragmentComposeScrollingBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentAvatarOverviewBinding { override fun createBinding(
return FragmentAvatarOverviewBinding.inflate(inflater, container, false) inflater: LayoutInflater,
container: ViewGroup?
): FragmentComposeScrollingBinding {
return FragmentComposeScrollingBinding.inflate(inflater, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onCreateView(
super.onViewCreated(view, savedInstanceState) inflater: LayoutInflater,
binding?.avatarSizeSpinner?.onItemSelectedListener = this container: ViewGroup?,
savedInstanceState: Bundle?
binding?.avatarShirtView?.setOnClickListener { displayCustomizationFragment("shirt", null) } ): View? {
binding?.avatarSkinView?.setOnClickListener { displayCustomizationFragment("skin", null) } val view = super.onCreateView(inflater, container, savedInstanceState)
binding?.avatarChairView?.setOnClickListener { displayCustomizationFragment("chair", null) } binding?.composeView?.apply {
binding?.avatarGlassesView?.setOnClickListener { displayEquipmentFragment("eyewear", "glasses") } setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
binding?.avatarAnimalEarsView?.setOnClickListener { displayEquipmentFragment("headAccessory", "animal") } setContent {
binding?.avatarAnimalTailView?.setOnClickListener { displayEquipmentFragment("back", "animal") } HabiticaTheme {
binding?.avatarHeadbandView?.setOnClickListener { displayEquipmentFragment("headAccessory", "headband") } AvatarOverviewView(userViewModel, { type, category ->
binding?.avatarHairColorView?.setOnClickListener { displayCustomizationFragment("hair", "color") } displayCustomizationFragment(type, category)
binding?.avatarHairBangsView?.setOnClickListener { displayCustomizationFragment("hair", "bangs") } }, { type, equipped, isCostume ->
binding?.avatarHairBaseView?.setOnClickListener { displayCustomizationFragment("hair", "base") } displayEquipmentFragment(type, equipped, isCostume)
binding?.avatarAccentView?.setOnClickListener { displayCustomizationFragment("hair", "flower") } })
binding?.avatarHairBeardView?.setOnClickListener { displayCustomizationFragment("hair", "beard") } }
binding?.avatarHairMustacheView?.setOnClickListener { displayCustomizationFragment("hair", "mustache") } }
binding?.avatarBackgroundView?.setOnClickListener { displayCustomizationFragment("background", null) } }
return view
userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
} }
override fun injectFragment(component: UserComponent) { override fun injectFragment(component: UserComponent) {
@ -52,46 +74,16 @@ class AvatarOverviewFragment : BaseMainFragment<FragmentAvatarOverviewBinding>()
} }
private fun displayCustomizationFragment(type: String, category: String?) { 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?) { private fun displayEquipmentFragment(type: String, equipped: String?, isCostume: Boolean = false) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: "")) MainNavigationController.navigate(EquipmentOverviewFragmentDirections.openEquipmentDetail(type, isCostume, equipped ?: ""))
}
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)
}
} }
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
@ -103,5 +95,68 @@ class AvatarOverviewFragment : BaseMainFragment<FragmentAvatarOverviewBinding>()
) )
} }
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)
})
}
}
}

View file

@ -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<FragmentEquipmentOverviewBinding>() {
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)
}
}
}

View file

@ -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
}

View file

@ -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, { _, _ -> })
}
}

View file

@ -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
}
}
)
}

View file

@ -14,7 +14,7 @@ buildscript {
coroutines_version = '1.6.4' coroutines_version = '1.6.4'
daggerhilt_version = '2.42' daggerhilt_version = '2.42'
firebase_bom = '30.2.0' firebase_bom = '30.2.0'
kotlin_version = '1.7.10' kotlin_version = '1.7.20'
lifecycle_version = '2.5.1' lifecycle_version = '2.5.1'
markwon_version = '4.6.2' markwon_version = '4.6.2'
moshi_version = '1.13.0' moshi_version = '1.13.0'