Initial party seeking version

This commit is contained in:
Phillip Thelen 2023-03-13 18:18:26 +01:00
parent 70a2d400e7
commit cdc105bec7
17 changed files with 516 additions and 48 deletions

View file

@ -276,7 +276,8 @@
• Your Task streaks and Habit counters will not reset\n
&#8226; Your damage to the Quest boss or found collection items will remain pending until you check out of the Inn</string>
<string name="empty_items">You don\'t have any %s</string>
<string name="user_level_with_class">Lvl %1$d %2$s</string>
<string name="level_abbreviated">Lv. %d</string>
<string name="user_level_with_class">Lv. %1$d %2$s</string>
<string name="user_level_with_class_unabbreviated">Level %1$d %2$s</string>
<string name="warrior">Warrior</string>
<string name="rogue">Rogue</string>
@ -710,6 +711,7 @@
<string name="month_reminder_text">Sometimes starting fresh is the best option, Habitica can help!</string>
<string name="last_login">Latest Check In</string>
<string name="total_checkins">Total Checkins</string>
<string name="x_checkins">%d Checkins</string>
<string name="two_handed">Two-Handed</string>
<string name="usernamePromptTitle">Its time to set your username!</string>
<string name="usernamePromptBody">Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.</string>
@ -913,6 +915,7 @@
<string name="apple_sign_in">Sign in with Apple</string>
<string name="google">Google</string>
<string name="send_invites">Send Invites</string>
<string name="send_invite">Send Invite</string>
<string name="cancelled">Cancelled</string>
<string name="not_recurring">Not Recurring</string>
<string name="ending_on">Ending on %s</string>
@ -1363,6 +1366,10 @@
<string name="list">List</string>
<string name="by_invite">By Invite</string>
<string name="habiticans_looking_party">Heres a list of Habiticans looking to join a Party</string>
<string name="invited">Invited</string>
<string name="invite_with_username_email">Invite with @username or email</string>
<string name="habiticans_send_invite">Send an invite directly to Habiticans you know</string>
<string name="username_or_email">Username or email address</string>
<plurals name="you_x_others">
<item quantity="zero">You</item>

View file

@ -515,7 +515,7 @@
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
<item name="android:windowBackground">@color/background_brand</item>
<item name="android:windowBackground">@color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
</style>

View file

@ -8,6 +8,7 @@ import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.responses.BuyResponse
@ -265,7 +266,7 @@ interface ApiService {
suspend fun seenMessages(@Path("gid") groupId: String): HabitResponse<Void>
@POST("groups/{gid}/invite")
suspend fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map<String, Any>): HabitResponse<List<Void>>
suspend fun inviteToGroup(@Path("gid") groupId: String, @Body inviteData: Map<String, Any>): HabitResponse<List<InviteResponse>>
@POST("groups/{gid}/reject-invite")
suspend fun rejectGroupInvite(@Path("gid") groupId: String): HabitResponse<Void>

View file

@ -8,6 +8,7 @@ import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.responses.BuyResponse
@ -166,7 +167,7 @@ interface ApiClient {
suspend fun seenMessages(groupId: String): Void?
suspend fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): List<Void>?
suspend fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): List<InviteResponse>?
suspend fun rejectGroupInvite(groupId: String): Void?

View file

@ -2,6 +2,7 @@ package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.ChatMessage
@ -81,7 +82,7 @@ interface SocialRepository : BaseRepository {
suspend fun getGroupMembers(id: String): Flow<List<Member>>
suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>?
suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>): List<Void>?
suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>): List<InviteResponse>?
suspend fun retrieveMember(userId: String?, fromHall: Boolean = false): Member?
suspend fun retrieveMemberWithUsername(username: String?, fromHall: Boolean): Member?

View file

@ -17,6 +17,7 @@ import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.responses.BuyResponse
@ -569,7 +570,7 @@ class ApiClientImpl(
return process { apiService.seenMessages(groupId) }
}
override suspend fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): List<Void>? {
override suspend fun inviteToGroup(groupId: String, inviteData: Map<String, Any>): List<InviteResponse>? {
return process { apiService.inviteToGroup(groupId, inviteData) }
}

View file

@ -0,0 +1,5 @@
package com.habitrpg.android.habitica.models.invitations
class InviteResponse {
}

View file

@ -28,4 +28,5 @@ open class MemberPreferences :
}
} else null
}
var language: String? = null
}

View file

@ -4,15 +4,66 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
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.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
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 androidx.fragment.app.viewModels
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.FragmentComposeBinding
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.models.invitations.InviteResponse
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.LoadingButtonState
import com.habitrpg.common.habitica.extensions.isValidEmail
import com.habitrpg.common.habitica.helpers.launchCatching
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@ -21,13 +72,30 @@ class PartyInviteViewModel @Inject constructor(
userViewModel : MainUserViewModel,
val socialRepository : SocialRepository
) : BaseViewModel(userRepository, userViewModel) {
val invites = mutableStateListOf("")
suspend fun sendInvites() : List<InviteResponse>? {
val inviteMap = mapOf<String, MutableList<String>>(
"emails" to mutableListOf(),
"uuids" to mutableListOf(),
"usernames" to mutableListOf()
)
for (invite in invites) {
if (invite.isValidEmail()) {
inviteMap["emails"]?.add(invite)
} else if (UUID.fromString(invite) != null) {
inviteMap["uuids"]?.add(invite)
} else if (invite.isNotBlank()) {
inviteMap["usernames"]?.add(invite)
}
}
return socialRepository.inviteToGroup("party", inviteMap)
}
}
@AndroidEntryPoint
class PartyInviteFragment : BaseFragment<FragmentComposeBinding>() {
@Inject
lateinit var configManager : AppConfigManager
val viewModel : PartyInviteViewModel by viewModels()
override var binding : FragmentComposeBinding? = null
@ -38,7 +106,117 @@ class PartyInviteFragment : BaseFragment<FragmentComposeBinding>() {
return FragmentComposeBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view : View, savedInstanceState : Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onCreateView(
inflater : LayoutInflater,
container : ViewGroup?,
savedInstanceState : Bundle?
) : View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
binding?.composeView?.setContent {
HabiticaTheme {
PartyInviteView(viewModel)
}
}
return view
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PartyInviteView(
viewModel : PartyInviteViewModel
) {
var inviteButtonState : LoadingButtonState by remember { mutableStateOf(LoadingButtonState.CONTENT) }
val scope = rememberCoroutineScope()
val scrollableState = rememberScrollState()
val invites = viewModel.invites
LazyColumn(
Modifier
.fillMaxWidth()
.padding(14.dp)
.scrollable(scrollableState, Orientation.Vertical)) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.fillMaxWidth()
.padding(top = 22.dp, bottom = 14.dp)
) {
Text(
stringResource(R.string.invite_with_username_email),
color = HabiticaTheme.colors.textPrimary,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
Text(
stringResource(R.string.habiticans_send_invite),
color = HabiticaTheme.colors.textSecondary
)
}
}
items(invites.indices.toList()) { index ->
val invite = invites[index]
val transition = updateTransition(invites.size - 1 == index, label = "isLast")
val rotation = transition.animateFloat(
label = "isAssigned",
transitionSpec = { spring(Spring.DampingRatioLowBouncy, Spring.StiffnessMediumLow) }
) {
if (it) 135f else 0f
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 4.dp)
.background(HabiticaTheme.colors.windowBackground, HabiticaTheme.shapes.medium)
.padding(4.dp, 4.dp)
.animateItemPlacement()) {
Image(
painterResource(R.drawable.ic_close_white_24dp),
null,
colorFilter = ColorFilter.tint(HabiticaTheme.colors.textPrimary),
modifier = Modifier
.rotate(rotation.value)
.size(32.dp)
.padding(3.dp)
)
TextField(
value = invite, onValueChange = { value ->
if (invites.size - 1 == index && invites[index].isBlank()) {
viewModel.invites.add("")
}
viewModel.invites[index] = value
},
singleLine = true,
placeholder = { Text(stringResource(R.string.username_or_email)) },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent),
modifier = Modifier
.onFocusChanged {
if (!it.isFocused) {
if (viewModel.invites[index].isBlank() && viewModel.invites.size - 1 != index && viewModel.invites.size > 1) {
viewModel.invites.removeAt(index)
}
}
}
)
}
}
item {
InviteButton(
state = if (invites.isNotEmpty()) inviteButtonState else LoadingButtonState.DISABLED,
modifier = Modifier.fillMaxWidth(),
onClick = {
inviteButtonState = LoadingButtonState.LOADING
scope.launchCatching({
inviteButtonState = LoadingButtonState.FAILED
}) {
val responses = viewModel.sendInvites()
if (responses?.isNotEmpty() == true) {
inviteButtonState = LoadingButtonState.SUCCESS
viewModel.invites.clear()
} else {
inviteButtonState = LoadingButtonState.FAILED
}
}
})
}
}
}

View file

@ -4,22 +4,38 @@ import android.os.Bundle
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.viewModels
@ -28,14 +44,21 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.FragmentComposeBinding
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
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.ClassIcon
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.getTranslatedClassName
import com.habitrpg.common.habitica.helpers.launchCatching
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
@ -43,9 +66,8 @@ class PartySeekingViewModel @Inject constructor(
userRepository : UserRepository,
userViewModel : MainUserViewModel,
val socialRepository : SocialRepository
): BaseViewModel(userRepository, userViewModel) {
) : BaseViewModel(userRepository, userViewModel) {
val isRefreshing = mutableStateOf(false)
val seekingUsers = mutableStateOf<List<Member>>(emptyList())
init {
@ -59,13 +81,21 @@ class PartySeekingViewModel @Inject constructor(
isRefreshing.value = false
}
}
suspend fun inviteUser(member : Member) : InviteResponse? {
return socialRepository.inviteToGroup(
"party", mapOf(
"uuids" to listOf(member.id ?: "")
)
)?.firstOrNull()
}
}
@AndroidEntryPoint
class PartySeekingFragment: BaseFragment<FragmentComposeBinding>() {
val viewModel: PartySeekingViewModel by viewModels()
class PartySeekingFragment : BaseFragment<FragmentComposeBinding>() {
val viewModel : PartySeekingViewModel by viewModels()
override var binding: FragmentComposeBinding? = null
override var binding : FragmentComposeBinding? = null
override fun createBinding(
inflater : LayoutInflater,
container : ViewGroup?
@ -87,39 +117,183 @@ class PartySeekingFragment: BaseFragment<FragmentComposeBinding>() {
return view
}
override fun onResume() {
super.onResume()
override fun onStart() {
super.onStart()
viewModel.retrieveUsers()
}
}
@Composable
fun PartySeekingListItem(user: Member,
modifier : Modifier = Modifier) {
Column(modifier.fillMaxWidth()) {
Text(user.username ?: "")
fun ClassText(
className : String?,
hasClass : Boolean,
fontSize : TextUnit,
modifier : Modifier = Modifier,
iconSize : Dp? = null
) {
if (!hasClass) return
val classColor = colorResource(
when (className) {
"warrior" -> R.color.text_red
"wizard" -> R.color.text_blue
"rogue" -> R.color.text_brand
"healer" -> R.color.text_yellow
else -> R.color.text_primary
}
)
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
ClassIcon(
className = className,
hasClass = true,
modifier = Modifier.size(iconSize ?: with(LocalDensity.current) {
fontSize.toDp()
})
)
Text(
getTranslatedClassName(LocalContext.current.resources, className),
fontSize = fontSize,
fontWeight = FontWeight.SemiBold,
color = classColor
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PartySeekingView(
viewModel: PartySeekingViewModel,
fun InviteButton(
state : LoadingButtonState,
onClick : () -> Unit,
modifier : Modifier = Modifier
) {
val users: List<Member> by viewModel.seekingUsers
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,
onInvite : suspend (Member) -> InviteResponse?
) {
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.preferences?.disableClasses == false
)
}
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
)
}
}
val scope = rememberCoroutineScope()
var inviteState : LoadingButtonState by remember { mutableStateOf(LoadingButtonState.CONTENT) }
InviteButton(state = inviteState, modifier = Modifier.fillMaxWidth(), onClick = {
scope.launchCatching({
inviteState = LoadingButtonState.FAILED
}) {
inviteState = LoadingButtonState.LOADING
val response = onInvite(user)
if (response != null) {
inviteState = LoadingButtonState.SUCCESS
} else {
inviteState = LoadingButtonState.FAILED
}
}
})
}
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable
fun PartySeekingView(
viewModel : PartySeekingViewModel,
modifier : Modifier = Modifier
) {
val users : List<Member> by viewModel.seekingUsers
val refreshing by viewModel.isRefreshing
val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.retrieveUsers() })
LazyColumn(modifier.pullRefresh(pullRefreshState)) {
item {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().padding(top = 22.dp, bottom = 14.dp)) {
Text(stringResource(R.string.find_more_members), color = HabiticaTheme.colors.textPrimary, fontSize = 16.sp, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.habiticans_looking_party), color = HabiticaTheme.colors.textSecondary)
Box(modifier = modifier.pullRefresh(pullRefreshState)) {
LazyColumn {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.fillMaxWidth()
.padding(top = 22.dp, bottom = 14.dp)
) {
Text(
stringResource(R.string.find_more_members),
color = HabiticaTheme.colors.textPrimary,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
Text(
stringResource(R.string.habiticans_looking_party),
color = HabiticaTheme.colors.textSecondary
)
}
}
items(users) {
PartySeekingListItem(user = it, modifier = Modifier.animateItemPlacement()) { member ->
return@PartySeekingListItem viewModel.inviteUser(member)
}
}
}
items(users) {
PartySeekingListItem(user = it)
}
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
}
}

View file

@ -143,7 +143,11 @@ object HabiticaTheme {
tintedUiMain = Color(context.getThemeColor(R.attr.tintedUiMain)),
tintedUiSub = Color(context.getThemeColor(R.attr.tintedUiSub)),
tintedUiDetails = Color(context.getThemeColor(R.attr.tintedUiDetails)),
pixelArtBackground = Color(context.getThemeColor(R.attr.colorContentBackground))
pixelArtBackground = Color(context.getThemeColor(R.attr.colorContentBackground)),
errorBackground = Color(ContextCompat.getColor(context, R.color.background_red)),
errorColor = Color(ContextCompat.getColor(context, R.color.text_red)),
successBackground = Color(ContextCompat.getColor(context, R.color.background_green)),
successColor = Color(ContextCompat.getColor(context, R.color.text_green))
)
}
}
@ -160,8 +164,13 @@ class HabiticaColors(
val tintedUiMain: Color,
val tintedUiSub: Color,
val tintedUiDetails: Color,
val pixelArtBackground: Color
val pixelArtBackground: Color,
val errorBackground : Color,
val errorColor : Color,
val successBackground : Color,
val successColor : Color
) {
@Composable
fun textPrimaryFor(task: Task?): Color {
return colorResource((if (isSystemInDarkTheme()) task?.extraExtraLightTaskColor else task?.extraDarkTaskColor) ?: R.color.text_primary)

View file

@ -72,7 +72,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.resources))
binding.sublineTextview.text = itemView.context.getString(R.string.user_level_with_class, user.stats?.lvl, getTranslatedClassName(itemView.context.resources, user.stats?.habitClass))
} else {
binding.sublineTextview.text = itemView.context.getString(R.string.user_level, user.stats?.lvl)
}

View file

@ -53,7 +53,6 @@ import com.habitrpg.android.habitica.models.user.Profile
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.Avatar
import com.habitrpg.shared.habitica.models.AvatarStats
import kotlin.random.Random
@Composable
@ -62,9 +61,10 @@ fun UserLevelText(user: Avatar) {
stringResource(
id = R.string.user_level_with_class,
user.stats?.lvl ?: 0,
user.stats?.getTranslatedClassName(
LocalContext.current.resources
) ?: ""
getTranslatedClassName(
LocalContext.current.resources,
user.stats?.habitClass
)
)
} else {
stringResource(id = R.string.user_level, user.stats?.lvl ?: 0)
@ -77,8 +77,8 @@ fun UserLevelText(user: Avatar) {
)
}
fun AvatarStats.getTranslatedClassName(resources: Resources): String {
return when (habitClass) {
fun getTranslatedClassName(resources: Resources, className: String?): String {
return when (className) {
Stats.HEALER -> resources.getString(R.string.healer)
Stats.ROGUE -> resources.getString(R.string.rogue)
Stats.WARRIOR -> resources.getString(R.string.warrior)
@ -258,7 +258,7 @@ fun AppHeaderView(
)
CurrencyText(
"gems",
user.gemCount?.toDouble() ?: 0.0,
user.gemCount.toDouble(),
modifier = Modifier.clickable {
MainNavigationController.navigate(R.id.gemPurchaseActivity)
},

View file

@ -0,0 +1,85 @@
package com.habitrpg.android.habitica.ui.views
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ButtonElevation
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.unit.dp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
enum class LoadingButtonState {
CONTENT,
DISABLED,
LOADING,
FAILED,
SUCCESS
}
@Composable
fun LoadingButton(
state : LoadingButtonState,
onClick : () -> Unit,
modifier : Modifier = Modifier,
elevation : ButtonElevation? = ButtonDefaults.elevation(0.dp),
shape : Shape = MaterialTheme.shapes.medium,
border : BorderStroke? = null,
colors : ButtonColors = ButtonDefaults.buttonColors(
backgroundColor = HabiticaTheme.colors.tintedUiSub,
contentColor = Color.White
),
contentPadding : PaddingValues = ButtonDefaults.ContentPadding,
successContent : @Composable RowScope.() -> Unit,
content : @Composable RowScope.() -> Unit
) {
val buttonColors = if (state == LoadingButtonState.FAILED) {
ButtonDefaults.buttonColors(backgroundColor = HabiticaTheme.colors.errorBackground)
} else if (state == LoadingButtonState.SUCCESS) {
ButtonDefaults.outlinedButtonColors(
backgroundColor = HabiticaTheme.colors.successColor,
contentColor = HabiticaTheme.colors.successColor
)
} else colors
Button(
{
if (state == LoadingButtonState.CONTENT || state == LoadingButtonState.FAILED) {
onClick()
}
},
modifier.requiredHeight(40.dp),
state != LoadingButtonState.DISABLED,
elevation = elevation,
shape = shape,
border = border,
colors = buttonColors,
contentPadding = contentPadding
) {
when (state) {
LoadingButtonState.LOADING -> CircularProgressIndicator(
color = Color.White,
modifier = Modifier.size(16.dp)
)
LoadingButtonState.SUCCESS -> successContent()
LoadingButtonState.FAILED -> Image(
painterResource(R.drawable.failed_loading),
stringResource(R.string.failed)
)
else -> content()
}
}
}

View file

@ -5,6 +5,7 @@ import android.text.Spannable
import android.text.SpannableString
import android.text.TextUtils
import android.text.util.Linkify
import android.util.Patterns
import java.util.Locale
fun String.fromHtml(): CharSequence {
@ -31,3 +32,6 @@ fun String.removeZeroWidthSpace(): String {
fun String.localizedCapitalize(): String {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
}
fun CharSequence?.isValidEmail() = !isNullOrEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches()

View file

@ -303,7 +303,7 @@ class AvatarView : FrameLayout {
layerMap[LayerType.BACK] = outfit.back
}
if (outfit.isAvailable(outfit.armor)) {
layerMap[LayerType.ARMOR] = prefs.size + "_" + outfit.armor
layerMap[LayerType.ARMOR] = (prefs.size ?: "broad") + "_" + outfit.armor
}
if (outfit.isAvailable(outfit.body)) {
layerMap[LayerType.BODY] = outfit.body
@ -326,7 +326,7 @@ class AvatarView : FrameLayout {
}
layerMap[LayerType.SKIN] = "skin_" + prefs.skin + if (prefs.sleep) "_sleep" else ""
layerMap[LayerType.SHIRT] = prefs.size + "_shirt_" + prefs.shirt
layerMap[LayerType.SHIRT] = (prefs.size ?: "broad") + "_shirt_" + prefs.shirt
layerMap[LayerType.HEAD_0] = "head_0"
if (hair != null) {

View file

@ -157,4 +157,5 @@
<color name="text_green">@color/green_10</color>
<color name="text_blue">@color/blue_10</color>
<color name="text_teal">@color/teal_10</color>
</resources>
<color name="divider_color">@color/gray_500</color>
</resources>