l4p fixes

This commit is contained in:
Phillip Thelen 2023-05-03 17:12:36 +02:00
parent c2547252de
commit 5c46ffeaa0
7 changed files with 322 additions and 146 deletions

View file

@ -1383,6 +1383,8 @@
<string name="shop_armoire_description">New gear is released during the seasonal Galas. Until then, theres %d pieces of gear in the Enchanted Armoire to find!</string>
<string name="shop_armoire_empty_description">New gear is released during the seasonal Galas. The Enchanted Armoire gets new stock every month too!</string>
<string name="unlock_gear_and_skills">Unlock %s gear and skills</string>
<string name="rescind_invite">Rescind Invite</string>
<string name="rescinded">Rescinded</string>
<plurals name="you_x_others">
<item quantity="zero">You</item>

View file

@ -5,18 +5,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Divider
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.pullRefresh
@ -28,7 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -55,18 +52,19 @@ import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.BaseViewModel
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.ClassText
import com.habitrpg.android.habitica.ui.views.ComposableAvatarView
import com.habitrpg.android.habitica.ui.views.LoadingButton
import com.habitrpg.android.habitica.ui.views.LoadingButtonState
import com.habitrpg.android.habitica.ui.views.progress.HabiticaCircularProgressView
import com.habitrpg.android.habitica.ui.views.progress.HabiticaPullRefreshIndicator
import com.habitrpg.android.habitica.ui.views.social.PartySeekingListItem
import com.habitrpg.common.habitica.helpers.launchCatching
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import java.util.Locale
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@HiltViewModel
class PartySeekingViewModel @Inject constructor(
@ -96,6 +94,10 @@ class PartySeekingViewModel @Inject constructor(
)
)?.firstOrNull()
}
suspend fun rescindInvite(member : Member) : Member? {
return socialRepository.removeMemberFromGroup("party", member.id)?.firstOrNull()
}
}
@AndroidEntryPoint
@ -129,93 +131,24 @@ class PartySeekingFragment : BaseFragment<FragmentComposeBinding>() {
fun InviteButton(
state : LoadingButtonState,
onClick : () -> Unit,
modifier : Modifier = Modifier
) {
LoadingButton(state = state, onClick = onClick, modifier = modifier, successContent = {
Text(stringResource(R.string.invited))
}) {
Text(stringResource(R.string.send_invite))
}
}
@Composable
fun PartySeekingListItem(
user : Member,
modifier : Modifier = Modifier,
inviteState : LoadingButtonState = LoadingButtonState.LOADING,
onInvite : (Member) -> Unit
isAlreadyInvited: Boolean = false,
) {
Column(
modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 4.dp)
.background(HabiticaTheme.colors.windowBackground, HabiticaTheme.shapes.large)
.padding(14.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.Top
) {
ComposableAvatarView(user, Modifier.size(94.dp, 98.dp))
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.fillMaxWidth()
) {
Text(
user.displayName,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = HabiticaTheme.colors.textPrimary
)
Text(
user.formattedUsername ?: "",
fontSize = 14.sp,
color = HabiticaTheme.colors.textTertiary
)
Divider(
color = colorResource(R.color.divider_color),
thickness = 1.dp,
modifier = Modifier.padding(vertical = 4.dp)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.level_abbreviated, user.stats?.lvl ?: 0),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = HabiticaTheme.colors.textPrimary
)
ClassText(
user.stats?.habitClass,
fontSize = 14.sp,
iconSize = 18.dp,
hasClass = user.hasClass
)
}
Text(
stringResource(R.string.x_checkins, user.loginIncentives),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = HabiticaTheme.colors.textPrimary
)
Text(
Locale(user.preferences?.language ?: "en").getDisplayName(Locale.getDefault()),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = HabiticaTheme.colors.textPrimary
)
}
LoadingButton(state = state, onClick = onClick, colors = ButtonDefaults.buttonColors(
backgroundColor = if (isAlreadyInvited) HabiticaTheme.colors.errorBackground else HabiticaTheme.colors.tintedUiSub,
contentColor = Color.White,
), modifier = modifier, successContent = {
if (isAlreadyInvited) {
Text(stringResource(R.string.invited))
} else {
Text(stringResource(R.string.rescinded))
}
}) {
if (isAlreadyInvited) {
Text(stringResource(R.string.rescind_invite))
} else {
Text(stringResource(R.string.send_invite))
}
InviteButton(
state = inviteState,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onClick = {
onInvite(user)
})
}
}
@ -230,6 +163,7 @@ fun PartySeekingView(
val pullRefreshState = rememberPullRefreshState(refreshing, { pageData.refresh() })
val scope = rememberCoroutineScope()
val successfulInvites = remember { mutableListOf<String>() }
val inviteStates = remember { mutableMapOf<String, LoadingButtonState>() }
Box(
@ -276,17 +210,25 @@ fun PartySeekingView(
PartySeekingListItem(
user = it,
inviteState = inviteStates[it.id] ?: LoadingButtonState.CONTENT,
isInvited = successfulInvites.contains(it.id),
modifier = Modifier.animateItemPlacement()
) { member ->
scope.launchCatching({
inviteStates[member.id] = LoadingButtonState.FAILED
}) {
inviteStates[member.id] = LoadingButtonState.LOADING
val response = viewModel.inviteUser(member)
inviteStates[member.id] = if (response != null) {
LoadingButtonState.SUCCESS
val response = if (successfulInvites.contains(member.id)) viewModel.inviteUser(member) else viewModel.inviteUser(member)
if (response != null) {
if (successfulInvites.contains(member.id)) {
successfulInvites.remove(member.id)
} else {
successfulInvites.add(member.id)
}
inviteStates[member.id] = LoadingButtonState.SUCCESS
delay(4.toDuration(DurationUnit.SECONDS))
inviteStates[member.id] = LoadingButtonState.CONTENT
} else {
LoadingButtonState.FAILED
inviteStates[member.id] = LoadingButtonState.FAILED
}
}
}
@ -304,7 +246,6 @@ fun PartySeekingView(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
HabiticaCircularProgressView()
}
}

View file

@ -31,6 +31,7 @@ import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ButtonElevation
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -44,8 +45,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import kotlinx.coroutines.delay
@ -125,53 +129,55 @@ fun LoadingButton(
colors = buttonColors,
contentPadding = PaddingValues(0.dp)
) {
AnimatedContent(
targetState = state,
transitionSpec = {
val isInitialShowingContent =
initialState == LoadingButtonState.CONTENT || initialState == LoadingButtonState.DISABLED || (initialState == LoadingButtonState.SUCCESS && successContent == null)
val isTargetShowingContent =
targetState == LoadingButtonState.CONTENT || targetState == LoadingButtonState.DISABLED || (targetState == LoadingButtonState.SUCCESS && successContent == null)
if (targetState == LoadingButtonState.FAILED) {
fadeIn(
animationSpec = tween(220, delayMillis = 90)
) +
slideInHorizontally(
animationSpec = spring(
dampingRatio = 0.2f,
stiffness = StiffnessMediumLow,
)
) with
fadeOut(animationSpec = tween(90))
} else if (isInitialShowingContent && isTargetShowingContent) {
fadeIn() with fadeOut()
} else {
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(
initialScale = 0.92f,
animationSpec = tween(220, delayMillis = 90, FastOutSlowInEasing)
) with
fadeOut(animationSpec = tween(90))
}
},
contentAlignment = Alignment.Center,
modifier = Modifier.padding(contentPadding)
) { state ->
when (state) {
LoadingButtonState.LOADING ->
CircularProgressIndicator(
color = contentColor.value,
modifier = Modifier.size(16.dp)
)
ProvideTextStyle(value = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) {
AnimatedContent(
targetState = state,
transitionSpec = {
val isInitialShowingContent =
initialState == LoadingButtonState.CONTENT || initialState == LoadingButtonState.DISABLED || (initialState == LoadingButtonState.SUCCESS && successContent == null)
val isTargetShowingContent =
targetState == LoadingButtonState.CONTENT || targetState == LoadingButtonState.DISABLED || (targetState == LoadingButtonState.SUCCESS && successContent == null)
if (targetState == LoadingButtonState.FAILED) {
fadeIn(
animationSpec = tween(220, delayMillis = 90)
) +
slideInHorizontally(
animationSpec = spring(
dampingRatio = 0.2f,
stiffness = StiffnessMediumLow,
)
) with
fadeOut(animationSpec = tween(90))
} else if (isInitialShowingContent && isTargetShowingContent) {
fadeIn() with fadeOut()
} else {
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(
initialScale = 0.92f,
animationSpec = tween(220, delayMillis = 90, FastOutSlowInEasing)
) with
fadeOut(animationSpec = tween(90))
}
},
contentAlignment = Alignment.Center,
modifier = Modifier.padding(contentPadding)
) { state ->
when (state) {
LoadingButtonState.LOADING ->
CircularProgressIndicator(
color = contentColor.value,
modifier = Modifier.size(16.dp)
)
LoadingButtonState.SUCCESS -> successContent?.let { it() } ?: content()
LoadingButtonState.FAILED ->
failedContent?.let { it() } ?: Image(
painterResource(R.drawable.failed_loading),
stringResource(R.string.failed),
Modifier.padding(horizontal = 8.dp)
)
else -> content()
LoadingButtonState.SUCCESS -> successContent?.let { it() } ?: content()
LoadingButtonState.FAILED ->
failedContent?.let { it() } ?: Image(
painterResource(R.drawable.failed_loading),
stringResource(R.string.failed),
Modifier.padding(horizontal = 8.dp)
)
else -> content()
}
}
}
}
@ -208,7 +214,10 @@ private fun Preview() {
}, content = {
Text("Do something")
}, modifier = Modifier.fillMaxWidth())
LoadingButton(LoadingButtonState.LOADING, {}, content = {
LoadingButton(LoadingButtonState.LOADING, {}, colors = ButtonDefaults.buttonColors(
backgroundColor = HabiticaTheme.colors.successBackground,
contentColor = Color.White
), content = {
Text("Do something")
})
LoadingButton(LoadingButtonState.LOADING, {}, content = {

View file

@ -9,6 +9,21 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.habitrpg.common.habitica.R
import com.habitrpg.common.habitica.models.PlayerTier
@ -45,7 +60,7 @@ class UsernameLabel @JvmOverloads constructor(
tierIconView.visibility = View.GONE
} else {
tierIconView.visibility = View.VISIBLE
tierIconView.setImageBitmap(com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper.imageOfContributorBadge(value.toFloat(), isNPC))
tierIconView.setImageBitmap(HabiticaIconsHelper.imageOfContributorBadge(value.toFloat(), isNPC))
}
}
@ -68,3 +83,24 @@ class UsernameLabel @JvmOverloads constructor(
addView(tierIconView, iconViewParams)
}
}
@Composable
fun ComposableUsernameLabel(
username: String,
tier: Int,
modifier : Modifier = Modifier,
isNPC: Boolean = false
) {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
ProvideTextStyle(value = TextStyle(fontWeight = FontWeight.SemiBold)) {
Text(username, color = if (isNPC) colorResource(id = R.color.contributor_npc) else Color(PlayerTier.getColorForTier(LocalContext.current, tier)))
if (tier > 0) {
Image(
bitmap = HabiticaIconsHelper.imageOfContributorBadge(tier.toFloat(), isNPC).asImageBitmap(),
contentDescription = null
)
}
}
}
}

View file

@ -0,0 +1,187 @@
package com.habitrpg.android.habitica.ui.views.social
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Divider
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.auth.LocalAuthentication
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.members.MemberFlags
import com.habitrpg.android.habitica.models.members.MemberPreferences
import com.habitrpg.android.habitica.models.user.Authentication
import com.habitrpg.android.habitica.models.user.ContributorInfo
import com.habitrpg.android.habitica.models.user.Profile
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.ui.fragments.social.party.InviteButton
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.views.ClassText
import com.habitrpg.android.habitica.ui.views.ComposableAvatarView
import com.habitrpg.android.habitica.ui.views.ComposableUsernameLabel
import com.habitrpg.android.habitica.ui.views.LoadingButtonState
import java.util.Locale
import kotlin.random.Random
@Composable
fun PartySeekingListItem(
user : Member,
modifier : Modifier = Modifier,
inviteState : LoadingButtonState = LoadingButtonState.LOADING,
isInvited: Boolean = false,
onInvite : (Member) -> Unit
) {
Column(
modifier
.fillMaxWidth()
.padding(horizontal = 14.dp)
.padding(bottom = 4.dp)
.background(HabiticaTheme.colors.windowBackground, HabiticaTheme.shapes.large)
.padding(14.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.Top
) {
ComposableAvatarView(user,
Modifier
.size(94.dp, 98.dp)
.padding(top = 4.dp))
Column(
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxWidth()
) {
ProvideTextStyle(value = TextStyle(fontSize = 14.sp)) {
ComposableUsernameLabel(
user.displayName,
user.contributor?.level ?: 0
)
}
Text(
user.formattedUsername ?: "",
fontSize = 14.sp,
color = HabiticaTheme.colors.textTertiary
)
Divider(
color = colorResource(R.color.divider_color),
thickness = 1.dp,
modifier = Modifier.padding(vertical = 6.dp)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.level_abbreviated, user.stats?.lvl ?: 0),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = HabiticaTheme.colors.textPrimary
)
ClassText(
user.stats?.habitClass,
fontSize = 14.sp,
iconSize = 18.dp,
hasClass = user.hasClass
)
}
Text(
stringResource(R.string.x_checkins, user.loginIncentives),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = HabiticaTheme.colors.textPrimary
)
Text(
Locale(user.preferences?.language ?: "en").getDisplayName(Locale.getDefault()),
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
color = HabiticaTheme.colors.textPrimary
)
}
}
InviteButton(
state = inviteState,
isAlreadyInvited = isInvited,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
onClick = {
onInvite(user)
})
}
}
private class MemberProvider: PreviewParameterProvider<Member> {
private fun generateMember() : Member {
val member = Member()
member.profile = Profile()
member.profile?.name = "User"
member.authentication = Authentication()
member.authentication?.localAuthentication = LocalAuthentication()
member.authentication?.localAuthentication?.username = "username"
member.preferences = MemberPreferences()
member.preferences?.disableClasses = false
member.flags = MemberFlags()
member.flags?.classSelected = true
member.stats = Stats()
member.stats?.hp = Random.nextDouble(from = 0.0, until = 50.0)
member.stats?.maxHealth = 50
member.stats?.toNextLevel = Random.nextInt(from = 0, until = 10000)
member.stats?.exp =
Random.nextDouble(until = (member.stats?.toNextLevel ?: 0).toDouble())
member.stats?.maxMP = Random.nextInt(from = 0, until = 10000)
member.stats?.mp = Random.nextDouble(until = (member.stats?.maxMP ?: 0).toDouble())
member.stats?.lvl = Random.nextInt(from = 0, until = 9999)
return member
}
override val values : Sequence<Member>
get() {
val list = mutableListOf<Member>()
val earlyMember = generateMember()
earlyMember.stats?.lvl = 5
list.add(earlyMember)
val needsClass = generateMember()
needsClass.stats?.lvl = 24
needsClass.stats?.habitClass = "healer"
needsClass.flags?.classSelected = false
needsClass.contributor = ContributorInfo()
needsClass.contributor?.level = 4
list.add(needsClass)
val classDisabled = generateMember()
classDisabled.stats?.lvl = 24
classDisabled.stats?.habitClass = "rogue"
classDisabled.preferences?.disableClasses = true
list.add(classDisabled)
val subscriber = generateMember()
subscriber.stats?.habitClass = "warrior"
list.add(subscriber)
val onlyHourglasses = generateMember()
onlyHourglasses.stats?.habitClass = "wizard"
list.add(onlyHourglasses)
return list.asSequence()
}
}
@Preview
@Composable
private fun Preview(@PreviewParameter(MemberProvider::class) data: Member) {
PartySeekingListItem(user = data, onInvite = {})
}

View file

@ -19,4 +19,5 @@
<color name="text_blue">@color/blue_100</color>
<color name="text_teal">@color/teal_100</color>
<color name="dim_background">#40502B94</color>
<color name="divider_color">@color/gray_100</color>
</resources>

View file

@ -1,2 +1,2 @@
NAME=4.2
CODE=5951
CODE=5961