diff --git a/Habitica/res/values/attrs.xml b/Habitica/res/values/attrs.xml index ef3dd7101..b6605a567 100644 --- a/Habitica/res/values/attrs.xml +++ b/Habitica/res/values/attrs.xml @@ -6,6 +6,7 @@ + diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml index 24af9b3e8..10ffc1745 100644 --- a/Habitica/res/values/strings.xml +++ b/Habitica/res/values/strings.xml @@ -1367,11 +1367,11 @@ Damage to a boss or found collection items will be stored until you resume damage List By Invite - Here’s a list of Habiticans looking to join a Party + Here’s a list of players looking to join a Party There\'s no one looking for a Party right now. You can check back later! Invited Invite with @username or email - Send an invite directly to Habiticans you know + Send an invite directly to players you know Username or email address Want to join a Party with others but don’t know any other players? Let Party leaders know you’re looking for an invite! Looking for a Party? @@ -1384,7 +1384,7 @@ New gear is released during the seasonal Galas. Until then, theres %d pieces of gear in the Enchanted Armoire to find! New gear is released during the seasonal Galas. The Enchanted Armoire gets new stock every month too! Unlock %s gear and skills - Rescind Invite + Cancel Invite Rescinded Pending Invite Input invalid diff --git a/Habitica/res/values/styles.xml b/Habitica/res/values/styles.xml index e87d077be..c3e7340fc 100644 --- a/Habitica/res/values/styles.xml +++ b/Habitica/res/values/styles.xml @@ -39,6 +39,7 @@ @color/content_background @color/content_background_offset + @color/offset_background @color/window_background @color/brand_800 @color/transparent diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt index 7947d5cdf..d89e39053 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt @@ -44,10 +44,8 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl internal val items: MutableList = ArrayList() var selectedItem: Int? = null set(value) { - val oldValue = field field = value - oldValue?.let { notifyItemChanged(it) } - value?.let { notifyItemChanged(it) } + notifyDataSetChanged() } var itemSelectedEvents: ((HabiticaDrawerItem) -> Unit)? = null diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt index c260a6d16..612516715 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/ChatFragment.kt @@ -8,6 +8,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -49,7 +51,9 @@ class ChatFragment : BaseFragment() { return FragmentChatBinding.inflate(inflater, container, false) } - lateinit var viewModel : GroupViewModel + val viewModel: GroupViewModel by viewModels( + ownerProducer = { parentFragment as Fragment } + ) @Inject lateinit var configManager : AppConfigManager @@ -131,6 +135,11 @@ class ChatFragment : BaseFragment() { } } + override fun onResume() { + super.onResume() + setNavigatedToFragment() + } + private fun setReplyTo(username : String?) { val previousMessage = binding?.chatBarView?.message ?: "" if (previousMessage.contains("@$username")) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/TavernFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/TavernFragment.kt index 5a2a04100..98874cb32 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/TavernFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/TavernFragment.kt @@ -18,8 +18,8 @@ import com.habitrpg.android.habitica.models.social.Group import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel import com.habitrpg.android.habitica.ui.viewmodels.GroupViewType -import javax.inject.Inject import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class TavernFragment : BaseMainFragment() { @@ -87,9 +87,7 @@ class TavernFragment : BaseMainFragment() { TavernDetailFragment() } 1 -> { - val fragment = ChatFragment() - fragment.viewModel = viewModel - fragment + ChatFragment() } else -> Fragment() } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt index 510a4881a..31dafad41 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildDetailFragment.kt @@ -9,6 +9,8 @@ import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.habitrpg.android.habitica.MainNavDirections import com.habitrpg.android.habitica.R @@ -54,16 +56,18 @@ class GuildDetailFragment : BaseFragment() { return FragmentGuildDetailBinding.inflate(inflater, container, false) } - var viewModel: GroupViewModel? = null + val viewModel: GroupViewModel by viewModels( + ownerProducer = { parentFragment as Fragment } + ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding?.refreshLayout?.setOnRefreshListener { this.refresh() } - viewModel?.getGroupData()?.observe(viewLifecycleOwner, { updateGuild(it) }) - viewModel?.getLeaderData()?.observe(viewLifecycleOwner, { setLeader(it) }) - viewModel?.getIsMemberData()?.observe(viewLifecycleOwner, { updateMembership(it) }) + viewModel.getGroupData().observe(viewLifecycleOwner, { updateGuild(it) }) + viewModel.getLeaderData().observe(viewLifecycleOwner, { setLeader(it) }) + viewModel.getIsMemberData().observe(viewLifecycleOwner, { updateMembership(it) }) binding?.guildDescription?.movementMethod = LinkMovementMethod.getInstance() binding?.guildSummary?.movementMethod = LinkMovementMethod.getInstance() @@ -72,7 +76,7 @@ class GuildDetailFragment : BaseFragment() { leaveGuild() } binding?.joinButton?.setOnClickListener { - viewModel?.joinGroup { + viewModel.joinGroup { (this.activity as? SnackbarActivity)?.showSnackbar(title = getString(R.string.joined_guild)) } } @@ -81,7 +85,7 @@ class GuildDetailFragment : BaseFragment() { sendInvitesResult.launch(intent) } binding?.leaderWrapper?.setOnClickListener { - viewModel?.leaderID?.let { leaderID -> + viewModel.leaderID?.let { leaderID -> val profileDirections = MainNavDirections.openProfileActivity(leaderID) MainNavigationController.navigate(profileDirections) } @@ -106,7 +110,7 @@ class GuildDetailFragment : BaseFragment() { private val sendInvitesResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { val inviteData = HashMap() - inviteData["inviter"] = viewModel?.user?.value?.profile?.name ?: "" + inviteData["inviter"] = viewModel.user.value?.profile?.name ?: "" val emails = it.data?.getStringArrayExtra(GroupInviteActivity.EMAILS_KEY) if (emails != null && emails.isNotEmpty()) { val invites = ArrayList>() @@ -124,12 +128,12 @@ class GuildDetailFragment : BaseFragment() { userIDs.forEach { invites.add(it) } inviteData["usernames"] = invites } - viewModel?.inviteToGroup(inviteData) + viewModel.inviteToGroup(inviteData) } } private fun refresh() { - viewModel?.retrieveGroup { + viewModel.retrieveGroup { binding?.refreshLayout?.isRefreshing = false } } @@ -140,7 +144,7 @@ class GuildDetailFragment : BaseFragment() { userRepository.getUser().collect { it?.challenges?.forEach { membership -> val challenge = challengeRepository.getChallenge(membership.challengeID).firstOrNull() - if (challenge != null && challenge.groupId == viewModel?.groupID) { + if (challenge != null && challenge.groupId == viewModel.groupID) { groupChallenges.add(challenge) } } @@ -200,12 +204,11 @@ class GuildDetailFragment : BaseFragment() { } companion object { - fun newInstance(viewModel: GroupViewModel?): GuildDetailFragment { + fun newInstance(): GuildDetailFragment { val args = Bundle() val fragment = GuildDetailFragment() fragment.arguments = args - fragment.viewModel = viewModel return fragment } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildFragment.kt index 008eb7932..f2ac76947 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/guilds/GuildFragment.kt @@ -90,7 +90,6 @@ class GuildFragment : BaseMainFragment() { } if (childFragment is GuildDetailFragment) { guildInformationFragment = childFragment - childFragment.viewModel = viewModel } } } @@ -144,12 +143,11 @@ class GuildFragment : BaseMainFragment() { when (position) { 0 -> { - guildInformationFragment = GuildDetailFragment.newInstance(viewModel) + guildInformationFragment = GuildDetailFragment.newInstance() fragment = guildInformationFragment } 1 -> { chatFragment = ChatFragment() - chatFragment?.viewModel = viewModel fragment = chatFragment } else -> fragment = Fragment() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt index 7c3f75808..0bf3eaf23 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyDetailFragment.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.Column import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.ChallengeRepository @@ -54,8 +56,9 @@ import javax.inject.Inject @AndroidEntryPoint class PartyDetailFragment : BaseFragment() { - - var viewModel: PartyViewModel? = null + val viewModel: PartyViewModel by viewModels( + ownerProducer = { parentFragment as Fragment } + ) override var binding: FragmentPartyDetailBinding? = null @@ -99,7 +102,7 @@ class PartyDetailFragment : BaseFragment() { binding?.invitationsView?.getLeader = null binding?.invitationsView?.acceptCall = { - viewModel?.joinGroup(it) { + viewModel.joinGroup(it) { lifecycleScope.launch(ExceptionHandler.coroutine()) { val user = userRepository.retrieveUser(false) parentFragmentManager.popBackStack() @@ -114,39 +117,38 @@ class PartyDetailFragment : BaseFragment() { binding?.invitationsView?.rejectCall = { lifecycleScope.launchCatching { socialRepository.rejectGroupInvite(it) - userRepository.retrieveUser(false, true) + userRepository.retrieveUser(withTasks = false, forced = true) } } binding?.invitesWrapper?.setContent { HabiticaTheme { - val invitedMembers = viewModel?.pendingInvites Column { - for (invitedMember in (invitedMembers ?: emptyList())) { - val state = viewModel?.pendingInviteStates?.getOrDefault( + for (invitedMember in viewModel.pendingInvites) { + val state = viewModel.pendingInviteStates.getOrDefault( invitedMember.id, LoadingButtonState.CONTENT - ) ?: LoadingButtonState.CONTENT + ) PartySeekingListItem( user = invitedMember, inviteState = state, - isInvited = state != LoadingButtonState.SUCCESS, + isInvited = true, showHeader = true, showExtendedInfo = false, onInvite = { - viewModel?.rescindInvite(invitedMember) + viewModel.rescindInvite(invitedMember) }) } } } } - viewModel?.getGroupData()?.observe(viewLifecycleOwner) { updateParty(it) } - viewModel?.user?.observe(viewLifecycleOwner) { updateUser(it) } - viewModel?.getMembersData()?.observe(viewLifecycleOwner) { updateMembersList(it) } + viewModel.getGroupData().observe(viewLifecycleOwner) { updateParty(it) } + viewModel.user.observe(viewLifecycleOwner) { updateUser(it) } + viewModel.getMembersData().observe(viewLifecycleOwner) { updateMembersList(it) } } private fun refreshParty() { - viewModel?.retrieveGroup { + viewModel.retrieveGroup { binding?.refreshLayout?.isRefreshing = false } } @@ -180,7 +182,7 @@ class PartyDetailFragment : BaseFragment() { binding?.questProgressView?.visibility = View.GONE } - binding?.findNewMember?.isVisible = viewModel?.isLeader == true + binding?.findNewMember?.isVisible = viewModel.isLeader == true } private fun updateUser(user: User?) { @@ -205,7 +207,7 @@ class PartyDetailFragment : BaseFragment() { } } - binding?.questProgressView?.configure(user, viewModel?.isUserOnQuest) + binding?.questProgressView?.configure(user, viewModel.isUserOnQuest) if ((user.invitations?.parties?.count() ?: 0) > 0) { binding?.partyInvitationWrapper?.visibility = View.VISIBLE @@ -243,7 +245,7 @@ class PartyDetailFragment : BaseFragment() { } private fun showParticipantButtons(): Boolean { - return viewModel?.showParticipantButtons() ?: false + return viewModel.showParticipantButtons() } private fun updateQuestContent(questContent: QuestContent) { @@ -277,15 +279,15 @@ class PartyDetailFragment : BaseFragment() { ) ) } - if (viewModel?.isQuestActive == true) { + if (viewModel.isQuestActive) { binding?.questProgressView?.visibility = View.VISIBLE binding?.questProgressView?.setData( questContent, - viewModel?.getGroupData()?.value?.quest?.progress + viewModel.getGroupData().value?.quest?.progress ) - val questParticipants = viewModel?.getGroupData()?.value?.quest?.members - if (questParticipants?.find { it.key == viewModel?.userViewModel?.userID } != null) { + val questParticipants = viewModel.getGroupData().value?.quest?.members + if (questParticipants?.find { it.key == viewModel.userViewModel.userID } != null) { binding?.questParticipationView?.text = context?.getString(R.string.number_participants, questParticipants.size) } else { @@ -304,7 +306,7 @@ class PartyDetailFragment : BaseFragment() { } } else { binding?.questProgressView?.visibility = View.GONE - val members = viewModel?.getGroupData()?.value?.quest?.members + val members = viewModel.getGroupData().value?.quest?.members val responded = members?.filter { it.isParticipating != null } binding?.questParticipationView?.text = context?.getString(R.string.number_responded, responded?.size, members?.size) @@ -312,7 +314,7 @@ class PartyDetailFragment : BaseFragment() { } private fun updateMembersList(members: List?) { - val leaderID = viewModel?.leaderID + val leaderID = viewModel.leaderID members?.forEachIndexed { index, member -> val memberView = ( if ((binding?.membersWrapper?.childCount ?: 0) > index) { @@ -324,18 +326,18 @@ class PartyDetailFragment : BaseFragment() { } ) ?: return@forEachIndexed val viewHolder = GroupMemberViewHolder(memberView) - viewHolder.bind(member, leaderID ?: "", viewModel?.user?.value?.id) + viewHolder.bind(member, leaderID ?: "", viewModel.user.value?.id) viewHolder.onClickEvent = { FullProfileActivity.open(member.id) } viewHolder.sendMessageEvent = { - member.id.let { showSendMessageToUserDialog(it, member.displayName) } + showSendMessageToUserDialog(member.id, member.displayName) } viewHolder.transferOwnershipEvent = { - member.id.let { showTransferOwnerShipDialog(it, member.displayName) } + showTransferOwnerShipDialog(member.id, member.displayName) } viewHolder.removeMemberEvent = { - member.id.let { showRemoveMemberDialog(it, member.displayName) } + showRemoveMemberDialog(member.id, member.displayName) } } } @@ -376,7 +378,7 @@ class PartyDetailFragment : BaseFragment() { val dialog = context?.let { HabiticaAlertDialog(it) } dialog?.addButton(R.string.transfer, true) { _, _ -> lifecycleScope.launch(ExceptionHandler.coroutine()) { - socialRepository.transferGroupOwnership(viewModel?.groupID ?: "", userID) + socialRepository.transferGroupOwnership(viewModel.groupID ?: "", userID) (activity as? MainActivity)?.snackbarContainer?.let { it1 -> HabiticaSnackbar.showSnackbar( it1, @@ -402,7 +404,7 @@ class PartyDetailFragment : BaseFragment() { val dialog = context?.let { HabiticaAlertDialog(it) } dialog?.addButton(R.string.remove, true) { _, _ -> lifecycleScope.launch(ExceptionHandler.coroutine()) { - socialRepository.removeMemberFromGroup(viewModel?.groupID ?: "", userID) + socialRepository.removeMemberFromGroup(viewModel.groupID ?: "", userID) (activity as? MainActivity)?.snackbarContainer?.let { it1 -> HabiticaSnackbar.showSnackbar( it1, @@ -434,7 +436,7 @@ class PartyDetailFragment : BaseFragment() { it?.challenges?.forEach { membership -> val challenge = challengeRepository.getChallenge(membership.challengeID).firstOrNull() - if (challenge != null && challenge.groupId == viewModel?.groupID) { + if (challenge != null && challenge.groupId == viewModel.groupID) { groupChallenges.add(challenge) } } @@ -454,7 +456,7 @@ class PartyDetailFragment : BaseFragment() { alert.setTitle(R.string.party_challenges) alert.setMessage(R.string.leave_party_challenges_confirmation) alert.addButton(R.string.keep_challenges, true) { _, _ -> - viewModel?.leaveGroup(groupChallenges, true) { + viewModel.leaveGroup(groupChallenges, true) { parentFragmentManager.popBackStack() MainNavigationController.navigate(R.id.noPartyFragment) } @@ -464,7 +466,7 @@ class PartyDetailFragment : BaseFragment() { false, isDestructive = true ) { _, _ -> - viewModel?.leaveGroup(groupChallenges, false) { + viewModel.leaveGroup(groupChallenges, false) { parentFragmentManager.popBackStack() MainNavigationController.navigate(R.id.noPartyFragment) } @@ -480,7 +482,7 @@ class PartyDetailFragment : BaseFragment() { isPrimary = true, isDestructive = true ) { _, _ -> - viewModel?.leaveGroup(groupChallenges, false) { + viewModel.leaveGroup(groupChallenges, false) { parentFragmentManager.popBackStack() MainNavigationController.navigate(R.id.noPartyFragment) } @@ -494,12 +496,12 @@ class PartyDetailFragment : BaseFragment() { private fun onQuestAccept() { HapticFeedbackManager.tap(requireView()) - viewModel?.acceptQuest() + viewModel.acceptQuest() } private fun onQuestReject() { HapticFeedbackManager.tap(requireView()) - viewModel?.rejectQuest() + viewModel.rejectQuest() } private fun questDetailButtonClicked() { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyFragment.kt index 451d9e7d3..3440e029e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyFragment.kt @@ -13,7 +13,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.databinding.FragmentViewpagerBinding @@ -30,7 +29,6 @@ import dagger.hilt.android.AndroidEntryPoint class PartyFragment : BaseMainFragment() { private var detailFragment: PartyDetailFragment? = null - private var chatFragment: ChatFragment? = null private var viewPagerAdapter: FragmentStateAdapter? = null internal val viewModel: PartyViewModel by viewModels() @@ -189,13 +187,10 @@ class PartyFragment : BaseMainFragment() { return when (position) { 0 -> { detailFragment = PartyDetailFragment() - detailFragment?.viewModel = viewModel detailFragment } 1 -> { - chatFragment = ChatFragment() - chatFragment?.viewModel = viewModel - chatFragment + ChatFragment() } else -> Fragment() } ?: Fragment() @@ -207,24 +202,6 @@ class PartyFragment : BaseMainFragment() { } binding?.viewPager?.adapter = viewPagerAdapter - binding?.viewPager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageScrolled( - position: Int, - positionOffset: Float, - positionOffsetPixels: Int - ) { - if (position == 1) { - chatFragment?.setNavigatedToFragment() - } - } - - override fun onPageSelected(position: Int) { - if (position == 1) { - chatFragment?.setNavigatedToFragment() - } - } - }) - tabLayout?.let { binding?.viewPager?.let { it1 -> TabLayoutMediator(it, it1) { tab, position -> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt index 72f7be3aa..a91c3c35c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter 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.unit.dp import androidx.compose.ui.unit.sp @@ -51,6 +52,7 @@ 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.MainNavigationController import com.habitrpg.android.habitica.models.invitations.InviteResponse import com.habitrpg.android.habitica.ui.fragments.BaseFragment import com.habitrpg.android.habitica.ui.theme.HabiticaTheme @@ -61,8 +63,11 @@ 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 kotlinx.coroutines.delay import java.util.UUID import javax.inject.Inject +import kotlin.time.DurationUnit +import kotlin.time.toDuration fun uUIDFromStringOrNull(name: String): UUID? { return try { @@ -120,7 +125,9 @@ class PartyInviteFragment : BaseFragment() { val view = super.onCreateView(inflater, container, savedInstanceState) binding?.composeView?.setContent { HabiticaTheme { - PartyInviteView(viewModel) + PartyInviteView(viewModel) { + MainNavigationController.navigateBack() + } } } return view @@ -130,7 +137,8 @@ class PartyInviteFragment : BaseFragment() { @OptIn(ExperimentalFoundationApi::class) @Composable fun PartyInviteView( - viewModel: PartyInviteViewModel + viewModel: PartyInviteViewModel, + dismiss: () -> Unit ) { var inviteButtonState: LoadingButtonState by remember { mutableStateOf(LoadingButtonState.CONTENT) } val scope = rememberCoroutineScope() @@ -152,11 +160,13 @@ fun PartyInviteView( stringResource(R.string.invite_with_username_email), color = HabiticaTheme.colors.textPrimary, fontSize = 16.sp, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 4.dp) ) Text( stringResource(R.string.habiticans_send_invite), - color = HabiticaTheme.colors.textSecondary + color = HabiticaTheme.colors.textSecondary, + style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Normal), ) } } @@ -209,6 +219,7 @@ fun PartyInviteView( viewModel.invites[index] = value }, singleLine = true, + textStyle = TextStyle(fontSize = 16.sp), placeholder = { Text(stringResource(R.string.username_or_email)) }, colors = TextFieldDefaults.textFieldColors( backgroundColor = Color.Transparent, @@ -241,6 +252,8 @@ fun PartyInviteView( inviteButtonState = LoadingButtonState.SUCCESS viewModel.invites.clear() viewModel.invites.add("") + delay(2.toDuration(DurationUnit.SECONDS)) + dismiss() } else { inviteButtonState = LoadingButtonState.FAILED } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt index f73d3c701..35f19080a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color 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.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -57,6 +58,7 @@ import com.habitrpg.android.habitica.ui.viewmodels.BaseViewModel import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel import com.habitrpg.android.habitica.ui.views.LoadingButton import com.habitrpg.android.habitica.ui.views.LoadingButtonState +import com.habitrpg.android.habitica.ui.views.LoadingButtonType 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 @@ -138,14 +140,16 @@ fun InviteButton( modifier : Modifier = Modifier, isAlreadyInvited: Boolean = false, ) { - LoadingButton(state = state, onClick = onClick, colors = ButtonDefaults.buttonColors( + LoadingButton(state = state, onClick = onClick, + type = if (isAlreadyInvited) LoadingButtonType.DESTRUCTIVE else LoadingButtonType.NORMAL, + 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)) + } else { + Text(stringResource(R.string.invited)) } }) { if (isAlreadyInvited) { @@ -177,18 +181,20 @@ fun PartySeekingView( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() - .padding(top = 22.dp, bottom = 14.dp) + .padding(top = 36.dp, bottom = 14.dp) ) { Text( stringResource(R.string.find_more_members), color = HabiticaTheme.colors.textPrimary, fontSize = 16.sp, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 4.dp) ) if (pageData.itemCount == 0 && pageData.loadState.refresh is LoadState.NotLoading && pageData.loadState.append is LoadState.NotLoading) { Text( stringResource(R.string.habiticans_looking_party_empty), textAlign = TextAlign.Center, + style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Normal), color = HabiticaTheme.colors.textSecondary, modifier = Modifier .width(250.dp) .align(alignment = Alignment.CenterHorizontally) @@ -226,13 +232,13 @@ fun PartySeekingView( viewModel.inviteStates[member.id] = LoadingButtonState.LOADING val response: Any? = if (viewModel.successfulInvites.contains(member.id)) viewModel.rescindInvite(member) else viewModel.inviteUser(member) if (response != null) { + viewModel.inviteStates[member.id] = LoadingButtonState.SUCCESS + delay(4.toDuration(DurationUnit.SECONDS)) if (viewModel.successfulInvites.contains(member.id)) { viewModel.successfulInvites.remove(member.id) } else { viewModel.successfulInvites.add(member.id) } - viewModel.inviteStates[member.id] = LoadingButtonState.SUCCESS - delay(4.toDuration(DurationUnit.SECONDS)) viewModel.inviteStates[member.id] = LoadingButtonState.CONTENT } else { viewModel.inviteStates[member.id] = LoadingButtonState.FAILED diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt index 8d473b0a7..6866ba14c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/theme/HabiticaTheme.kt @@ -136,6 +136,7 @@ object HabiticaTheme { windowBackground = Color(context.getThemeColor(R.attr.colorWindowBackground)), contentBackground = Color(context.getThemeColor(R.attr.colorContentBackground)), contentBackgroundOffset = Color(context.getThemeColor(R.attr.colorContentBackgroundOffset)), + offsetBackground = Color(context.getThemeColor(R.attr.colorOffsetBackground)), textPrimary = Color(context.getThemeColor(R.attr.textColorPrimary)), textSecondary = Color(context.getThemeColor(R.attr.textColorSecondary)), textTertiary = Color(ContextCompat.getColor(context, R.color.text_ternary)), @@ -157,6 +158,7 @@ class HabiticaColors( val windowBackground: Color, val contentBackground: Color, val contentBackgroundOffset: Color, + val offsetBackground: Color, val textPrimary: Color, val textSecondary: Color, val textTertiary: Color, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt index cd92725ab..e5c9f7e92 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/TodoViewHolder.kt @@ -7,7 +7,7 @@ import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider import com.habitrpg.android.habitica.models.tasks.ChecklistItem import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.shared.habitica.models.responses.TaskDirection -import java.text.DateFormat +import java.text.SimpleDateFormat class TodoViewHolder( itemView: View, @@ -18,7 +18,7 @@ class TodoViewHolder( assignedTextProvider: GroupPlanInfoProvider? ) : ChecklistedViewHolder(itemView, scoreTaskFunc, scoreChecklistItemFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) { - private val dateFormatter: DateFormat = android.text.format.DateFormat.getDateFormat(context) + private val dateFormatter = SimpleDateFormat.getDateInstance() override fun bind( data: Task, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LoadingButton.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LoadingButton.kt index 6a7cdb249..a5b76a0af 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LoadingButton.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LoadingButton.kt @@ -63,38 +63,52 @@ enum class LoadingButtonState { SUCCESS } +enum class LoadingButtonType { + NORMAL, + DESTRUCTIVE +} + @OptIn(ExperimentalAnimationApi::class) @Composable fun LoadingButton( state : LoadingButtonState, onClick : () -> Unit, modifier : Modifier = Modifier, + type : LoadingButtonType = LoadingButtonType.NORMAL, 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 - ), + colors : ButtonColors? = null, contentPadding : PaddingValues = ButtonDefaults.ContentPadding, successContent : (@Composable RowScope.() -> Unit)? = null, failedContent : (@Composable RowScope.() -> Unit)? = null, content : @Composable RowScope.() -> Unit ) { + val colorStyle = if (type == LoadingButtonType.DESTRUCTIVE) ButtonDefaults.buttonColors( + backgroundColor = HabiticaTheme.colors.errorBackground, + contentColor = Color.White, + disabledBackgroundColor = HabiticaTheme.colors.offsetBackground, + disabledContentColor = HabiticaTheme.colors.textQuad + ) else ButtonDefaults.buttonColors( + backgroundColor = HabiticaTheme.colors.tintedUiSub, + contentColor = Color.White, + disabledBackgroundColor = HabiticaTheme.colors.offsetBackground, + disabledContentColor = HabiticaTheme.colors.textQuad + ) val colorSpec = tween(350) val backgroundColor = animateColorAsState( targetValue = when (state) { LoadingButtonState.FAILED -> HabiticaTheme.colors.errorBackground LoadingButtonState.SUCCESS -> Color.Transparent - else -> colors.backgroundColor(enabled = state != LoadingButtonState.DISABLED).value + else -> colorStyle.backgroundColor(enabled = state != LoadingButtonState.DISABLED).value }, animationSpec = colorSpec ) val contentColor = animateColorAsState( targetValue = when (state) { LoadingButtonState.FAILED -> Color.White - LoadingButtonState.SUCCESS -> HabiticaTheme.colors.successColor - else -> colors.contentColor(enabled = state != LoadingButtonState.DISABLED).value + LoadingButtonState.SUCCESS -> if (type == LoadingButtonType.DESTRUCTIVE) HabiticaTheme.colors.errorColor else HabiticaTheme.colors.successColor + else -> colorStyle.contentColor(enabled = state != LoadingButtonState.DISABLED).value }, animationSpec = colorSpec ) @@ -124,7 +138,7 @@ fun LoadingButton( shape = shape, border = if (state == LoadingButtonState.SUCCESS) BorderStroke( borderWidth.value, - HabiticaTheme.colors.successBackground + if (type == LoadingButtonType.DESTRUCTIVE) HabiticaTheme.colors.errorColor else HabiticaTheme.colors.successColor ) else border, colors = buttonColors, contentPadding = PaddingValues(0.dp) diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 3829a57c6..b1919b8c1 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,9 +1,9 @@ -New in 4.1.9: --Various subscription status optimizations --Party invites will show the sender’s username and profile --You can view, complete, assign, and add tasks to your Group Plan’s shared task board --Tap your name on a task screen to switch to different task boards --More intuitive system notification settings --New Avatar Customization interface --Audio will be controlled by media volume now --Task details are now tinted based on task health +New in 4.2: +It's easier than ever to party up with our newest feature: Look for Party and Find Members! + +- Solo players can let Party leaders know they want an invite by going to Menu > Party and tapping 'Look for Party' +- Party leaders can see players looking for Party by tapping 'Find Members' on the Party screen and send invites from the list +- Armoire shows in Market when you have all gear +- Change class from Market when viewing gear from another class +- Various bug fixes and improvements + diff --git a/version.properties b/version.properties index 960d7e317..297f3909d 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ NAME=4.2 -CODE=5991 \ No newline at end of file +CODE=6001 \ No newline at end of file