diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 5d1b0ad38..8b2b67212 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -118,6 +118,7 @@ dependencies { implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "androidx.compose.material:material:1.3.1" implementation "androidx.compose.animation:animation:$compose_version" + implementation "androidx.compose.ui:ui-text-google-fonts:$compose_version" implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt index 0198ab7eb..e225a07bd 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt @@ -464,6 +464,6 @@ interface ApiService { @POST("tasks/{taskID}/needs-work/{userID}") suspend fun markTaskNeedsWork(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse - @GET("party-seekers") + @GET("looking-for-party") suspend fun retrievePartySeekingUsers(): HabitResponse> } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/InventoryRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/InventoryRepositoryImpl.kt index d9a2165c9..55fe6b4f8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/InventoryRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/InventoryRepositoryImpl.kt @@ -139,9 +139,9 @@ class InventoryRepositoryImpl( return sellItem(item) } - override suspend fun sellItem(ownedItem: OwnedItem): User? { - val item = localRepository.getItem(ownedItem.itemType ?: "", ownedItem.key ?: "").firstOrNull() ?: return null - return sellItem(item, ownedItem) + override suspend fun sellItem(item: OwnedItem): User? { + val itemData = localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull() ?: return null + return sellItem(itemData, item) } override fun getLatestMysteryItem(): Flow { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/LifecycleCollect.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/LifecycleCollect.kt new file mode 100644 index 000000000..1c2099659 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/LifecycleCollect.kt @@ -0,0 +1,30 @@ +package com.habitrpg.android.habitica.helpers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import kotlinx.coroutines.flow.Flow +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +@Composable +fun rememberFlow( + flow: Flow, + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current +): Flow { + return remember(key1 = flow, key2 = lifecycleOwner) { flow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) } +} + +@Composable +fun Flow.collectAsStateLifecycleAware( + initial: R, + context: CoroutineContext = EmptyCoroutineContext +): State { + val lifecycleAwareFlow = rememberFlow(flow = this) + return lifecycleAwareFlow.collectAsState(initial = initial, context = context) +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/User.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/User.kt index b56a2ae65..cebe904f7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/User.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/User.kt @@ -100,8 +100,11 @@ open class User : RealmObject(), BaseMainObject, Avatar, VersionedObject { val contributorColor: Int get() = this.contributor?.contributorColor ?: R.color.text_primary - override val hourglassCount: Int + override var hourglassCount: Int get() = purchased?.plan?.consecutive?.trinkets ?: 0 + set(value) { + purchased?.plan?.consecutive?.trinkets = value + } val hasParty: Boolean get() { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.kt index 2f97fc2aa..80d203050 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/modules/UserModule.kt @@ -14,6 +14,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Named +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -38,6 +39,7 @@ class UserModule { } @Provides + @Singleton fun providesUserViewModel( @Named(NAMED_USER_ID) userID: String, userRepository: UserRepository, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BirthdayActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BirthdayActivity.kt index 825104961..314c34856 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BirthdayActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/BirthdayActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -23,7 +22,6 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.DrawerState import androidx.compose.material.DrawerValue -import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Scaffold import androidx.compose.material.ScaffoldState import androidx.compose.material.SnackbarHostState @@ -44,7 +42,6 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale @@ -60,7 +57,6 @@ import coil.compose.AsyncImage import com.android.billingclient.api.ProductDetails import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.extensions.addCloseButton import com.habitrpg.android.habitica.helpers.AppConfigManager @@ -69,6 +65,7 @@ import com.habitrpg.android.habitica.helpers.PurchaseHandler import com.habitrpg.android.habitica.ui.theme.HabiticaTheme import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel import com.habitrpg.android.habitica.ui.views.CurrencyText +import com.habitrpg.android.habitica.ui.views.HabiticaButton import com.habitrpg.android.habitica.ui.views.PixelArtView import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog @@ -613,34 +610,6 @@ fun FourFreeItem( } } -@Composable -fun HabiticaButton( - background: Color, - color: Color, - onClick: () -> Unit, - modifier: Modifier = Modifier, - content: @Composable () -> Unit -) { - Box( - contentAlignment = Alignment.Center, - modifier = modifier - .background(background, HabiticaTheme.shapes.medium) - .clickable { onClick() } - .fillMaxWidth() - .padding(8.dp) - ) { - ProvideTextStyle( - value = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.SemiBold, - color = color - ) - ) { - content() - } - } -} - @Preview(device = Devices.PIXEL_4) @Preview(device = Devices.PIXEL_4, uiMode = UI_MODE_NIGHT_YES) @Composable diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt index 7b04b59dc..407001e4b 100755 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt @@ -9,6 +9,7 @@ import android.content.pm.PackageManager import android.content.res.Configuration import android.os.Build import android.os.Bundle +import android.util.Log import android.view.KeyEvent import android.view.MenuItem import android.view.View @@ -42,6 +43,7 @@ import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.MainNavigationController import com.habitrpg.android.habitica.helpers.NotificationOpenHandler import com.habitrpg.android.habitica.helpers.SoundManager +import com.habitrpg.android.habitica.helpers.collectAsStateLifecycleAware import com.habitrpg.android.habitica.interactors.CheckClassSelectionUseCase import com.habitrpg.android.habitica.interactors.DisplayItemDropUseCase import com.habitrpg.android.habitica.interactors.NotifyUserUseCase @@ -255,12 +257,18 @@ open class MainActivity : BaseActivity(), SnackbarActivity { setupNotifications() setupBottomnavigationLayoutListener() + lifecycleScope.launch { + viewModel.userViewModel.currentTeamPlan.collect { + Log.d("asdf", it?.toString() ?: "") + } + } + binding.content.headerView.setContent { HabiticaTheme { val user by viewModel.user.observeAsState(null) - val teamPlan by viewModel.userViewModel.currentTeamPlan.collectAsState(null) + val teamPlan by viewModel.userViewModel.currentTeamPlan.collectAsStateLifecycleAware(null) val teamPlanMembers by viewModel.userViewModel.currentTeamPlanMembers.observeAsState() - AppHeaderView(user, teamPlan, teamPlanMembers) { + AppHeaderView(user, teamPlan = teamPlan, teamPlanMembers = teamPlanMembers) { showAsBottomSheet { onClose -> val group by viewModel.userViewModel.currentTeamPlanGroup.collectAsState(null) val members by viewModel.userViewModel.currentTeamPlanMembers.observeAsState() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskSummaryActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskSummaryActivity.kt index 34b49f837..99e87b92d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskSummaryActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskSummaryActivity.kt @@ -57,11 +57,14 @@ import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper import com.habitrpg.android.habitica.ui.views.UserRow import com.habitrpg.shared.habitica.models.tasks.TaskType import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import java.text.DateFormat import java.util.Date +import javax.inject.Inject -class TaskSummaryViewModel( +@HiltViewModel +class TaskSummaryViewModel @Inject constructor( userRepository : UserRepository, userViewModel : MainUserViewModel, val taskRepository : TaskRepository, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt index 12f22328b..8b180f0ae 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt @@ -41,7 +41,7 @@ import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManag import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.prefs.TimePreference import com.habitrpg.android.habitica.ui.activities.ClassSelectionActivity -import com.habitrpg.android.habitica.ui.activities.HabiticaButton +import com.habitrpg.android.habitica.ui.views.HabiticaButton import com.habitrpg.android.habitica.ui.activities.MainActivity import com.habitrpg.android.habitica.ui.activities.PrefsActivity import com.habitrpg.android.habitica.ui.theme.HabiticaTheme diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInvitePagerFragmennt.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInvitePagerFragmennt.kt index 9e2db96da..d96437ea9 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInvitePagerFragmennt.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInvitePagerFragmennt.kt @@ -31,6 +31,7 @@ class PartyInvitePagerFragment : BaseMainFragment() { ) : View? { this.usesTabLayout = true this.hidesToolbar = true + showsBackButton = true return super.onCreateView(inflater, container, savedInstanceState) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt index 7559c6ded..64d3a8819 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt @@ -425,7 +425,8 @@ class TasksFragment : BaseMainFragment(), SearchView.O mainActivity?.title = viewModel.ownerTitle MainNavigationController.updateLabel(R.id.tasksFragment, viewModel.ownerTitle.toString()) } - viewModel.userViewModel.currentTeamPlan.value = viewModel.teamPlans[viewModel.ownerID.value] + val teamPlan = viewModel.teamPlans[viewModel.ownerID.value] + viewModel.userViewModel.currentTeamPlan.tryEmit(teamPlan) lifecycleScope.launchCatching { bottomNavigation?.canAddTasks = viewModel.canAddTasks() } 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 72ca2276a..debc0f55c 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 @@ -118,6 +118,7 @@ val Typography.subtitle3 fontSize = 16.sp, letterSpacing = 0.15.sp ) + object HabiticaTheme { val typography: Typography @Composable @@ -205,3 +206,7 @@ class HabiticaColors( } } } + +class HabiticaTypography { + +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt index 3e48da07a..05cf1376a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/MainUserViewModel.kt @@ -1,5 +1,6 @@ package com.habitrpg.android.habitica.ui.viewmodels +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import com.habitrpg.android.habitica.data.SocialRepository @@ -10,7 +11,8 @@ import com.habitrpg.android.habitica.models.user.User import com.habitrpg.common.habitica.helpers.ExceptionHandler import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.MainScope -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest @@ -37,11 +39,16 @@ class MainUserViewModel @Inject constructor(private val providedUserID: String, get() = user.value?.preferences?.tasks?.mirrorGroupTasks ?: emptyList() val user: LiveData = userRepository.getUser().asLiveData() - var currentTeamPlan: MutableStateFlow = MutableStateFlow(null) + var currentTeamPlan = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) @OptIn(ExperimentalCoroutinesApi::class) var currentTeamPlanGroup = currentTeamPlan .filterNotNull() - .distinctUntilChanged { old, new -> old.id == new.id } + .distinctUntilChanged { old, new -> + Log.d("asfd", "${old.id} - ${new.id}") + old.id == new.id } .flatMapLatest { socialRepository.getGroup(it.id) } @OptIn(ExperimentalCoroutinesApi::class) var currentTeamPlanMembers: LiveData> = currentTeamPlan diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt index af7a25d75..0332c7d2c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/AppHeaderView.kt @@ -1,7 +1,12 @@ package com.habitrpg.android.habitica.ui.views import android.content.res.Resources +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally @@ -14,6 +19,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize @@ -26,6 +32,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material.primarySurface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,14 +56,19 @@ import com.habitrpg.android.habitica.models.TeamPlan import com.habitrpg.android.habitica.models.auth.LocalAuthentication import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.user.Authentication +import com.habitrpg.android.habitica.models.user.Flags +import com.habitrpg.android.habitica.models.user.Preferences import com.habitrpg.android.habitica.models.user.Profile +import com.habitrpg.android.habitica.models.user.Purchases import com.habitrpg.android.habitica.models.user.Stats +import com.habitrpg.android.habitica.models.user.SubscriptionPlan import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.ui.theme.HabiticaTheme import com.habitrpg.shared.habitica.models.Avatar import kotlin.random.Random @Composable -fun UserLevelText(user: Avatar) { +fun UserLevelText(user : Avatar) { val text = if (user.hasClass) { stringResource( id = R.string.user_level_with_class, @@ -77,7 +89,7 @@ fun UserLevelText(user: Avatar) { ) } -fun getTranslatedClassName(resources: Resources, className: String?): String { +fun getTranslatedClassName(resources : Resources, className : String?) : String { return when (className) { Stats.HEALER -> resources.getString(R.string.healer) Stats.ROGUE -> resources.getString(R.string.rogue) @@ -87,14 +99,16 @@ fun getTranslatedClassName(resources: Resources, className: String?): String { } } +@OptIn(ExperimentalAnimationApi::class) @Composable fun AppHeaderView( - user: Avatar?, - teamPlan: TeamPlan? = null, - teamPlanMembers: List? = null, - onMemberRowClicked: () -> Unit + user : Avatar?, + modifier : Modifier = Modifier, + teamPlan : TeamPlan? = null, + teamPlanMembers : List? = null, + onMemberRowClicked : () -> Unit ) { - Column { + Column(modifier) { Row { ComposableAvatarView( user, @@ -105,9 +119,15 @@ fun AppHeaderView( MainNavigationController.navigate(R.id.avatarOverviewFragment) } ) - val animationValue = animateFloatAsState(targetValue = if (teamPlan != null) 1f else 0f).value + val animationValue = + animateFloatAsState(targetValue = if (teamPlan != null) 1f else 0f).value Box(modifier = Modifier.height(100.dp)) { - Column(Modifier.padding(bottom = (animationValue * 48f).dp, end = (animationValue * 80f).dp)) { + Column( + Modifier.padding( + bottom = (animationValue * 48f).dp, + end = (animationValue * 80f).dp + ) + ) { LabeledBar( icon = HabiticaIconsHelper.imageOfHeartLightBg(), label = stringResource(R.string.HP_default), @@ -155,6 +175,21 @@ fun AppHeaderView( disabled = true, modifier = Modifier.weight(1f) ) + } else if (user?.preferences?.disableClasses != true && user?.flags?.classSelected == false) { + HabiticaButton( + background = MaterialTheme.colors.primarySurface, + color = MaterialTheme.colors.onPrimary, + onClick = { + MainNavigationController.navigate(R.id.classSelectionActivity) + }, + contentPadding = PaddingValues(0.dp), + fontSize = 14.sp, + modifier = Modifier.height(28.dp) + ) { + Text(stringResource(R.string.choose_class)) + } + } else { + Spacer(modifier = Modifier.weight(1f)) } } val animWidth = with(LocalDensity.current) { 48.dp.roundToPx() } @@ -197,47 +232,67 @@ fun AppHeaderView( exit = slideOutVertically { animHeight } + fadeOut(), modifier = Modifier.align(Alignment.BottomCenter) ) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - .width(72.dp) - .clip(MaterialTheme.shapes.medium) - .background( - colorResource(R.color.window_background) + AnimatedContent(targetState = teamPlanMembers?.filter { it.id != user?.id }, + transitionSpec = { + ContentTransform( + targetContentEnter = fadeIn(animationSpec = tween(200, easing = FastOutSlowInEasing)) + slideInVertically { height -> height }, + initialContentExit = fadeOut(animationSpec = tween(200)) + slideOutVertically { height -> -height } ) - .padding(start = 12.dp, end = 12.dp) - .clickable { - onMemberRowClicked() - } - ) { - for (member in teamPlanMembers?.filter { it.id != user?.id }?.sortedByDescending { it.authentication?.timestamps?.lastLoggedIn }?.take(6) ?: emptyList()) { - Box( - modifier = Modifier - .clip(CircleShape) - .size(26.dp) - .padding(end = 6.dp, top = 4.dp) - ) { - ComposableAvatarView( - avatar = member, - Modifier - .size(64.dp) - .requiredSize(64.dp) + }) {members -> + Row( + horizontalArrangement = Arrangement.spacedBy( + 12.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .width(72.dp) + .clip(MaterialTheme.shapes.medium) + .background( + colorResource(R.color.window_background) ) + .padding(start = 12.dp, end = 12.dp) + .clickable { + onMemberRowClicked() + } + ) { + for (member in members + ?.sortedByDescending { it.authentication?.timestamps?.lastLoggedIn } + ?.take(6) ?: emptyList()) { + Box( + modifier = Modifier + .clip(CircleShape) + .size(26.dp) + .padding(end = 6.dp, top = 4.dp) + ) { + ComposableAvatarView( + avatar = member, + Modifier + .size(64.dp) + .requiredSize(64.dp) + ) + } } } } } } } - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.defaultMinSize(minHeight = 28.dp)) { - ClassIcon(className = user?.stats?.habitClass, hasClass = user?.hasClass ?: false, modifier = Modifier.padding(4.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.defaultMinSize(minHeight = 28.dp) + ) { + ClassIcon( + className = user?.stats?.habitClass, + hasClass = user?.hasClass ?: false, + modifier = Modifier.padding(4.dp) + ) user?.let { UserLevelText(it) } Spacer(Modifier.weight(1f)) if (user is User) { - if (user.isSubscribed) { + if (user.isSubscribed || user.hourglassCount > 0) { CurrencyText( "hourglasses", user.hourglassCount.toDouble(), @@ -269,33 +324,77 @@ fun AppHeaderView( } } -private class UserProvider : PreviewParameterProvider { - override val values: Sequence +private class UserProvider : PreviewParameterProvider> { + + private fun generateMember() : User { + val member = User() + member.profile = Profile() + member.profile?.name = "User" + member.authentication = Authentication() + member.authentication?.localAuthentication = LocalAuthentication() + member.authentication?.localAuthentication?.username = "username" + member.preferences = Preferences() + member.preferences?.disableClasses = false + member.flags = Flags() + member.flags?.classSelected = true + member.purchased = Purchases() + member.purchased?.plan = SubscriptionPlan() + member.stats = Stats() + member.stats?.hp = Random.nextDouble(from = 0.0, until = 50.0) + member.stats?.maxHealth = 50 + member.stats?.toNextLevel = Random.nextInt(from = 0, until = 10000) + member.stats?.exp = + Random.nextDouble(until = (member.stats?.toNextLevel ?: 0).toDouble()) + member.stats?.maxMP = Random.nextInt(from = 0, until = 10000) + member.stats?.mp = Random.nextDouble(until = (member.stats?.maxMP ?: 0).toDouble()) + member.stats?.lvl = Random.nextInt(from = 0, until = 9999) + return member + } + + override val values : Sequence> get() { - val list = mutableListOf() - val member = User() - member.profile = Profile() - member.profile?.name = "User" - member.authentication = Authentication() - member.authentication?.localAuthentication = LocalAuthentication() - member.authentication?.localAuthentication?.username = "username" - member.stats = Stats() - member.stats?.hp = Random.nextDouble() - member.stats?.maxHealth = 50 - member.stats?.toNextLevel = Random.nextInt() - member.stats?.exp = - Random.nextDouble(until = (member.stats?.toNextLevel ?: 0).toDouble()) - member.stats?.maxMP = Random.nextInt() - member.stats?.mp = Random.nextDouble(until = (member.stats?.maxMP ?: 0).toDouble()) - member.stats?.lvl = Random.nextInt() - list.add(member) + val list = mutableListOf>() + val earlyMember = generateMember() + earlyMember.stats?.lvl = 5 + list.add(Pair(earlyMember, null)) + val needsClass = generateMember() + needsClass.stats?.lvl = 24 + needsClass.stats?.habitClass = "healer" + needsClass.flags?.classSelected = false + list.add(Pair(needsClass, null)) + val classDisabled = generateMember() + classDisabled.stats?.lvl = 24 + classDisabled.stats?.habitClass = "rogue" + classDisabled.preferences?.disableClasses = true + list.add(Pair(classDisabled, null)) + val subscriber = generateMember() + subscriber.purchased?.plan?.planId = "basic_earned" + subscriber.purchased?.plan?.customerId = "123" + subscriber.stats?.habitClass = "warrior" + list.add(Pair(subscriber, null)) + val onlyHourglasses = generateMember() + onlyHourglasses.hourglassCount = 3 + onlyHourglasses.stats?.habitClass = "wizard" + list.add(Pair(onlyHourglasses, null)) + + val teamplanUser = generateMember() + val teamPlan = TeamPlan() + list.add(Pair(teamplanUser, teamPlan)) return list.asSequence() } } @Composable @Preview -private fun Preview(@PreviewParameter(UserProvider::class) user: User) { - AppHeaderView(user) { +private fun Preview(@PreviewParameter(UserProvider::class) data: Pair) { + HabiticaTheme { + AppHeaderView( + data.first, + teamPlan = data.second, + modifier = Modifier + .background(HabiticaTheme.colors.contentBackground) + .padding(8.dp) + ) { + } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/HabiticaButton.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/HabiticaButton.kt new file mode 100644 index 000000000..d553f6b14 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/HabiticaButton.kt @@ -0,0 +1,49 @@ +package com.habitrpg.android.habitica.ui.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.habitrpg.android.habitica.ui.theme.HabiticaTheme + +@Composable +fun HabiticaButton( + background : Color, + color : Color, + onClick : () -> Unit, + modifier : Modifier = Modifier, + contentPadding : PaddingValues = PaddingValues(8.dp), + fontSize : TextUnit = 18.sp, + content : @Composable () -> Unit +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .background(background, HabiticaTheme.shapes.medium) + .clickable { onClick() } + .fillMaxWidth() + .padding(contentPadding) + ) { + ProvideTextStyle( + value = TextStyle( + fontSize = fontSize, + fontWeight = FontWeight.SemiBold, + color = color + ) + ) { + content() + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt index a751bc9ac..2d1699bf0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/LabeledBar.kt @@ -2,12 +2,17 @@ package com.habitrpg.android.habitica.ui.views import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable 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.Spacer @@ -19,6 +24,10 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -39,19 +48,19 @@ import java.text.NumberFormat @Composable fun LabeledBar( - modifier: Modifier = Modifier, - icon: Bitmap? = null, - label: String? = null, - color: Color = colorResource(R.color.brand), - barColor: Color = HabiticaTheme.colors.windowBackground, - value: Double, - maxValue: Double, - displayCompact: Boolean = false, - barHeight: Dp = 8.dp, - disabled: Boolean = false, - abbreviateValue: Boolean = true, - abbreviateMax: Boolean = true, - animated: Boolean = true + modifier : Modifier = Modifier, + icon : Bitmap? = null, + label : String? = null, + color : Color = colorResource(R.color.brand), + barColor : Color = HabiticaTheme.colors.windowBackground, + value : Double, + maxValue : Double, + displayCompact : Boolean = false, + barHeight : Dp = 8.dp, + disabled : Boolean = false, + abbreviateValue : Boolean = true, + abbreviateMax : Boolean = true, + animated : Boolean = true ) { val cleanedMaxValue = java.lang.Double.max(1.0, maxValue) @@ -62,22 +71,31 @@ fun LabeledBar( val formatter = NumberFormat.getNumberInstance() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 - Row( - verticalAlignment = Alignment.CenterVertically, + + val animatedPadding = animateDpAsState( + targetValue = if (displayCompact) { + 0.dp + } else { + 24.dp + } + ) + + Box( modifier = modifier.alpha(if (disabled) 0.5f else 1.0f) ) { icon?.let { AnimatedVisibility( visible = !displayCompact, - enter = slideInHorizontally { -18 }, - exit = slideOutHorizontally { -18 } + enter = fadeIn() + slideInHorizontally { -18 }, + exit = fadeOut() + slideOutHorizontally { -18 }, + modifier = Modifier.align(Alignment.CenterStart) ) { Image( - it.asImageBitmap(), null, modifier = Modifier.padding(end = 8.dp) + it.asImageBitmap(), null, modifier = Modifier ) } } - Column(modifier = Modifier.weight(1f)) { + Column(modifier = Modifier.padding(start = animatedPadding.value)) { LinearProgressIndicator( progress = (animatedValue / cleanedMaxValue).toFloat(), Modifier @@ -122,14 +140,22 @@ fun LabeledBar( @Composable @Preview private fun Preview() { - Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.width(180.dp)) { + var compact : Boolean by remember { mutableStateOf(false) } + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .width(240.dp) + .padding(8.dp) + .clickable { + compact = !compact + }) { LabeledBar( icon = HabiticaIconsHelper.imageOfHeartLightBg(), label = stringResource(id = R.string.health), color = colorResource(R.color.hpColor), value = 10.0, maxValue = 50.0, - displayCompact = false + displayCompact = compact ) LabeledBar( icon = HabiticaIconsHelper.imageOfExperience(), @@ -137,7 +163,7 @@ private fun Preview() { color = colorResource(R.color.xpColor), value = 100123.0, maxValue = 50000000000000.0, - displayCompact = false, + displayCompact = compact, abbreviateValue = false ) LabeledBar( @@ -145,8 +171,8 @@ private fun Preview() { label = stringResource(id = R.string.XP_default), color = colorResource(R.color.xpColor), value = 100123.0, - maxValue = 50000000000000.0, - displayCompact = false, + maxValue = 500000000000.0, + displayCompact = compact, abbreviateValue = false, abbreviateMax = false ) @@ -156,8 +182,8 @@ private fun Preview() { color = colorResource(R.color.mpColor), value = 10.0, maxValue = 5000.0, - displayCompact = false, - disabled = true + displayCompact = compact, + disabled = false ) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/utils/MemberSerialization.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/MemberSerialization.kt index d296b562b..129def0bf 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/utils/MemberSerialization.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/MemberSerialization.kt @@ -28,11 +28,12 @@ class MemberSerialization : JsonDeserializer { val realm = Realm.getDefaultInstance() var member = realm.where(Member::class.java).equalTo("id", id).findFirst() ?: Member() - if (member.id == null) { + if (member.id.isBlank()) { member.id = id } else { member = realm.copyFromRealm(member) } + realm.close() if (obj.has("flags")) { member.flags = context.deserialize(obj.get("flags"), MemberFlags::class.java) @@ -99,8 +100,6 @@ class MemberSerialization : JsonDeserializer { } member.id = member.id - - realm.close() return member } } diff --git a/Habitica/src/release/java/com/habitrpg/android/habitica/HabiticaApplication.java b/Habitica/src/release/java/com/habitrpg/android/habitica/HabiticaApplication.java index 77bc47386..afc43c2c9 100644 --- a/Habitica/src/release/java/com/habitrpg/android/habitica/HabiticaApplication.java +++ b/Habitica/src/release/java/com/habitrpg/android/habitica/HabiticaApplication.java @@ -5,12 +5,4 @@ import com.habitrpg.android.habitica.components.DaggerAppComponent; import com.habitrpg.android.habitica.modules.AppModule; public class HabiticaApplication extends HabiticaBaseApplication { - @Override - protected AppComponent initDagger() { - return DaggerAppComponent.builder() - .appModule(new AppModule(this)) - .developerModule(new ReleaseDeveloperModule()) - .build(); - } - } diff --git a/version.properties b/version.properties index 53c8a08a7..41a04c539 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ NAME=4.1.8 -CODE=5681 \ No newline at end of file +CODE=5701 \ No newline at end of file