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'