diff --git a/.gitignore b/.gitignore
index b8dd120d49..428073f23e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,3 +72,4 @@ fastlane/metadata/android/*/changelogs/
output-metadata.json
/Habitica/jacoco.exec
*.dm
+/fastlane/upload_slack.py
diff --git a/Habitica/res/navigation/navigation.xml b/Habitica/res/navigation/navigation.xml
index 5417093318..75ccee4373 100644
--- a/Habitica/res/navigation/navigation.xml
+++ b/Habitica/res/navigation/navigation.xml
@@ -114,6 +114,9 @@
+
@@ -129,6 +132,17 @@
android:name="category"
app:argType="string" />
+
+
+
+
Spring
Summer
Fall
+ Animal Tails
- You
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 35c531ab5a..4ed05397a4 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
@@ -148,7 +148,11 @@ open class AvatarOverviewFragment :
type: String,
category: String?,
) {
- MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
+ if (appConfigManager.enableCustomizationShop()) {
+ MainNavigationController.navigate(AvatarOverviewFragmentDirections.openComposeAvatarEquipment(type, category ?: ""))
+ } else {
+ MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
+ }
}
private fun displayEquipmentFragment(
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt
new file mode 100644
index 0000000000..d0a5e19298
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/ComposeAvatarEquipmentFragment.kt
@@ -0,0 +1,314 @@
+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.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
+import com.habitrpg.android.habitica.R
+import com.habitrpg.android.habitica.data.InventoryRepository
+import com.habitrpg.android.habitica.databinding.FragmentComposeBinding
+import com.habitrpg.android.habitica.helpers.Analytics
+import com.habitrpg.android.habitica.helpers.AppConfigManager
+import com.habitrpg.android.habitica.models.inventory.Customization
+import com.habitrpg.android.habitica.models.inventory.Equipment
+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.theme.colors
+import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
+import com.habitrpg.android.habitica.ui.views.PixelArtView
+import com.habitrpg.common.habitica.helpers.MainNavigationController
+import com.habitrpg.common.habitica.helpers.launchCatching
+import com.habitrpg.common.habitica.theme.HabiticaTheme
+import com.habitrpg.common.habitica.views.ComposableAvatarView
+import com.habitrpg.shared.habitica.models.Avatar
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.combine
+import javax.inject.Inject
+
+class AvatarEquipmentViewModel : ViewModel() {
+ var type: String? = null
+ var category: String? = null
+
+ val items = mutableStateListOf()
+ val activeEquipment = mutableStateOf(null)
+
+ val typeNameId: Int
+ get() =
+ when (type) {
+ "headAccessory" -> R.string.animal_ears
+ "back" -> R.string.animal_tails
+ else -> R.string.customizations
+ }
+}
+
+@AndroidEntryPoint
+class ComposeAvatarEquipmentFragment :
+ BaseMainFragment() {
+ private val viewModel: AvatarEquipmentViewModel by viewModels()
+
+ @Inject
+ lateinit var inventoryRepository: InventoryRepository
+
+ @Inject
+ lateinit var userViewModel: MainUserViewModel
+
+ @Inject
+ lateinit var configManager: AppConfigManager
+
+ override var binding: FragmentComposeBinding? = null
+
+ override fun createBinding(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ ): FragmentComposeBinding {
+ return FragmentComposeBinding.inflate(inflater, container, false)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ showsBackButton = true
+ hidesToolbar = true
+ val view = super.onCreateView(inflater, container, savedInstanceState)
+ binding?.composeView?.apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ HabiticaTheme {
+ val activeEquipment by viewModel.activeEquipment
+ val avatar by userViewModel.user.observeAsState()
+ AvatarEquipmentView(avatar = avatar, configManager = configManager, viewModel.items, viewModel.type, stringResource(viewModel.typeNameId), activeEquipment) { equipment ->
+ lifecycleScope.launchCatching {
+ if (equipment.key?.isNotBlank() != true) {
+ inventoryRepository.equip(viewModel.type ?: "", activeEquipment ?: "")
+ } else {
+ inventoryRepository.equip(
+ equipment.type ?: "",
+ equipment.key ?: "",
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ return view
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?,
+ ) {
+ showsBackButton = true
+ super.onViewCreated(view, savedInstanceState)
+ arguments?.let {
+ val args = ComposeAvatarEquipmentFragmentArgs.fromBundle(it)
+ viewModel.type = args.type
+ if (args.category.isNotEmpty()) {
+ viewModel.category = args.category
+ }
+ }
+ this.loadEquipment()
+
+ userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
+
+ Analytics.sendNavigationEvent("${viewModel.type} screen")
+ }
+
+ private fun loadEquipment() {
+ val type = viewModel.type ?: return
+ lifecycleScope.launchCatching {
+ inventoryRepository.getEquipmentType(type, viewModel.category ?: "")
+ .combine(inventoryRepository.getOwnedEquipment(type), ::Pair)
+ .collect { (equipment, ownedEquipment) ->
+ viewModel.items.clear()
+ viewModel.items.addAll(equipment.filter {
+ ownedEquipment.firstOrNull { owned -> owned.key == it.key } != null
+ })
+ }
+ }
+ }
+
+ fun updateUser(user: User?) {
+ this.updateActiveCustomization(user)
+ }
+
+ private fun updateActiveCustomization(user: User?) {
+ if (viewModel.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 (viewModel.type) {
+ "headAccessory" -> outfit?.headAccessory
+ "back" -> outfit?.back
+ "eyewear" -> outfit?.eyeWear
+ else -> ""
+ }
+ if (activeEquipment != null) {
+ viewModel.activeEquipment.value = activeEquipment
+ }
+ }
+}
+
+
+@Composable
+private fun AvatarEquipmentView(
+ avatar: Avatar?,
+ configManager: AppConfigManager,
+ items: List,
+ type: String?,
+ typeName: String,
+ activeCustomization: String?,
+ onSelect: (Equipment) -> Unit,
+) {
+ val nestedScrollInterop = rememberNestedScrollInteropConnection()
+ val totalWidth = LocalConfiguration.current.screenWidthDp.dp
+ val horizontalPadding = (totalWidth - (84.dp * 3)) / 2
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.background(colorResource(R.color.window_background))) {
+ ComposableAvatarView(
+ avatar = avatar,
+ configManager = configManager,
+ modifier =
+ Modifier
+ .padding(vertical = 24.dp)
+ .size(140.dp, 147.dp),
+ )
+ Box(
+ Modifier
+ .background(colorResource(R.color.content_background), RoundedCornerShape(topStart = 22.dp, topEnd = 22.dp))
+ .fillMaxWidth()
+ .height(22.dp),
+ )
+ }
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(76.dp),
+ horizontalArrangement = Arrangement.Center,
+ contentPadding = PaddingValues(horizontal = horizontalPadding),
+ modifier =
+ Modifier
+ .nestedScroll(nestedScrollInterop)
+ .background(colorResource(R.color.content_background)),
+ ) {
+ item(span = { GridItemSpan(3) }) {
+ Text(
+ typeName.uppercase(),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = colorResource(id = R.color.text_ternary),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(10.dp),
+ )
+ }
+ if (items.size > 1) {
+ items(items, span = { item -> if (item is Customization) GridItemSpan(1) else GridItemSpan(3) }) { item ->
+ if (item is Equipment) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier =
+ Modifier
+ .padding(4.dp)
+ .border(if (activeCustomization == item.key) 2.dp else 0.dp, if (activeCustomization == item.key) HabiticaTheme.colors.tintedUiMain else colorResource(R.color.transparent), RoundedCornerShape(8.dp))
+ .size(76.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .clickable {
+ onSelect(item)
+ }
+ .background(colorResource(id = R.color.window_background)),
+ ) {
+ if (item.key.isNullOrBlank() || item.key == "0") {
+ Image(painterResource(R.drawable.empty_slot), contentDescription = null, contentScale = ContentScale.None, modifier = Modifier.size(68.dp))
+ } else {
+ PixelArtView(
+ imageName = item.key,
+ Modifier.size(68.dp),
+ )
+ }
+ }
+ } else if (item is String) {
+ Text(
+ item.uppercase(),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = colorResource(id = R.color.text_ternary),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(10.dp).padding(top = 16.dp),
+ )
+ }
+ }
+ }
+ item(span = { GridItemSpan(3) }) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ Modifier.padding(top = 56.dp).clickable {
+ MainNavigationController.navigate(R.id.customizationsShopFragment)
+ },
+ ) {
+ Image(
+ painterResource(if (type == "backgrounds") R.drawable.customization_background else R.drawable.customization_mix),
+ null,
+ modifier = Modifier.padding(bottom = 16.dp),
+ )
+ if (items.size <= 1) {
+ Text(
+ stringResource(R.string.customizations_no_owned), fontSize = 16.sp, fontWeight = FontWeight.Bold, color = colorResource(R.color.text_secondary),
+ modifier = Modifier.padding(bottom = 2.dp))
+ Text(stringResource(R.string.customization_shop_check_out), fontSize = 14.sp, color = colorResource(R.color.text_ternary), textAlign = TextAlign.Center)
+ } else {
+ Text(
+ stringResource(R.string.looking_for_more), fontSize = 16.sp, fontWeight = FontWeight.Bold, color = colorResource(R.color.text_secondary),
+ modifier = Modifier.padding(bottom = 2.dp))
+ Text(stringResource(R.string.customization_shop_more), fontSize = 14.sp, color = colorResource(R.color.text_ternary), textAlign = TextAlign.Center)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/version.properties b/version.properties
index 7f88b48c04..ee5bd64594 100644
--- a/version.properties
+++ b/version.properties
@@ -1,2 +1,2 @@
NAME=4.3.7
-CODE=7611
\ No newline at end of file
+CODE=7631
\ No newline at end of file
diff --git a/wearos/src/main/res/layout/row_daily.xml b/wearos/src/main/res/layout/row_daily.xml
index 95898e2549..e1ea83b52d 100644
--- a/wearos/src/main/res/layout/row_daily.xml
+++ b/wearos/src/main/res/layout/row_daily.xml
@@ -1,37 +1,42 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/chip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="52dp"
+ style="@style/Chip">
+
+ android:id="@+id/checkbox_wrapper"
+ android:layout_width="34dp"
+ android:layout_height="34dp"
+ android:background="@drawable/circle"
+ android:layout_marginEnd="6dp"
+ android:foreground="@drawable/task_score_feedback">
+
+ android:id="@+id/checkbox"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:background="@drawable/daily_square"
+ android:scaleType="center"
+ android:layout_gravity="center" />
+
-
+ android:orientation="vertical">
+
+
diff --git a/wearos/src/main/res/layout/row_habit.xml b/wearos/src/main/res/layout/row_habit.xml
index cba44ada12..090395545a 100644
--- a/wearos/src/main/res/layout/row_habit.xml
+++ b/wearos/src/main/res/layout/row_habit.xml
@@ -1,38 +1,39 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="52dp"
+ style="@style/Chip">
+ android:id="@+id/habit_button"
+ android:layout_width="34dp"
+ android:layout_height="34dp"
+ android:background="@drawable/circle"
+ android:layout_marginEnd="@dimen/spacing_medium"
+ android:foreground="@drawable/task_score_feedback">
+ android:id="@+id/habit_button_icon"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="center" />
-
-
+ android:orientation="vertical">
+
+
diff --git a/wearos/src/main/res/layout/row_reward.xml b/wearos/src/main/res/layout/row_reward.xml
index dd7e477776..91ab0d6b4b 100644
--- a/wearos/src/main/res/layout/row_reward.xml
+++ b/wearos/src/main/res/layout/row_reward.xml
@@ -2,7 +2,8 @@
-
-
-
-
+
+
+
-
-
-
\ No newline at end of file
+ style="@style/Text.Body1" />
+
+