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" /> + +