Merge branch 'version/4.1' into Fiz/reminder-notification-permission

This commit is contained in:
Phillip Thelen 2023-01-27 13:05:21 +01:00 committed by GitHub
commit 975df93006
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 1268 additions and 554 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -2,5 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="32dp" android:height="32dp" />
<stroke android:width="2dp" android:color="?colorTintedBackgroundOffset" />
<stroke android:width="2dp" android:color="?textColorTintedSecondary" />
</shape>

View file

@ -2,5 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="32dp" android:height="32dp" />
<solid android:color="@color/white" />
<solid android:color="?tintedUiMain" />
</shape>

View file

@ -2,11 +2,17 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="?colorPrimaryDistinct" />
<solid android:color="?textColorTintedSecondary" />
<corners android:radius="4dp" android:topLeftRadius="8dp" android:topRightRadius="8dp" />
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="?colorTintedBackground" />
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp" android:bottomLeftRadius="2dp" android:bottomRightRadius="2dp" />
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="?colorTintedBackgroundOffset" />

View file

@ -95,7 +95,8 @@
app:layout_anchorGravity="bottom"
app:layout_collapseMode="pin"
app:tabGravity="fill"
app:tabIndicatorColor="?colorPrimary"
app:tabIndicatorColor="?textColorPrimary"
app:tabSelectedTextColor="?textColorPrimary"
app:tabMode="fixed" />
<FrameLayout
android:id="@+id/connection_issue_view"

View file

@ -177,13 +177,14 @@
android:id="@+id/habit_adjust_positive_input_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:boxBackgroundColor="?attr/colorTintedBackgroundOffset"
android:background="@drawable/task_form_control_bg"
android:hint="@string/positive_habit_form"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/habit_adjust_positive_streak_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<Space
@ -194,12 +195,13 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/negative_habit_form"
app:boxBackgroundColor="?attr/colorTintedBackgroundOffset"
android:background="@drawable/task_form_control_bg"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/habit_adjust_negative_streak_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View file

@ -114,7 +114,7 @@
android:text="@string/gem_purchase_subtitle"
android:gravity="center"
android:textStyle="normal|bold"
android:textColor="?colorPrimary"
android:textColor="?colorPrimaryText"
android:textSize="16sp"
android:lineSpacingExtra="4dp"
android:layout_marginTop="23dp"
@ -234,7 +234,7 @@
android:layout_height="wrap_content"
android:text="@string/gift_gems"
android:background="@color/transparent"
android:textColor="?colorAccent"
android:textColor="?colorPrimaryText"
android:textAllCaps="false"/>
<TextView android:id="@+id/supportTextView"
android:layout_height="wrap_content"

View file

@ -75,5 +75,11 @@
style="@style/HabiticaButton.Purple"
android:layout_marginTop="@dimen/spacing_medium"
android:text="@string/send_gift" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -17,6 +17,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/promo_compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/promo_banner"
android:layout_width="match_parent"

View file

@ -9,12 +9,13 @@
android:paddingEnd="20dp"
android:paddingBottom="10dp">
<ImageView
<com.habitrpg.common.habitica.views.PixelArtView
android:id="@+id/notification_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="20dp"
android:background="@color/transparent"
android:layout_gravity="center_vertical"
android:visibility="gone" />
<TextView

View file

@ -47,6 +47,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/dismiss_tutorial"
android:textColor="?textColorTintedSecondary"
android:theme="@style/DialogButton"/>
<Button
@ -57,6 +58,7 @@
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:text="@string/complete_tutorial"
android:textColor="?textColorTintedSecondary"
android:theme="@style/DialogButton" />
</LinearLayout>
</LinearLayout>

View file

@ -259,6 +259,7 @@
android:gravity="center"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:visibility="gone"
android:text="@string/subscribers_mythic_hourglasses" />
<ImageView

View file

@ -12,17 +12,37 @@
android:paddingBottom="@dimen/task_top_bottom_padding"
android:layout_marginEnd="@dimen/task_text_padding"
android:layout_marginStart="@dimen/task_text_padding">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/assigned_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Caption4"
style="@style/Caption3"
android:text="@string/pending_approval"
android:textColor="@color/text_ternary"
android:drawableStart="@drawable/assign"
android:drawablePadding="@dimen/spacing_small"
android:layout_marginBottom="2dp"
/>
<TextView
android:id="@+id/completed_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Caption4"
android:text="@string/pending_approval"
android:textColor="@color/text_ternary"
android:drawablePadding="@dimen/spacing_small"
android:drawableStart="@drawable/completed"
android:drawableTint="@color/text_ternary"
android:paddingVertical="1dp"
android:paddingHorizontal="6dp"
android:background="@drawable/pill_bg_gray"
android:layout_marginStart="10dp"
/>
</LinearLayout>
<com.habitrpg.android.habitica.ui.views.EllipsisTextView
android:id="@+id/checkedTextView"
style="@style/Subheader3"

View file

@ -244,7 +244,7 @@
android:id="@+id/birthdayActivity"
android:name="com.habitrpg.android.habitica.ui.activities.BirthdayActivity"
android:label="@string/gem_purchase_toolbartitle" >
<deepLink app:uri="habitica.com/birthday" />
<deepLink app:uri="habitica.com/promo/birthday" />
</activity>
<fragment
android:id="@+id/newsFragment"

View file

@ -32,6 +32,7 @@
<color name="account_dialog_bars">@color/gray_1</color>
<color name="habit_inactive_gray">@color/gray_10</color>
<color name="equipment_overview_background">@color/gray_10</color>
<color name="equipment_column_background">@color/gray_10</color>
<color name="inverted_background">@color/gray_10</color>
<color name="inverted_background_offset">@color/gray_50</color>
<color name="background_red">@color/red_50</color>

View file

@ -17,6 +17,9 @@
<attr name="colorPrimaryText" format="color" />
<attr name="colorBoxStroke" format="color" />
<attr name="taskFormTint" format="color" />
<attr name="tintedUiMain" format="color" />
<attr name="tintedUiSub" format="color" />
<attr name="tintedUiDetails" format="color" />
<attr name="textColorSecondaryDark" format="color" />
<attr name="statsColor" format="color" />
<attr name="statsTitle" format="string" />

View file

@ -86,6 +86,7 @@
<color name="system_bars">@color/brand_200</color>
<color name="account_dialog_bars">@color/white</color>
<color name="equipment_overview_background">@color/gray_50</color>
<color name="equipment_column_background">@color/gray_600</color>
<color name="inverted_background">@color/gray_100</color>
<color name="inverted_background_offset">@color/gray_200</color>
<color name="background_red">@color/red_100</color>

View file

@ -747,7 +747,7 @@
<string name="gift_confirmation_title">Your gift was sent!</string>
<string name="gift_confirmation_text_sub_g1g1">You sent %1$s a %2$s-month Habitica subscription and the same subscription was applied to your account for our Gift One Get One promotion!</string>
<string name="gift_confirmation_text_sub">You sent @%1$s a %2$s-month Habitica subscription.</string>
<string name="gift_confirmation_text_gems_new">You sent @%s %s gems.</string>
<string name="gift_confirmation_text_gems_new">You sent @%1$s %2$s gems.</string>
<string name="subscription_confirmation">You are now subscribed for 1 month</string>
<string name="subscription_confirmation_multiple">You are now subscribed for %s months</string>
<string name="gem_purchase_confirmation">You gained %s gems.</string>
@ -1313,24 +1313,33 @@
<string name="third">third</string>
<string name="fourth">fourth</string>
<string name="fifth">fifth</string>
<string name="animated_gryphatrice_pet">Animated Gryphatrice Pet</string>
<string name="animated_gryphatrice_pet">Jubilant Gryphatrice Pet</string>
<string name="birthday_title_description">Celebrate Habiticas 10th birthday with gifts and exclusive items below!</string>
<string name="limited_edition">Limited Edition</string>
<string name="gryphatrice_description">The rare, mystical Gryphatrice joins the birthday bash! Dont miss your chance to own this exclusive animated Pet.</string>
<string name="gryphatrice_description">The rare, Jubilant Gryphatrice joins the birthday bash! Dont miss your chance to own this exclusive animated Pet.</string>
<string name="thanks_for_support">Thanks for your support!</string>
<string name="plenty_of_potions">Plenty of Potions</string>
<string name="for_for_free">For for Free</string>
<string name="for_for_free">Four for Free</string>
<string name="buy_for_x">Buy for %s</string>
<string name="buy_for">Buy for</string>
<string name="plenty_of_potions_description">Were bringing back 10 of the communitys favorite Magic Hatching Potions. Head over to the Market to fill out your collection!</string>
<string name="for_for_free_description">To keep the party going, well be giving away Party Robes, 20 Gems, and a limited edition Cape set and Background!</string>
<string name="birthday_limitations">This is a limited time event that starts on January 23rd at 8:00 AM ET (13:00 UTC) and will end February 1st at 8:00 PM ET (01:00 UTC). The Limited Edition Gryphatrice and ten Magic Hatching Potions will be available to buy during this time. The other Gifts will be automatically delivered to all accounts active in the previous 30 days.</string>
<string name="birthday_limitations">This is a limited time event that starts on %1$s and will end %2$s. The Limited Edition Jubilant Gryphatrice and ten Magic Hatching Potions will be available to buy during this time. The other Gifts listed in the Four for Free section will be automatically delivered to all accounts that were active in the 30 days prior to day the gift is sent. Accounts created after the gifts are sent will not be able to claim them.</string>
<string name="visit_the_market">Visit the Market</string>
<string name="exclusive_items_await">Exclusive items and gifts await</string>
<string name="ends_in_x">Ends in %s</string>
<string name="see_more">See More</string>
<string name="jubilant_gryphatrice_confirmation">You purchased the Jubilant Gryphatrice!</string>
<string name="jubilant_gryphatrice_confirmation_gift">You gifted the Jubilant Gryphatrice!</string>
<string name="open_settings">Open Settings</string>
<string name="undo">Undo</string>
<string name="day_x">Day %d</string>
<string name="a_party_robe">A Party Robe</string>
<string name="twenty_gems">20 Gems</string>
<string name="background">Background</string>
<string name="birthday_set">Birthday Set</string>
<string name="you_equipped_x">You equipped %s</string>
<string name="purchase_gryphatrice_confirmation">Purchase the Jubilant Gryphatrice for %d Gems?</string>
<plurals name="you_x_others">
<item quantity="zero">You</item>

View file

@ -40,9 +40,12 @@
<item name="colorContentBackgroundOffset">@color/content_background_offset</item>
<item name="colorWindowBackground">@color/window_background</item>
<item name="colorTintedBackground">@color/brand_800</item>
<item name="colorTintedBackgroundOffset">@color/brand_700</item>
<item name="colorTintedBackgroundOffset">@color/brand_50012</item>
<item name="textColorTintedSecondary">@color/brand_sub_text</item>
<item name="textColorTintedPrimary">@color/brand_100</item>
<item name="tintedUiMain">@color/brand_500</item>
<item name="tintedUiSub">@color/brand_400</item>
<item name="tintedUiDetails">@color/brand_100</item>
<item name="popupMenuStyle">@style/PopupTheme</item>
<item name="actionOverflowMenuStyle">@style/PopupTheme</item>
@ -79,7 +82,7 @@
<item name="textColorTintedSecondary">@color/brand_500</item>
<item name="colorPrimaryText">@color/brand_600</item>
<item name="colorPrimaryDark">@color/brand_400</item>
<item name="textColorTintedPrimary">@color/brand_700</item>
<item name="textColorTintedPrimary">@color/brand_800</item>
</style>
@ -102,9 +105,12 @@
<item name="toolbarContentColor">@color/red_1</item>
<item name="colorPrimaryText">@color/red_1</item>
<item name="colorTintedBackground">@color/red_700</item>
<item name="colorTintedBackgroundOffset">@color/red_600</item>
<item name="colorTintedBackgroundOffset">@color/red_50012</item>
<item name="textColorTintedSecondary">@color/red_sub_text</item>
<item name="textColorTintedPrimary">@color/red_100</item>
<item name="textColorTintedPrimary">@color/red_1</item>
<item name="tintedUiMain">@color/red_400</item>
<item name="tintedUiSub">@color/red_10</item>
<item name="tintedUiDetails">@color/red_1</item>
</style>
<style name="MainAppTheme.Red.Dark">
@ -114,10 +120,9 @@
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="toolbarContentColor">@color/red_1</item>
<item name="colorTintedBackground">@color/red_00</item>
<item name="colorTintedBackgroundOffset">@color/red_0</item>
<item name="colorPrimaryText">@color/red_600</item>
<item name="textColorTintedSecondary">@color/red_500</item>
<item name="textColorTintedPrimary">@color/red_600</item>
<item name="textColorTintedPrimary">@color/red_700</item>
</style>
<style name="MainAppTheme.Maroon">
@ -138,9 +143,12 @@
<item name="taskFormTint">@color/maroon_50</item>
<item name="colorPrimaryText">@color/maroon_1</item>
<item name="colorTintedBackground">@color/maroon_700</item>
<item name="colorTintedBackgroundOffset">@color/maroon_600</item>
<item name="colorTintedBackgroundOffset">@color/maroon_50012</item>
<item name="textColorTintedSecondary">@color/maroon_sub_text</item>
<item name="textColorTintedPrimary">@color/maroon_100</item>
<item name="textColorTintedPrimary">@color/maroon_1</item>
<item name="tintedUiMain">@color/maroon_400</item>
<item name="tintedUiSub">@color/maroon_10</item>
<item name="tintedUiDetails">@color/maroon_1</item>
</style>
<style name="MainAppTheme.Maroon.Dark">
@ -149,10 +157,9 @@
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="toolbarContentColor">@color/white</item>
<item name="colorTintedBackground">@color/maroon_00</item>
<item name="colorTintedBackgroundOffset">@color/maroon_0</item>
<item name="colorPrimaryText">@color/maroon_600</item>
<item name="textColorTintedSecondary">@color/maroon_500</item>
<item name="textColorTintedPrimary">@color/maroon_600</item>
<item name="textColorTintedPrimary">@color/maroon_700</item>
</style>
<style name="MainAppTheme.Orange">
@ -174,9 +181,12 @@
<item name="toolbarContentColor">@color/orange_1</item>
<item name="colorPrimaryText">@color/orange_1</item>
<item name="colorTintedBackground">@color/orange_700</item>
<item name="colorTintedBackgroundOffset">@color/orange_600</item>
<item name="colorTintedBackgroundOffset">@color/orange_50012</item>
<item name="textColorTintedSecondary">@color/orange_sub_text</item>
<item name="textColorTintedPrimary">@color/orange_100</item>
<item name="textColorTintedPrimary">@color/orange_1</item>
<item name="tintedUiMain">@color/orange_400</item>
<item name="tintedUiSub">@color/orange_10</item>
<item name="tintedUiDetails">@color/orange_1</item>
</style>
<style name="MainAppTheme.Orange.Dark">
@ -186,10 +196,9 @@
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="toolbarContentColor">@color/orange_1</item>
<item name="colorTintedBackground">@color/orange_00</item>
<item name="colorTintedBackgroundOffset">@color/orange_0</item>
<item name="colorPrimaryText">@color/orange_600</item>
<item name="textColorTintedSecondary">@color/orange_500</item>
<item name="textColorTintedPrimary">@color/orange_600</item>
<item name="textColorTintedPrimary">@color/orange_700</item>
</style>
<style name="MainAppTheme.Yellow">
@ -211,9 +220,12 @@
<item name="toolbarContentColor">@color/yellow_1</item>
<item name="colorPrimaryText">@color/yellow_1</item>
<item name="colorTintedBackground">@color/yellow_700</item>
<item name="colorTintedBackgroundOffset">@color/yellow_600</item>
<item name="colorTintedBackgroundOffset">@color/yellow_50012</item>
<item name="textColorTintedSecondary">@color/yellow_sub_text</item>
<item name="textColorTintedPrimary">@color/yellow_10</item>
<item name="textColorTintedPrimary">@color/yellow_1</item>
<item name="tintedUiMain">@color/yellow_400</item>
<item name="tintedUiSub">@color/yellow_10</item>
<item name="tintedUiDetails">@color/yellow_1</item>
</style>
<style name="MainAppTheme.Yellow.Dark">
@ -223,10 +235,9 @@
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="toolbarContentColor">@color/yellow_1</item>
<item name="colorTintedBackground">@color/yellow_00</item>
<item name="colorTintedBackgroundOffset">@color/yellow_0</item>
<item name="colorPrimaryText">@color/yellow_600</item>
<item name="textColorTintedSecondary">@color/yellow_500</item>
<item name="textColorTintedPrimary">@color/yellow_600</item>
<item name="textColorTintedPrimary">@color/yellow_700</item>
</style>
<style name="MainAppTheme.Green">
@ -248,9 +259,12 @@
<item name="toolbarContentColor">@color/green_1</item>
<item name="colorPrimaryText">@color/green_1</item>
<item name="colorTintedBackground">@color/green_700</item>
<item name="colorTintedBackgroundOffset">@color/green_600</item>
<item name="colorTintedBackgroundOffset">@color/green_50012</item>
<item name="textColorTintedSecondary">@color/green_sub_text</item>
<item name="textColorTintedPrimary">@color/green_100</item>
<item name="textColorTintedPrimary">@color/green_1</item>
<item name="tintedUiMain">@color/green_400</item>
<item name="tintedUiSub">@color/green_10</item>
<item name="tintedUiDetails">@color/green_1</item>
</style>
<style name="MainAppTheme.Green.Dark">
@ -260,10 +274,9 @@
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="toolbarContentColor">@color/green_1</item>
<item name="colorTintedBackground">@color/green_00</item>
<item name="colorTintedBackgroundOffset">@color/green_0</item>
<item name="colorPrimaryText">@color/green_600</item>
<item name="textColorTintedSecondary">@color/green_500</item>
<item name="textColorTintedPrimary">@color/green_600</item>
<item name="textColorTintedPrimary">@color/green_700</item>
</style>
<style name="MainAppTheme.Teal">
@ -285,9 +298,12 @@
<item name="toolbarContentColor">@color/teal_1</item>
<item name="colorPrimaryText">@color/teal_1</item>
<item name="colorTintedBackground">@color/teal_700</item>
<item name="colorTintedBackgroundOffset">@color/teal_600</item>
<item name="colorTintedBackgroundOffset">@color/teal_50012</item>
<item name="textColorTintedSecondary">@color/teal_sub_text</item>
<item name="textColorTintedPrimary">@color/teal_100</item>
<item name="textColorTintedPrimary">@color/teal_1</item>
<item name="tintedUiMain">@color/teal_400</item>
<item name="tintedUiSub">@color/teal_10</item>
<item name="tintedUiDetails">@color/teal_1</item>
</style>
<style name="MainAppTheme.Teal.Dark">
@ -297,10 +313,9 @@
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="toolbarContentColor">@color/teal_1</item>
<item name="colorTintedBackground">@color/teal_00</item>
<item name="colorTintedBackgroundOffset">@color/teal_0</item>
<item name="colorPrimaryText">@color/teal_600</item>
<item name="textColorTintedSecondary">@color/teal_500</item>
<item name="textColorTintedPrimary">@color/teal_600</item>
<item name="textColorTintedPrimary">@color/teal_700</item>
</style>
<style name="MainAppTheme.Blue">
@ -322,9 +337,12 @@
<item name="toolbarContentColor">@color/blue_1</item>
<item name="colorPrimaryText">@color/blue_1</item>
<item name="colorTintedBackground">@color/blue_700</item>
<item name="colorTintedBackgroundOffset">@color/blue_600</item>
<item name="colorTintedBackgroundOffset">@color/blue_50012</item>
<item name="textColorTintedSecondary">@color/blue_sub_text</item>
<item name="textColorTintedPrimary">@color/blue_100</item>
<item name="textColorTintedPrimary">@color/blue_1</item>
<item name="tintedUiMain">@color/blue_400</item>
<item name="tintedUiSub">@color/blue_10</item>
<item name="tintedUiDetails">@color/blue_1</item>
</style>
<style name="MainAppTheme.Blue.Dark">
@ -334,10 +352,9 @@
<item name="colorBoxStroke">@color/blue_10</item>
<item name="toolbarContentColor">@color/blue_1</item>
<item name="colorTintedBackground">@color/blue_00</item>
<item name="colorTintedBackgroundOffset">@color/blue_0</item>
<item name="colorPrimaryText">@color/blue_600</item>
<item name="textColorTintedSecondary">@color/blue_500</item>
<item name="textColorTintedPrimary">@color/blue_600</item>
<item name="textColorTintedPrimary">@color/blue_700</item>
</style>
<style name="MyWidgetTheme">
@ -457,7 +474,7 @@
<style name="DialogButton" parent="android:Widget.Button">
<item name="android:background">@android:color/transparent</item>
<item name="android:textColor">@color/brand_100</item>
<item name="android:textColor">?colorAccent</item>
</style>
<style name="DialogTheme" parent="Theme.AppCompat.DayNight.Dialog">

View file

@ -17,7 +17,7 @@ import io.github.kakaocup.kakao.text.KTextView
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import org.junit.runner.RunWith
@ -38,7 +38,7 @@ class PartyDetailFragmentTest : FragmentTestCase<PartyDetailFragment, FragmentPa
override fun makeFragment() {
val group = Group()
group.name = "Group Name"
every { socialRepository.getGroup(any()) } returns Flowable.just(group)
every { socialRepository.getGroup(any()) } returns flowOf(group)
viewModel = PartyViewModel(false)
viewModel.socialRepository = socialRepository
viewModel.userRepository = userRepository

View file

@ -10,10 +10,11 @@ import com.habitrpg.shared.habitica.models.tasks.Attribute
import io.github.kakaocup.kakao.common.views.KView
import io.github.kakaocup.kakao.screen.Screen
import io.github.kakaocup.kakao.text.KButton
import io.mockk.coVerify
import io.mockk.every
import io.mockk.spyk
import io.mockk.verify
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -63,9 +64,9 @@ class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding,
fun setUpUser() {
user.stats?.lvl = 20
user.stats?.points = 30
userState.onNext(user)
userState.value = user
every { inventoryRepository.getEquipment(listOf()) } returns Flowable.just(listOf())
every { inventoryRepository.getEquipment(listOf()) } returns flowOf(listOf())
}
@Test
@ -96,13 +97,13 @@ class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding,
fun allocatesOnClick() {
screen {
strengthAllocateButton.click()
verify { userRepository.allocatePoint(Attribute.STRENGTH) }
coVerify { userRepository.allocatePoint(Attribute.STRENGTH) }
intelligenceAllocateButton.click()
verify { userRepository.allocatePoint(Attribute.INTELLIGENCE) }
coVerify { userRepository.allocatePoint(Attribute.INTELLIGENCE) }
constitutionAllocateButton.click()
verify { userRepository.allocatePoint(Attribute.CONSTITUTION) }
coVerify { userRepository.allocatePoint(Attribute.CONSTITUTION) }
perceptionAllocateButton.click()
verify { userRepository.allocatePoint(Attribute.PERCEPTION) }
coVerify { userRepository.allocatePoint(Attribute.PERCEPTION) }
}
}
}

View file

@ -15,12 +15,14 @@ import io.github.kakaocup.kakao.recycler.KRecyclerView
import io.github.kakaocup.kakao.screen.Screen
import io.kotest.matchers.shouldBe
import io.mockk.CapturingSlot
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.spyk
import io.mockk.verify
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
class PetDetailScreen : Screen<PetDetailScreen>() {
@ -35,12 +37,12 @@ class PetDetailScreen : Screen<PetDetailScreen>() {
internal class PetDetailRecyclerFragmentTest :
FragmentTestCase<PetDetailRecyclerFragment, FragmentRecyclerviewBinding, PetDetailScreen>(false) {
override fun makeFragment() {
every { inventoryRepository.getOwnedPets() } returns Flowable.just(user.items?.pets!!)
every { inventoryRepository.getOwnedMounts() } returns Flowable.just(user.items?.mounts!!)
every { inventoryRepository.getOwnedItems("food") } returns Flowable.just(user.items?.food!!.filter { it.numberOwned > 0 })
every { inventoryRepository.getOwnedPets() } returns flowOf(user.items?.pets!!)
every { inventoryRepository.getOwnedMounts() } returns flowOf(user.items?.mounts!!)
every { inventoryRepository.getOwnedItems("food") } returns flowOf(user.items?.food!!.filter { it.numberOwned > 0 })
val saddle = OwnedItem()
saddle.numberOwned = 1
every { inventoryRepository.getOwnedItems(true) } returns Flowable.just(
every { inventoryRepository.getOwnedItems(true) } returns flowOf(
mapOf(
Pair(
"Saddle-food",
@ -69,21 +71,21 @@ internal class PetDetailRecyclerFragmentTest :
@Test
fun canFeedPet() {
val slot = CapturingSlot<FeedPetUseCase.RequestValues>()
every { feedPetUseCase.callInteractor(capture(slot)) } returns mockk(relaxed = true)
coEvery { feedPetUseCase.callInteractor(capture(slot)) } returns mockk(relaxed = true)
every {
inventoryRepository.getPets(
any(),
any(),
any()
)
} returns Flowable.just(content.pets.filter { it.animal == "Cactus" })
} returns flowOf(content.pets.filter { it.animal == "Cactus" })
every {
inventoryRepository.getMounts(
any(),
any(),
any()
)
} returns Flowable.just(content.mounts.filter { it.animal == "Cactus" })
} returns flowOf(content.mounts.filter { it.animal == "Cactus" })
launchFragment(
PetDetailRecyclerFragmentArgs.Builder("cactus", "drop", "").build().toBundle()
)
@ -92,7 +94,7 @@ internal class PetDetailRecyclerFragmentTest :
childWith<PetItem> { withContentDescription("Skeleton Cactus") }.click()
KView { withText(R.string.feed) }.click()
KView { withText("Meat") }.click()
verify { feedPetUseCase.callInteractor(any()) }
coVerify { feedPetUseCase.callInteractor(any()) }
slot.captured.pet.key shouldBe "Cactus-Skeleton"
slot.captured.food.key shouldBe "Meat"
}
@ -102,27 +104,27 @@ internal class PetDetailRecyclerFragmentTest :
@Test
fun canUseSaddle() {
val slot = CapturingSlot<FeedPetUseCase.RequestValues>()
every { feedPetUseCase.callInteractor(capture(slot)) } returns mockk(relaxed = true)
coEvery { feedPetUseCase.callInteractor(capture(slot)) } returns mockk(relaxed = true)
every {
inventoryRepository.getPets(
any(),
any(),
any()
)
} returns Flowable.just(content.pets.filter { it.animal == "Fox" })
} returns flowOf(content.pets.filter { it.animal == "Fox" })
every {
inventoryRepository.getMounts(
any(),
any(),
any()
)
} returns Flowable.just(content.mounts.filter { it.animal == "Fox" })
} returns flowOf(content.mounts.filter { it.animal == "Fox" })
launchFragment(PetDetailRecyclerFragmentArgs.Builder("fox", "drop", "").build().toBundle())
screen {
recycler {
childWith<PetItem> { withContentDescription("Shade Fox") }.click()
KView { withText(R.string.use_saddle) }.click()
verify { feedPetUseCase.callInteractor(any()) }
coVerify { feedPetUseCase.callInteractor(any()) }
slot.captured.pet.key shouldBe "Fox-Shade"
slot.captured.food.key shouldBe "Saddle"
}

View file

@ -15,7 +15,7 @@ import io.github.kakaocup.kakao.text.KTextView
import io.mockk.every
import io.mockk.spyk
import io.mockk.verify
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.flowOf
import org.hamcrest.Matcher
import org.junit.Test
@ -40,8 +40,8 @@ class StableScreen : Screen<StableScreen>() {
internal class StableRecyclerFragmentTest : FragmentTestCase<StableRecyclerFragment, FragmentRecyclerviewBinding, StableScreen>(false) {
override fun makeFragment() {
every { inventoryRepository.getOwnedPets() } returns Flowable.just(user.items?.pets!!)
every { inventoryRepository.getOwnedMounts() } returns Flowable.just(user.items?.mounts!!)
every { inventoryRepository.getOwnedPets() } returns flowOf(user.items?.pets!!)
every { inventoryRepository.getOwnedMounts() } returns flowOf(user.items?.mounts!!)
fragment = spyk()
fragment.shouldInitializeComponent = false
fragment.itemType = "pets"

View file

@ -138,6 +138,8 @@ interface ApiService {
@POST("tasks/user")
suspend fun createTask(@Body item: Task): HabitResponse<Task>
@POST("tasks/group/{groupId}")
suspend fun createGroupTask(@Path("groupId") groupId: String, @Body item: Task): HabitResponse<Task>
@POST("tasks/user")
suspend fun createTasks(@Body tasks: List<Task>): HabitResponse<List<Task>>
@ -198,7 +200,7 @@ interface ApiService {
suspend fun disableClasses(): HabitResponse<User>
@POST("user/mark-pms-read")
suspend fun markPrivateMessagesRead(): Void
suspend fun markPrivateMessagesRead(): Void?
/* Group API */
@ -457,4 +459,7 @@ interface ApiService {
@GET("hall/heroes/{memberID}")
suspend fun getHallMember(@Path("memberID") memberID: String): HabitResponse<Member>
@POST("tasks/{taskID}/needs-work/{userID}")
suspend fun markTaskNeedsWork(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse<Task>
}

View file

@ -97,6 +97,7 @@ interface ApiClient {
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
suspend fun createTask(item: Task): Task?
suspend fun createGroupTask(groupId: String, item: Task): Task?
suspend fun createTasks(tasks: List<Task>): List<Task>?
@ -130,7 +131,7 @@ interface ApiClient {
suspend fun disableClasses(): User?
suspend fun markPrivateMessagesRead(): Void?
suspend fun markPrivateMessagesRead()
/* Group API */
@ -274,4 +275,5 @@ interface ApiClient {
suspend fun unassignFromTask(taskId: String, userID: String): Task?
suspend fun updateMember(memberID: String, updateData: Map<String, Any?>): Member?
suspend fun getHallMember(userId: String): Member?
suspend fun markTaskNeedsWork(taskID: String, userID: String): Task?
}

View file

@ -7,6 +7,6 @@ import kotlinx.coroutines.flow.Flow
interface ContentRepository: BaseRepository {
suspend fun retrieveContent(forced: Boolean = false): ContentResult?
suspend fun retrieveWorldState(): WorldState?
suspend fun retrieveWorldState(forced: Boolean = false): WorldState?
fun getWorldState(): Flow<WorldState>
}

View file

@ -18,7 +18,7 @@ interface SocialRepository : BaseRepository {
fun getUserGroups(type: String?): Flow<List<Group>>
suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>?
fun getGroupChat(groupId: String): Flow<out List<ChatMessage>>
fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
suspend fun markMessagesSeen(seenGroupId: String)
@ -92,7 +92,7 @@ interface SocialRepository : BaseRepository {
id: String? = null
): List<FindUsernameResult>?
suspend fun markPrivateMessagesRead(user: User?): Void?
suspend fun markPrivateMessagesRead(user: User?)
fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>)

View file

@ -65,6 +65,8 @@ interface TaskRepository : BaseRepository {
suspend fun retrieveCompletedTodos(userId: String? = null): TaskList?
suspend fun syncErroredTasks(): List<Task>?
suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void?
fun getTasksForChallenge(challengeID: String?): Flow<out List<Task>>
fun getTasksForChallenge(challengeID: String?): Flow<List<Task>>
suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData?
suspend fun markTaskNeedsWork(task: Task, userID: String)
}

View file

@ -447,6 +447,10 @@ class ApiClientImpl(
return process { apiService.createTask(item) }
}
override suspend fun createGroupTask(groupId: String, item: Task): Task? {
return process { apiService.createGroupTask(groupId, item) }
}
override suspend fun createTasks(tasks: List<Task>): List<Task>? {
return process { apiService.createTasks(tasks) }
}
@ -495,8 +499,8 @@ class ApiClientImpl(
override suspend fun disableClasses(): User? = process { apiService.disableClasses() }
override suspend fun markPrivateMessagesRead(): Void {
return apiService.markPrivateMessagesRead()
override suspend fun markPrivateMessagesRead() {
apiService.markPrivateMessagesRead()
}
override suspend fun listGroups(type: String): List<Group>? {
@ -602,14 +606,22 @@ class ApiClientImpl(
return process { apiService.leaveQuest(groupId) }
}
private val lastPurchaseValidation: Date? = null
override suspend fun validatePurchase(request: PurchaseValidationRequest): PurchaseValidationResult? {
return process { apiService.validatePurchase(request) }
// make sure a purchase attempt doesn't happen
return if (lastPurchaseValidation == null || Date().time - lastPurchaseValidation.time > 5000) {
return process { apiService.validatePurchase(request) }
} else null
}
override suspend fun changeCustomDayStart(updateObject: Map<String, Any>): User? {
return process { apiService.changeCustomDayStart(updateObject) }
}
override suspend fun markTaskNeedsWork(taskID: String, userID: String): Task? {
return process { apiService.markTaskNeedsWork(taskID, userID) }
}
override suspend fun getMember(memberId: String) = processResponse(apiService.getMember(memberId))
override suspend fun getMemberWithUsername(username: String) = processResponse(apiService.getMemberWithUsername(username))

View file

@ -36,9 +36,9 @@ class ContentRepositoryImpl<T : ContentLocalRepository>(
return null
}
override suspend fun retrieveWorldState(): WorldState? {
override suspend fun retrieveWorldState(forced: Boolean): WorldState? {
val now = Date().time
if (now - this.lastWorldStateSync > 3600000) {
if (forced || now - this.lastWorldStateSync > 3600000) {
val state = apiClient.getWorldState() ?: return null
lastWorldStateSync = now
localRepository.save(state)

View file

@ -89,11 +89,10 @@ class SocialRepositoryImpl(
return null
}
val liked = chatMessage.userLikesMessage(userID)
if (chatMessage.isManaged) {
localRepository.likeMessage(chatMessage, userID, !liked)
}
localRepository.likeMessage(chatMessage, userID, !liked)
val message = apiClient.likeMessage(chatMessage.groupId ?: "", chatMessage.id)
message?.groupId = chatMessage.groupId
message?.let { localRepository.save(it) }
return null
}
@ -275,7 +274,7 @@ class SocialRepositoryImpl(
return apiClient.findUsernames(username, context, id)
}
override suspend fun markPrivateMessagesRead(user: User?): Void? {
override suspend fun markPrivateMessagesRead(user: User?) {
if (user?.isManaged == true) {
localRepository.modify(user) {
it.inbox?.hasUserSeenInbox = true

View file

@ -147,6 +147,16 @@ class TaskRepositoryImpl(
bgTask.counterDown = (bgTask.counterDown ?: 0) + 1
}
}
if (bgTask.isGroupTask) {
val entry = bgTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == user.id }
entry?.completed = up
if (up) {
entry?.completedDate = Date()
} else {
entry?.completedDate = null
}
}
}
res._tmp?.drop?.key?.let { key ->
val type = when (res._tmp?.drop?.type?.lowercase(Locale.US)) {
@ -183,6 +193,19 @@ class TaskRepositoryImpl(
}
}
override suspend fun markTaskNeedsWork(task: Task, userID: String) {
val savedTask = apiClient.markTaskNeedsWork(task.id ?: "", userID)
if (savedTask != null) {
savedTask.id = task.id
savedTask.position = task.position
savedTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == userID }?.let {
it.completed = false
it.completedDate = null
}
localRepository.save(savedTask)
}
}
override suspend fun taskChecked(
user: User?,
taskId: String,
@ -223,7 +246,11 @@ class TaskRepositoryImpl(
}
localRepository.save(task)
val savedTask = apiClient.createTask(task)
val savedTask = if (task.isGroupTask) {
apiClient.createGroupTask(task.group?.groupID ?: "", task)
} else {
apiClient.createTask(task)
}
savedTask?.dateCreated = Date()
if (savedTask != null) {
savedTask.tags = task.tags
@ -315,7 +342,7 @@ class TaskRepositoryImpl(
val savedTask = apiClient.assignToTask(taskID, assignments) ?: return@let
savedTask.id = task.id
savedTask.position = task.position
localRepository.save(task)
localRepository.save(savedTask)
}
assignChanges["unassign"]?.let { unassignments ->
@ -326,7 +353,7 @@ class TaskRepositoryImpl(
if (savedTask != null) {
savedTask.id = task.id
savedTask.position = task.position
localRepository.save(task)
localRepository.save(savedTask)
}
}
}

View file

@ -20,9 +20,11 @@ import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import com.habitrpg.shared.habitica.models.tasks.Attribute
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.util.Date
import java.util.GregorianCalendar
import java.util.concurrent.TimeUnit
@ -36,7 +38,11 @@ class UserRepositoryImpl(
private val analyticsManager: AnalyticsManager
) : BaseRepositoryImpl<UserLocalRepository>(localRepository, apiClient, userID), UserRepository {
private var lastSync: Date? = null
companion object {
private var lastReadNotification: String? = null
private var lastSync: Date? = null
}
override fun getUser(): Flow<User?> = getUser(userID)
override fun getUser(userID: String): Flow<User?> = localRepository.getUser(userID)
@ -62,10 +68,12 @@ class UserRepositoryImpl(
@Suppress("ReturnCount")
override suspend fun retrieveUser(withTasks: Boolean, forced: Boolean, overrideExisting: Boolean): User? {
// Only retrieve again after 3 minutes or it's forced.
if (forced || this.lastSync == null || Date().time - (this.lastSync?.time ?: 0) > 180000) {
if (forced || lastSync == null || Date().time - (lastSync?.time ?: 0) > 180000) {
val user = apiClient.retrieveUser(withTasks) ?: return null
lastSync = Date()
localRepository.saveUser(user)
withContext(Dispatchers.Main) {
localRepository.saveUser(user)
}
if (withTasks) {
val id = user.id
val tasksOrder = user.tasksOrder
@ -104,7 +112,7 @@ class UserRepositoryImpl(
override suspend fun sleep(user: User): User {
val newValue = !(user.preferences?.sleep ?: false)
localRepository.modify(user) { it.preferences?.sleep = newValue }
if (apiClient.sleep() != true) {
if (apiClient.sleep() == null) {
localRepository.modify(user) { it.preferences?.sleep = !newValue }
}
return user
@ -150,11 +158,12 @@ class UserRepositoryImpl(
override suspend fun unlockPath(path: String, price: Int): UnlockResponse? {
val unlockResponse = apiClient.unlockPath(path) ?: return null
val user = localRepository.getUser(userID).firstOrNull() ?: return unlockResponse
user.preferences = unlockResponse.preferences
user.purchased = unlockResponse.purchased
user.items = unlockResponse.items
user.balance = user.balance - (price / 4.0)
localRepository.saveUser(user, false)
localRepository.modify(user) { liveUser ->
liveUser.preferences = unlockResponse.preferences
liveUser.purchased = unlockResponse.purchased
liveUser.items = unlockResponse.items
liveUser.balance = liveUser.balance - (price / 4.0)
}
return unlockResponse
}
@ -162,7 +171,11 @@ class UserRepositoryImpl(
runCron(ArrayList())
}
override suspend fun readNotification(id: String) = apiClient.readNotification(id)
override suspend fun readNotification(id: String): List<Any>? {
if (lastReadNotification == id) return null
lastReadNotification = id
return apiClient.readNotification(id)
}
override fun getUserQuestStatus(): Flow<UserQuestStatus> {
return localRepository.getUserQuestStatus(userID)
}

View file

@ -216,7 +216,7 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
liveMessage?.likeCount = liveMessage?.likes?.size ?: 0
}
} else {
liveMessage?.likes?.filter { userId == it.id }?.forEach { like ->
liveMessage?.likes?.filter { userId == it.id && it.isManaged }?.forEach { like ->
executeTransaction {
like.deleteFromRealm()
}

View file

@ -14,6 +14,7 @@ import com.habitrpg.android.habitica.models.promotions.HabiticaWebPromotion
import com.habitrpg.android.habitica.models.promotions.getHabiticaPromotionFromKey
import com.habitrpg.common.habitica.helpers.AppTestingLevel
import kotlinx.coroutines.MainScope
import java.util.Date
class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.common.habitica.helpers.AppConfigManager() {
@ -169,6 +170,6 @@ class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.comm
fun getBirthdayEvent(): WorldStateEvent? {
val events = ((worldState?.events as? List<WorldStateEvent>) ?: listOf(worldState?.currentEvent))
return events.firstOrNull { it?.eventKey == "birthday10" }
return events.firstOrNull { it?.eventKey == "birthday10" && it.end?.after(Date()) == true }
}
}

View file

@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.helpers
import android.util.Log
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -40,7 +41,8 @@ class ExceptionHandler {
!HttpException::class.java.isAssignableFrom(throwable.javaClass) &&
!retrofit2.HttpException::class.java.isAssignableFrom(throwable.javaClass) &&
!EOFException::class.java.isAssignableFrom(throwable.javaClass) &&
throwable !is ConnectionShutdownException
throwable !is ConnectionShutdownException &&
throwable !is CancellationException
) {
instance.analyticsManager?.logException(throwable)
}

View file

@ -54,7 +54,14 @@ object MainNavigationController {
fun navigate(uriString: String) {
val uri = Uri.parse(uriString)
navigate(uri)
var builder = uri.buildUpon()
if (uri.scheme == null) {
builder = builder.scheme("https")
}
if (uri.host == null) {
builder = builder.authority("habitica.com")
}
navigate(builder.build())
}
fun navigate(uri: Uri) {

View file

@ -5,7 +5,7 @@ import androidx.core.os.bundleOf
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class NotificationOpenHandler {
@ -13,7 +13,7 @@ class NotificationOpenHandler {
companion object {
fun handleOpenedByNotification(identifier: String, intent: Intent) {
GlobalScope.launch(context = Dispatchers.Main) {
MainScope().launch(context = Dispatchers.Main) {
when (identifier) {
PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY -> openPartyScreen()
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY -> openPartyScreen()
@ -27,6 +27,7 @@ class NotificationOpenHandler {
PushNotificationManager.G1G1_PROMO_KEY -> openGiftOneGetOneInfoScreen()
else -> {
intent.getStringExtra("openURL")?.let {
MainNavigationController.navigate(it)
}
}

View file

@ -211,7 +211,9 @@ class PurchaseHandler(
}
val flowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(skuDetails).map {
BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(skuDetails)
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(skuDetails)
.setOfferToken(skuDetails.subscriptionOfferDetails?.first()?.offerToken ?: "")
.build()
})
.build()
@ -245,7 +247,7 @@ class PurchaseHandler(
apiClient.validatePurchase(validationRequest)
processedPurchase(purchase)
val gift = removeGift(sku)
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
withContext(Dispatchers.IO) {
consume(purchase)
}
displayGryphatriceConfirmationDialog(purchase, gift?.third)
@ -261,7 +263,7 @@ class PurchaseHandler(
apiClient.validatePurchase(validationRequest)
processedPurchase(purchase)
val gift = removeGift(sku)
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
withContext(Dispatchers.IO) {
consume(purchase)
}
displayConfirmationDialog(purchase, gift?.third)
@ -277,7 +279,7 @@ class PurchaseHandler(
apiClient.validateNoRenewSubscription(validationRequest)
processedPurchase(purchase)
val gift = removeGift(sku)
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
withContext(Dispatchers.IO) {
consume(purchase)
}
displayConfirmationDialog(purchase, gift?.third)
@ -424,11 +426,17 @@ class PurchaseHandler(
}
}
private val displayedConfirmations = mutableListOf<String>()
private fun displayConfirmationDialog(purchase: Purchase, giftedTo: String? = null) {
CoroutineScope(Dispatchers.Main).launch(ExceptionHandler.coroutine()) {
if (displayedConfirmations.contains(purchase.orderId)) {
return
}
displayedConfirmations.add(purchase.orderId)
CoroutineScope(Dispatchers.Main).launchCatching {
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launch
val sku = purchase.products.firstOrNull() ?: return@launch
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launchCatching
val sku = purchase.products.firstOrNull() ?: return@launchCatching
var title = context.getString(R.string.successful_purchase_generic)
val message = when {
PurchaseTypes.allSubscriptionNoRenewTypes.contains(sku) -> {
@ -478,7 +486,7 @@ class PurchaseHandler(
}
private fun displayGryphatriceConfirmationDialog(purchase: Purchase, giftedTo: String? = null) {
CoroutineScope(Dispatchers.Main).launch(ExceptionHandler.coroutine()) {
MainScope().launch(ExceptionHandler.coroutine()) {
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launch
val title = context.getString(R.string.successful_purchase_generic)

View file

@ -1,7 +1,7 @@
package com.habitrpg.android.habitica.helpers
object PurchaseTypes {
const val JubilantGrphatrice = "com.habitrpg.android.habitica.iap.gryphatrice"
const val JubilantGrphatrice = "com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant"
const val Purchase4Gems = "com.habitrpg.android.habitica.iap.4gems"
const val Purchase21Gems = "com.habitrpg.android.habitica.iap.21gems"
const val Purchase42Gems = "com.habitrpg.android.habitica.iap.42gems"

View file

@ -11,6 +11,7 @@ import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
import kotlinx.coroutines.MainScope
import java.io.IOException
class PushNotificationManager(
var apiClient: ApiClient,
@ -52,8 +53,12 @@ class PushNotificationManager(
addRefreshToken()
} else {
FirebaseMessaging.getInstance().token.addOnCompleteListener {
refreshedToken = it.result
addRefreshToken()
try {
refreshedToken = it.result
addRefreshToken()
} catch (_: IOException) {
// This can happen during google test runs
}
}
}
}

View file

@ -104,6 +104,7 @@ class ShowNotificationInteractor(
}
lifecycleScope.launch(context = Dispatchers.Main) {
if (activity.isFinishing) return@launch
val alert = HabiticaAlertDialog(activity)
alert.setAdditionalContentView(view)
alert.setTitle(title)

View file

@ -9,4 +9,18 @@ data class CustomizationFilter(
get() {
return onlyPurchased || months.isNotEmpty()
}
override fun equals(other: Any?): Boolean {
if (other is CustomizationFilter) {
return onlyPurchased == other.onlyPurchased && ascending == other.ascending && months.size == other.months.size && months.containsAll(other.months)
}
return super.equals(other)
}
override fun hashCode(): Int {
var result = onlyPurchased.hashCode()
result = 31 * result + ascending.hashCode()
result = 31 * result + months.hashCode()
return result
}
}

View file

@ -70,6 +70,7 @@ open class ShopItem : RealmObject(), BaseObject {
fun canAfford(user: User?, quantity: Int): Boolean = when (currency) {
"gold" -> (value * quantity) <= (user?.stats?.gp ?: 0.0)
"gems" -> (value * quantity) <= (user?.gemCount ?: 0)
else -> true
}

View file

@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.models.social
import com.google.gson.annotations.SerializedName
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.user.SubscriptionPlan
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import io.realm.RealmList
import io.realm.RealmObject
@ -11,6 +12,10 @@ import io.realm.annotations.PrimaryKey
open class Group : RealmObject(), BaseMainObject {
val isGroupPlan: Boolean
get() {
return purchased?.isActive == true
}
override val realmClass: Class<Group>
get() = Group::class.java
override val primaryIdentifier: String?
@ -38,6 +43,7 @@ open class Group : RealmObject(), BaseMainObject {
var leaderOnlyChallenges: Boolean = false
var leaderOnlyGetGems: Boolean = false
var categories: RealmList<GroupCategory>? = null
var purchased: SubscriptionPlan? = null
@Ignore
var tasksOrder: TasksOrder? = null

View file

@ -423,7 +423,7 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
else -> false
}
} else if (type == TaskType.TODO) {
return dueDate != task.dueDate
return (dueDate != task.dueDate && task.dueDate != null)
} else if (type == TaskType.REWARD) {
return value != task.value
} else {

View file

@ -18,6 +18,7 @@ open class SubscriptionPlan : RealmObject(), BaseObject {
@JvmField
var planId: String? = null
var active: Boolean? = null
var gemsBought: Int? = null
var extraMonths: Int? = null
var quantity: Int? = null
@ -34,7 +35,7 @@ open class SubscriptionPlan : RealmObject(), BaseObject {
val isActive: Boolean
get() {
val today = Date()
return customerId != null && (dateTerminated == null || dateTerminated!!.after(today))
return customerId != null && (dateTerminated == null || dateTerminated!!.after(today) || active == true)
}
val totalNumberOfGems: Int

View file

@ -1,8 +1,8 @@
package com.habitrpg.android.habitica.ui.activities
import android.app.Activity
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import android.text.format.DateFormat
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -20,10 +20,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
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
import androidx.compose.material.Text
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -31,6 +38,7 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -50,9 +58,11 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope
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
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.PurchaseHandler
@ -60,23 +70,37 @@ import com.habitrpg.android.habitica.helpers.launchCatching
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.PixelArtView
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog
import com.habitrpg.common.habitica.extensions.DataBindingUtils
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import javax.inject.Inject
class BirthdayActivity : BaseActivity() {
@Inject
lateinit var userViewModel: MainUserViewModel
@Inject
lateinit var purchaseHandler: PurchaseHandler
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var configManager: AppConfigManager
val scaffoldState: ScaffoldState =
ScaffoldState(DrawerState(DrawerValue.Closed), SnackbarHostState())
private val isPurchasing = mutableStateOf(false)
private val price = mutableStateOf("")
private val hasGryphatrice = mutableStateOf(false)
private val hasEquipped = mutableStateOf(false)
private var gryphatriceProductDetails: ProductDetails? = null
override fun getLayoutResId(): Int? = null
@ -86,20 +110,56 @@ class BirthdayActivity : BaseActivity() {
val event = configManager.getBirthdayEvent()
setContent {
HabiticaTheme {
val user = userViewModel.user.observeAsState()
BirthdayActivityView(hasGryphatrice.value, price.value, event?.start ?: Date(), event?.end ?: Date(), {
gryphatriceProductDetails?.let {
purchaseHandler.purchase(this, it)
BirthdayActivityView(
scaffoldState,
isPurchasing.value,
hasGryphatrice.value,
hasEquipped.value,
price.value,
event?.start ?: Date(),
event?.end ?: Date(),
{
gryphatriceProductDetails?.let {
isPurchasing.value = true
purchaseHandler.purchase(this, it)
}
},
{
lifecycleScope.launchCatching({
isPurchasing.value = false
}) {
if ((userViewModel.user.value?.gemCount ?: 0) < 60) {
val dialog = InsufficientGemsDialog(this@BirthdayActivity, 3)
dialog.show()
return@launchCatching
}
isPurchasing.value = true
val dialog = HabiticaAlertDialog(this@BirthdayActivity)
dialog.setTitle(
getString(
R.string.purchase_gryphatrice_confirmation,
60
)
)
dialog.addButton(
getString(R.string.buy_for_x, "60 Gems"),
true
) { _, _ ->
lifecycleScope.launchCatching {
purchaseWithGems()
}
}
dialog.addCloseButton { _, _ ->
isPurchasing.value = false
}
dialog.show()
}
}
}, {
) {
lifecycleScope.launchCatching {
inventoryRepository.purchaseItem("", "Gryphatrice-Jubilant", 1)
inventoryRepository.equip("pet", "Gryphatrice-Jubilant")
}
}, {
lifecycleScope.launchCatching {
inventoryRepository.equip("pets", "Gryphatrice-Jubilant")
}
})
}
}
}
@ -112,6 +172,22 @@ class BirthdayActivity : BaseActivity() {
hasGryphatrice.value = (it?.trained ?: 0) >= 5
}
}
userViewModel.user.observe(this) {
hasEquipped.value = it?.items?.currentPet == "Gryphatrice-Jubilant"
}
lifecycleScope.launchCatching {
gryphatriceProductDetails = purchaseHandler.getGryphatriceSKU()
price.value =
gryphatriceProductDetails?.oneTimePurchaseOfferDetails?.formattedPrice ?: ""
}
}
private suspend fun purchaseWithGems() {
inventoryRepository.purchaseItem("pets", "Gryphatrice-Jubilant", 1)
userRepository.retrieveUser(false, true)
isPurchasing.value = false
}
override fun injectActivity(component: UserComponent?) {
@ -150,207 +226,303 @@ fun BirthdayTitle(text: String) {
}
}
@Composable
fun BirthdayActivityView(hasGryphatrice: Boolean, price: String, startDate: Date, endDate: Date, onPurchaseClick: () -> Unit, onGemPurchaseClick: () -> Unit, onEquipClick: () -> Unit) {
fun BirthdayActivityView(
scaffoldState: ScaffoldState,
isPurchasing: Boolean,
hasGryphatrice: Boolean,
hasEquipped: Boolean,
price: String,
startDate: Date,
endDate: Date,
onPurchaseClick: () -> Unit,
onGemPurchaseClick: () -> Unit,
onEquipClick: () -> Unit
) {
val activity = LocalContext.current as? Activity
val dateFormat = DateFormat.getDateFormat(activity)
val dateFormat = SimpleDateFormat("MMM dd", java.util.Locale.getDefault())
val complexDateFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL)
val textColor = Color.White
val specialTextColor = colorResource(R.color.yellow_50)
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.background(
Brush.verticalGradient(
Pair(0.0f, colorResource(id = R.color.brand_300)),
Pair(1.0f, colorResource(id = R.color.brand_200))
)
)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Button(
onClick = {
if (activity != null) {
activity.finish()
return@Button
}
MainNavigationController.navigateBack()
},
colors = ButtonDefaults.textButtonColors(contentColor = textColor),
elevation = ButtonDefaults.elevation(0.dp, 0.dp),
modifier = Modifier.align(Alignment.Start)
) {
Image(
painterResource(R.drawable.arrow_back),
stringResource(R.string.action_back),
colorFilter = ColorFilter.tint(
textColor
)
)
}
val systemUiController = rememberSystemUiController()
val statusbarColor = colorResource(R.color.brand_300)
val navigationbarColor = colorResource(R.color.brand_50)
DisposableEffect(systemUiController) {
systemUiController.setStatusBarColor(statusbarColor, darkIcons = false)
systemUiController.setNavigationBarColor(navigationbarColor)
onDispose {}
}
Scaffold(
scaffoldState = scaffoldState
) { padding ->
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth()) {
Image(painterResource(R.drawable.birthday_header), null, Modifier.padding(bottom = 8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Image(painterResource(R.drawable.birthday_gifts), null)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 22.dp)
) {
Text(
stringResource(id = R.string.limited_event).toUpperCase(Locale.current),
fontSize = 12.sp,
color = specialTextColor,
fontWeight = FontWeight.Bold
.background(
Brush.verticalGradient(
Pair(0.0f, colorResource(id = R.color.brand_300)),
Pair(1.0f, colorResource(id = R.color.brand_200))
)
)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(padding)
) {
Button(
onClick = {
if (activity != null) {
activity.finish()
return@Button
}
MainNavigationController.navigateBack()
},
colors = ButtonDefaults.textButtonColors(contentColor = textColor),
elevation = ButtonDefaults.elevation(0.dp, 0.dp),
modifier = Modifier.align(Alignment.Start)
) {
Image(
painterResource(R.drawable.arrow_back),
stringResource(R.string.action_back),
colorFilter = ColorFilter.tint(
textColor
)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth()
) {
Image(
painterResource(R.drawable.birthday_header),
null,
Modifier.padding(bottom = 8.dp)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Image(painterResource(R.drawable.birthday_gifts), null)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 22.dp)
) {
Text(
stringResource(id = R.string.limited_event).toUpperCase(Locale.current),
fontSize = 12.sp,
color = specialTextColor,
fontWeight = FontWeight.Bold
)
Text(
stringResource(
R.string.x_to_y,
dateFormat.format(startDate),
dateFormat.format(endDate)
),
fontSize = 12.sp,
color = textColor,
fontWeight = FontWeight.Bold
)
}
// right image should be flipped
Image(
painterResource(id = R.drawable.birthday_gifts),
null,
modifier = Modifier.scale(-1f, 1f)
)
}
Text(
stringResource(R.string.birthday_title_description),
fontSize = 16.sp,
color = specialTextColor,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 22.dp)
)
BirthdayTitle(stringResource(id = R.string.animated_gryphatrice_pet))
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(vertical = 20.dp)
.size(161.dp, 129.dp)
.background(colorResource(R.color.brand_50), RoundedCornerShape(8.dp))
) {
PixelArtView(
imageName = "stable_Pet-Gryphatrice-Jubilant",
Modifier.size(104.dp)
)
}
Text(
stringResource(R.string.limited_edition).toUpperCase(Locale.current),
fontSize = 12.sp,
color = specialTextColor,
fontWeight = FontWeight.Bold
)
Text(
stringResource(R.string.gryphatrice_description),
fontSize = 16.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp,
modifier = Modifier.padding(bottom = 16.dp)
)
if (hasGryphatrice) {
Text(
stringResource(R.string.x_to_y, dateFormat.format(startDate), dateFormat.format(endDate)),
stringResource(R.string.thanks_for_support),
fontSize = 12.sp,
color = textColor,
fontWeight = FontWeight.Bold
fontWeight = FontWeight.SemiBold
)
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{
onEquipClick()
},
modifier = Modifier.padding(top = 20.dp)
) {
Text(
stringResource(if (hasEquipped) R.string.unequip else R.string.equip),
fontSize = 18.sp
)
}
} else if (isPurchasing) {
CircularProgressIndicator()
} else {
Text(buildAnnotatedString {
append("Buy for ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(price)
}
append(" or ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append("60 Gems")
}
}, color = Color.White)
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{
onPurchaseClick()
},
modifier = Modifier.padding(top = 20.dp)
) {
Text(stringResource(R.string.buy_for_x, price), fontSize = 18.sp)
}
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{
onGemPurchaseClick()
},
modifier = Modifier.padding(top = 20.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(stringResource(R.string.buy_for), fontSize = 18.sp)
CurrencyText(currency = "gems", value = 60, fontSize = 18.sp)
}
}
}
// right image should be flipped
Image(
painterResource(id = R.drawable.birthday_gifts),
null,
modifier = Modifier.scale(-1f, 1f)
)
}
Text(
stringResource(R.string.birthday_title_description),
fontSize = 16.sp,
color = specialTextColor,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 22.dp)
)
BirthdayTitle(stringResource(id = R.string.animated_gryphatrice_pet))
Box(
Modifier
.size(161.dp, 129.dp)
.padding(vertical = 20.dp)
.background(colorResource(R.color.brand_50), RoundedCornerShape(8.dp))
) {
}
Text(
stringResource(R.string.limited_edition).toUpperCase(Locale.current),
fontSize = 12.sp,
color = specialTextColor,
fontWeight = FontWeight.Bold
)
Text(
stringResource(R.string.gryphatrice_description),
fontSize = 16.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp,
modifier = Modifier.padding(bottom = 16.dp)
)
if (hasGryphatrice) {
BirthdayTitle(stringResource(id = R.string.plenty_of_potions))
Text(
stringResource(R.string.thanks_for_support),
fontSize = 12.sp,
stringResource(R.string.plenty_of_potions_description),
fontSize = 16.sp,
color = textColor,
fontWeight = FontWeight.SemiBold
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{},
modifier = Modifier.padding(top = 20.dp)
) {
Text(stringResource(R.string.equip))
}
} else {
Text(buildAnnotatedString {
append("Buy for ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(price)
}
append(" or ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append("60 Gems")
}
}, color = Color.White)
PotionGrid()
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{
onPurchaseClick()
MainScope().launchCatching {
activity?.finish()
delay(500)
MainNavigationController.navigate(R.id.marketFragment)
}
},
modifier = Modifier.padding(top = 20.dp)
) {
Text(stringResource(R.string.buy_for_x, ""))
Text(stringResource(R.string.visit_the_market), fontSize = 18.sp)
}
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{
onGemPurchaseClick()
},
modifier = Modifier.padding(top = 20.dp)
BirthdayTitle(stringResource(id = R.string.for_for_free))
Text(
stringResource(R.string.for_for_free_description),
fontSize = 16.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
Column(
verticalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.padding(vertical = 20.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.buy_for))
CurrencyText(currency = "gems", value = 60)
Row(
horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.fillMaxWidth()
) {
FourFreeItem(
day = 1,
title = stringResource(R.string.a_party_robe),
imageName = "birthday10_robes",
modifier = Modifier.weight(1f)
)
FourFreeItem(
day = 1,
title = stringResource(R.string.twenty_gems),
image = painterResource(R.drawable.birthday_gems),
modifier = Modifier.weight(1f)
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.fillMaxWidth()
) {
FourFreeItem(
day = 5,
title = stringResource(R.string.birthday_set),
imageName = "birthday10_hero",
modifier = Modifier.weight(1f)
)
FourFreeItem(
day = 10,
title = stringResource(R.string.background),
imageName = "birthday10_background",
modifier = Modifier.weight(1f)
)
}
}
}
BirthdayTitle(stringResource(id = R.string.plenty_of_potions))
Text(
stringResource(R.string.plenty_of_potions_description),
fontSize = 16.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
PotionGrid()
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{
onEquipClick()
},
modifier = Modifier.padding(top = 20.dp)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(top = 20.dp)
.background(colorResource(R.color.brand_50))
.padding(horizontal = 20.dp)
.padding(top = 20.dp, bottom = 60.dp)
) {
Text(stringResource(R.string.visit_the_market))
Text(
stringResource(R.string.limitations),
fontSize = 16.sp,
color = colorResource(R.color.brand_600),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
Text(
stringResource(
R.string.birthday_limitations,
complexDateFormat.format(startDate),
complexDateFormat.format(endDate)
),
fontSize = 14.sp,
color = colorResource(R.color.brand_600),
lineHeight = 20.sp,
textAlign = TextAlign.Center
)
}
BirthdayTitle(stringResource(id = R.string.for_for_free))
Text(
stringResource(R.string.for_for_free_description),
fontSize = 16.sp,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(top = 20.dp)
.background(colorResource(R.color.brand_50))
.padding(horizontal = 20.dp)
.padding(top = 20.dp, bottom = 60.dp)
) {
Text(
stringResource(R.string.limitations),
fontSize = 16.sp,
color = colorResource(R.color.brand_600),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
Text(
stringResource(R.string.birthday_limitations),
fontSize = 14.sp,
color = colorResource(R.color.brand_600),
lineHeight = 20.sp,
textAlign = TextAlign.Center
)
}
}
}
@ -369,15 +541,24 @@ fun PotionGrid() {
"Peppermint",
"Shimmer"
).windowed(4, 4, true)
Column(verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(top = 20.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 20.dp)
) {
for (potionGroup in potions) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
for (potion in potionGroup) {
Box(
Modifier
.size(68.dp)
.background(colorResource(R.color.brand_50), RoundedCornerShape(8.dp))) {
AsyncImage(model = DataBindingUtils.BASE_IMAGE_URL + DataBindingUtils.getFullFilename("Pet_HatchingPotion_$potion"), null, Modifier.size(68.dp))
.background(colorResource(R.color.brand_50), RoundedCornerShape(8.dp))
) {
AsyncImage(
model = DataBindingUtils.BASE_IMAGE_URL + DataBindingUtils.getFullFilename(
"Pet_HatchingPotion_$potion"
), null, Modifier.size(68.dp)
)
}
}
}
@ -385,6 +566,48 @@ fun PotionGrid() {
}
}
@Composable
fun FourFreeItem(
day: Int,
title: String,
modifier: Modifier = Modifier,
imageName: String? = null,
image: Painter? = null
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(18.dp),
modifier = modifier
.background(colorResource(R.color.brand_50), HabiticaTheme.shapes.medium)
.padding(16.dp)
) {
Text(
stringResource(R.string.day_x, day).uppercase(),
color = colorResource(R.color.yellow_50),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(121.dp, 84.dp)
.background(colorResource(R.color.brand_100), HabiticaTheme.shapes.medium)
) {
if (image != null) {
Image(image, null)
} else {
PixelArtView(
imageName,
Modifier
.size(84.dp)
)
}
}
Text(title, color = Color.White, fontSize = 16.sp)
}
}
@Composable
fun HabiticaButton(
background: Color,
@ -411,9 +634,10 @@ fun HabiticaButton(
}
@Preview(device = Devices.PIXEL_4)
@Preview(device = Devices.PIXEL_4, uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
BirthdayActivityView(false, "", Date(), Date(), {
}, {}, {})
val scaffoldState = rememberScaffoldState()
BirthdayActivityView(scaffoldState, true, false, false, "", Date(), Date(), {
}, {}) {}
}

View file

@ -126,7 +126,6 @@ class LoginActivity : BaseActivity() {
}
override fun getLayoutResId(): Int {
window.requestFeature(Window.FEATURE_ACTION_BAR)
return R.layout.activity_login
}
@ -136,6 +135,7 @@ class LoginActivity : BaseActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
window.requestFeature(Window.FEATURE_ACTION_BAR)
super.onCreate(savedInstanceState)
viewModel = AuthenticationViewModel()
supportActionBar?.hide()

View file

@ -10,7 +10,6 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
@ -18,7 +17,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@ -124,6 +122,21 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
private var sideAvatarView: AvatarView? = null
private var drawerFragment: NavigationDrawerFragment? = null
var drawerToggle: ActionBarDrawerToggle? = null
var showBirthdayIcon = false
var showBackButton: Boolean? = null
set(value) {
if (field == value) return
if (value == true && showBirthdayIcon) {
drawerToggle?.isDrawerIndicatorEnabled = false
drawerToggle?.setHomeAsUpIndicator(R.drawable.arrow_back)
} else if (value == false && showBirthdayIcon) {
drawerToggle?.isDrawerIndicatorEnabled = false
drawerToggle?.setHomeAsUpIndicator(R.drawable.icon_birthday)
} else {
drawerToggle?.isDrawerIndicatorEnabled = value != true
}
field = value
}
private var resumeFromActivity = false
private var userQuestStatus = UserQuestStatus.NO_QUEST
private var lastNotificationOpen: Long? = null
@ -335,7 +348,7 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
return if (binding.root.parent is DrawerLayout && drawerToggle?.onOptionsItemSelected(item) == true) {
true
} else if (item.itemId == android.R.id.home) {
if (drawerToggle?.isDrawerIndicatorEnabled == true) {
if (showBackButton != true) {
drawerFragment?.toggleDrawer()
} else {
MainNavigationController.navigateBack()

View file

@ -24,16 +24,19 @@ import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.common.habitica.models.Notification
import com.habitrpg.common.habitica.models.notifications.GroupTaskApprovedData
import com.habitrpg.common.habitica.models.notifications.GroupTaskNeedsWorkData
import com.habitrpg.common.habitica.models.notifications.GroupTaskRequiresApprovalData
import com.habitrpg.common.habitica.models.notifications.GuildInvitationData
import com.habitrpg.common.habitica.models.notifications.ItemReceivedData
import com.habitrpg.common.habitica.models.notifications.NewChatMessageData
import com.habitrpg.common.habitica.models.notifications.NewStuffData
import com.habitrpg.common.habitica.models.notifications.PartyInvitationData
import com.habitrpg.common.habitica.models.notifications.QuestInvitationData
import com.habitrpg.common.habitica.models.notifications.UnallocatedPointsData
import com.habitrpg.common.habitica.views.PixelArtView
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -134,6 +137,7 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget
Notification.Type.PARTY_INVITATION.type -> createPartyInvitationNotification(it)
Notification.Type.GUILD_INVITATION.type -> createGuildInvitationNotification(it)
Notification.Type.QUEST_INVITATION.type -> createQuestInvitationNotification(it)
Notification.Type.ITEM_RECEIVED.type -> createItemReceivedNotification(it)
else -> null
}
@ -165,6 +169,15 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget
)
}
private fun createItemReceivedNotification(notification: Notification): View? {
val data = notification.data as? ItemReceivedData
return createDismissableNotificationItem(
notification,
fromHtml("<b>" + data?.title + "</b><br>" + data?.text),
imageName = data?.icon
)
}
private fun createNewStuffNotification(notification: Notification): View? {
val data = notification.data as? NewStuffData
val text = if (data?.title != null) {
@ -206,7 +219,7 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget
notification,
fromHtml(message),
null,
R.color.yellow_5
textColor = R.color.yellow_5
)
}
@ -218,7 +231,7 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget
notification,
fromHtml(message),
null,
R.color.green_10
textColor = R.color.green_10
)
}
@ -252,6 +265,7 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget
notification: Notification,
messageText: CharSequence,
imageResourceId: Int? = null,
imageName: String? = null,
textColor: Int? = null
): View? {
val item = inflater?.inflate(R.layout.notification_item, binding.notificationItems, false)
@ -276,6 +290,12 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget
notificationImage?.visibility = View.VISIBLE
}
if (imageName != null) {
val notificationImage = item?.findViewById(R.id.notification_image) as? PixelArtView
notificationImage?.loadImage(imageName)
notificationImage?.visibility = View.VISIBLE
}
if (textColor != null) {
messageTextView?.setTextColor(ContextCompat.getColor(this, textColor))
}

View file

@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
@ -17,7 +16,6 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.CheckBox
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.compose.runtime.mutableStateListOf
@ -256,6 +254,12 @@ class TaskFormActivity : BaseActivity() {
{
showAssignDialog()
},
{
taskCompletedMap.remove(it)
lifecycleScope.launchCatching {
task?.let { it1 -> taskRepository.markTaskNeedsWork(it1, it) }
}
},
showEditButton = true
)
}
@ -517,7 +521,7 @@ class TaskFormActivity : BaseActivity() {
val view = CheckBox(this)
view.setPadding(padding, view.paddingTop, view.paddingRight, view.paddingBottom)
view.text = tag.name
view.setTextColor(getThemeColor(R.attr.colorPrimaryDark))
view.setTextColor(getThemeColor(R.attr.textColorTintedPrimary))
if (preselectedTags?.contains(tag.id) == true) {
view.isChecked = true
}

View file

@ -21,7 +21,7 @@ import com.habitrpg.common.habitica.views.PixelArtView
class CustomizationEquipmentRecyclerViewAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder>() {
var gemBalance: Int = 0
var gemBalance: Int? = null
var equipmentList: MutableList<Equipment> =
ArrayList()
set(value) {
@ -111,7 +111,7 @@ class CustomizationEquipmentRecyclerViewAdapter : androidx.recyclerview.widget.R
imageView.loadImage("shop_" + this.equipment?.key)
val priceLabel = dialogContent.findViewById<TextView>(R.id.priceLabel)
priceLabel.text = if (equipment?.gearSet == "animal") {
priceLabel?.text = if (equipment?.gearSet == "animal") {
2.0
} else {
equipment?.value ?: 0
@ -122,9 +122,14 @@ class CustomizationEquipmentRecyclerViewAdapter : androidx.recyclerview.widget.R
val dialog = HabiticaAlertDialog(itemView.context)
dialog.addButton(R.string.purchase_button, true) { _, _ ->
if ((equipment?.value ?: 0.0) > gemBalance) {
MainNavigationController.navigate(R.id.gemPurchaseActivity, bundleOf(Pair("openSubscription", false)))
return@addButton
gemBalance?.let {
if ((equipment?.value ?: 0.0) > it) {
MainNavigationController.navigate(
R.id.gemPurchaseActivity,
bundleOf(Pair("openSubscription", false))
)
return@addButton
}
}
equipment?.let {

View file

@ -110,18 +110,19 @@ class CustomizationRecyclerViewAdapter() : androidx.recyclerview.widget.Recycler
var lastSet = CustomizationSet()
val today = Date()
for (customization in newCustomizationList) {
val isOwned = ownedCustomizations.contains(customization.id)
val isUsable = customization.isUsable(isOwned)
if (customization.availableFrom != null || customization.availableUntil != null) {
if (((customization.availableFrom?.compareTo(today)
?: 0) > 0 || (customization.availableUntil?.compareTo(today)
?: 0) < 0) && !customization.isUsable(
ownedCustomizations.contains(
customization.id
)
)
?: 0) < 0) && !isUsable
) {
continue
}
}
if (customization.identifier?.contains("birthday_bash") == true && !isOwned) {
continue
}
if (customization.customizationSet != null && customization.customizationSet != lastSet.identifier) {
if (lastSet.hasPurchasable && lastSet.price > 0) {
customizationList.add(lastSet)
@ -136,7 +137,7 @@ class CustomizationRecyclerViewAdapter() : androidx.recyclerview.widget.Recycler
}
customizationList.add(customization)
lastSet.customizations.add(customization)
if (customization.isUsable(ownedCustomizations.contains(customization.id)) && lastSet.hasPurchasable) {
if (isUsable && lastSet.hasPurchasable) {
lastSet.ownedCustomizations.add(customization)
if (!lastSet.isSetDeal()) {
lastSet.hasPurchasable = false

View file

@ -189,7 +189,7 @@ class ItemRecyclerAdapter(val context: Context) : BaseRecyclerViewAdapter<OwnedI
menu.setSelectionRunnable { index ->
item?.let { selectedItem ->
if (!(selectedItem is QuestContent || selectedItem is SpecialItem || ownedItem?.itemType == "special") && index == 0) {
ownedItem?.let { selectedOwnedItem -> sellItemEvents.onNext(selectedOwnedItem) }
ownedItem?.let { selectedOwnedItem -> onSellItem?.invoke(selectedOwnedItem) }
return@let
}
when (selectedItem) {

View file

@ -100,7 +100,7 @@ class RewardsRecyclerViewAdapter(
}
override fun getItemViewType(position: Int): Int {
return if (customRewards != null && position < customRewardCount) {
return if ((customRewards != null && position < customRewardCount) || (customRewardCount == 0 && inAppRewardCount == 0)) {
VIEWTYPE_CUSTOM_REWARD
} else {
VIEWTYPE_IN_APP_REWARD
@ -139,6 +139,6 @@ class RewardsRecyclerViewAdapter(
companion object {
private const val VIEWTYPE_CUSTOM_REWARD = 0
private const val VIEWTYPE_IN_APP_REWARD = 2
private const val VIEWTYPE_IN_APP_REWARD = 3
}
}

View file

@ -81,7 +81,7 @@ abstract class BaseMainFragment<VB : ViewBinding> : BaseFragment<VB>() {
override fun onResume() {
super.onResume()
activity?.drawerToggle?.isDrawerIndicatorEnabled = !showsBackButton
activity?.showBackButton = showsBackButton
activity?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

View file

@ -104,7 +104,7 @@ class NavigationDrawerFragment : DialogFragment() {
val context = context
adapter = if (context != null) {
NavigationDrawerAdapter(
context.getThemeColor(R.attr.colorPrimary),
context.getThemeColor(R.attr.colorPrimaryText),
context.getThemeColor(R.attr.colorPrimaryOffset)
)
} else {
@ -140,15 +140,15 @@ class NavigationDrawerFragment : DialogFragment() {
false
initializeMenuItems()
adapter.itemSelectedEvents = {
setSelection(it.transitionId, it.bundle, true)
}
adapter.promoClosedSubject = {
sharedPreferences.edit {
putBoolean("hide$it", true)
}
updatePromo()
adapter.itemSelectedEvents = {
setSelection(it.transitionId, it.bundle, true)
}
adapter.promoClosedSubject = {
sharedPreferences.edit {
putBoolean("hide$it", true)
}
updatePromo()
}
lifecycleScope.launchCatching {
contentRepository.getWorldState()
@ -167,6 +167,23 @@ class NavigationDrawerFragment : DialogFragment() {
}) {
updateSeasonalMenuEntries(gearEvent, pair.second)
}
val event = configManager.getBirthdayEvent()
val item = getItemWithIdentifier(SIDEBAR_BIRTHDAY)
if (event != null && item == null) {
adapter.currentEvent = event
val birthdayItem = HabiticaDrawerItem(R.id.birthdayActivity, SIDEBAR_BIRTHDAY)
birthdayItem.itemViewType = 6
val newItems = mutableListOf<HabiticaDrawerItem>()
newItems.addAll(adapter.items)
newItems.add(0, birthdayItem)
adapter.updateItems(newItems)
(activity as? MainActivity)?.showBirthdayIcon = true
} else if (event == null && item != null) {
item.isVisible = false
adapter.updateItem(item)
(activity as? MainActivity)?.showBirthdayIcon = false
}
}
}
@ -551,12 +568,6 @@ class NavigationDrawerFragment : DialogFragment() {
item.itemViewType = 2
items.add(item)
}
configManager.getBirthdayEvent()?.let {
val birthdayItem = HabiticaDrawerItem(R.id.birthdayActivity, SIDEBAR_BIRTHDAY)
birthdayItem.itemViewType = 6
items.add(0, birthdayItem)
}
adapter.updateItems(items)
}

View file

@ -71,7 +71,7 @@ class AvatarCustomizationFragment :
internal var adapter: CustomizationRecyclerViewAdapter = CustomizationRecyclerViewAdapter()
internal var layoutManager: FlexboxLayoutManager = FlexboxLayoutManager(activity, ROW)
private val currentFilter = MutableStateFlow(CustomizationFilter(false, type == "background"))
private val currentFilter = MutableStateFlow(CustomizationFilter(false, true))
private val ownedCustomizations = MutableStateFlow<List<OwnedCustomization>>(emptyList())
override fun onCreateView(
@ -112,6 +112,7 @@ class AvatarCustomizationFragment :
if (args.category.isNotEmpty()) {
category = args.category
}
currentFilter.value.ascending = type != "background"
}
adapter.customizationType = type
binding?.refreshLayout?.setOnRefreshListener(this)
@ -334,6 +335,8 @@ class AvatarCustomizationFragment :
button.text
button.setOnCheckedChangeListener { _, isChecked ->
val newFilter = filter.copy()
newFilter.months = mutableListOf()
newFilter.months.addAll(currentFilter.value.months)
if (!isChecked && newFilter.months.contains(identifier)) {
button.typeface = Typeface.create("sans-serif", Typeface.NORMAL)
newFilter.months.remove(identifier)

View file

@ -191,6 +191,8 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
val usePushPreference = findPreference("usePushNotifications") as? CheckBoxPreference
usePushPreference?.isChecked = true
pushNotificationManager.addPushDeviceUsingStoredToken()
} else {
//If user denies notification settings originally - they must manually enable it through notification settings.
@ -226,13 +228,14 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
TaskAlarmManager.scheduleDailyReminder(context)
}
"usePushNotifications" -> {
val notifPermissionEnabled: Boolean = pushNotificationManager.notificationPermissionEnabled()
val usePushNotifications = sharedPreferences.getBoolean(key, true)
pushNotificationsPreference?.isEnabled = usePushNotifications
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.pushNotifications.unsubscribeFromAll", !usePushNotifications)
}
if (usePushNotifications) {
if (!pushNotificationManager.notificationPermissionEnabled() && Build.VERSION.SDK_INT >= 33) {
if (!notifPermissionEnabled && Build.VERSION.SDK_INT >= 33) {
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
} else {
pushNotificationManager.addPushDeviceUsingStoredToken()
@ -389,12 +392,13 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
val inbox = user?.inbox
disablePMsPreference?.isChecked = inbox?.optOut ?: true
val notifPermissionEnabled: Boolean = pushNotificationManager.notificationPermissionEnabled()
val usePushPreference = findPreference("usePushNotifications") as? CheckBoxPreference
pushNotificationsPreference = findPreference("pushNotifications") as? PreferenceScreen
val usePushNotifications = !(user?.preferences?.pushNotifications?.unsubscribeFromAll ?: false)
pushNotificationsPreference?.isEnabled = usePushNotifications
usePushPreference?.isChecked = usePushNotifications
if (!pushNotificationManager.notificationPermissionEnabled() && Build.VERSION.SDK_INT >= 33 && !usePushNotifications) {
usePushPreference?.isChecked = (usePushNotifications && notifPermissionEnabled)
if (!notifPermissionEnabled) {
usePushPreference?.summary = getString(R.string.push_notification_system_settings_description)
} else {
usePushPreference?.summary = ""

View file

@ -6,6 +6,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.android.billingclient.api.ProductDetails
@ -26,7 +31,9 @@ import com.habitrpg.android.habitica.ui.activities.GiftGemsActivity
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.fragments.PromoInfoFragment
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.promo.BirthdayBanner
import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -92,6 +99,17 @@ class GemsPurchaseFragment : BaseFragment<FragmentGemPurchaseBinding>() {
binding?.promoBanner?.visibility = View.GONE
}
val birthdayEventEnd = appConfigManager.getBirthdayEvent()?.end
if (birthdayEventEnd != null) {
binding?.promoComposeView?.setContent {
HabiticaTheme {
BirthdayBanner(endDate = birthdayEventEnd, Modifier.padding(horizontal = 20.dp).clip(HabiticaTheme.shapes.medium)
.padding(bottom = 20.dp))
}
}
binding?.promoComposeView?.isVisible = true
}
AmplitudeManager.sendNavigationEvent("gem screen")
}

View file

@ -5,28 +5,41 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.FragmentGiftGemBalanceBinding
import com.habitrpg.android.habitica.extensions.addCloseButton
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import javax.inject.Inject
class GiftBalanceGemsFragment : BaseFragment<FragmentGiftGemBalanceBinding>() {
@Inject
lateinit var socialRepository: SocialRepository
@Inject
lateinit var userRepository: UserRepository
override var binding: FragmentGiftGemBalanceBinding? = null
private var isGifting = false
set(value) {
field = value
binding?.giftButton?.isVisible = !isGifting
binding?.progressBar?.isVisible = isGifting
}
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGiftGemBalanceBinding {
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentGiftGemBalanceBinding {
return FragmentGiftGemBalanceBinding.inflate(inflater, container, false)
}
@ -61,15 +74,29 @@ class GiftBalanceGemsFragment : BaseFragment<FragmentGiftGemBalanceBinding>() {
if (isGifting) return
isGifting = true
try {
val amount = binding?.giftEditText?.text.toString().toInt()
val amount = binding?.giftEditText?.text.toString().strip().toInt()
giftedMember?.id?.let {
lifecycleScope.launchCatching({
activity?.lifecycleScope?.launchCatching({
isGifting = false
}) {
socialRepository.transferGems(it, amount)
userRepository.retrieveUser(false)
val dialog = context?.let { it1 -> HabiticaAlertDialog(it1) }
dialog?.setTitle(R.string.gift_confirmation_title)
dialog?.setMessage(
getString(
R.string.gift_confirmation_text_gems_new,
giftedMember?.username,
amount.toString()
)
)
dialog?.addCloseButton { _, _ ->
activity?.finish()
}
dialog?.show()
}
}
} catch (ignored: NumberFormatException) {}
} catch (ignored: NumberFormatException) {
}
}
}

View file

@ -7,6 +7,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.android.billingclient.api.ProductDetails
@ -26,7 +31,9 @@ import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.activities.GiftSubscriptionActivity
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.fragments.PromoInfoFragment
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.promo.BirthdayBanner
import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionView
import com.habitrpg.common.habitica.extensions.isUsingNightModeResources
import com.habitrpg.common.habitica.extensions.layoutInflater
@ -87,6 +94,17 @@ class SubscriptionFragment : BaseFragment<FragmentSubscriptionBinding>() {
binding?.promoBanner?.visibility = View.GONE
}
val birthdayEventEnd = appConfigManager.getBirthdayEvent()?.end
if (birthdayEventEnd != null) {
binding?.promoComposeView?.setContent {
HabiticaTheme {
BirthdayBanner(endDate = birthdayEventEnd, Modifier.padding(horizontal = 20.dp).clip(HabiticaTheme.shapes.medium)
.padding(bottom = 10.dp))
}
}
binding?.promoComposeView?.isVisible = true
}
binding?.refreshLayout?.setOnRefreshListener { refresh() }
lifecycleScope.launchCatching {

View file

@ -7,7 +7,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.habitrpg.android.habitica.MainNavDirections
@ -32,7 +31,7 @@ import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class ChatFragment() : BaseFragment<FragmentChatBinding>() {
class ChatFragment : BaseFragment<FragmentChatBinding>() {
override var binding: FragmentChatBinding? = null
@ -40,9 +39,7 @@ class ChatFragment() : BaseFragment<FragmentChatBinding>() {
return FragmentChatBinding.inflate(inflater, container, false)
}
val viewModel: GroupViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
lateinit var viewModel: GroupViewModel
@Inject
lateinit var configManager: AppConfigManager

View file

@ -91,7 +91,9 @@ class TavernFragment : BaseMainFragment<FragmentViewpagerBinding>() {
TavernDetailFragment()
}
1 -> {
ChatFragment()
val fragment = ChatFragment()
fragment.viewModel = viewModel
fragment
}
else -> Fragment()
}

View file

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.MainNavDirections
import com.habitrpg.android.habitica.R
@ -197,6 +198,8 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
binding?.guildBankText?.text = guild?.gemCount.toString()
binding?.guildSummary?.setMarkdown(guild?.summary)
binding?.guildDescription?.setMarkdown(guild?.description)
binding?.inviteButton?.isVisible = guild?.isGroupPlan != true
}
companion object {

View file

@ -154,6 +154,7 @@ class GuildFragment : BaseMainFragment<FragmentViewpagerBinding>() {
}
1 -> {
chatFragment = ChatFragment()
chatFragment?.viewModel = viewModel
fragment = chatFragment
}
else -> fragment = Fragment()

View file

@ -98,8 +98,8 @@ class GuildOverviewFragment : BaseMainFragment<FragmentViewpagerBinding>(), Sear
val uriUrl = "https://habitica.com/groups/myGuilds".toUri()
val launchBrowser = Intent(Intent.ACTION_VIEW, uriUrl)
val l = context.packageManager.queryIntentActivities(launchBrowser, PackageManager.MATCH_DEFAULT_ONLY)
val notHabitica = l.first { !it.activityInfo.processName.contains("habitica") }
launchBrowser.setPackage(notHabitica.activityInfo.processName)
val notHabitica = l.firstOrNull() { !it.activityInfo.processName.contains("habitica") }
launchBrowser.setPackage(notHabitica?.activityInfo?.processName)
startActivity(launchBrowser)
}
dialog.addCloseButton()

View file

@ -196,6 +196,7 @@ class PartyFragment : BaseMainFragment<FragmentViewpagerBinding>() {
}
1 -> {
chatFragment = ChatFragment()
chatFragment?.viewModel = viewModel
chatFragment
}
else -> Fragment()

View file

@ -45,7 +45,7 @@ class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
(layoutManager as? GridLayoutManager)?.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if ((recyclerAdapter?.getItemViewType(position) ?: 0) < 2) {
return if ((recyclerAdapter?.getItemViewType(position) ?: 0) < 3) {
(layoutManager as? GridLayoutManager)?.spanCount ?: 1
} else {
1

View file

@ -141,6 +141,10 @@ object HabiticaTheme {
textTertiary = Color(ContextCompat.getColor(context, R.color.text_ternary)),
textQuad = Color(ContextCompat.getColor(context, R.color.text_quad)),
textDimmed = Color(ContextCompat.getColor(context, R.color.text_dimmed)),
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))
)
}
}
@ -153,7 +157,11 @@ class HabiticaColors(
val textSecondary: Color,
val textTertiary: Color,
val textQuad: Color,
val textDimmed: Color
val textDimmed: Color,
val tintedUiMain: Color,
val tintedUiSub: Color,
val tintedUiDetails: Color,
val pixelArtBackground: Color
) {
@Composable
fun textPrimaryFor(task: Task?): Color {
@ -179,4 +187,13 @@ class HabiticaColors(
fun contentBackgroundFor(task: Task?): Color {
return (if (isSystemInDarkTheme()) task?.darkestTaskColor else task?.lightestTaskColor)?.let { colorResource(it) } ?: windowBackground
}
@Composable
fun pixelArtBackground(hasIcon: Boolean): Color {
return if (isSystemInDarkTheme()) {
colorResource(if (hasIcon) R.color.gray_200 else R.color.gray_5)
} else {
colorResource(if (hasIcon) R.color.content_background else R.color.content_background_offset)
}
}
}

View file

@ -44,6 +44,7 @@ abstract class BaseTaskViewHolder constructor(
protected var context: Context
private val mainTaskWrapper: ViewGroup = itemView.findViewById(R.id.main_task_wrapper)
protected val assignedTextView: TextView = itemView.findViewById(R.id.assigned_textview)
protected val completedCountTextView: TextView = itemView.findViewById(R.id.completed_textview)
protected val titleTextView: EllipsisTextView = itemView.findViewById(R.id.checkedTextView)
protected val notesTextView: EllipsisTextView? = itemView.findViewById(R.id.notesTextView)
protected val calendarIconView: ImageView? = itemView.findViewById(R.id.iconViewCalendar)
@ -249,6 +250,14 @@ abstract class BaseTaskViewHolder constructor(
assignedTextView.visibility = View.GONE
}
val completedCount = data.group?.assignedUsersDetail?.filter { it.completed }?.size ?: 0
if (completedCount > 0) {
completedCountTextView.text = "$completedCount/${data?.group?.assignedUsersDetail?.size}"
completedCountTextView.visibility = View.VISIBLE
} else {
completedCountTextView.visibility = View.GONE
}
syncingView?.visibility = if (task?.isSaving == true) View.VISIBLE else View.GONE
errorIconView?.visibility = if (task?.hasErrored == true) View.VISIBLE else View.GONE
}

View file

@ -207,7 +207,7 @@ abstract class ChecklistedViewHolder(
override fun onLeftActionTouched() {
super.onLeftActionTouched()
if (task?.isValid == true) {
if (task?.isValid == true && !isLocked) {
onCheckedChanged(!(task?.completed(userID) ?: false))
}
}

View file

@ -137,12 +137,16 @@ class HabitViewHolder(
override fun onLeftActionTouched() {
super.onLeftActionTouched()
onPlusButtonClicked()
if (!isLocked) {
onPlusButtonClicked()
}
}
override fun onRightActionTouched() {
super.onRightActionTouched()
onMinusButtonClicked()
if (!isLocked) {
onMinusButtonClicked()
}
}
private fun onPlusButtonClicked() {

View file

@ -202,10 +202,13 @@ open class GroupViewModel(initializeComponent: Boolean) : BaseViewModel(initiali
}
fun likeMessage(message: ChatMessage) {
val index = _chatMessages.value?.indexOf(message)
if (index == null || index < 0) return
viewModelScope.launchCatching {
val message = socialRepository.likeMessage(message)
val index = _chatMessages.value?.indexOfFirst { it.id == message?.id }
if (index == null || index < 0) {
retrieveGroupChat { }
return@launchCatching
}
val list = _chatMessages.value?.toMutableList()
if (message != null) {
list?.set(index, message)
@ -246,7 +249,10 @@ open class GroupViewModel(initializeComponent: Boolean) : BaseViewModel(initiali
}
fun retrieveGroupChat(onComplete: () -> Unit) {
val groupID = groupID
var groupID = groupID
if (groupViewType == GroupViewType.PARTY) {
groupID = "party"
}
if (groupID.isNullOrEmpty()) {
onComplete()
return

View file

@ -16,6 +16,7 @@ import com.habitrpg.common.habitica.models.Notification
import com.habitrpg.common.habitica.models.notifications.GroupTaskRequiresApprovalData
import com.habitrpg.common.habitica.models.notifications.GuildInvitationData
import com.habitrpg.common.habitica.models.notifications.GuildInvite
import com.habitrpg.common.habitica.models.notifications.ItemReceivedData
import com.habitrpg.common.habitica.models.notifications.NewChatMessageData
import com.habitrpg.common.habitica.models.notifications.NewStuffData
import com.habitrpg.common.habitica.models.notifications.PartyInvitationData
@ -41,7 +42,8 @@ open class NotificationsViewModel : BaseViewModel() {
Notification.Type.NEW_MYSTERY_ITEMS.type,
Notification.Type.GROUP_TASK_NEEDS_WORK.type,
Notification.Type.GROUP_TASK_APPROVED.type,
Notification.Type.UNALLOCATED_STATS_POINTS.type
Notification.Type.UNALLOCATED_STATS_POINTS.type,
Notification.Type.ITEM_RECEIVED.type
)
private val actionableNotificationTypes = listOf(
@ -262,6 +264,19 @@ open class NotificationsViewModel : BaseViewModel() {
// Group tasks should go to Group tasks view if that is added to this app at some point
Notification.Type.GROUP_TASK_APPROVED.type -> navController.navigate(R.id.tasksFragment)
Notification.Type.GROUP_TASK_NEEDS_WORK.type -> navController.navigate(R.id.tasksFragment)
Notification.Type.ITEM_RECEIVED.type -> clickItemReceivedNotification(notification, navController)
}
}
private fun clickItemReceivedNotification(
notification: Notification,
navController: MainNavigationController
) {
val data = notification.data as? ItemReceivedData
when (data?.destination) {
"equipment" -> navController.navigate(R.id.equipmentOverviewFragment)
"customization" -> navController.navigate(R.id.avatarCustomizationFragment)
else -> navController.navigate(R.id.itemsFragment)
}
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.colorResource
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.R
@ -22,17 +23,19 @@ fun CurrencyText(
currency: String,
value: Int,
modifier: Modifier = Modifier,
fontSize: TextUnit = 12.sp,
decimals: Int = 0,
minForAbbreviation: Int = 0,
animated: Boolean = true
) {
CurrencyText(currency = currency, value = value.toDouble(), modifier, decimals, minForAbbreviation, animated)
CurrencyText(currency = currency, value = value.toDouble(), modifier, fontSize, decimals, minForAbbreviation, animated)
}
@Composable
fun CurrencyText(
currency: String,
value: Double,
modifier: Modifier = Modifier,
fontSize: TextUnit = 12.sp,
decimals: Int = 0,
minForAbbreviation: Int = 0,
animated: Boolean = true
@ -56,7 +59,7 @@ fun CurrencyText(
"hourglasses" -> colorResource(R.color.text_brand)
else -> colorResource(R.color.text_primary)
},
fontSize = 12.sp,
fontSize = fontSize,
fontWeight = FontWeight.SemiBold
)
}

View file

@ -26,6 +26,7 @@ fun UserRow(
username: String,
avatar: Avatar?,
modifier: Modifier = Modifier,
mainContentModifier: Modifier = Modifier,
extraContent: @Composable (() -> Unit)? = null,
endContent: @Composable (() -> Unit)? = null,
color: Color? = null
@ -46,7 +47,7 @@ fun UserRow(
}
}
Column {
Column(mainContentModifier) {
Text(
"@$username",
fontSize = 16.sp,

View file

@ -46,7 +46,7 @@ fun OverviewItem(
Modifier
.size(70.dp)
.clip(MaterialTheme.shapes.small)
.background(colorResource(if (hasIcon) R.color.content_background else R.color.content_background_offset)),
.background(HabiticaTheme.colors.pixelArtBackground(hasIcon)),
contentAlignment = Alignment.Center
) {
if (isTwoHanded) {
@ -55,6 +55,7 @@ fun OverviewItem(
PixelArtView(
imageName = iconName, modifier = Modifier
.size(70.dp)
)
} else {
Image(painterResource(R.drawable.empty_slot), null)
@ -82,7 +83,7 @@ fun EquipmentOverviewView(
modifier = modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(colorResource(R.color.offset_background))
.background(colorResource(R.color.equipment_column_background))
.padding(12.dp)
) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
@ -133,7 +134,7 @@ fun AvatarCustomizationOverviewView(
modifier = modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(colorResource(R.color.offset_background))
.background(colorResource(R.color.equipment_column_background))
.padding(12.dp)
) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
@ -228,7 +229,8 @@ fun AvatarCustomizationOverviewView(
@Composable
fun EquipmentOverviewItemPreview() {
Column(Modifier.width(320.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(modifier = Modifier.background(colorResource(id = R.color.equipment_overview_background)),
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OverviewItem("Main-Hand", "shop_weapon_warrior_1")
OverviewItem("Off-Hand", null, isTwoHanded = true)
OverviewItem("Armor", null)

View file

@ -1,78 +1,101 @@
package com.habitrpg.android.habitica.ui.views.promo
import android.os.Handler
import android.os.Looper
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
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
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.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.R
import com.habitrpg.android.habitica.extensions.getShortRemainingString
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.ui.views.PixelArtView
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import java.util.Date
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@Composable
fun BirthdayBanner(endDate: Date) {
var value by remember { mutableStateOf(0) }
DisposableEffect(Unit) {
val handler = Handler(Looper.getMainLooper())
val runnable = {
value += 1
}
handler.postDelayed(runnable, 1000)
onDispose {
handler.removeCallbacks(runnable)
}
fun BirthdayBanner(endDate: Date, modifier: Modifier = Modifier) {
if (endDate.before(Date())) {
return
}
Column(
Modifier
modifier
.fillMaxWidth()
.clickable {
MainNavigationController.navigate(R.id.birthdayActivity)
}
) {
}) {
Column(Modifier.fillMaxWidth()) {
Column(
verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically),
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier
.height(67.dp)
.fillMaxWidth()
.background(colorResource(R.color.brand_100))
.padding(start = 10.dp)) {
Image(
painterResource(R.drawable.birthday_menu_text), null
)
Text(
stringResource(R.string.exclusive_items_await),
color = colorResource(R.color.yellow_100),
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
) {
Row(
Modifier.align(Alignment.CenterEnd)
) {
Image(
painterResource(R.drawable.birthday_menu_gems),
null,
modifier = Modifier
.align(Alignment.Top)
.offset((40).dp)
)
PixelArtView(
imageName = "stable_Pet-Gryphatrice-Jubilant",
Modifier
.requiredSize(104.dp)
.scale(-1f, 1f)
.offset((-30).dp)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(
2.dp, Alignment.CenterVertically
), modifier = Modifier.padding(start = 8.dp)
) {
Image(
painterResource(R.drawable.birthday_menu_text), null
)
Text(
stringResource(R.string.exclusive_items_await),
color = colorResource(R.color.yellow_100),
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(start = 2.dp)
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
@ -82,8 +105,9 @@ fun BirthdayBanner(endDate: Date) {
.background(colorResource(R.color.brand_300))
.padding(horizontal = 10.dp)
) {
Text(
stringResource(R.string.ends_in_x, endDate.getShortRemainingString()).uppercase(),
TimeRemainingText(
endDate,
R.string.ends_in_x,
color = colorResource(R.color.yellow_50),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
@ -98,4 +122,37 @@ fun BirthdayBanner(endDate: Date) {
}
}
}
}
@Composable
private fun buildString(
value: Int, endDate: Date, formatString: Int
): String {
return stringResource(
formatString, endDate.getShortRemainingString()
).uppercase()
}
@Composable
fun TimeRemainingText(
endDate: Date, formatString: Int, color: Color, fontSize: TextUnit, fontWeight: FontWeight
) {
var value by remember { mutableStateOf(0) }
LaunchedEffect(value) {
val diff = endDate.time - Date().time
if (diff.milliseconds > 1.hours) {
delay(1.minutes)
} else if (diff < 0) {
this.cancel()
} else {
delay(1.seconds)
}
value += 1
}
Text(
buildString(value = value, endDate = endDate, formatString = formatString),
color = color,
fontSize = fontSize,
fontWeight = fontWeight
)
}

View file

@ -401,9 +401,13 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
observable = { inventoryRepository.purchaseItem(shopItem.purchaseType, shopItem.key, quantity) }
}
lifecycleScope.launchCatching {
val result = observable() ?: return@launchCatching
val text = snackbarText[0].ifEmpty {
context.getString(R.string.successful_purchase, shopItem.text)
observable()
val text = snackbarText[0].ifBlank {
if (shopItem.text?.isNotBlank() == true) {
context.getString(R.string.successful_purchase, shopItem.text)
} else {
context.getString(R.string.purchased)
}
}
val rightTextColor = when (item.currency) {
"gold" -> ContextCompat.getColor(context, R.color.text_yellow)
@ -418,6 +422,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
rightText = "-" + priceLabel.text
)
inventoryRepository.retrieveInAppRewards()
userRepository.retrieveUser()
if (item.isTypeGear || item.currency == "hourglasses") {
onGearPurchased?.invoke(item)
}

View file

@ -9,7 +9,6 @@ import com.habitrpg.android.habitica.extensions.fromHtml
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.extensions.loadGif
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.common.habitica.views.PixelArtView
@ -29,7 +28,7 @@ abstract class PurchaseDialogContent @JvmOverloads constructor(
open fun setItem(item: ShopItem) {
if (item.path?.contains("timeTravelBackgrounds") == true) {
imageView.loadGif(item.imageName?.replace("icon_", ""))
imageView.loadImage(item.imageName?.replace("icon_", ""))
val params = imageView.layoutParams
params.height = 147.dpToPx(context)
params.width = 140.dpToPx(context)

View file

@ -9,7 +9,6 @@ import android.widget.ImageView
import android.widget.TextView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.DialogPurchaseContentQuestBinding
import com.habitrpg.android.habitica.extensions.fromHtml
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.inventory.QuestDropItem
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
@ -26,7 +25,6 @@ class PurchaseDialogQuestContent(context: Context) : PurchaseDialogContent(conte
override fun setQuestContentItem(questContent: QuestContent) {
super.setQuestContentItem(questContent)
binding.notesTextView.setText(questContent.notes.fromHtml(), TextView.BufferType.SPANNABLE)
binding.rageMeterView.visibility = View.GONE
if (questContent.isBossQuest) {
binding.questTypeTextView.setText(R.string.boss_quest)

View file

@ -50,14 +50,19 @@ class SubscriptionDetailsView : LinearLayout {
var duration: String? = null
if (plan.planId != null && plan.dateTerminated == null) {
if (plan.planId == SubscriptionPlan.PLANID_BASIC || plan.planId == SubscriptionPlan.PLANID_BASICEARNED) {
duration = resources.getString(R.string.month)
} else if (plan.planId == SubscriptionPlan.PLANID_BASIC3MONTH) {
duration = resources.getString(R.string.three_months)
} else if (plan.planId == SubscriptionPlan.PLANID_BASIC6MONTH || plan.planId == SubscriptionPlan.PLANID_GOOGLE6MONTH) {
duration = resources.getString(R.string.six_months)
} else if (plan.planId == SubscriptionPlan.PLANID_BASIC12MONTH) {
duration = resources.getString(R.string.twelve_months)
when (plan.planId) {
SubscriptionPlan.PLANID_BASIC, SubscriptionPlan.PLANID_BASICEARNED -> {
duration = resources.getString(R.string.month)
}
SubscriptionPlan.PLANID_BASIC3MONTH -> {
duration = resources.getString(R.string.three_months)
}
SubscriptionPlan.PLANID_BASIC6MONTH, SubscriptionPlan.PLANID_GOOGLE6MONTH -> {
duration = resources.getString(R.string.six_months)
}
SubscriptionPlan.PLANID_BASIC12MONTH -> {
duration = resources.getString(R.string.twelve_months)
}
}
}

View file

@ -3,23 +3,31 @@ package com.habitrpg.android.habitica.ui.views.tasks
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.Assignable
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.views.CompletedAt
import com.habitrpg.android.habitica.ui.views.UserRow
import java.util.Date
@ -31,6 +39,7 @@ fun AssignedView(
backgroundColor: Color,
color: Color,
onEditClick: () -> Unit,
onUndoClick: (String) -> Unit,
modifier: Modifier = Modifier,
showEditButton: Boolean = false
) {
@ -41,17 +50,30 @@ fun AssignedView(
backgroundColor,
MaterialTheme.shapes.medium
)
.padding(15.dp, 12.dp)
.heightIn(min = 24.dp)
.heightIn(min = 66.dp)
.padding(start = 16.dp)
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
for (assignable in assigned) {
UserRow(
username = assignable.identifiableName,
avatar = assignable.avatar,
modifier = rowModifier,
mainContentModifier = Modifier
.padding(vertical = 12.dp)
.heightIn(min = 24.dp),
color = color,
extraContent = {
completedAt[assignable.id]?.let { CompletedAt(completedAt = it) }
},
endContent = {
completedAt[assignable.id]?.let {
if (showEditButton) {
UndoTaskCompletion(Modifier.clickable {
assignable.id?.let { it1 -> onUndoClick(it1) }
})
}
}
}
)
}
@ -71,7 +93,7 @@ fun AssignedView(
Image(
painterResource(R.drawable.edit),
null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
colorFilter = ColorFilter.tint(color)
)
Text(
stringResource(R.string.edit_assignees), color = color,
@ -80,4 +102,31 @@ fun AssignedView(
}
}
}
}
}
@Composable
fun UndoTaskCompletion(modifier: Modifier = Modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier
.width(51.dp)
.heightIn(min = 66.dp)
.fillMaxHeight()
.background(HabiticaTheme.colors.contentBackgroundOffset)
) {
Image(
painterResource(R.drawable.checkmark),
null,
contentScale = ContentScale.None,
modifier = Modifier
.size(24.dp)
.background(HabiticaTheme.colors.windowBackground, HabiticaTheme.shapes.small)
)
Text(
stringResource(R.string.undo),
fontSize = 12.sp,
color = HabiticaTheme.colors.textSecondary
)
}
}

View file

@ -1,11 +1,6 @@
package com.habitrpg.android.habitica.ui.views.tasks.form
import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
@ -38,7 +33,7 @@ class ChecklistItemFormView @JvmOverloads constructor(
binding.editText.setText(item.text)
}
var tintColor: Int = context.getThemeColor(R.attr.taskFormTint)
var tintColor: Int = context.getThemeColor(R.attr.tintedUiSub)
var textChangedListener: ((String) -> Unit)? = null
var animDuration = 0L
var isAddButton: Boolean = true

View file

@ -32,7 +32,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -40,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.common.habitica.extensions.getThemeColor
@Composable
@ -80,14 +80,15 @@ private fun HabitScoringSelection(
) {
val selectedState = updateTransition(selected)
val context = LocalContext.current
val borderColor = selectedState.animateColor {
if (it) HabiticaTheme.colors.tintedUiMain else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
val iconColor = selectedState.animateColor {
if (it) Color(context.getThemeColor(R.attr.colorTintedBackground)) else colorResource(R.color.text_dimmed)
if (it) HabiticaTheme.colors.tintedUiDetails else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
val textColor = selectedState.animateColor {
if (it) MaterialTheme.colors.primary else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
val borderColor = selectedState.animateColor {
if (it) MaterialTheme.colors.primary else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
if (it) Color(context.getThemeColor(R.attr.textColorTintedPrimary)) else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier) {
Box(
@ -107,7 +108,7 @@ private fun HabitScoringSelection(
Box(
Modifier
.size(32.dp)
.background(MaterialTheme.colors.primary, CircleShape)
.background(HabiticaTheme.colors.tintedUiMain, CircleShape)
)
}
Image(icon, null, colorFilter = ColorFilter.tint(iconColor.value))

View file

@ -62,7 +62,7 @@ class ReminderItemFormView @JvmOverloads constructor(
var firstDayOfWeek: Int? = null
var tintColor: Int = context.getThemeColor(R.attr.taskFormTint)
var tintColor: Int = context.getThemeColor(R.attr.tintedUiSub)
var valueChangedListener: ((Date) -> Unit)? = null
var animDuration = 0L
var isAddButton: Boolean = true

View file

@ -41,6 +41,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.common.habitica.extensions.nameRes
@ -79,10 +80,10 @@ private fun TaskDifficultySelection(
val selectedState = updateTransition(selected)
val context = LocalContext.current
val iconColor = selectedState.animateColor {
if (it) Color(context.getThemeColor(R.attr.colorTintedBackground)) else MaterialTheme.colors.primary
if (it) HabiticaTheme.colors.tintedUiDetails else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
val textColor = selectedState.animateColor {
if (it) MaterialTheme.colors.primary else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
if (it) Color(context.getThemeColor(R.attr.textColorTintedPrimary)) else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp), modifier = modifier) {
Box(
@ -104,7 +105,7 @@ private fun TaskDifficultySelection(
Box(
Modifier
.size(57.dp)
.background(MaterialTheme.colors.primary, MaterialTheme.shapes.medium)
.background(HabiticaTheme.colors.tintedUiMain, MaterialTheme.shapes.medium)
)
}
Image(icon, null, colorFilter = ColorFilter.tint(iconColor.value))

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.common.habitica.extensions.getThemeColor
data class LabeledValue<V>(val label: String, val value: V)
@ -76,8 +77,9 @@ private fun <V> TaskFormSelection(
modifier: Modifier = Modifier
) {
val selectedState = updateTransition(selected)
val context = LocalContext.current
val textColor = selectedState.animateColor {
if (it) Color(LocalContext.current.getThemeColor(R.attr.colorTintedBackground)) else MaterialTheme.colors.primary
if (it) HabiticaTheme.colors.tintedUiDetails else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
Box(
contentAlignment = Alignment.Center, modifier = modifier
@ -97,7 +99,7 @@ private fun <V> TaskFormSelection(
) {
Box(
Modifier
.background(MaterialTheme.colors.primary, MaterialTheme.shapes.medium)
.background(HabiticaTheme.colors.tintedUiMain, MaterialTheme.shapes.medium)
.matchParentSize()
)
}

View file

@ -287,11 +287,10 @@ class TaskSchedulingControls @JvmOverloads constructor(
button.tag = weekdayCode
if (isActive) {
button.background = ContextCompat.getDrawable(context, R.drawable.habit_scoring_circle_selected)
button.background.mutate().setTint(tintColor)
button.setTextColor(context.getThemeColor(R.attr.colorTintedBackground))
button.setTextColor(context.getThemeColor(R.attr.tintedUiDetails))
} else {
button.background = ContextCompat.getDrawable(context, R.drawable.habit_scoring_circle)
button.setTextColor(context.getThemeColor(R.attr.colorPrimaryDark))
button.setTextColor(context.getThemeColor(R.attr.textColorTintedSecondary))
}
button.setOnClickListener {
setWeekdayActive(weekdayCode, !isActive)

View file

@ -47,7 +47,8 @@ abstract class TaskListFactory internal constructor(
return
}
CoroutineScope(Dispatchers.Main + job).launch(ExceptionHandler.coroutine()) {
val tasks = taskRepository.getTasks(taskType, null, emptyArray()).firstOrNull()?.filter { task ->
val mirroredTasks = userRepository.getUser().firstOrNull()?.preferences?.tasks?.mirrorGroupTasks?.toTypedArray()
val tasks = taskRepository.getTasks(taskType, null, mirroredTasks ?: emptyArray()).firstOrNull()?.filter { task ->
task.type == TaskType.TODO && !task.completed || task.isDisplayedActive
} ?: return@launch
taskList = taskRepository.getTaskCopies(tasks)

View file

@ -4,27 +4,27 @@ import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.local.TaskLocalRepository
import com.habitrpg.android.habitica.models.BaseObject
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import io.kotest.common.ExperimentalKotest
import io.kotest.core.spec.style.WordSpec
import io.kotest.framework.concurrency.eventually
import io.kotest.matchers.shouldBe
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.subscribers.TestSubscriber
import io.realm.Realm
import kotlinx.coroutines.flow.flowOf
import java.util.UUID
@OptIn(ExperimentalKotest::class)
@ -52,12 +52,10 @@ class TaskRepositoryImplTest : WordSpec({
"retrieveTasks" should {
"save tasks locally" {
val list = TaskList()
every { apiClient.tasks } returns Flowable.just(list)
coEvery { apiClient.getTasks() } returns list
every { localRepository.saveTasks("", any(), any()) } returns Unit
val order = TasksOrder()
val subscriber = TestSubscriber<TaskList>()
repository.retrieveTasks("", order).subscribe(subscriber)
subscriber.assertComplete()
repository.retrieveTasks("", order)
verify { localRepository.saveTasks("", order, list) }
}
}
@ -70,32 +68,25 @@ class TaskRepositoryImplTest : WordSpec({
user.stats = Stats()
}
"debounce" {
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(
TaskDirectionData()
)
repository.taskChecked(user, task, true, false, null).subscribe()
repository.taskChecked(user, task, true, false, null).subscribe()
verify(exactly = 1) { apiClient.postTaskDirection(any(), any()) }
coEvery { apiClient.postTaskDirection(any(), "up") } returns TaskDirectionData()
repository.taskChecked(user, task, true, false, null)
repository.taskChecked(user, task, true, false, null)
coVerify(exactly = 1) { apiClient.postTaskDirection(any(), any()) }
}
"get user if not passed" {
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(
TaskDirectionData()
)
every { localRepository.getUserFlowable("") } returns Flowable.just(user)
coEvery { apiClient.postTaskDirection(any(), "up") } returns TaskDirectionData()
coEvery { localRepository.getUser("") } returns flowOf(user)
repository.taskChecked(null, task, true, false, null)
eventually(5000) {
localRepository.getUserFlowable("")
localRepository.getUser("")
}
}
"does not update user for team tasks" {
val data = TaskDirectionData()
data.lvl = 0
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(data)
val subscriber = TestSubscriber<TaskScoringResult>()
repository.taskChecked(user, task, true, false, null).subscribe(subscriber)
subscriber.assertComplete()
coEvery { apiClient.postTaskDirection(any(), "up") } returns data
repository.taskChecked(user, task, true, false, null)
verify(exactly = 0) { user.stats }
subscriber.values().first().level shouldBe null
}
"builds task result correctly" {
val data = TaskDirectionData()
@ -106,67 +97,53 @@ class TaskRepositoryImplTest : WordSpec({
user.stats?.lvl = 10
user.stats?.hp = 8.0
user.stats?.mp = 4.0
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(data)
val subscriber = TestSubscriber<TaskScoringResult>()
repository.taskChecked(user, task, true, false, null).subscribe(subscriber)
subscriber.assertComplete()
subscriber.values().first().level shouldBe 10
subscriber.values().first().healthDelta shouldBe 12.0
subscriber.values().first().manaDelta shouldBe 26.0
subscriber.values().first().hasLeveledUp shouldBe false
coEvery { apiClient.postTaskDirection(any(), "up") } returns data
val result = repository.taskChecked(user, task, true, false, null)
result?.level shouldBe 10
result?.healthDelta shouldBe 12.0
result?.manaDelta shouldBe 26.0
result?.hasLeveledUp shouldBe false
}
"set hasLeveledUp correctly" {
val subscriber = TestSubscriber<TaskScoringResult>()
val data = TaskDirectionData()
data.lvl = 11
user.stats?.lvl = 10
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(data)
repository.taskChecked(user, task, true, false, null).subscribe(subscriber)
subscriber.assertComplete()
subscriber.values().first().level shouldBe 11
subscriber.values().first().hasLeveledUp shouldBe true
coEvery { apiClient.postTaskDirection(any(), "up") } returns data
val result = repository.taskChecked(user, task, true, false, null)
result?.level shouldBe 11
result?.hasLeveledUp shouldBe true
}
"handle stats not being there" {
val subscriber = TestSubscriber<TaskScoringResult>()
val data = TaskDirectionData()
data.lvl = 1
user.stats = null
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(data)
repository.taskChecked(user, task, true, false, null).subscribe(subscriber)
subscriber.assertComplete()
coEvery { apiClient.postTaskDirection(any(), "up") } returns data
repository.taskChecked(user, task, true, false, null)
}
"update daily streak" {
val subscriber = TestSubscriber<TaskScoringResult>()
val data = TaskDirectionData()
data.delta = 1.0f
data.lvl = 1
task.type = TaskType.DAILY
task.value = 0.0
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(data)
repository.taskChecked(user, task, true, false, null).subscribe(subscriber)
subscriber.assertComplete()
coEvery { apiClient.postTaskDirection(any(), "up") } returns data
repository.taskChecked(user, task, true, false, null)
task.streak shouldBe 1
task.completed shouldBe true
}
"update habit counter" {
val subscriber = TestSubscriber<TaskScoringResult>()
val data = TaskDirectionData()
data.delta = 1.0f
data.lvl = 1
task.type = TaskType.HABIT
task.value = 0.0
every { apiClient.postTaskDirection(any(), "up") } returns Flowable.just(data)
repository.taskChecked(user, task, true, false, null).subscribe(subscriber)
subscriber.assertComplete()
coEvery { apiClient.postTaskDirection(any(), "up") } returns data
repository.taskChecked(user, task, true, false, null)
task.counterUp shouldBe 1
data.delta = -10.0f
every { apiClient.postTaskDirection(any(), "down") } returns Flowable.just(data)
val downSubscriber = TestSubscriber<TaskScoringResult>()
repository.taskChecked(user, task, false, true, null).subscribe(downSubscriber)
downSubscriber.assertComplete()
coEvery { apiClient.postTaskDirection(any(), "down") } returns data
repository.taskChecked(user, task, false, true, null)
task.counterUp shouldBe 1
task.counterDown shouldBe 1
}

View file

@ -36,7 +36,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.android.tools.build:gradle:7.4.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'com.google.gms:google-services:4.3.14'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'

View file

@ -12,6 +12,7 @@ import com.habitrpg.common.habitica.BuildConfig
fun Application.setupCoil() {
var builder = ImageLoader.Builder(this)
.allowHardware(false)
.crossfade(false)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())

View file

@ -2,6 +2,7 @@ package com.habitrpg.common.habitica.extensions
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.view.View
import android.view.animation.Animation
@ -13,7 +14,6 @@ import coil.imageLoader
import coil.request.ImageRequest
import com.habitrpg.android.habitica.extensions.setTintWith
import com.habitrpg.common.habitica.R
import com.habitrpg.common.habitica.extensions.DataBindingUtils.BASE_IMAGE_URL
import com.habitrpg.common.habitica.helpers.AppConfigManager
import com.habitrpg.common.habitica.views.PixelArtView
import java.util.Collections
@ -26,30 +26,22 @@ fun PixelArtView.loadImage(imageName: String?, imageFormat: String? = null) {
return
}
tag = fullname
bitmap = null
setImageDrawable(null)
DataBindingUtils.loadImage(context, imageName, imageFormat) {
if (tag == fullname) {
bitmap = it.toBitmap()
if (fullname.endsWith("gif")) {
setImageDrawable(it)
if (it is Animatable) {
it.start()
}
} else {
bitmap = it.toBitmap()
}
}
}
}
}
fun PixelArtView.loadGif(
imageName: String?,
builder: ImageRequest.Builder.() -> Unit = {}
) {
if (imageName != null) {
val fullname = BASE_IMAGE_URL + DataBindingUtils.getFullFilename(imageName)
val request = ImageRequest.Builder(context)
.data(fullname)
.target(this)
.apply(builder)
.build()
context.imageLoader.enqueue(request)
}
}
object DataBindingUtils {
fun loadImage(context: Context, imageName: String, imageResult: (Drawable) -> Unit) {
@ -158,6 +150,8 @@ object DataBindingUtils {
tempMap["quest_solarSystem"] = "gif"
tempMap["quest_virtualpet"] = "gif"
tempMap["Pet_HatchingPotion_VirtualPet"] = "gif"
tempMap["Pet-Gryphatrice-Jubilant"] = "gif"
tempMap["stable_Pet-Gryphatrice-Jubilant"] = "gif"
FILEFORMAT_MAP = Collections.unmodifiableMap(tempMap)
val tempNameMap = HashMap<String, String>()

View file

@ -91,8 +91,12 @@ object MarkdownParser {
return SpannableString("")
}
val hashCode = input.hashCode()
if (cache.containsKey(hashCode)) {
return cache[hashCode] ?: SpannableString(input)
try {
if (cache.containsKey(hashCode)) {
return cache[hashCode] ?: SpannableString(input)
}
} catch (_: NullPointerException) {
// Sometimes happens
}
val text = EmojiParser.parseEmojis(input) ?: input
// Adding this space here bc for some reason some markdown is not rendered correctly when the whole string is supposed to be formatted

View file

@ -7,6 +7,7 @@ import com.habitrpg.common.habitica.models.notifications.GroupTaskApprovedData
import com.habitrpg.common.habitica.models.notifications.GroupTaskNeedsWorkData
import com.habitrpg.common.habitica.models.notifications.GroupTaskRequiresApprovalData
import com.habitrpg.common.habitica.models.notifications.GuildInvitationData
import com.habitrpg.common.habitica.models.notifications.ItemReceivedData
import com.habitrpg.common.habitica.models.notifications.LoginIncentiveData
import com.habitrpg.common.habitica.models.notifications.NewChatMessageData
import com.habitrpg.common.habitica.models.notifications.NewStuffData
@ -27,6 +28,7 @@ class Notification {
GROUP_TASK_REQUIRES_APPROVAL("GROUP_TASK_REQUIRES_APPROVAL"),
UNALLOCATED_STATS_POINTS("UNALLOCATED_STATS_POINTS"),
WON_CHALLENGE("WON_CHALLENGE"),
ITEM_RECEIVED("ITEM_RECEIVED"),
// Achievements
ACHIEVEMENT_PARTY_UP("ACHIEVEMENT_PARTY_UP"),
@ -93,6 +95,7 @@ class Notification {
Type.FIRST_DROP.type -> FirstDropData::class.java
Type.ACHIEVEMENT_GENERIC.type -> AchievementData::class.java
Type.WON_CHALLENGE.type -> ChallengeWonData::class.java
Type.ITEM_RECEIVED.type -> ItemReceivedData::class.java
Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> AchievementData::class.java
Type.ACHIEVEMENT_BACK_TO_BASICS.type -> AchievementData::class.java

View file

@ -0,0 +1,8 @@
package com.habitrpg.common.habitica.models.notifications
open class ItemReceivedData: NotificationData {
var title: String? = null
var text: String? = null
var icon: String? = null
var destination: String? = null
}

Some files were not shown because too many files have changed in this diff Show more