diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 356787ad7..5ce8fdf5a 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -98,7 +98,7 @@ dependencies { implementation 'com.google.firebase:firebase-messaging-ktx' implementation 'com.google.firebase:firebase-config-ktx' implementation 'com.google.firebase:firebase-perf-ktx' - implementation 'com.google.android.gms:play-services-ads:21.1.0' + implementation 'com.google.android.gms:play-services-ads:21.2.0' implementation "com.google.android.gms:play-services-auth:$play_auth_version" implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation "com.google.android.gms:play-services-wearable:$play_wearables_version" @@ -107,12 +107,20 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.1' + 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.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 'androidx.activity:activity-compose:1.5.1' + 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 'com.willowtreeapps:signinwithapplebutton:0.3' @@ -160,6 +168,11 @@ android { buildFeatures { viewBinding true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.3.1" } signingConfigs { diff --git a/Habitica/res/layout/activity_main_content.xml b/Habitica/res/layout/activity_main_content.xml index 7553ad017..a575dd7e5 100644 --- a/Habitica/res/layout/activity_main_content.xml +++ b/Habitica/res/layout/activity_main_content.xml @@ -28,16 +28,15 @@ app:expandedTitleMarginStart="0dp" app:layout_scrollFlags="scroll|exitUntilCollapsed"> - + app:layout_collapseMode="parallax"/> - - #2B203A + @color/yellow_100 diff --git a/Habitica/res/values/colors.xml b/Habitica/res/values/colors.xml index c6be00303..9d565e0b7 100644 --- a/Habitica/res/values/colors.xml +++ b/Habitica/res/values/colors.xml @@ -119,4 +119,5 @@ @color/brand_700 @color/white @color/maroon_5 + @color/yellow_1 diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt index c23b8c098..2550e226c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt @@ -137,9 +137,7 @@ class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.comm } fun enableTeamBoards(): Boolean { - if (BuildConfig.DEBUG) { - return true - } + return true return remoteConfig.getBoolean("enableTeamBoards") } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt index ba9d71278..12193ec65 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/PurchaseHandler.kt @@ -26,18 +26,15 @@ import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.extensions.addOkButton import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler -import com.habitrpg.common.habitica.models.IAPGift -import com.habitrpg.common.habitica.models.PurchaseValidationRequest -import com.habitrpg.common.habitica.models.Transaction import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.proxy.AnalyticsManager import com.habitrpg.android.habitica.ui.activities.PurchaseActivity import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog +import com.habitrpg.common.habitica.models.IAPGift +import com.habitrpg.common.habitica.models.PurchaseValidationRequest +import com.habitrpg.common.habitica.models.Transaction import io.reactivex.rxjava3.core.Flowable -import java.util.Date -import kotlin.time.DurationUnit -import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -45,6 +42,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject import retrofit2.HttpException +import java.util.Date +import kotlin.time.DurationUnit +import kotlin.time.toDuration class PurchaseHandler( private val context: Context, @@ -186,7 +186,8 @@ class PurchaseHandler( return skuDetailsResult.skuDetailsList } - fun purchase(activity: Activity, skuDetails: SkuDetails, recipient: String? = null) { + fun purchase(activity: Activity, skuDetails: SkuDetails, recipient: String? = null, isSaleGemPurchase: Boolean = false) { + this.isSaleGemPurchase = isSaleGemPurchase recipient?.let { addGift(skuDetails.sku, it) } @@ -346,13 +347,26 @@ class PurchaseHandler( } } + private var isSaleGemPurchase = false + private fun gemAmountString(sku: String): String { - return when (sku) { - PurchaseTypes.Purchase4Gems -> "4" - PurchaseTypes.Purchase21Gems -> "21" - PurchaseTypes.Purchase42Gems -> "42" - PurchaseTypes.Purchase84Gems -> "84" - else -> "" + if (isSaleGemPurchase) { + isSaleGemPurchase = false + return when (sku) { + PurchaseTypes.Purchase4Gems -> "5" + PurchaseTypes.Purchase21Gems -> "30" + PurchaseTypes.Purchase42Gems -> "60" + PurchaseTypes.Purchase84Gems -> "125" + else -> "" + } + } else { + return when (sku) { + PurchaseTypes.Purchase4Gems -> "4" + PurchaseTypes.Purchase21Gems -> "21" + PurchaseTypes.Purchase42Gems -> "42" + PurchaseTypes.Purchase84Gems -> "84" + else -> "" + } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Stats.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Stats.kt index 43a1b322f..6758fa30a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Stats.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Stats.kt @@ -1,6 +1,6 @@ package com.habitrpg.android.habitica.models.user -import android.content.Context +import android.content.res.Resources import com.google.gson.annotations.SerializedName import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.models.BaseObject @@ -50,13 +50,13 @@ open class Stats : RealmObject(), AvatarStats, BaseObject { } } - fun getTranslatedClassName(context: Context): String { + fun getTranslatedClassName(resources: Resources): String { return when (habitClass) { - HEALER -> context.getString(R.string.healer) - ROGUE -> context.getString(R.string.rogue) - WARRIOR -> context.getString(R.string.warrior) - MAGE -> context.getString(R.string.mage) - else -> context.getString(R.string.warrior) + HEALER -> resources.getString(R.string.healer) + ROGUE -> resources.getString(R.string.rogue) + WARRIOR -> resources.getString(R.string.warrior) + MAGE -> resources.getString(R.string.mage) + else -> resources.getString(R.string.warrior) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.java b/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.java index 3de77ecbe..5b45562fa 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.java @@ -38,7 +38,7 @@ public class UserModule { @Provides @UserScope - MainUserViewModel providesUserViewModel(UserRepository userRepository) { - return new MainUserViewModel(userRepository); + MainUserViewModel providesUserViewModel(String userID, UserRepository userRepository) { + return new MainUserViewModel(userID, userRepository); } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarWithBarsViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarWithBarsViewModel.kt index f15bdade5..83839e640 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarWithBarsViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/AvatarWithBarsViewModel.kt @@ -60,7 +60,7 @@ class AvatarWithBarsViewModel( binding.avatarView.setAvatar(user) if (stats.habitClass != null && stats is Stats) { - userClass = stats.getTranslatedClassName(context) + userClass = stats.getTranslatedClassName(context.resources) } binding.mpBar.visibility = if (stats.habitClass == null || (stats.lvl 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 30f23bcde..a40af0bb9 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 @@ -22,6 +22,7 @@ import androidx.navigation.NavDestination import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import com.google.android.gms.wearable.Wearable +import com.google.android.material.composethemeadapter.MdcTheme import com.google.firebase.perf.FirebasePerformance import com.habitrpg.android.habitica.BuildConfig import com.habitrpg.android.habitica.R @@ -29,7 +30,6 @@ import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.data.TaskRepository -import com.habitrpg.android.habitica.models.user.UserQuestStatus import com.habitrpg.android.habitica.databinding.ActivityMainBinding import com.habitrpg.android.habitica.extensions.hideKeyboard import com.habitrpg.android.habitica.extensions.observeOnce @@ -46,11 +46,12 @@ import com.habitrpg.android.habitica.interactors.DisplayItemDropUseCase import com.habitrpg.android.habitica.interactors.NotifyUserUseCase import com.habitrpg.android.habitica.models.TutorialStep import com.habitrpg.android.habitica.models.user.User -import com.habitrpg.android.habitica.ui.AvatarWithBarsViewModel +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.viewmodels.MainActivityViewModel import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel +import com.habitrpg.android.habitica.ui.views.AppHeaderView import com.habitrpg.android.habitica.ui.views.SnackbarActivity import com.habitrpg.android.habitica.ui.views.dialogs.QuestCompletedDialog import com.habitrpg.android.habitica.ui.views.yesterdailies.YesterdailyDialog @@ -61,9 +62,9 @@ import com.habitrpg.android.habitica.widget.TodoListWidgetProvider import com.habitrpg.common.habitica.extensions.dpToPx import com.habitrpg.common.habitica.extensions.getThemeColor import com.habitrpg.common.habitica.extensions.isUsingNightModeResources +import com.habitrpg.common.habitica.views.AvatarView import com.habitrpg.shared.habitica.models.responses.MaintenanceResponse import com.habitrpg.shared.habitica.models.responses.TaskScoringResult -import com.habitrpg.common.habitica.views.AvatarView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -100,7 +101,6 @@ open class MainActivity : BaseActivity(), SnackbarActivity { val snackbarContainer: ViewGroup get() = binding.content.snackbarContainer - private var avatarInHeader: AvatarWithBarsViewModel? = null val notificationsViewModel: NotificationsViewModel by viewModels() val viewModel: MainActivityViewModel by viewModels() private var sideAvatarView: AvatarView? = null @@ -147,7 +147,6 @@ open class MainActivity : BaseActivity(), SnackbarActivity { setupToolbar(binding.content.toolbar) - avatarInHeader = AvatarWithBarsViewModel(this, binding.content.avatarWithBars, viewModel.userViewModel) sideAvatarView = AvatarView(this, showBackground = true, showMount = false, showPet = false) viewModel.user.observe(this) { @@ -210,6 +209,12 @@ open class MainActivity : BaseActivity(), SnackbarActivity { supportActionBar?.setHomeButtonEnabled(true) setupNotifications() setupBottomnavigationLayoutListener() + + binding.content.headerView.setContent { + MdcTheme(setTextColors = true) { + AppHeaderView(viewModel.userViewModel) + } + } viewModel.onCreate() } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt index 18a67721d..9ee6475fb 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/BaseMainFragment.kt @@ -118,11 +118,11 @@ abstract class BaseMainFragment : BaseFragment() { } private fun hideToolbar() { - activity?.binding?.content?.avatarWithBars?.root?.visibility = View.GONE + activity?.binding?.content?.headerView?.visibility = View.GONE } private fun showToolbar() { - activity?.binding?.content?.avatarWithBars?.root?.visibility = View.VISIBLE + activity?.binding?.content?.headerView?.visibility = View.VISIBLE } private fun disableToolbarScrolling() { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GemsPurchaseFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GemsPurchaseFragment.kt index b6734195e..8ed0ae758 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GemsPurchaseFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/purchases/GemsPurchaseFragment.kt @@ -13,7 +13,6 @@ import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.databinding.FragmentGemPurchaseBinding import com.habitrpg.android.habitica.extensions.addCancelButton -import com.habitrpg.common.habitica.extensions.isUsingNightModeResources import com.habitrpg.android.habitica.helpers.AmplitudeManager import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.PurchaseHandler @@ -25,11 +24,12 @@ import com.habitrpg.android.habitica.ui.fragments.BaseFragment import com.habitrpg.android.habitica.ui.fragments.PromoInfoFragment import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog -import javax.inject.Inject +import com.habitrpg.common.habitica.extensions.isUsingNightModeResources import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.inject.Inject class GemsPurchaseFragment : BaseFragment() { @@ -50,6 +50,8 @@ class GemsPurchaseFragment : BaseFragment() { component.inject(this) } + private var isGemSaleHappening = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -67,6 +69,7 @@ class GemsPurchaseFragment : BaseFragment() { val promo = appConfigManager.activePromo() if (promo != null) { binding?.let { + isGemSaleHappening = true promo.configurePurchaseBanner(it) if (promo.promoType != PromoType.SUBSCRIPTION) { promo.configureGemView(it.gems4View.binding, 4) @@ -122,7 +125,7 @@ class GemsPurchaseFragment : BaseFragment() { private fun purchaseGems(view: GemPurchaseOptionsView?) { val identifier = view?.sku ?: return - activity?.let { purchaseHandler.purchase(it, identifier) } + activity?.let { purchaseHandler.purchase(it, identifier, null, isGemSaleHappening) } } private fun showGiftGemsDialog() { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt index d7a70ecbd..f55bdb09e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt @@ -422,6 +422,7 @@ class TasksFragment : BaseMainFragment(), SearchView.O if (viewModel.ownerTitle.isNotBlank()) { activity?.title = viewModel.ownerTitle } + viewModel.userViewModel.currentTeamPlan = viewModel.teamPlans[viewModel.ownerID.value] val isPersonalBoard = viewModel.isPersonalBoard bottomNavigation?.canAddTasks = isPersonalBoard } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/GroupMemberViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/GroupMemberViewHolder.kt index 8bdc10c86..7f5910529 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/GroupMemberViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/GroupMemberViewHolder.kt @@ -71,7 +71,7 @@ class GroupMemberViewHolder(itemView: View) : androidx.recyclerview.widget.Recyc binding.displayNameTextview.tier = user.contributor?.level ?: 0 if (user.hasClass) { - binding.sublineTextview.text = itemView.context.getString(R.string.user_level_with_class, user.stats?.lvl, user.stats?.getTranslatedClassName(itemView.context)) + binding.sublineTextview.text = itemView.context.getString(R.string.user_level_with_class, user.stats?.lvl, user.stats?.getTranslatedClassName(itemView.context.resources)) } else { binding.sublineTextview.text = itemView.context.getString(R.string.user_level, user.stats?.lvl) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt index 3b29243c2..95e65b90d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt @@ -2,27 +2,28 @@ package com.habitrpg.android.habitica.ui.viewmodels import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData -import com.habitrpg.android.habitica.HabiticaBaseApplication +import com.habitrpg.android.habitica.data.SocialRepository import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.helpers.RxErrorHandler +import com.habitrpg.android.habitica.models.TeamPlan import com.habitrpg.android.habitica.models.invitations.PartyInvite import com.habitrpg.android.habitica.models.user.User -import com.habitrpg.android.habitica.modules.AppModule import io.reactivex.rxjava3.disposables.CompositeDisposable -import javax.inject.Inject -import javax.inject.Named +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest -class MainUserViewModel(val userRepository: UserRepository) { +class MainUserViewModel(private val providedUserID: String, val userRepository: UserRepository, val socialRepository: SocialRepository) { - @field:[Inject Named(AppModule.NAMED_USER_ID)] - lateinit var injectedUserID: String val formattedUsername: CharSequence? get() = user.value?.formattedUsername val partyInvitations: List get() = user.value?.invitations?.parties ?: emptyList() val userID: String - get() = user.value?.id ?: injectedUserID + get() = user.value?.id ?: providedUserID val username: CharSequence get() = user.value?.username ?: "" val displayName: CharSequence @@ -36,12 +37,13 @@ class MainUserViewModel(val userRepository: UserRepository) { val mirrorGroupTasks: List get() = user.value?.preferences?.tasks?.mirrorGroupTasks ?: emptyList() - val user: LiveData - - init { - HabiticaBaseApplication.userComponent?.inject(this) - user = userRepository.getUser().asLiveData() - } + val user: LiveData = userRepository.getUser().asLiveData() + var currentTeamPlan: MutableStateFlow = MutableStateFlow(null) + @OptIn(ExperimentalCoroutinesApi::class) + var currentTeamPlanGroup = currentTeamPlan + .filterNotNull() + .distinctUntilChanged { old, new -> old.id == new.id } + .flatMapLatest { socialRepository.getGroup(it.id) } fun onCleared() { userRepository.close() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/TasksViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/TasksViewModel.kt index 3b778a0e0..cae2c10d9 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/TasksViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/TasksViewModel.kt @@ -11,6 +11,7 @@ import com.habitrpg.android.habitica.data.TaskRepository import com.habitrpg.android.habitica.helpers.AmplitudeManager import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.RxErrorHandler +import com.habitrpg.android.habitica.models.TeamPlan import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.shared.habitica.models.responses.TaskDirection import com.habitrpg.shared.habitica.models.responses.TaskScoringResult @@ -45,6 +46,7 @@ class TasksViewModel : BaseViewModel() { val ownerID: MutableLiveData by lazy { MutableLiveData() } + var teamPlans = mapOf() var initialPreferenceFilterSet: Boolean = false val isPersonalBoard: Boolean @@ -60,8 +62,9 @@ class TasksViewModel : BaseViewModel() { if (appConfigManager.enableTeamBoards()) { viewModelScope.launch { userRepository.getTeamPlans() - .collect { - owners = listOf(Pair(userViewModel.userID, userViewModel.displayName)) + it.map { + .collect { plans -> + teamPlans = plans.associateBy { it.id } + owners = listOf(Pair(userViewModel.userID, userViewModel.displayName)) + plans.map { Pair( it.id, it.summary diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt new file mode 100644 index 000000000..cee872e8d --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt @@ -0,0 +1,266 @@ +package com.habitrpg.android.habitica.ui.views + +import android.graphics.Bitmap +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.ProgressIndicatorDefaults +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel +import com.habitrpg.common.habitica.helpers.NumberAbbreviator +import java.math.RoundingMode +import java.text.NumberFormat + +@Composable +fun UserLevelText(user: User) { + val text = if (user.hasClass) { + stringResource( + id = R.string.user_level_with_class, + user.stats?.lvl ?: 0, + user.stats?.getTranslatedClassName( + LocalContext.current.resources + ) ?: "" + ) + } else { + stringResource(id = R.string.user_level, user.stats?.lvl ?: 0) + } + Text( + text, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + color = colorResource(R.color.text_primary) + ) +} + +@Composable +fun CurrencyText( + currency: String, + value: Double, + modifier: Modifier = Modifier, + decimals: Int = 2, + minForAbbrevation: Int = 0 +) { + Row(verticalAlignment = Alignment.CenterVertically) { + when (currency) { + "gold" -> HabiticaIconsHelper.imageOfGold() + "gems" -> HabiticaIconsHelper.imageOfGem() + "hourglasses" -> HabiticaIconsHelper.imageOfHourglass() + else -> null + }?.asImageBitmap()?.let { Image(it, null, Modifier.padding(end = 5.dp)) } + Text( + NumberAbbreviator.abbreviate(null, value, decimals, minForAbbrevation), + color = when (currency) { + "gold" -> colorResource(R.color.text_gold) + "gems" -> colorResource(R.color.text_green) + "hourglasses" -> colorResource(R.color.text_brand) + else -> colorResource(R.color.text_primary) + }, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + modifier = modifier + ) + } +} + +@Composable +fun AppHeaderView( + viewModel: MainUserViewModel, +) { + val user by viewModel.user.observeAsState(null) + val displayedTeamPlan = viewModel.currentTeamPlan + Column { + Row { + ComposableAvatarView( + user, + Modifier + .size(110.dp, 100.dp) + .padding(end = 16.dp) + ) + Column(modifier = Modifier.height(100.dp)) { + Row(modifier = Modifier.weight(1f)) { + Column(modifier = Modifier.weight(1f)) { + LabeledBar( + icon = HabiticaIconsHelper.imageOfHeartLightBg(), + label = stringResource(R.string.HP_default), + color = colorResource(R.color.hpColor), + value = user?.stats?.hp ?: 0.0, + maxValue = user?.stats?.maxHealth?.toDouble() ?: 0.0, + displayCompact = displayedTeamPlan != null, + modifier = Modifier.weight(1f) + ) + LabeledBar( + icon = HabiticaIconsHelper.imageOfExperience(), + label = stringResource(R.string.XP_default), + color = colorResource(R.color.xpColor), + value = user?.stats?.exp ?: 0.0, + maxValue = user?.stats?.toNextLevel?.toDouble() ?: 0.0, + displayCompact = displayedTeamPlan != null, + modifier = Modifier.weight(1f) + ) + if (user?.hasClass == true) { + LabeledBar( + icon = HabiticaIconsHelper.imageOfMagic(), + label = stringResource(R.string.MP_default), + color = colorResource(R.color.mpColor), + value = user?.stats?.mp ?: 0.0, + maxValue = user?.stats?.maxMP?.toDouble() ?: 0.0, + displayCompact = displayedTeamPlan != null, + modifier = Modifier.weight(1f) + ) + } + } + val animWidth = with(LocalDensity.current) { 48.dp.roundToPx() } + AnimatedVisibility( + visible = displayedTeamPlan != null, + enter = slideInHorizontally { animWidth } + fadeIn(), + exit = slideOutHorizontally { animWidth } + fadeOut()) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(72.dp) + .width(48.dp) + .padding(start = 12.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + colorResource(R.color.window_background) + ) + ) { + Text("M") + } + } + } + val animHeight = with(LocalDensity.current) { 40.dp.roundToPx() } + AnimatedVisibility( + visible = displayedTeamPlan != null, + enter = slideInVertically { animHeight } + fadeIn(), + exit = slideOutVertically { animHeight } + fadeOut()) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .padding(top = 12.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + colorResource(R.color.window_background) + ) + ) { + Text("A") + } + } + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + if (user?.hasClass == true) { + val icon = when (user?.stats?.habitClass) { + "warrior" -> HabiticaIconsHelper.imageOfWarriorLightBg().asImageBitmap() + "wizard" -> HabiticaIconsHelper.imageOfMageLightBg().asImageBitmap() + "healer" -> HabiticaIconsHelper.imageOfHealerLightBg().asImageBitmap() + "rogue" -> HabiticaIconsHelper.imageOfRogueLightBg().asImageBitmap() + else -> null + } + if (icon != null) { + Image(bitmap = icon, "", modifier = Modifier.padding(end = 4.dp)) + } + } + user?.let { UserLevelText(it) } + Spacer(Modifier.weight(1f)) + user?.hourglassCount?.toDouble() + ?.let { CurrencyText("hourglasses", it, modifier = Modifier.padding(end = 12.dp)) } + CurrencyText("gold", user?.stats?.gp ?: 0.0, modifier = Modifier.padding(end = 12.dp)) + CurrencyText("gems", user?.gemCount?.toDouble() ?: 0.0) + } + } +} + +@Composable +fun LabeledBar( + icon: Bitmap, + label: String, + color: Color, + value: Double, + maxValue: Double, + displayCompact: Boolean, + modifier: Modifier = Modifier +) { + val formatter = NumberFormat.getInstance() + formatter.maximumFractionDigits = 1 + formatter.roundingMode = RoundingMode.UP + formatter.isGroupingUsed = true + + val animatedValue = animateFloatAsState( + targetValue = value.toFloat(), + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + ).value + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + AnimatedVisibility( + visible = !displayCompact, + enter = slideInHorizontally { -18 }, + exit = slideOutHorizontally { -18 }) { + Image(icon.asImageBitmap(), null, modifier = Modifier.padding(end = 8.dp)) + } + Column(modifier = Modifier.weight(1f)) { + LinearProgressIndicator( + progress = (animatedValue / maxValue).toFloat(), + Modifier + .fillMaxWidth() + .clip(CircleShape) + .height(8.dp), + backgroundColor = colorResource(R.color.window_background), + color = color + ) + AnimatedVisibility(visible = !displayCompact) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 2.dp) + ) { + Text( + "${formatter.format(animatedValue)} / ${formatter.format(maxValue)}", + fontSize = 12.sp, + color = colorResource(R.color.text_ternary) + ) + Spacer(Modifier.weight(1f)) + Text(label, fontSize = 12.sp, color = colorResource(R.color.text_ternary)) + } + } + } + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ComposableAvatarView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ComposableAvatarView.kt new file mode 100644 index 000000000..8b2180169 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ComposableAvatarView.kt @@ -0,0 +1,25 @@ +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.views.AvatarView +import com.habitrpg.shared.habitica.models.Avatar + +@Composable +fun ComposableAvatarView( + avatar: Avatar?, + modifier: Modifier = Modifier +) { + AndroidView( + modifier = modifier, // Occupy the max size in the Compose UI tree + factory = { context -> + AvatarView(context) + }, + update = { view -> + if (avatar != null) { + view.setAvatar(avatar) + } + } + ) +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/CurrencyView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/CurrencyView.kt index d62f07d79..551af68c0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/CurrencyView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/CurrencyView.kt @@ -69,21 +69,21 @@ class CurrencyView : androidx.appcompat.widget.AppCompatTextView { private fun configureCurrency() { if ("gold" == currency) { - icon = com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper.imageOfGold() + icon = HabiticaIconsHelper.imageOfGold() if (lightBackground) { setTextColor(ContextCompat.getColor(context, R.color.yellow_1)) } else { setTextColor(ContextCompat.getColor(context, R.color.yellow_100)) } } else if ("gems" == currency) { - icon = com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper.imageOfGem() + icon = HabiticaIconsHelper.imageOfGem() if (lightBackground) { setTextColor(ContextCompat.getColor(context, R.color.green_10)) } else { setTextColor(ContextCompat.getColor(context, R.color.green_50)) } } else if ("hourglasses" == currency) { - icon = com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper.imageOfHourglass() + icon = HabiticaIconsHelper.imageOfHourglass() if (lightBackground) { setTextColor(ContextCompat.getColor(context, R.color.brand_300)) } else { diff --git a/build.gradle b/build.gradle index 653c19d3f..9e0ea00e0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,25 +2,27 @@ buildscript { ext { - target_sdk = 32 + target_sdk = 33 app_version_name = '' app_version_code = 0 amplitude_version = '3.35.1' - appcompat_version = '1.5.0' + appcompat_version = '1.5.1' coil_version = '2.1.0' - core_ktx_version = '1.8.0' - coroutines_version = '1.6.2' + compose_version = '1.2.1' + core_ktx_version = '1.9.0' + coroutines_version = '1.6.4' daggerhilt_version = '2.42' firebase_bom = '30.2.0' kotlin_version = '1.7.10' - lifecycle_version = '2.5.0' + lifecycle_version = '2.5.1' markwon_version = '4.6.2' moshi_version = '1.13.0' + navigation_version = '2.5.2' okhttp_version = '4.9.3' - play_wearables_version = '17.1.0' - play_auth_version = '20.2.0' + play_wearables_version = '18.0.0' + play_auth_version = '20.3.0' preferences_version = '1.2.0' realm_version = '1.0.2' retrofit_version = '2.9.0' @@ -33,15 +35,15 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.3.0' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' - classpath 'com.google.gms:google-services:4.3.13' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' + classpath 'com.google.gms:google-services:4.3.14' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' classpath "io.realm:realm-gradle-plugin:10.11.0" classpath("io.realm.kotlin:gradle-plugin:$realm_version") classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.19.0" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" classpath 'com.google.firebase:perf-plugin:1.4.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerhilt_version" } diff --git a/common/build.gradle b/common/build.gradle index 0a56516b6..71188b0a7 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -33,6 +33,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + namespace 'com.habitrpg.common.habitica' } dependencies { diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index 411eba234..a5918e68a 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt b/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt index 7ffdc4647..11e29dfe4 100644 --- a/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt +++ b/common/src/main/java/com/habitrpg/common/habitica/helpers/NumberAbbreviator.kt @@ -7,7 +7,7 @@ import java.text.DecimalFormat object NumberAbbreviator { - fun abbreviate(context: Context, number: Double, numberOfDecimals: Int = 2, minForAbbrevation: Int = 0): String { + fun abbreviate(context: Context?, number: Double, numberOfDecimals: Int = 2, minForAbbrevation: Int = 0): String { var usedNumber = number var counter = 0 while (usedNumber >= 1000 && number >= minForAbbrevation) { @@ -23,11 +23,11 @@ object NumberAbbreviator { return formatter.format(usedNumber) } - private fun abbreviationForCounter(context: Context, counter: Int): String = when (counter) { - 1 -> context.getString(R.string.thousand_abbrev) - 2 -> context.getString(R.string.million_abbrev) - 3 -> context.getString(R.string.billion_abbrev) - 4 -> context.getString(R.string.trillion_abbrev) + private fun abbreviationForCounter(context: Context?, counter: Int): String = when (counter) { + 1 -> context?.getString(R.string.thousand_abbrev) ?: "k" + 2 -> context?.getString(R.string.million_abbrev) ?: "m" + 3 -> context?.getString(R.string.billion_abbrev) ?: "b" + 4 -> context?.getString(R.string.trillion_abbrev) ?: "t" else -> "" } } diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index adef2cfa0..994f82a89 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -83,7 +83,7 @@ @color/yellow_100 @color/blue_100 - @color/gray_10 + @color/gray_50 @color/gray_100 @color/gray_200 @color/gray_300 diff --git a/fastlane/README.md b/fastlane/README.md index 8df76ccf8..f57928b5d 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -29,7 +29,7 @@ Runs all the tests [bundle exec] fastlane android staffapk ``` -Build Staff APK for sara +Build Staff APK ### android staff diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 3eb9c4730..45b8abcd3 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,11 +1,10 @@ -New in 4.0.2: +New in 4.0.3: -Habitica has a brand new WearOS app for smart watches! +-Reminders for tasks done the previous day will show again -Group Plan subscribers can switch on displaying shared tasks from Settings --Past To Do reminders will not constantly show anymore -Pet category labels show again -Newly designed Backgrounds section -Ability to filter, preview, and pin Backgrounds -New bottom sheet designs in Items, Pets & Mounts, and Filters -New Day Start Adjustment interface -Improvements to payment and subscription handling - diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 5eb9e78bf..a58de7160 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -60,4 +60,5 @@ android { minSdk = 21 targetSdk = 32 } + namespace = "com.habitrpg.shared.habitica" } \ No newline at end of file diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml index b7e9ba4e0..568741e54 100644 --- a/shared/src/androidMain/AndroidManifest.xml +++ b/shared/src/androidMain/AndroidManifest.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/version.properties b/version.properties index cbb87ac43..59fa6aff3 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ NAME=4.0.3 -CODE=4521 \ No newline at end of file +CODE=4561 \ No newline at end of file diff --git a/wearos/build.gradle b/wearos/build.gradle index de91fee40..e3cb89504 100644 --- a/wearos/build.gradle +++ b/wearos/build.gradle @@ -88,6 +88,7 @@ android { buildFeatures { viewBinding true } + namespace 'com.habitrpg.android.habitica' } dependencies { diff --git a/wearos/src/main/AndroidManifest.xml b/wearos/src/main/AndroidManifest.xml index 09f409142..69db7d18d 100644 --- a/wearos/src/main/AndroidManifest.xml +++ b/wearos/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - +