mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +00:00
Initial party seeking version
This commit is contained in:
parent
70a2d400e7
commit
cdc105bec7
17 changed files with 516 additions and 48 deletions
|
|
@ -276,7 +276,8 @@
|
|||
• Your Task streaks and Habit counters will not reset\n
|
||||
• 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">It’s 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">Here’s 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.habitrpg.android.habitica.models.invitations
|
||||
|
||||
class InviteResponse {
|
||||
|
||||
}
|
||||
|
|
@ -28,4 +28,5 @@ open class MemberPreferences :
|
|||
}
|
||||
} else null
|
||||
}
|
||||
var language: String? = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue