Begin implementing new header

This commit is contained in:
Phillip Thelen 2022-09-26 13:05:22 +02:00
parent 616b77cf0a
commit bf5bb9939b
32 changed files with 424 additions and 95 deletions

View file

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

View file

@ -28,16 +28,15 @@
app:expandedTitleMarginStart="0dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include
android:id="@+id/avatar_with_bars"
layout="@layout/avatar_with_bars"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/header_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginEnd="@dimen/header_border_spacing"
android:layout_marginStart="@dimen/header_border_spacing"
android:layout_marginBottom="@dimen/spacing_medium"
app:layout_collapseMode="parallax" />
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"

View file

@ -17,10 +17,6 @@
android:contentDescription="@string/gems"
android:layout_marginTop="20dp"/>
<com.habitrpg.android.habitica.ui.views.SparkView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -68,4 +68,5 @@
<color name="widget_background">#2B203A</color>
<color name="text_gold">@color/yellow_100</color>
</resources>

View file

@ -119,4 +119,5 @@
<color name="lightly_tinted_background">@color/brand_700</color>
<color name="dialog_background">@color/white</color>
<color name="error_banner_background">@color/maroon_5</color>
<color name="text_gold">@color/yellow_1</color>
</resources>

View file

@ -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")
}

View file

@ -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 -> ""
}
}
}

View file

@ -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)
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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()
}

View file

@ -118,11 +118,11 @@ abstract class BaseMainFragment<VB : ViewBinding> : BaseFragment<VB>() {
}
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() {

View file

@ -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<FragmentGemPurchaseBinding>() {
@ -50,6 +50,8 @@ class GemsPurchaseFragment : BaseFragment<FragmentGemPurchaseBinding>() {
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<FragmentGemPurchaseBinding>() {
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<FragmentGemPurchaseBinding>() {
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() {

View file

@ -422,6 +422,7 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), 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
}

View file

@ -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)
}

View file

@ -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<PartyInvite>
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<String>
get() = user.value?.preferences?.tasks?.mirrorGroupTasks ?: emptyList()
val user: LiveData<User?>
init {
HabiticaBaseApplication.userComponent?.inject(this)
user = userRepository.getUser().asLiveData()
}
val user: LiveData<User?> = userRepository.getUser().asLiveData()
var currentTeamPlan: MutableStateFlow<TeamPlan?> = 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()

View file

@ -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<String?> by lazy {
MutableLiveData()
}
var teamPlans = mapOf<String, TeamPlan>()
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

View file

@ -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))
}
}
}
}
}

View file

@ -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)
}
}
)
}

View file

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

View file

@ -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"
}

View file

@ -33,6 +33,7 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
namespace 'com.habitrpg.common.habitica'
}
dependencies {

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.habitrpg.common.habitica">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -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 -> ""
}
}

View file

@ -83,7 +83,7 @@
<color name="xpColor">@color/yellow_100</color>
<color name="mpColor">@color/blue_100</color>
<color name="text_primary">@color/gray_10</color>
<color name="text_primary">@color/gray_50</color>
<color name="text_secondary">@color/gray_100</color>
<color name="text_ternary">@color/gray_200</color>
<color name="text_quad">@color/gray_300</color>

View file

@ -29,7 +29,7 @@ Runs all the tests
[bundle exec] fastlane android staffapk
```
Build Staff APK for sara
Build Staff APK
### android staff

View file

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

View file

@ -60,4 +60,5 @@ android {
minSdk = 21
targetSdk = 32
}
namespace = "com.habitrpg.shared.habitica"
}

View file

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.habitrpg.shared.habitica" />
<manifest />

View file

@ -1,2 +1,2 @@
NAME=4.0.3
CODE=4521
CODE=4561

View file

@ -88,6 +88,7 @@ android {
buildFeatures {
viewBinding true
}
namespace 'com.habitrpg.android.habitica'
}
dependencies {

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.habitrpg.android.habitica">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />