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