preparing

This commit is contained in:
Phillip Thelen 2023-01-04 14:34:12 +01:00
parent 3cfdf48fe4
commit beb20de968
97 changed files with 1682 additions and 459 deletions

View file

@ -80,6 +80,12 @@
android:screenOrientation="unspecified"
tools:ignore="UnusedAttribute">
</activity>
<activity
android:name=".ui.activities.BirthdayActivity"
android:parentActivityName=".ui.activities.MainActivity"
android:screenOrientation="unspecified"
tools:ignore="UnusedAttribute">
</activity>
<activity
android:name=".ui.activities.NotificationsActivity"
android:parentActivityName=".ui.activities.MainActivity"

View file

@ -56,29 +56,31 @@ dependencies {
implementation "com.android.billingclient:billing-ktx:5.1.0"
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1@aar'
implementation("io.coil-kt:coil-compose:$coil_version")
//Analytics
implementation "com.amplitude:analytics-android:$amplitude_version"
//Tests
testImplementation 'io.kotest:kotest-runner-junit5:5.3.0'
testImplementation 'androidx.test:core:1.4.0'
testImplementation 'io.mockk:mockk:1.12.3'
testImplementation 'io.mockk:mockk-android:1.12.3'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'io.mockk:mockk:1.13.3'
testImplementation 'io.mockk:mockk-android:1.13.3'
testImplementation 'io.kotest:kotest-assertions-core:5.3.0'
testImplementation 'io.kotest:kotest-framework-datatest:5.3.0'
androidTestImplementation ('com.kaspersky.android-components:kaspresso:1.4.1') {
exclude module: "protobuf-lite"
}
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
debugImplementation 'androidx.fragment:fragment-testing:1.5.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test:runner:1.5.1'
androidTestImplementation 'androidx.test:rules:1.5.0'
debugImplementation 'androidx.fragment:fragment-testing:1.5.5'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.4'
androidTestImplementation 'io.mockk:mockk-android:1.12.3'
androidTestImplementation 'io.mockk:mockk-android:1.13.3'
androidTestImplementation 'io.kotest:kotest-assertions-core:5.3.0'
androidTestUtil("androidx.test:orchestrator:1.4.1")
androidTestUtil("androidx.test:orchestrator:1.4.2")
implementation 'com.facebook.shimmer:shimmer:0.5.0'
@ -91,7 +93,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-config-ktx'
implementation 'com.google.firebase:firebase-perf-ktx'
implementation 'com.google.android.gms:play-services-ads:21.3.0'
implementation 'com.google.android.gms:play-services-ads:21.4.0'
implementation "com.google.android.gms:play-services-auth:$play_auth_version"
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation "com.google.android.gms:play-services-wearable:$play_wearables_version"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -16,6 +16,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"/>
</com.google.android.material.appbar.AppBarLayout>

View file

@ -16,6 +16,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/PopupTheme"/>

View file

@ -36,6 +36,8 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorContentBackground"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_collapseMode="pin"
app:popupTheme="@style/PopupTheme" />
</com.google.android.material.appbar.AppBarLayout>

View file

@ -17,6 +17,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/PopupTheme"/>

View file

@ -17,6 +17,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/PopupTheme" />

View file

@ -20,6 +20,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

View file

@ -17,6 +17,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Dark"/>

View file

@ -17,6 +17,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/PopupTheme"/>

View file

@ -17,6 +17,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/Toolbar.Modern"
style="@style/ToolbarTitleStyle"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/PopupTheme"/>

View file

@ -37,7 +37,7 @@
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:background="@color/transparent"
android:textColor="?colorPrimaryDark"
android:textColor="?textColorTintedPrimary"
android:textSize="16sp"
tools:text="100.0"
android:inputType="numberDecimal"

View file

@ -10,6 +10,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

@ -257,8 +257,8 @@
android:layout_marginTop="4dp"
android:fontFamily="sans-serif"
android:gravity="center"
android:paddingStart="50dp"
android:paddingEnd="50dp"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:text="@string/subscribers_mythic_hourglasses" />
<ImageView

View file

@ -123,6 +123,20 @@
<action
android:id="@+id/openEquipmentDetail"
app:destination="@id/equipmentDetailFragment" />
<action
android:id="@+id/openAvatarEquipment"
app:destination="@id/avatarEquipmentFragment" />
</fragment>
<fragment
android:id="@+id/avatarEquipmentFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarEquipmentFragment"
android:label="@string/sidebar_avatar" >
<argument
android:name="type"
app:argType="string" />
<argument
android:name="category"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/equipmentOverviewFragment"
@ -226,6 +240,12 @@
app:argType="string"
app:nullable="true" />
</activity>
<activity
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" />
</activity>
<fragment
android:id="@+id/newsFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.NewsFragment"

View file

@ -9,7 +9,7 @@
<color name="text_quad">@color/gray_300</color>
<color name="text_dimmed">@color/gray_200</color>
<color name="text_inverted">@color/gray_10</color>
<color name="text_brand">@color/brand_400</color>
<color name="text_brand">@color/brand_600</color>
<color name="text_brand_neon">@color/brand_500</color>
<color name="text_red">@color/red_100</color>
<color name="text_orange">@color/orange_100</color>

View file

@ -9,6 +9,8 @@
<attr name="colorWindowBackground" format="color" />
<attr name="textColorPrimary" format="color" />
<attr name="textColorSecondary" format="color" />
<attr name="textColorTintedPrimary" format="color" />
<attr name="textColorTintedSecondary" format="color" />
<attr name="textColorPrimaryDark" format="color" />
<attr name="barColor" format="color" />
<attr name="toolbarContentColor" format="color" />

View file

@ -60,6 +60,8 @@
<string name="start_date">Start Date</string>
<string name="positive_habit_form">Positive</string>
<string name="negative_habit_form">Negative</string>
<string name="positive_sentence">positive</string>
<string name="negative_sentence">negative</string>
<string name="on">On</string>
<string name="off">Off</string>
<string name="selected">Selected</string>
@ -739,8 +741,8 @@
<string name="gift_gems_subtitle">Choose the gem packet youd like to gift below!</string>
<string name="server">Server</string>
<string name="gift_confirmation_title">Your gift was sent!</string>
<string name="gift_confirmation_text_sub_g1g1">You sent %s a %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 @%s a %s-month Habitica subscription.</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="subscription_confirmation">You are now subscribed for 1 month</string>
<string name="subscription_confirmation_multiple">You are now subscribed for %s months</string>
@ -1048,11 +1050,11 @@
<string name="view_onboarding_tasks">View Onboarding Tasks</string>
<string name="suggest_pet_hatch_missing_egg">You still need a %s Egg to hatch this pet</string>
<string name="suggest_pet_hatch_missing_potion">You still need a %s Potion to hatch this pet</string>
<string name="suggest_pet_hatch_missing_both">You need a %s and %s Potion to hatch this pet</string>
<string name="suggest_pet_hatch_missing_both">You need a %1$s and %2$s Potion to hatch this pet</string>
<string name="suggest_pet_hatch_again_missing_egg">You still need a %s Egg to hatch this pet again</string>
<string name="suggest_pet_hatch_again_missing_potion">You still need a %s Potion to hatch this pet again</string>
<string name="suggest_pet_hatch_again_missing_both">You need a %s and %s Potion to hatch this pet again</string>
<string name="can_hatch_pet">Combine your %s Egg and %s Potion to hatch this pet!</string>
<string name="suggest_pet_hatch_again_missing_both">You need a %1$s and %2$s Potion to hatch this pet again</string>
<string name="can_hatch_pet">Combine your %1$s Egg and %2$s Potion to hatch this pet!</string>
<string name="hatch_pet">Hatch Pet</string>
<string name="unhatched_pet">Unhatched Pet</string>
<string name="hatch_pet_again">Hatch Pet again</string>
@ -1081,7 +1083,7 @@
<string name="pms_disabled">Private Messages are disabled</string>
<string name="pms_disabled_description">You can still send messages, but no one can send them to you. You can enable again from Settings.</string>
<string name="usually_x_gems">Usually %d Gems</string>
<string name="x_to_y">%s to %s</string>
<string name="x_to_y">%1$s to %2$s</string>
<string name="how_it_works">How it works</string>
<string name="limitations">Limitations</string>
<string name="fall_promo_info_instructions">Between %s and %s, simply purchase any Gem bundle like usual and your account will be credited with the promotional amount of Gems. More Gems to spend, share, or save for any future releases!</string>
@ -1259,10 +1261,10 @@
<string name="copy_shared_tasks">Copy shared tasks</string>
<string name="group_plan_settings">Group Plan Settings</string>
<string name="task_summary">Task Summary</string>
<string name="habit_summary_description">This is a **%s** Habit that is **%s**.</string>
<string name="todo_summary_description_duedate">This is a **%s** Task that is due **%s**.</string>
<string name="todo_summary_description">This is a **%s** Task that does not have a due date.</string>
<string name="daily_summary_description">This is a **%s** Task that repeats **%s**.</string>
<string name="habit_summary_description">This is a **%1$s** Habit that is **%2$s**.</string>
<string name="todo_summary_description_duedate">This is a **%1$s** Task that is due **%2$s**.</string>
<string name="todo_summary_description">This is a **%1$s** Task that does not have a due date.</string>
<string name="daily_summary_description">This is a **%1$s** Task that repeats **%2$s%3$s**.</string>
<string name="positive_and_negative">positive and negative</string>
<string name="assigned_to">Assigned to</string>
<string name="completed_at">Completed at %s</string>
@ -1293,6 +1295,36 @@
<string name="paypal">PayPal</string>
<string name="stripe_payment">Stripe</string>
<string name="shadow_muted_hidden">Shadow muted, hidden</string>
<string name="assigned_to_you_by">Assigned to you by @%1$s on %2$s</string>
<string name="never">never</string>
<string name="on_x">on %s</string>
<string name="on_weekdays">on weekdays</string>
<string name="on_weekends">on weekends</string>
<string name="on_every_day_of_week">on every day of the week</string>
<string name="x_and_y">%1$s and %2$s</string>
<string name="on_the_x">on the %s</string>
<string name="first">first</string>
<string name="on_the_x_of_month">on the %1$s %2$s of the month</string>
<string name="second">second</string>
<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="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="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="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="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>
<plurals name="you_x_others">
<item quantity="zero">You</item>
<item quantity="one">You, %d other</item>
@ -1302,4 +1334,24 @@
<item quantity="one">%d person</item>
<item quantity="other">%d people</item>
</plurals>
<plurals name="repeat_daily">
<item quantity="one">every day</item>
<item quantity="two">every other day</item>
<item quantity="other">every %d days</item>
</plurals>
<plurals name="repeat_weekly">
<item quantity="one">every week</item>
<item quantity="two">every other week</item>
<item quantity="other">every %d weeks</item>
</plurals>
<plurals name="repeat_monthly">
<item quantity="one">every month</item>
<item quantity="two">every other month</item>
<item quantity="other">every %d months</item>
</plurals>
<plurals name="repeat_yearly">
<item quantity="one">every year</item>
<item quantity="two">every other year</item>
<item quantity="other">every %d years</item>
</plurals>
</resources>

View file

@ -41,6 +41,8 @@
<item name="colorWindowBackground">@color/window_background</item>
<item name="colorTintedBackground">@color/brand_800</item>
<item name="colorTintedBackgroundOffset">@color/brand_700</item>
<item name="textColorTintedSecondary">@color/brand_sub_text</item>
<item name="textColorTintedPrimary">@color/brand_100</item>
<item name="popupMenuStyle">@style/PopupTheme</item>
<item name="actionOverflowMenuStyle">@style/PopupTheme</item>
@ -70,11 +72,14 @@
<item name="colorBoxStroke">@color/brand_50</item>
<item name="colorPrimaryOffset">@color/gray_10</item>
<item name="textColorPrimaryDark">@color/gray_200</item>
<item name="colorPrimaryDistinct">@color/white</item>
<item name="toolbarContentColor">@color/white</item>
<item name="colorTintedBackground">@color/brand_00</item>
<item name="colorTintedBackgroundOffset">@color/brand_0</item>
<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>
</style>
@ -98,6 +103,8 @@
<item name="colorPrimaryText">@color/red_1</item>
<item name="colorTintedBackground">@color/red_700</item>
<item name="colorTintedBackgroundOffset">@color/red_600</item>
<item name="textColorTintedSecondary">@color/red_sub_text</item>
<item name="textColorTintedPrimary">@color/red_100</item>
</style>
<style name="MainAppTheme.Red.Dark">
@ -109,6 +116,8 @@
<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>
</style>
<style name="MainAppTheme.Maroon">
@ -130,6 +139,8 @@
<item name="colorPrimaryText">@color/maroon_1</item>
<item name="colorTintedBackground">@color/maroon_700</item>
<item name="colorTintedBackgroundOffset">@color/maroon_600</item>
<item name="textColorTintedSecondary">@color/maroon_sub_text</item>
<item name="textColorTintedPrimary">@color/maroon_100</item>
</style>
<style name="MainAppTheme.Maroon.Dark">
@ -140,6 +151,8 @@
<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>
</style>
<style name="MainAppTheme.Orange">
@ -162,6 +175,8 @@
<item name="colorPrimaryText">@color/orange_1</item>
<item name="colorTintedBackground">@color/orange_700</item>
<item name="colorTintedBackgroundOffset">@color/orange_600</item>
<item name="textColorTintedSecondary">@color/orange_sub_text</item>
<item name="textColorTintedPrimary">@color/orange_100</item>
</style>
<style name="MainAppTheme.Orange.Dark">
@ -173,6 +188,8 @@
<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>
</style>
<style name="MainAppTheme.Yellow">
@ -181,7 +198,7 @@
<item name="colorAccent">@color/yellow_100</item>
<item name="android:colorPrimary">@color/yellow_10</item>
<item name="android:colorPrimaryDark">@color/yellow_5</item>
<item name="android:colorAccent">@color/yellow_100</item>
<item name="android:colorAccent">@color/yellow_50</item>
<item name="colorBoxStroke">@color/yellow_5</item>
<item name="colorPrimaryOffset">@color/yellow_50</item>
<item name="colorPrimaryDistinct">@color/yellow_500</item>
@ -195,6 +212,8 @@
<item name="colorPrimaryText">@color/yellow_1</item>
<item name="colorTintedBackground">@color/yellow_700</item>
<item name="colorTintedBackgroundOffset">@color/yellow_600</item>
<item name="textColorTintedSecondary">@color/yellow_sub_text</item>
<item name="textColorTintedPrimary">@color/yellow_10</item>
</style>
<style name="MainAppTheme.Yellow.Dark">
@ -206,6 +225,8 @@
<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>
</style>
<style name="MainAppTheme.Green">
@ -228,6 +249,8 @@
<item name="colorPrimaryText">@color/green_1</item>
<item name="colorTintedBackground">@color/green_700</item>
<item name="colorTintedBackgroundOffset">@color/green_600</item>
<item name="textColorTintedSecondary">@color/green_sub_text</item>
<item name="textColorTintedPrimary">@color/green_100</item>
</style>
<style name="MainAppTheme.Green.Dark">
@ -239,6 +262,8 @@
<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>
</style>
<style name="MainAppTheme.Teal">
@ -261,6 +286,8 @@
<item name="colorPrimaryText">@color/teal_1</item>
<item name="colorTintedBackground">@color/teal_700</item>
<item name="colorTintedBackgroundOffset">@color/teal_600</item>
<item name="textColorTintedSecondary">@color/teal_sub_text</item>
<item name="textColorTintedPrimary">@color/teal_100</item>
</style>
<style name="MainAppTheme.Teal.Dark">
@ -272,6 +299,8 @@
<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>
</style>
<style name="MainAppTheme.Blue">
@ -294,6 +323,8 @@
<item name="colorPrimaryText">@color/blue_1</item>
<item name="colorTintedBackground">@color/blue_700</item>
<item name="colorTintedBackgroundOffset">@color/blue_600</item>
<item name="textColorTintedSecondary">@color/blue_sub_text</item>
<item name="textColorTintedPrimary">@color/blue_100</item>
</style>
<style name="MainAppTheme.Blue.Dark">
@ -305,6 +336,8 @@
<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>
</style>
<style name="MyWidgetTheme">
@ -920,7 +953,7 @@
<style name="TaskFormSectionheader">
<item name="android:textSize">16sp</item>
<item name="android:textColor">?colorPrimaryDark</item>
<item name="android:textColor">?textColorTintedPrimary</item>
<item name="android:layout_marginTop">@dimen/spacing_large</item>
<item name="android:layout_marginBottom">@dimen/spacing_medium</item>
<item name="android:accessibilityHeading">true</item>

View file

@ -29,6 +29,7 @@ import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import com.habitrpg.android.habitica.modules.UserModule
import com.habitrpg.android.habitica.modules.UserRepositoryModule
@ -41,6 +42,7 @@ import com.habitrpg.common.habitica.helpers.LanguageHelper
import com.habitrpg.common.habitica.helpers.MarkdownParser
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.MainScope
import java.lang.ref.WeakReference
import javax.inject.Inject
@ -250,26 +252,28 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
}
fun logout(context: Context) {
getInstance(context)?.pushNotificationManager?.removePushDeviceUsingStoredToken()
val realm = Realm.getDefaultInstance()
getInstance(context)?.deleteDatabase(realm.path)
realm.close()
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val useReminder = preferences.getBoolean("use_reminder", false)
val reminderTime = preferences.getString("reminder_time", "19:00")
val lightMode = preferences.getString("theme_mode", "system")
val launchScreen = preferences.getString("launch_screen", "")
preferences.edit {
clear()
putBoolean("use_reminder", useReminder)
putString("reminder_time", reminderTime)
putString("theme_mode", lightMode)
putString("launch_screen", launchScreen)
MainScope().launchCatching {
getInstance(context)?.pushNotificationManager?.removePushDeviceUsingStoredToken()
val realm = Realm.getDefaultInstance()
getInstance(context)?.deleteDatabase(realm.path)
realm.close()
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val useReminder = preferences.getBoolean("use_reminder", false)
val reminderTime = preferences.getString("reminder_time", "19:00")
val lightMode = preferences.getString("theme_mode", "system")
val launchScreen = preferences.getString("launch_screen", "")
preferences.edit {
clear()
putBoolean("use_reminder", useReminder)
putString("reminder_time", reminderTime)
putString("theme_mode", lightMode)
putString("launch_screen", launchScreen)
}
reloadUserComponent()
getInstance(context)?.lazyApiHelper?.updateAuthenticationCredentials(null, null)
Wearable.getCapabilityClient(context).removeLocalCapability("provide_auth")
startActivity(LoginActivity::class.java, context)
}
reloadUserComponent()
getInstance(context)?.lazyApiHelper?.updateAuthenticationCredentials(null, null)
Wearable.getCapabilityClient(context).removeLocalCapability("provide_auth")
startActivity(LoginActivity::class.java, context)
}
fun reloadUserComponent() {

View file

@ -15,6 +15,7 @@ import com.habitrpg.android.habitica.receivers.TaskAlarmBootReceiver;
import com.habitrpg.android.habitica.receivers.TaskReceiver;
import com.habitrpg.android.habitica.ui.activities.AdventureGuideActivity;
import com.habitrpg.android.habitica.ui.activities.ArmoireActivity;
import com.habitrpg.android.habitica.ui.activities.BirthdayActivity;
import com.habitrpg.android.habitica.ui.activities.ChallengeFormActivity;
import com.habitrpg.android.habitica.ui.activities.ClassSelectionActivity;
import com.habitrpg.android.habitica.ui.activities.DeathActivity;
@ -53,6 +54,7 @@ import com.habitrpg.android.habitica.ui.fragments.PromoInfoFragment;
import com.habitrpg.android.habitica.ui.fragments.PromoWebFragment;
import com.habitrpg.android.habitica.ui.fragments.StatsFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarCustomizationFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarEquipmentFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarOverviewFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.equipment.EquipmentDetailFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.items.ItemDialogFragment;
@ -370,4 +372,8 @@ public interface UserComponent {
void inject(@NotNull TaskSummaryViewModel taskSummaryViewModel);
void inject(@NotNull TaskFormViewModel taskFormViewModel);
void inject(@NotNull AvatarEquipmentFragment avatarEquipmentFragment);
void inject(@NotNull BirthdayActivity birthdayActivity);
}

View file

@ -231,11 +231,6 @@ class ApiClientImpl(
return
}
if (status == 401 && !hostConfig.hasAuthentication()) {
// if a request was accidentally made that needs authentication, before the user has logged in just ignore the error
return
}
if (status in 400..499) {
if (res.displayMessage.isNotEmpty()) {
showConnectionProblemDialog("", res.displayMessage)

View file

@ -0,0 +1,17 @@
package com.habitrpg.android.habitica.extensions
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import com.habitrpg.android.habitica.ui.activities.BaseActivity
import java.util.Locale
fun Resources.forceLocale(activity: BaseActivity, locale: Locale) {
Locale.setDefault(locale)
val configuration = Configuration()
configuration.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.createConfigurationContext(configuration)
}
updateConfiguration(configuration, displayMetrics)
}

View file

@ -140,11 +140,6 @@ class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.comm
return Gson().fromJson(remoteConfig.getString("knownIssues"), type)
}
fun enableTeamBoards(): Boolean {
return true
return remoteConfig.getBoolean("enableTeamBoards")
}
fun enableArmoireAds(): Boolean {
return remoteConfig.getBoolean("enableArmoireAds")
}
@ -160,4 +155,8 @@ class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.comm
fun enableNewArmoire(): Boolean {
return remoteConfig.getBoolean("enableNewArmoire")
}
fun isBirthday(): Boolean {
return BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == AppTestingLevel.STAFF.name
}
}

View file

@ -1,27 +1,48 @@
package com.habitrpg.android.habitica.helpers
import android.content.Context
import android.icu.text.MessageFormat
import android.os.Build
import android.text.format.DateUtils
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.common.habitica.extensions.nameRes
import com.habitrpg.common.habitica.extensions.nameSentenceRes
import com.habitrpg.shared.habitica.models.tasks.Frequency
import com.habitrpg.shared.habitica.models.tasks.TaskDifficulty
import com.habitrpg.shared.habitica.models.tasks.TaskType
import java.text.DateFormat
import java.util.Date
import java.util.Locale
class TaskDescriptionBuilder(private val context: Context) {
fun describe(task: Task): String {
return when (task.type) {
TaskType.HABIT -> context.getString(R.string.habit_summary_description, describeHabitDirections(task.up ?: false, task.down ?: false), describeDifficulty(task.priority))
TaskType.HABIT -> context.getString(
R.string.habit_summary_description,
describeHabitDirections(task.up ?: false, task.down ?: false),
describeDifficulty(task.priority)
)
TaskType.TODO -> {
if (task.dueDate != null) {
context.getString(R.string.todo_summary_description_duedate, describeDifficulty(task.priority), describeDate(task.dueDate!!))
context.getString(
R.string.todo_summary_description_duedate,
describeDifficulty(task.priority),
describeDate(task.dueDate!!)
)
} else {
context.getString(R.string.todo_summary_description, describeDifficulty(task.priority))
context.getString(
R.string.todo_summary_description,
describeDifficulty(task.priority)
)
}
}
TaskType.DAILY -> context.getString(R.string.daily_summary_description, describeDifficulty(task.priority), "sometimes")
TaskType.DAILY -> context.getString(
R.string.daily_summary_description,
describeDifficulty(task.priority),
describeRepeatInterval(task.frequency, task.everyX ?: 1),
describeRepeatDays(task)
)
else -> ""
}
}
@ -32,17 +53,99 @@ class TaskDescriptionBuilder(private val context: Context) {
return dateFormatter.format(date)
}
private fun describeRepeatDays(task: Task): Any {
if (task.everyX == 0) {
return ""
}
return when (task.frequency) {
Frequency.WEEKLY -> {
" " + if (task.repeat?.isEveryDay == true) {
context.getString(R.string.on_every_day_of_week)
} else {
if (task.repeat?.isOnlyWeekdays == true) {
context.getString(R.string.on_weekdays)
} else if (task.repeat?.isOnlyWeekends == true) {
context.getString(R.string.on_weekends)
} else {
val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
joinToCount(dayStrings)
}
}
}
Frequency.MONTHLY -> {
" " + if (task.getDaysOfMonth()?.isNotEmpty() == true) {
val dayList = task.getDaysOfMonth()?.map {
withOrdinal(it)
}
context.getString(R.string.on_the_x, joinToCount(dayList))
} else if (task.getWeeksOfMonth()?.isNotEmpty() == true) {
val occurrence = when (task.getWeeksOfMonth()?.first()) {
0 -> context.getString(R.string.first)
1 -> context.getString(R.string.second)
2 -> context.getString(R.string.third)
3 -> context.getString(R.string.fourth)
4 -> context.getString(R.string.fifth)
else -> return ""
}
val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
context.getString(R.string.on_the_x_of_month, occurrence, joinToCount(dayStrings))
} else {
""
}
}
Frequency.YEARLY -> " " + context.getString(R.string.on_x,
task.startDate?.let {
val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR
DateUtils.formatDateTime(context, it.time, flags)
} ?: "")
else -> ""
}
}
private fun joinToCount(dayStrings: List<String>?) =
if (dayStrings?.size == 2) {
context.getString(R.string.x_and_y, dayStrings[0], dayStrings[1])
} else {
dayStrings?.joinToString(", ") ?: ""
}
private fun withOrdinal(day: Int): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val formatter = MessageFormat("{0,ordinal}", Locale.getDefault())
formatter.format(arrayOf(day))
} else {
day.toString()
}
}
private fun describeRepeatInterval(interval: Frequency?, everyX: Int): String {
if (everyX == 0) {
return context.getString(R.string.never)
}
return when (interval) {
Frequency.DAILY -> context.resources.getQuantityString(R.plurals.repeat_daily, everyX, everyX)
Frequency.WEEKLY -> context.resources.getQuantityString(R.plurals.repeat_weekly, everyX, everyX)
Frequency.MONTHLY -> context.resources.getQuantityString(
R.plurals.repeat_monthly,
everyX, everyX
)
Frequency.YEARLY -> context.resources.getQuantityString(R.plurals.repeat_yearly, everyX, everyX)
null -> ""
}
}
private fun describeHabitDirections(up: Boolean, down: Boolean): String {
return if (up && down) {
context.getString(R.string.positive_and_negative)
} else if (up) {
context.getString(R.string.positive_habit_form)
context.getString(R.string.positive_sentence)
} else {
context.getString(R.string.negative_habit_form)
context.getString(R.string.negative_sentence)
}
}
private fun describeDifficulty(difficulty: Float): String {
return context.getString(TaskDifficulty.valueOf(difficulty).nameRes)
return context.getString(TaskDifficulty.valueOf(difficulty).nameSentenceRes)
}
}

View file

@ -2,9 +2,6 @@ package com.habitrpg.android.habitica.helpers.notifications
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.edit
import com.google.firebase.messaging.FirebaseMessaging
@ -73,13 +70,11 @@ class PushNotificationManager(
}
}
fun removePushDeviceUsingStoredToken() {
suspend fun removePushDeviceUsingStoredToken() {
if (this.refreshedToken.isEmpty() || !userHasPushDevice()) {
return
}
MainScope().launchCatching {
apiClient.deletePushDevice(refreshedToken)
}
}
private fun userHasPushDevice(): Boolean {

View file

@ -1,10 +1,18 @@
package com.habitrpg.android.habitica.models.tasks
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import com.habitrpg.android.habitica.R
@io.realm.annotations.RealmClass(embedded = true)
open class Days() : io.realm.RealmObject(), Parcelable {
val isEveryDay: Boolean
get() = m && t && w && th && f && s && su
val isOnlyWeekdays: Boolean
get() = m && t && w && th && f && !s && !su
val isOnlyWeekends: Boolean
get() = !m && !t && !w && !th && !f && s && su
var m: Boolean = true
var t: Boolean = true
var w: Boolean = true
@ -37,6 +45,18 @@ open class Days() : io.realm.RealmObject(), Parcelable {
return 0
}
fun dayStrings(context: Context): List<String> {
val days = mutableListOf<String>()
if (m) days.add(context.getString(R.string.monday))
if (t) days.add(context.getString(R.string.tuesday))
if (w) days.add(context.getString(R.string.wednesday))
if (th) days.add(context.getString(R.string.thursday))
if (f) days.add(context.getString(R.string.friday))
if (s) days.add(context.getString(R.string.saturday))
if (su) days.add(context.getString(R.string.sunday))
return days
}
companion object CREATOR : Parcelable.Creator<Days> {
override fun createFromParcel(parcel: Parcel): Days {
return Days(parcel)

View file

@ -104,8 +104,8 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
var isCreating: Boolean = false
var yesterDaily: Boolean = true
private var daysOfMonthString: String? = null
private var weeksOfMonthString: String? = null
var daysOfMonthString: String? = null
var weeksOfMonthString: String? = null
@Ignore
private var daysOfMonth: List<Int>? = null
@ -246,6 +246,19 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
}
}
val lowSaturationTaskColor: Int
get() {
return when {
this.value < -20 -> return R.color.maroon_sub_text
this.value < -10 -> return R.color.red_sub_text
this.value < -1 -> return R.color.orange_sub_text
this.value < 1 -> return R.color.yellow_sub_text
this.value < 5 -> return R.color.green_sub_text
this.value < 10 -> return R.color.teal_sub_text
else -> R.color.blue_sub_text
}
}
val extraExtraDarkTaskColor: Int
get() {
return when {

View file

@ -19,6 +19,9 @@ open class GroupAssignedDetails: RealmObject(), BaseObject {
@RealmClass(embedded = true)
open class TaskGroupPlan : RealmObject(), BaseObject {
fun assignedDetailsFor(userID: String): GroupAssignedDetails? {
return assignedUsersDetail.firstOrNull { it.assignedUserID == userID }
}
@SerializedName("id")
var groupID: String? = null

View file

@ -26,6 +26,7 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.extensions.forceLocale
import com.habitrpg.android.habitica.extensions.updateStatusBarColor
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.NotificationsManager
@ -251,14 +252,4 @@ abstract class BaseActivity : AppCompatActivity() {
overridePendingTransition(R.anim.activity_fade_in, R.anim.activity_fade_out)
startActivity(intent)
}
}
private fun Resources.forceLocale(activity: BaseActivity, locale: Locale) {
Locale.setDefault(locale)
val configuration = Configuration()
configuration.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.createConfigurationContext(configuration)
}
updateConfiguration(configuration, displayMetrics)
}
}

View file

@ -0,0 +1,365 @@
package com.habitrpg.android.habitica.ui.activities
import android.app.Activity
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
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.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.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.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.user.User
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.common.habitica.extensions.DataBindingUtils
import javax.inject.Inject
class BirthdayActivity : BaseActivity() {
@Inject
lateinit var userViewModel: MainUserViewModel
override fun getLayoutResId(): Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HabiticaTheme {
val user = userViewModel.user.observeAsState()
BirthdayActivityView(user.value)
}
}
}
override fun injectActivity(component: UserComponent?) {
component?.inject(this)
}
}
@Composable
fun BirthdayTitle(text: String) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 8.dp)
) {
Box(
modifier = Modifier
.height(1.dp)
.weight(1f)
.background(colorResource(id = R.color.brand_50))
)
Image(painterResource(id = R.drawable.birthday_textdeco_left), null)
Text(
text,
fontSize = 16.sp,
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 16.dp)
)
Image(painterResource(id = R.drawable.birthday_textdeco_right), null)
Box(
modifier = Modifier
.height(1.dp)
.weight(1f)
.background(colorResource(id = R.color.brand_50))
)
}
}
@Composable
fun BirthdayActivityView(user: User?) {
val activity = LocalContext.current as? Activity
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
)
)
}
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(
"X to Y",
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(
Modifier
.padding(vertical = 20.dp)
.size(161.dp, 129.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)
)
val ownsGryphatrice = false
if (ownsGryphatrice) {
Text(
stringResource(R.string.thanks_for_support),
fontSize = 12.sp,
color = textColor,
fontWeight = FontWeight.SemiBold
)
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("")
}
append(" or ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append("60 Gems")
}
}, color = Color.White)
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{},
modifier = Modifier.padding(top = 20.dp)
) {
Text(stringResource(R.string.buy_for_x, ""))
}
HabiticaButton(
Color.White,
colorResource(R.color.brand_200),
{},
modifier = Modifier.padding(top = 20.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.buy_for))
CurrencyText(currency = "gems", value = 60)
}
}
}
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),
{},
modifier = Modifier.padding(top = 20.dp)
) {
Text(stringResource(R.string.visit_the_market))
}
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
)
}
}
}
@Composable
fun PotionGrid() {
val potions = listOf(
"Porcelain",
"Vampire",
"Aquatic",
"StainedGlass",
"Celestial",
"Glow",
"AutumnLeaf",
"SandSculpture",
"Peppermint",
"Shimmer"
).windowed(4, 4, true)
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))
}
}
}
}
}
}
@Composable
fun HabiticaButton(
background: Color,
color: Color,
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Box(contentAlignment = Alignment.Center, modifier = modifier
.background(background, HabiticaTheme.shapes.medium)
.clickable { onClick() }
.fillMaxWidth()
.padding(8.dp)) {
ProvideTextStyle(
value = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = color
)
) {
content()
}
}
}
@Preview(device = Devices.PIXEL_4)
@Composable
private fun Preview() {
BirthdayActivityView(null)
}

View file

@ -131,6 +131,7 @@ class FullProfileActivity : BaseActivity() {
binding.adminStatusView.isVisible = isModerator
if (isModerator) {
val member = socialRepository.retrieveMember(userID, true)
member?.stats = this@FullProfileActivity.member.value?.stats
if (member != null) {
updateView(member)
}

View file

@ -15,7 +15,6 @@ import android.text.InputType
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.UnderlineSpan
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.Window
@ -43,7 +42,6 @@ import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
@ -70,43 +68,59 @@ class LoginActivity : BaseActivity() {
private val loginClick = View.OnClickListener {
binding.PBAsyncTask.visibility = View.VISIBLE
if (isRegistering) {
val username: String = binding.username.text.toString().trim { it <= ' ' }
val email: String = binding.email.text.toString().trim { it <= ' ' }
val password: String = binding.password.text.toString()
val confirmPassword: String = binding.confirmPassword.text.toString()
if (username.isEmpty() || password.isEmpty() || email.isEmpty() || confirmPassword.isEmpty()) {
showValidationError(R.string.login_validation_error_fieldsmissing)
return@OnClickListener
}
if (password.length < configManager.minimumPasswordLength()) {
showValidationError(getString(R.string.password_too_short, configManager.minimumPasswordLength()))
return@OnClickListener
}
lifecycleScope.launch(ExceptionHandler.coroutine {
hideProgress()
ExceptionHandler.reportError(it)
}) {
val response = apiClient.registerUser(username, email, password, confirmPassword)
if (response != null) {
handleAuthResponse(response)
}
}
registerWithPassword()
} else {
val username: String = binding.username.text.toString().trim { it <= ' ' }
val password: String = binding.password.text.toString()
if (username.isEmpty() || password.isEmpty()) {
showValidationError(R.string.login_validation_error_fieldsmissing)
return@OnClickListener
}
Log.d("LoginActivity", ": $username, $password")
lifecycleScope.launch(ExceptionHandler.coroutine {
loginWithPassword()
}
}
private fun loginWithPassword() {
val username: String = binding.username.text.toString().trim { it <= ' ' }
val password: String = binding.password.text.toString()
if (username.isEmpty() || password.isEmpty()) {
showValidationError(R.string.login_validation_error_fieldsmissing)
return
}
lifecycleScope.launch(ExceptionHandler.coroutine {
hideProgress()
ExceptionHandler.reportError(it)
}) {
val response = apiClient.connectUser(username, password)
if (response != null) {
handleAuthResponse(response)
} else {
hideProgress()
}
}
}
private fun registerWithPassword() {
val username: String = binding.username.text.toString().trim { it <= ' ' }
val email: String = binding.email.text.toString().trim { it <= ' ' }
val password: String = binding.password.text.toString()
val confirmPassword: String = binding.confirmPassword.text.toString()
if (username.isEmpty() || password.isEmpty() || email.isEmpty() || confirmPassword.isEmpty()) {
showValidationError(R.string.login_validation_error_fieldsmissing)
return
}
if (password.length < configManager.minimumPasswordLength()) {
showValidationError(
getString(
R.string.password_too_short,
configManager.minimumPasswordLength()
)
)
return
}
lifecycleScope.launch(ExceptionHandler.coroutine {
hideProgress()
ExceptionHandler.reportError(it)
}) {
val response = apiClient.registerUser(username, email, password, confirmPassword)
if (response != null) {
handleAuthResponse(response)
} else {
hideProgress()
ExceptionHandler.reportError(it)
}) {
val response = apiClient.connectUser(username, password)
if (response != null) {
handleAuthResponse(response)
}
}
}
}
@ -264,15 +278,10 @@ class LoginActivity : BaseActivity() {
} catch (e: Exception) {
// Wearable API is not available on this device.
}
lifecycleScope.launch(ExceptionHandler.coroutine()) {
val user = userRepository.retrieveUser(true)
if (user != null) {
handleAuthResponse(user, response.newUser)
}
}
handleAuthResponse(response.newUser)
}
private fun handleAuthResponse(user: User, isNew: Boolean) {
private fun handleAuthResponse(isNew: Boolean) {
hideProgress()
dismissKeyboard()
@ -319,8 +328,8 @@ class LoginActivity : BaseActivity() {
private val pickAccountResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
viewModel.handleGoogleLoginResult(this, recoverFromPlayServicesErrorResult) { user, isNew ->
handleAuthResponse(user, isNew)
viewModel.handleGoogleLoginResult(this, recoverFromPlayServicesErrorResult) { isNew ->
handleAuthResponse(isNew)
}
}
}
@ -329,8 +338,8 @@ class LoginActivity : BaseActivity() {
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_CANCELED) {
viewModel.handleGoogleLoginResult(this, null) { user, isNew ->
handleAuthResponse(user, isNew)
viewModel.handleGoogleLoginResult(this, null) { isNew ->
handleAuthResponse(isNew)
}
}
}

View file

@ -10,6 +10,7 @@ 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
@ -17,6 +18,7 @@ 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

View file

@ -30,7 +30,6 @@ import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.fragments.setup.AvatarSetupFragment
import com.habitrpg.android.habitica.ui.fragments.setup.TaskSetupFragment
import com.habitrpg.android.habitica.ui.fragments.setup.WelcomeFragment
import com.habitrpg.common.habitica.api.HostConfig
import com.viewpagerindicator.IconPagerAdapter
import kotlinx.coroutines.launch
import java.util.Calendar
@ -44,8 +43,6 @@ class SetupActivity : BaseActivity(), ViewPager.OnPageChangeListener {
@Inject
lateinit var apiClient: ApiClient
@Inject
lateinit var hostConfig: HostConfig
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var taskRepository: TaskRepository

View file

@ -4,15 +4,14 @@ 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
import android.os.Handler
import android.text.Spannable
import android.text.SpannableString
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.text.style.ForegroundColorSpan
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
@ -25,10 +24,12 @@ import androidx.appcompat.widget.AppCompatCheckBox
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.toMutableStateList
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.forEachIndexed
import androidx.core.view.isVisible
import androidx.core.view.iterator
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
@ -50,6 +51,7 @@ import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.social.Challenge
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskGroupPlan
import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
@ -169,6 +171,9 @@ class TaskFormActivity : BaseActivity() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
if (forcedTheme == "taskform" || forcedTheme == "maroon") {
ToolbarColorHelper.colorizeToolbar(binding.toolbar, this, ContextCompat.getColor(this, R.color.white))
}
tintColor = getThemeColor(R.attr.taskFormTint)
val upperTintColor =
if (forcedTheme == "taskform") getThemeColor(R.attr.taskFormTint) else getThemeColor(R.attr.colorAccent)
@ -388,6 +393,13 @@ class TaskFormActivity : BaseActivity() {
menuInflater.inflate(R.menu.menu_task_edit, menu)
}
menu.findItem(R.id.action_save).isEnabled = canSave
if (forcedTheme == "taskform" || forcedTheme == "maroon") {
menu.iterator().forEach {
val spannable = SpannableString(it.title)
spannable.setSpan(ForegroundColorSpan(Color.WHITE), 0, spannable.length, 0)
it.title = spannable
}
}
return true
}

View file

@ -6,6 +6,7 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -53,14 +55,18 @@ import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.BaseViewModel
import com.habitrpg.android.habitica.ui.views.CompletedAt
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.UserRow
import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.flow.Flow
import java.text.DateFormat
import java.util.Date
import javax.inject.Inject
class TaskSummaryViewModel(val taskId: String) : BaseViewModel() {
@Inject
lateinit var taskRespository: TaskRepository
@Inject
lateinit var socialRepository: SocialRepository
@ -117,7 +123,9 @@ fun TaskSummaryView(viewModel: TaskSummaryViewModel) {
if (task != null) {
val darkestColor = HabiticaTheme.colors.textPrimaryFor(task)
val topTextColor = if ((task?.value ?: 0.0) >= -20) colorResource(task?.extraDarkTaskColor ?: R.color.white) else Color.White
val topTextColor = if ((task?.value ?: 0.0) >= -20) colorResource(
task?.extraDarkTaskColor ?: R.color.white
) else Color.White
val systemUiController = rememberSystemUiController()
val statusBarColor = HabiticaTheme.colors.primaryBackgroundFor(task)
val lightestColor = HabiticaTheme.colors.contentBackgroundFor(task)
@ -204,11 +212,40 @@ fun TaskSummaryView(viewModel: TaskSummaryViewModel) {
fontWeight = FontWeight.Medium,
modifier = titleModifier
)
Text(task?.let { taskDescriptionBuilder.describe(it) }!!.makeBoldComposable(),
Text(
task?.let { taskDescriptionBuilder.describe(it) }!!.makeBoldComposable(),
fontSize = 16.sp,
color = darkestColor,
fontWeight = FontWeight.Normal,
modifier = textModifier)
modifier = textModifier
)
}
if (task?.type == TaskType.REWARD) {
Text(
stringResource(R.string.cost),
fontSize = 16.sp,
color = darkestColor,
fontWeight = FontWeight.Medium,
modifier = titleModifier.padding(bottom = 4.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.padding(vertical = 4.dp)
.background(
HabiticaTheme.colors.windowBackgroundFor(task),
MaterialTheme.shapes.medium
)
.padding(15.dp)
.fillMaxWidth()
) {
Image(HabiticaIconsHelper.imageOfGold().asImageBitmap(), null)
Text("${task?.value}",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = darkestColor
)
}
}
if (task?.checklist?.isNotEmpty() == true) {
task?.checklist?.let { checklist ->
@ -263,8 +300,22 @@ fun TaskSummaryView(viewModel: TaskSummaryViewModel) {
}) else null
)
}
task?.group?.assignedUsersDetail?.find { it.assignedUserID == viewModel.userViewModel.userID }?.let {
Text("", )
task?.group?.assignedUsersDetail?.find { it.assignedUserID == viewModel.userViewModel.userID }
?.let {
Text("")
}
task?.group?.assignedDetailsFor(viewModel.userViewModel.userID)?.let {
val formatter = DateFormat.getDateInstance(DateFormat.SHORT)
Text(
stringResource(
R.string.assigned_to_you_by,
it.assigningUsername ?: "",
formatter.format(it.assignedDate ?: Date())
),
fontSize = 14.sp,
color = HabiticaTheme.colors.textSecondaryFor(task),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)
)
}
}
}

View file

@ -0,0 +1,150 @@
package com.habitrpg.android.habitica.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.CustomizationGridItemBinding
import com.habitrpg.android.habitica.databinding.DialogPurchaseCustomizationBinding
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.inventory.CustomizationSet
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.common.habitica.views.PixelArtView
class CustomizationEquipmentRecyclerViewAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<androidx.recyclerview.widget.RecyclerView.ViewHolder>() {
var gemBalance: Int = 0
var equipmentList: MutableList<Equipment> =
ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
var activeEquipment: String? = null
set(value) {
field = value
this.notifyDataSetChanged()
}
var onSelect: ((Equipment) -> Unit)? = null
var onUnlock: ((Equipment) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): androidx.recyclerview.widget.RecyclerView.ViewHolder {
val viewID: Int = R.layout.customization_grid_item
val view = LayoutInflater.from(parent.context).inflate(viewID, parent, false)
return EquipmentViewHolder(view)
}
override fun onBindViewHolder(
holder: androidx.recyclerview.widget.RecyclerView.ViewHolder,
position: Int
) {
(holder as EquipmentViewHolder).bind(equipmentList[position])
}
override fun getItemCount(): Int {
return equipmentList.size
}
override fun getItemViewType(position: Int): Int {
if (equipmentList.size <= position) return 0
return if (this.equipmentList[position].javaClass == CustomizationSet::class.java) {
0
} else {
1
}
}
fun setEquipment(newEquipmentList: List<Equipment>) {
this.equipmentList = newEquipmentList.toMutableList()
val emptyEquipment = Equipment()
equipmentList.add(0, emptyEquipment)
this.notifyDataSetChanged()
}
internal inner class EquipmentViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val binding = CustomizationGridItemBinding.bind(itemView)
var equipment: Equipment? = null
init {
itemView.setOnClickListener(this)
}
fun bind(equipment: Equipment) {
this.equipment = equipment
binding.imageView.loadImage("shop_" + this.equipment?.key)
if (equipment.owned == true || equipment.value == 0.0) {
binding.buyButton.visibility = View.GONE
} else {
binding.buyButton.visibility = View.VISIBLE
binding.priceLabel.currency = "gems"
binding.priceLabel.value = if (equipment.gearSet == "animal") {
2.0
} else {
equipment.value
}
}
if (activeEquipment == equipment.key || (activeEquipment?.contains("base_0") == true && equipment.key?.isNotBlank() != true)) {
binding.wrapper.background = ContextCompat.getDrawable(itemView.context, R.drawable.layout_rounded_bg_window_tint_border)
} else {
binding.wrapper.background = ContextCompat.getDrawable(itemView.context, R.drawable.layout_rounded_bg_window)
}
}
override fun onClick(v: View) {
if (equipment?.owned != true && (equipment?.value ?: 0.0) > 0.0) {
val dialogContent = LinearLayout(itemView.context)
DialogPurchaseCustomizationBinding.inflate(LayoutInflater.from(itemView.context), dialogContent)
val imageView = dialogContent.findViewById<PixelArtView>(R.id.imageView)
imageView.loadImage("shop_" + this.equipment?.key)
val priceLabel = dialogContent.findViewById<TextView>(R.id.priceLabel)
priceLabel.text = if (equipment?.gearSet == "animal") {
2.0
} else {
equipment?.value ?: 0
}.toString()
(dialogContent.findViewById<View>(R.id.gem_icon) as? ImageView)?.setImageBitmap(
HabiticaIconsHelper.imageOfGem())
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
}
equipment?.let {
onUnlock?.invoke(it)
}
}
dialog.setTitle(R.string.purchase_customization)
dialog.setAdditionalContentView(dialogContent)
dialog.addButton(R.string.reward_dialog_dismiss, false)
dialog.show()
return
}
if (equipment?.key == activeEquipment) {
return
}
equipment?.let {
onSelect?.invoke(it)
}
}
}
}

View file

@ -3,12 +3,15 @@ package com.habitrpg.android.habitica.ui.adapter
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.ui.platform.ComposeView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.inflate
import com.habitrpg.android.habitica.models.promotions.HabiticaPromotion
import com.habitrpg.android.habitica.ui.menu.HabiticaDrawerItem
import com.habitrpg.android.habitica.ui.viewHolders.ComposableViewHolder
import com.habitrpg.android.habitica.ui.views.promo.BirthdayBanner
import com.habitrpg.android.habitica.ui.views.promo.PromoMenuView
import com.habitrpg.android.habitica.ui.views.promo.PromoMenuViewHolder
import com.habitrpg.android.habitica.ui.views.promo.SubscriptionBuyGemsPromoView
@ -89,6 +92,11 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
}
}
}
getItemViewType(position) == 6 -> {
(holder.itemView as? ComposeView)?.setContent {
BirthdayBanner()
}
}
}
}
@ -122,6 +130,7 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
)
PromoMenuViewHolder(promoView)
}
6 -> ComposableViewHolder(ComposeView(parent.context))
1 -> SectionHeaderViewHolder(parent.inflate(R.layout.drawer_main_section_header))
else -> DrawerItemViewHolder(parent.inflate(R.layout.drawer_main_item))
}

View file

@ -546,6 +546,12 @@ class NavigationDrawerFragment : DialogFragment() {
item.itemViewType = 2
items.add(item)
}
if (configManager.isBirthday()) {
val birthdayItem = HabiticaDrawerItem(R.id.birthdayActivity, SIDEBAR_BIRTHDAY)
birthdayItem.itemViewType = 6
items.add(0, birthdayItem)
}
adapter.updateItems(items)
}
@ -805,6 +811,7 @@ class NavigationDrawerFragment : DialogFragment() {
const val SIDEBAR_GEMS = "gems"
const val SIDEBAR_SUBSCRIPTION = "subscription"
const val SIDEBAR_SUBSCRIPTION_PROMO = "subscriptionpromo"
const val SIDEBAR_BIRTHDAY = "birthday"
const val SIDEBAR_PROMO = "promo"
const val SIDEBAR_ABOUT_HEADER = "about_header"
const val SIDEBAR_NEWS = "news"

View file

@ -0,0 +1,148 @@
package com.habitrpg.android.habitica.ui.fragments.inventory.customization
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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.databinding.FragmentRefreshRecyclerviewBinding
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.CustomizationEquipmentRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.helpers.MarginDecoration
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class AvatarEquipmentFragment :
BaseMainFragment<FragmentRefreshRecyclerviewBinding>(),
SwipeRefreshLayout.OnRefreshListener {
@Inject
lateinit var inventoryRepository: InventoryRepository
@Inject
lateinit var userViewModel: MainUserViewModel
override var binding: FragmentRefreshRecyclerviewBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRefreshRecyclerviewBinding {
return FragmentRefreshRecyclerviewBinding.inflate(inflater, container, false)
}
var type: String? = null
var category: String? = null
private var activeEquipment: String? = null
internal var adapter: CustomizationEquipmentRecyclerViewAdapter = CustomizationEquipmentRecyclerViewAdapter()
internal var layoutManager: GridLayoutManager = GridLayoutManager(activity, 2)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
showsBackButton = true
adapter.onSelect = { equipment ->
val key = (if (equipment.key?.isNotBlank() != true) activeEquipment else equipment.key) ?: ""
lifecycleScope.launchCatching {
inventoryRepository.equip(if (userViewModel.user.value?.preferences?.costume == true) "costume" else "equipped", key)
}
}
adapter.onUnlock = { equipment ->
lifecycleScope.launchCatching { }
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
showsBackButton = true
super.onViewCreated(view, savedInstanceState)
arguments?.let {
val args = AvatarEquipmentFragmentArgs.fromBundle(it)
type = args.type
if (args.category.isNotEmpty()) {
category = args.category
}
}
binding?.refreshLayout?.setOnRefreshListener(this)
setGridSpanCount(view.width)
val layoutManager = GridLayoutManager(activity, 4)
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (adapter.getItemViewType(position) == 0) {
layoutManager.spanCount
} else {
1
}
}
}
binding?.recyclerView?.layoutManager = layoutManager
binding?.recyclerView?.addItemDecoration(MarginDecoration(context))
binding?.recyclerView?.adapter = adapter
binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator()
this.loadEquipment()
userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
}
override fun injectFragment(component: UserComponent) {
component.inject(this)
}
private fun loadEquipment() {
val type = this.type ?: return
lifecycleScope.launchCatching {
inventoryRepository.getEquipmentType(type, category ?: "").collect {
adapter.setEquipment(it)
}
}
}
private fun setGridSpanCount(width: Int) {
val itemWidth = context?.resources?.getDimension(R.dimen.customization_width) ?: 0F
var spanCount = (width / itemWidth).toInt()
if (spanCount == 0) {
spanCount = 1
}
layoutManager.spanCount = spanCount
}
fun updateUser(user: User?) {
this.updateActiveCustomization(user)
this.adapter.gemBalance = user?.gemCount ?: 0
adapter.notifyDataSetChanged()
}
private fun updateActiveCustomization(user: User?) {
if (this.type == null || user?.preferences == null) {
return
}
val outfit = if (user.preferences?.costume == true) user.items?.gear?.costume else user.items?.gear?.equipped
val activeEquipment = when (this.type) {
"headAccessory" -> outfit?.headAccessory
"back" -> outfit?.back
"eyewear" -> outfit?.eyeWear
else -> ""
}
if (activeEquipment != null) {
this.activeEquipment = activeEquipment
this.adapter.activeEquipment = activeEquipment
}
}
override fun onRefresh() {
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.retrieveUser(true, true)
binding?.refreshLayout?.isRefreshing = false
}
}
}

View file

@ -29,9 +29,9 @@ import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.AvatarCustomizationOverviewView
import com.habitrpg.android.habitica.ui.views.EquipmentOverviewView
import com.habitrpg.android.habitica.ui.views.SegmentedControl
import com.habitrpg.android.habitica.ui.views.equipment.AvatarCustomizationOverviewView
import com.habitrpg.android.habitica.ui.views.equipment.EquipmentOverviewView
import javax.inject.Inject
open class AvatarOverviewFragment : BaseMainFragment<FragmentComposeScrollingBinding>(),
@ -65,7 +65,9 @@ open class AvatarOverviewFragment : BaseMainFragment<FragmentComposeScrollingBin
showCustomization, !showCustomization,
{ type, category ->
displayCustomizationFragment(type, category)
}, { type, equipped, isCostume ->
}, { type, category ->
displayAvatarEquipmentFragment(type, category)
}, { type, equipped, isCostume ->
displayEquipmentFragment(type, equipped, isCostume)
})
}
@ -87,6 +89,10 @@ open class AvatarOverviewFragment : BaseMainFragment<FragmentComposeScrollingBin
)
}
private fun displayAvatarEquipmentFragment(type: String, category: String?) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openAvatarEquipment(type, category ?: ""))
}
private fun displayEquipmentFragment(type: String, equipped: String?, isCostume: Boolean = false) {
MainNavigationController.navigate(AvatarOverviewFragmentDirections.openEquipmentDetail(type, isCostume, equipped ?: ""))
}
@ -108,6 +114,7 @@ fun AvatarOverviewView(userViewModel: MainUserViewModel,
showCustomization: Boolean = true,
showEquipment: Boolean = true,
onCustomizationTap: (String, String?) -> Unit,
onAvatarEquipmentTap: (String, String?) -> Unit,
onEquipmentTap: (String, String?, Boolean) -> Unit
) {
val user by userViewModel.user.observeAsState()
@ -139,7 +146,7 @@ fun AvatarOverviewView(userViewModel: MainUserViewModel,
)
})
}
AvatarCustomizationOverviewView(user?.preferences, onCustomizationTap)
AvatarCustomizationOverviewView(user?.preferences, user?.items?.gear?.equipped, onCustomizationTap, onAvatarEquipmentTap)
}
if (showEquipment) {
Row(

View file

@ -220,7 +220,7 @@ class AccountPreferenceFragment :
if (it.resultCode == Activity.RESULT_OK) {
viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
activity?.let { it1 ->
viewModel.handleGoogleLoginResult(it1, recoverFromPlayServicesErrorResult) { _, _ ->
viewModel.handleGoogleLoginResult(it1, recoverFromPlayServicesErrorResult) { _ ->
displayAuthenticationSuccess(getString(R.string.google))
}
}
@ -246,7 +246,7 @@ class AccountPreferenceFragment :
) {
if (it.resultCode != Activity.RESULT_CANCELED) {
activity?.let { it1 ->
viewModel.handleGoogleLoginResult(it1, null) { _, _ ->
viewModel.handleGoogleLoginResult(it1, null) { _ ->
displayAuthenticationSuccess(getString(R.string.google))
}
}

View file

@ -225,7 +225,9 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
pushNotificationManager.addPushDeviceUsingStoredToken()
}
} else {
pushNotificationManager.removePushDeviceUsingStoredToken()
lifecycleScope.launchCatching {
pushNotificationManager.removePushDeviceUsingStoredToken()
}
}
}
"useEmails" -> {

View file

@ -137,19 +137,19 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
context?.let { recyclerAdapter?.taskDisplayMode = configManager.taskDisplayMode(it) }
recyclerAdapter?.errorButtonEvents = {
lifecycleScope.launchCatching {
taskRepository.syncErroredTasks()
}
lifecycleScope.launchCatching {
taskRepository.syncErroredTasks()
}
}
recyclerAdapter?.taskOpenEvents = { task, view ->
openTaskForm(task)
}
recyclerAdapter?.taskScoreEvents = { task, direction ->
playSound(direction)
context?.let { it1 -> notificationsManager.dismissTaskNotification(it1, task) }
playSound(direction)
context?.let { it1 -> notificationsManager.dismissTaskNotification(it1, task) }
scoreTask(task, direction)
}
recyclerAdapter?.checklistItemScoreEvents = { task, item ->
}
recyclerAdapter?.checklistItemScoreEvents = { task, item ->
scoreChecklistItem(task, item)
}
recyclerAdapter?.brokenTaskEvents = { showBrokenChallengeDialog(it) }
@ -165,6 +165,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
lifecycleScope.launch {
viewModel.userViewModel.user.asFlow()
.onEach { recyclerAdapter?.user = it }
.map { it?.preferences?.tasks?.mirrorGroupTasks }
.distinctUntilChanged()
.collect {
@ -364,6 +365,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
recyclerAdapter?.user = it
}
}
setPreferenceTaskFilters()
}
private fun updateTaskSubscription(ownerID: String?) {
@ -419,215 +421,215 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
dialog.setExtraCloseButtonVisibility(View.VISIBLE)
dialog.show()
}
}
}
private fun setEmptyLabels() {
binding?.recyclerView?.emptyItem = if (viewModel.filterCount(taskType) > 0) {
when (this.taskType) {
TaskType.HABIT -> {
EmptyItem(
getString(R.string.empty_title_habits_filtered),
getString(R.string.empty_description_habits_filtered),
R.drawable.icon_habits
)
}
TaskType.DAILY -> {
EmptyItem(
getString(R.string.empty_title_dailies_filtered),
getString(R.string.empty_description_dailies_filtered),
R.drawable.icon_dailies
)
}
TaskType.TODO -> {
EmptyItem(
getString(R.string.empty_title_todos_filtered),
getString(R.string.empty_description_todos_filtered),
R.drawable.icon_todos
)
}
TaskType.REWARD -> {
EmptyItem(
getString(R.string.empty_title_rewards_filtered),
null,
R.drawable.icon_rewards
)
}
else -> EmptyItem("")
}
} else {
when (this.taskType) {
TaskType.HABIT -> {
EmptyItem(
getString(R.string.empty_title_habits),
getString(R.string.empty_description_habits),
R.drawable.icon_habits
)
}
TaskType.DAILY -> {
EmptyItem(
getString(R.string.empty_title_dailies),
getString(R.string.empty_description_dailies),
R.drawable.icon_dailies
)
}
TaskType.TODO -> {
EmptyItem(
getString(R.string.empty_title_todos),
getString(R.string.empty_description_todos),
R.drawable.icon_todos
)
}
TaskType.REWARD -> {
EmptyItem(
getString(R.string.empty_title_rewards),
null,
R.drawable.icon_rewards
)
}
else -> EmptyItem("")
}
}
}
private fun scoreTask(task: Task, direction: TaskDirection) {
viewModel.scoreTask(task, direction) { result, value ->
handleTaskResult(result, value)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(CLASS_TYPE_KEY, this.taskType.value)
}
override val displayedClassName: String?
get() = this.taskType.value + super.displayedClassName
override fun onRefresh() {
binding?.refreshLayout?.isRefreshing = true
viewModel.refreshData {
binding?.refreshLayout?.isRefreshing = false
}
}
override fun onResume() {
super.onResume()
context?.let { recyclerAdapter?.taskDisplayMode = configManager.taskDisplayMode(it) }
setInnerAdapter()
}
fun setActiveFilter(activeFilter: String) {
viewModel.setActiveFilter(taskType, activeFilter)
recyclerAdapter?.filter()
setEmptyLabels()
if (activeFilter == Task.FILTER_COMPLETED) {
lifecycleScope.launchCatching {
taskRepository.retrieveCompletedTodos()
}
}
}
private fun setPreferenceTaskFilters() {
(activity as? MainActivity)?.viewModel?.user?.observeOnce(this) {
if (it != null) {
when (taskType) {
TaskType.TODO -> viewModel.setActiveFilter(
TaskType.TODO,
Task.FILTER_ACTIVE
)
TaskType.DAILY -> {
if (!viewModel.initialPreferenceFilterSet) {
viewModel.initialPreferenceFilterSet = true
if (it.isValid && it.preferences?.dailyDueDefaultView == true) {
viewModel.setActiveFilter(TaskType.DAILY, Task.FILTER_ACTIVE)
}
}
}
else -> {}
}
}
}
}
private fun openTaskForm(task: Task) {
if (Date().time - (TasksFragment.lastTaskFormOpen?.time
?: 0) < 2000 || !task.isValid
) {
return
}
val bundle = Bundle()
bundle.putString(TaskFormActivity.TASK_TYPE_KEY, task.type?.value)
bundle.putString(TaskFormActivity.TASK_ID_KEY, task.id)
bundle.putString(TaskFormActivity.GROUP_ID_KEY, task.group?.groupID)
bundle.putDouble(TaskFormActivity.TASK_VALUE_KEY, task.value)
lifecycleScope.launchCatching {
val id = if (viewModel.canEditTask(task)) {
R.id.taskFormActivity
} else {
R.id.taskSummaryActivity
}
withContext(Dispatchers.Main) {
MainNavigationController.navigate(id, bundle)
}
}
TasksFragment.lastTaskFormOpen = Date()
}
companion object {
private const val CLASS_TYPE_KEY = "CLASS_TYPE_KEY"
fun newInstance(context: Context?, classType: TaskType): TaskRecyclerViewFragment {
val fragment = TaskRecyclerViewFragment()
fragment.taskType = classType
var tutorialTexts: List<String>? = null
if (context != null) {
when (fragment.taskType) {
private fun setEmptyLabels() {
binding?.recyclerView?.emptyItem = if (viewModel.filterCount(taskType) > 0) {
when (this.taskType) {
TaskType.HABIT -> {
fragment.tutorialStepIdentifier = "habits"
tutorialTexts = listOf(
context.getString(R.string.tutorial_overview),
context.getString(R.string.tutorial_habits_1),
context.getString(R.string.tutorial_habits_2),
context.getString(R.string.tutorial_habits_3),
context.getString(R.string.tutorial_habits_4)
EmptyItem(
getString(R.string.empty_title_habits_filtered),
getString(R.string.empty_description_habits_filtered),
R.drawable.icon_habits
)
}
TaskType.DAILY -> {
fragment.tutorialStepIdentifier = "dailies"
tutorialTexts = listOf(
context.getString(R.string.tutorial_dailies_1),
context.getString(R.string.tutorial_dailies_2)
EmptyItem(
getString(R.string.empty_title_dailies_filtered),
getString(R.string.empty_description_dailies_filtered),
R.drawable.icon_dailies
)
}
TaskType.TODO -> {
fragment.tutorialStepIdentifier = "todos"
tutorialTexts = listOf(
context.getString(R.string.tutorial_todos_1),
context.getString(R.string.tutorial_todos_2)
EmptyItem(
getString(R.string.empty_title_todos_filtered),
getString(R.string.empty_description_todos_filtered),
R.drawable.icon_todos
)
}
TaskType.REWARD -> {
fragment.tutorialStepIdentifier = "rewards"
tutorialTexts = listOf(
context.getString(R.string.tutorial_rewards_1),
context.getString(R.string.tutorial_rewards_2)
EmptyItem(
getString(R.string.empty_title_rewards_filtered),
null,
R.drawable.icon_rewards
)
}
else -> EmptyItem("")
}
} else {
when (this.taskType) {
TaskType.HABIT -> {
EmptyItem(
getString(R.string.empty_title_habits),
getString(R.string.empty_description_habits),
R.drawable.icon_habits
)
}
TaskType.DAILY -> {
EmptyItem(
getString(R.string.empty_title_dailies),
getString(R.string.empty_description_dailies),
R.drawable.icon_dailies
)
}
TaskType.TODO -> {
EmptyItem(
getString(R.string.empty_title_todos),
getString(R.string.empty_description_todos),
R.drawable.icon_todos
)
}
TaskType.REWARD -> {
EmptyItem(
getString(R.string.empty_title_rewards),
null,
R.drawable.icon_rewards
)
}
else -> EmptyItem("")
}
}
}
private fun scoreTask(task: Task, direction: TaskDirection) {
viewModel.scoreTask(task, direction) { result, value ->
handleTaskResult(result, value)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(CLASS_TYPE_KEY, this.taskType.value)
}
override val displayedClassName: String?
get() = this.taskType.value + super.displayedClassName
override fun onRefresh() {
binding?.refreshLayout?.isRefreshing = true
viewModel.refreshData {
binding?.refreshLayout?.isRefreshing = false
}
}
override fun onResume() {
super.onResume()
context?.let { recyclerAdapter?.taskDisplayMode = configManager.taskDisplayMode(it) }
setInnerAdapter()
}
fun setActiveFilter(activeFilter: String) {
viewModel.setActiveFilter(taskType, activeFilter)
recyclerAdapter?.filter()
setEmptyLabels()
if (activeFilter == Task.FILTER_COMPLETED) {
lifecycleScope.launchCatching {
taskRepository.retrieveCompletedTodos()
}
}
}
private fun setPreferenceTaskFilters() {
(activity as? MainActivity)?.viewModel?.user?.observeOnce(this) {
if (it != null) {
when (taskType) {
TaskType.TODO -> viewModel.setActiveFilter(
TaskType.TODO,
Task.FILTER_ACTIVE
)
TaskType.DAILY -> {
if (!viewModel.initialPreferenceFilterSet) {
viewModel.initialPreferenceFilterSet = true
if (it.isValid && it.preferences?.dailyDueDefaultView == true) {
viewModel.setActiveFilter(TaskType.DAILY, Task.FILTER_ACTIVE)
}
}
}
else -> {}
}
}
}
}
if (tutorialTexts != null) {
fragment.tutorialTexts = ArrayList(tutorialTexts)
private fun openTaskForm(task: Task) {
if (Date().time - (TasksFragment.lastTaskFormOpen?.time
?: 0) < 2000 || !task.isValid
) {
return
}
fragment.tutorialCanBeDeferred = false
return fragment
val bundle = Bundle()
bundle.putString(TaskFormActivity.TASK_TYPE_KEY, task.type?.value)
bundle.putString(TaskFormActivity.TASK_ID_KEY, task.id)
bundle.putString(TaskFormActivity.GROUP_ID_KEY, task.group?.groupID)
bundle.putDouble(TaskFormActivity.TASK_VALUE_KEY, task.value)
lifecycleScope.launchCatching {
val id = if (viewModel.canEditTask(task)) {
R.id.taskFormActivity
} else {
R.id.taskSummaryActivity
}
withContext(Dispatchers.Main) {
MainNavigationController.navigate(id, bundle)
}
}
TasksFragment.lastTaskFormOpen = Date()
}
companion object {
private const val CLASS_TYPE_KEY = "CLASS_TYPE_KEY"
fun newInstance(context: Context?, classType: TaskType): TaskRecyclerViewFragment {
val fragment = TaskRecyclerViewFragment()
fragment.taskType = classType
var tutorialTexts: List<String>? = null
if (context != null) {
when (fragment.taskType) {
TaskType.HABIT -> {
fragment.tutorialStepIdentifier = "habits"
tutorialTexts = listOf(
context.getString(R.string.tutorial_overview),
context.getString(R.string.tutorial_habits_1),
context.getString(R.string.tutorial_habits_2),
context.getString(R.string.tutorial_habits_3),
context.getString(R.string.tutorial_habits_4)
)
}
TaskType.DAILY -> {
fragment.tutorialStepIdentifier = "dailies"
tutorialTexts = listOf(
context.getString(R.string.tutorial_dailies_1),
context.getString(R.string.tutorial_dailies_2)
)
}
TaskType.TODO -> {
fragment.tutorialStepIdentifier = "todos"
tutorialTexts = listOf(
context.getString(R.string.tutorial_todos_1),
context.getString(R.string.tutorial_todos_2)
)
}
TaskType.REWARD -> {
fragment.tutorialStepIdentifier = "rewards"
tutorialTexts = listOf(
context.getString(R.string.tutorial_rewards_1),
context.getString(R.string.tutorial_rewards_2)
)
}
}
}
if (tutorialTexts != null) {
fragment.tutorialTexts = ArrayList(tutorialTexts)
}
fragment.tutorialCanBeDeferred = false
return fragment
}
}
}
}

View file

@ -26,8 +26,8 @@ import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import com.habitrpg.android.habitica.R
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.waitForLayout
import com.habitrpg.common.habitica.extensions.getThemeColor
/**
* Helper class that iterates through Toolbar views, and sets dynamically icons and texts color
@ -42,10 +42,12 @@ object ToolbarColorHelper {
fun colorizeToolbar(
toolbar: Toolbar,
activity: Activity?,
iconColor: Int? = null,
backgroundColor: Int? = null
) {
if (activity == null) return
toolbar.setBackgroundColor(activity.getThemeColor(R.attr.headerBackgroundColor))
val toolbarIconsColor = activity.getThemeColor(R.attr.headerTextColor)
toolbar.setBackgroundColor(backgroundColor ?: activity.getThemeColor(R.attr.headerBackgroundColor))
val toolbarIconsColor = iconColor ?: activity.getThemeColor(R.attr.headerTextColor)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
activity.window.statusBarColor = activity.getThemeColor(R.attr.colorPrimaryDark)
}

View file

@ -139,6 +139,7 @@ object HabiticaTheme {
textPrimary = Color(context.getThemeColor(R.attr.textColorPrimary)),
textSecondary = Color(context.getThemeColor(R.attr.textColorSecondary)),
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)),
)
}
@ -151,12 +152,19 @@ class HabiticaColors(
val textPrimary: Color,
val textSecondary: Color,
val textTertiary: Color,
val textQuad: Color,
val textDimmed: Color
) {
@Composable
fun textPrimaryFor(task: Task?): Color {
return colorResource((if (isSystemInDarkTheme()) task?.extraExtraLightTaskColor else task?.extraDarkTaskColor) ?: R.color.text_primary)
}
@Composable
fun textSecondaryFor(task: Task?): Color {
return colorResource((if (isSystemInDarkTheme()) task?.extraLightTaskColor else task?.lowSaturationTaskColor) ?: R.color.brand_sub_text)
}
@Composable
fun primaryBackgroundFor(task: Task?): Color {
return colorResource((if (isSystemInDarkTheme()) task?.mediumTaskColor else task?.lightTaskColor) ?: R.color.brand_400)

View file

@ -211,14 +211,15 @@ class ChatRecyclerMessageViewHolder(
binding.buttonsWrapper.visibility = View.GONE
}
if ((chatMessage?.flagCount ?: 0) > 0) {
binding.flagCountTextview.text = if (chatMessage?.flagCount == 10) {
val flagCount = (chatMessage?.flagCount ?: 0)
if (flagCount > 0) {
binding.flagCountTextview.text = if (flagCount == 10) {
context.getString(R.string.shadow_muted_hidden)
} else {
context.resources.getQuantityString(R.plurals.flagged_count, (chatMessage?.flagCount ?: 0))
context.resources.getQuantityString(R.plurals.flagged_count, flagCount, flagCount)
}
binding.flagCountTextview.isVisible = true
if (chatMessage?.flagCount == 1) {
if (flagCount == 1) {
binding.flagCountTextview.setTextColor(ContextCompat.getColor(context, R.color.text_orange))
} else {
binding.flagCountTextview.setTextColor(ContextCompat.getColor(context, R.color.text_red))

View file

@ -0,0 +1,7 @@
package com.habitrpg.android.habitica.ui.viewHolders
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.RecyclerView
class ComposableViewHolder(view: ComposeView) : RecyclerView.ViewHolder(view) {
}

View file

@ -6,6 +6,7 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.edit
import androidx.fragment.app.FragmentManager
@ -26,7 +27,6 @@ import com.habitrpg.android.habitica.extensions.addCloseButton
import com.habitrpg.android.habitica.helpers.SignInWithAppleResult
import com.habitrpg.android.habitica.helpers.SignInWithAppleService
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.common.habitica.api.HostConfig
@ -107,12 +107,13 @@ class AuthenticationViewModel() {
fun handleGoogleLoginResult(
activity: Activity,
recoverFromPlayServicesErrorResult: ActivityResultLauncher<Intent>?,
onSuccess: (User, Boolean) -> Unit
onSuccess: (Boolean) -> Unit
) {
val scopesString = Scopes.PROFILE + " " + Scopes.EMAIL
val scopes = "oauth2:$scopesString"
var newUser = false
CoroutineScope(Dispatchers.IO).launchCatching({ throwable ->
Log.e("Auth", throwable.localizedMessage, throwable)
if (recoverFromPlayServicesErrorResult == null) return@launchCatching
throwable.cause?.let {
if (GoogleAuthException::class.java.isAssignableFrom(it.javaClass)) {
@ -128,8 +129,7 @@ class AuthenticationViewModel() {
val response = apiClient.connectSocial("google", googleEmail ?: "", token) ?: return@launchCatching
newUser = response.newUser
handleAuthResponse(response)
val user = userRepository.retrieveUser(true, true) ?: return@launchCatching
onSuccess(user, newUser)
onSuccess(newUser)
}
}

View file

@ -14,7 +14,6 @@ import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
@ -79,9 +78,6 @@ class TasksViewModel : BaseViewModel(), GroupPlanInfoProvider {
}
}
}
viewModelScope.launchCatching {
userRepository.retrieveTeamPlans()
}
}
internal fun refreshData(onComplete: () -> Unit) {

View file

@ -17,6 +17,7 @@ import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
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
@ -30,6 +31,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import kotlinx.coroutines.launch
@ -74,6 +76,13 @@ private fun BottomSheetWrapper(
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var isSheetOpened by remember { mutableStateOf(false) }
val systemUiController = rememberSystemUiController()
val statusBarColor = colorResource(R.color.content_background).copy(alpha = 0.3f)
DisposableEffect(systemUiController) {
systemUiController.setStatusBarColor(statusBarColor, darkIcons = true)
onDispose {}
}
val radius = 20.dp
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,

View file

@ -17,6 +17,17 @@ import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.common.habitica.helpers.NumberAbbreviator
@Composable
fun CurrencyText(
currency: String,
value: Int,
modifier: Modifier = Modifier,
decimals: Int = 0,
minForAbbrevation: Int = 0,
animated: Boolean = true
) {
CurrencyText(currency = currency, value = value.toDouble(), modifier, decimals, minForAbbrevation, animated)
}
@Composable
fun CurrencyText(
currency: String,

View file

@ -39,7 +39,7 @@ fun SegmentedControl(
onItemSelection: (selectedItemIndex: Int) -> Unit
) {
val selectedIndex = remember { mutableStateOf(defaultSelectedItemIndex) }
val color = MaterialTheme.colors.surface
val color = MaterialTheme.colors.primary
Row(
modifier = Modifier
) {

View file

@ -1,4 +1,4 @@
package com.habitrpg.android.habitica.ui.views
package com.habitrpg.android.habitica.ui.views.equipment
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -28,6 +28,7 @@ import com.habitrpg.android.habitica.models.user.Outfit
import com.habitrpg.android.habitica.models.user.Preferences
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.theme.caption2
import com.habitrpg.android.habitica.ui.views.PixelArtView
@Composable
fun OverviewItem(
@ -36,7 +37,7 @@ fun OverviewItem(
modifier: Modifier = Modifier,
isTwoHanded: Boolean = false
) {
val hasIcon = iconName?.isNotBlank() == true
val hasIcon = iconName?.isNotBlank() == true && iconName != "shirt_"
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier
.width(70.dp)
@ -121,7 +122,9 @@ fun EquipmentOverviewView(
@Composable
fun AvatarCustomizationOverviewView(
preferences: Preferences?,
outfit: Outfit?,
onCustomizationTap: (String, String?) -> Unit,
onAvatarEquipmentTap: (String, String?) -> Unit,
modifier: Modifier = Modifier
) {
Column(
@ -135,25 +138,25 @@ fun AvatarCustomizationOverviewView(
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
OverviewItem(
stringResource(R.string.avatar_shirt),
preferences?.shirt.let { "${preferences?.size}_shirt$it" }, Modifier.clickable {
preferences?.shirt.let { "icon_${preferences?.size}_shirt_$it" }, Modifier.clickable {
onCustomizationTap("shirt", null)
})
OverviewItem(
stringResource(R.string.avatar_skin),
preferences?.skin.let { "skin_$it" },
preferences?.skin.let { "icon_skin_$it" },
Modifier.clickable {
onCustomizationTap("skin", null)
})
OverviewItem(
stringResource(R.string.avatar_hair_color),
if (preferences?.hair?.color != null && preferences.hair?.color != "") "hair_bangs_1_" + preferences.hair?.color else "",
if (preferences?.hair?.color != null && preferences.hair?.color != "") "icon_hair_bangs_1_" + preferences.hair?.color else "",
Modifier.clickable {
onCustomizationTap("hair", "color")
}
)
OverviewItem(
stringResource(R.string.avatar_hair_bangs),
if (preferences?.hair?.bangs != null && preferences.hair?.bangs != 0) "hair_bangs_" + preferences.hair?.bangs + "_" + preferences.hair?.color else "",
if (preferences?.hair?.bangs != null && preferences.hair?.bangs != 0) "icon_hair_bangs_" + preferences.hair?.bangs + "_" + preferences.hair?.color else "",
Modifier.clickable {
onCustomizationTap("hair", "bangs")
}
@ -162,28 +165,28 @@ fun AvatarCustomizationOverviewView(
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
OverviewItem(
stringResource(R.string.avatar_style),
if (preferences?.hair?.base != null && preferences.hair?.base != 0) "hair_base_" + preferences.hair?.base + "_" + preferences.hair?.color else "",
if (preferences?.hair?.base != null && preferences.hair?.base != 0) "icon_hair_base_" + preferences.hair?.base + "_" + preferences.hair?.color else "",
Modifier.clickable {
onCustomizationTap("hair", "base")
}
)
OverviewItem(
stringResource(R.string.avatar_mustache),
if (preferences?.hair?.mustache != null && preferences.hair?.mustache != 0) "hair_mustache_" + preferences.hair?.mustache + "_" + preferences.hair?.color else "",
if (preferences?.hair?.mustache != null && preferences.hair?.mustache != 0) "icon_hair_mustache_" + preferences.hair?.mustache + "_" + preferences.hair?.color else "",
Modifier.clickable {
onCustomizationTap("hair", "mustache")
}
)
OverviewItem(
stringResource(R.string.avatar_beard),
if (preferences?.hair?.beard != null && preferences.hair?.beard != 0) "hair_beard_" + preferences.hair?.beard + "_" + preferences.hair?.color else "",
if (preferences?.hair?.beard != null && preferences.hair?.beard != 0) "icon_hair_beard_" + preferences.hair?.beard + "_" + preferences.hair?.color else "",
Modifier.clickable {
onCustomizationTap("hair", "beard")
}
)
OverviewItem(
stringResource(R.string.avatar_flower),
if (preferences?.hair?.flower != null && preferences.hair?.flower != 0) "hair_flower_" + preferences.hair?.flower else "",
if (preferences?.hair?.flower != null && preferences.hair?.flower != 0) "icon_hair_flower_" + preferences.hair?.flower else "",
Modifier.clickable {
onCustomizationTap("hair", "flower")
}
@ -192,18 +195,30 @@ fun AvatarCustomizationOverviewView(
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
OverviewItem(
stringResource(R.string.avatar_wheelchair),
preferences?.chair?.let { if (it.startsWith("handleless")) "chair_$it" else it },
preferences?.chair?.let { if (it.startsWith("handleless")) "icon_chair_$it" else "icon_$it" },
Modifier.clickable {
onCustomizationTap("chair", null)
})
OverviewItem(
stringResource(R.string.avatar_background),
preferences?.background.let { "background_$it" },
preferences?.background.let { "icon_background_$it" },
Modifier.clickable {
onCustomizationTap("background", null)
})
Box(Modifier.size(70.dp))
Box(Modifier.size(70.dp))
OverviewItem(
stringResource(R.string.animal_ears),
outfit?.headAccessory.let { "shop_$it" },
Modifier.clickable {
onAvatarEquipmentTap("headAccessory", "animal")
}
)
OverviewItem(
stringResource(R.string.animal_tail),
outfit?.back.let { "shop_$it" },
Modifier.clickable {
onAvatarEquipmentTap("back", "animal")
}
)
}
}
}
@ -218,6 +233,6 @@ fun EquipmentOverviewItemPreview() {
OverviewItem("Armor", null)
}
EquipmentOverviewView(null, { _, _ -> })
AvatarCustomizationOverviewView(null, { _, _ -> })
AvatarCustomizationOverviewView(null, null, { _, _ -> }, { _, _ -> })
}
}

View file

@ -0,0 +1,77 @@
package com.habitrpg.android.habitica.ui.views.promo
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.dp
import androidx.compose.ui.unit.sp
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.MainNavigationController
@Composable
fun BirthdayBanner() {
Column(
Modifier
.fillMaxWidth()
.clickable {
MainNavigationController.navigate(R.id.birthdayActivity)
}
) {
Column(Modifier.fillMaxWidth()) {
Column(
verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically),
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(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(33.dp)
.background(colorResource(R.color.brand_300))
.padding(horizontal = 10.dp)
) {
Text(
stringResource(R.string.ends_in_x).uppercase(),
color = colorResource(R.color.yellow_50),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
Spacer(Modifier.weight(1f))
Text(
stringResource(R.string.see_more).uppercase(),
color = colorResource(R.color.white),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}

View file

@ -7,7 +7,6 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -24,6 +23,8 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
@ -33,6 +34,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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
@ -69,65 +71,91 @@ fun AssignSheet(
}
for (member in members) {
val isAssigned = assignedMembers.contains(member.id)
val transition = updateTransition(isAssigned, label = "isAssigned")
val rotation = transition.animateFloat(
label = "isAssigned",
transitionSpec = { spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow) }) {
if (it) 0f else 45f
}
val backgroundColor = transition.animateColor(
label = "isAssigned",
transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) {
if (it) MaterialTheme.colors.primary else colorResource(id = R.color.transparent)
}
val color = transition.animateColor(
label = "isAssigned",
transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) {
fadeIn(tween(10000))
colorResource(if (it) R.color.white else R.color.text_dimmed)
}
val borderColor = transition.animateColor(
label = "isAssigned",
transitionSpec = { tween(400, easing = FastOutLinearInEasing) }) {
fadeIn(tween(10000))
if (it) MaterialTheme.colors.primary else colorResource(id = R.color.text_dimmed)
}
UserRow(
username = member.displayName,
avatar = member,
color = colorResource(R.color.text_primary),
extraContent = {
Text(
member.formattedUsername ?: "",
color = colorResource(R.color.text_ternary)
)
}, endContent = {
Image(
painterResource(R.drawable.ic_close_white_24dp),
null,
colorFilter = ColorFilter.tint(color.value),
modifier = Modifier
.rotate(rotation.value)
.size(24.dp)
.background(
backgroundColor.value,
CircleShape
)
.border(
2.dp,
borderColor.value,
CircleShape
)
.padding(3.dp)
)
}, modifier = Modifier
.clickable {
member.id?.let { onAssignClick(it) }
}
.padding(30.dp, 12.dp)
.heightIn(min = 24.dp)
.fillMaxWidth()
)
AssignSheetRow(member = member, isAssigned = isAssigned, onAssignClick = onAssignClick)
}
}
}
@Composable
fun AssignSheetRow(
member: Member,
isAssigned: Boolean,
onAssignClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
UserRow(
username = member.displayName,
avatar = member,
color = colorResource(R.color.text_primary),
extraContent = {
Text(
member.formattedUsername ?: "",
color = colorResource(R.color.text_ternary)
)
}, endContent = {
IsAssignedIndicator(isAssigned = isAssigned)
}, modifier = modifier
.clickable {
member.id?.let { onAssignClick(it) }
}
.padding(30.dp, 12.dp)
.heightIn(min = 24.dp)
.fillMaxWidth()
)
}
@Composable
private fun IsAssignedIndicator(
isAssigned: Boolean,
modifier: Modifier = Modifier
) {
val transition = updateTransition(isAssigned, label = "isAssigned")
val rotation = transition.animateFloat(
label = "isAssigned",
transitionSpec = { spring(Spring.DampingRatioLowBouncy, Spring.StiffnessMediumLow) }) {
if (it) 0f else 135f
}
val backgroundColor = transition.animateColor(
label = "isAssigned",
transitionSpec = { tween(450, easing = FastOutLinearInEasing) }) {
if (it) MaterialTheme.colors.primary else colorResource(id = R.color.transparent)
}
val color = transition.animateColor(
label = "isAssigned",
transitionSpec = { tween(450, easing = FastOutLinearInEasing) }) {
colorResource(if (it) R.color.white else R.color.text_dimmed)
}
val borderColor = transition.animateColor(
label = "isAssigned",
transitionSpec = { tween(450, easing = FastOutLinearInEasing) }) {
if (it) MaterialTheme.colors.primary else colorResource(id = R.color.text_dimmed)
}
Image(
painterResource(R.drawable.ic_close_white_24dp),
null,
colorFilter = ColorFilter.tint(color.value),
modifier = Modifier
.rotate(rotation.value)
.size(24.dp)
.background(
backgroundColor.value,
CircleShape
)
.border(
2.dp,
borderColor.value,
CircleShape
)
.padding(3.dp)
)
}
@Composable
@Preview
private fun IsAssignedIndicatorPreview() {
val isAssigned = remember { mutableStateOf(false) }
Column {
IsAssignedIndicator(isAssigned = isAssigned.value)
IsAssignedIndicator(isAssigned = !isAssigned.value)
}
}

View file

@ -79,15 +79,15 @@ private fun HabitScoringSelection(
modifier: Modifier = Modifier
) {
val selectedState = updateTransition(selected)
val context = LocalContext.current
val iconColor = selectedState.animateColor {
val context = LocalContext.current
if (it) Color(context.getThemeColor(R.attr.colorTintedBackground)) else colorResource(R.color.text_dimmed)
}
val textColor = selectedState.animateColor {
if (it) MaterialTheme.colors.primary else colorResource(R.color.text_ternary)
if (it) MaterialTheme.colors.primary else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
val borderColor = selectedState.animateColor {
if (it) MaterialTheme.colors.primary else colorResource(R.color.text_dimmed)
if (it) MaterialTheme.colors.primary else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier) {
Box(

View file

@ -77,12 +77,12 @@ private fun TaskDifficultySelection(
modifier: Modifier = Modifier
) {
val selectedState = updateTransition(selected)
val context = LocalContext.current
val iconColor = selectedState.animateColor {
val context = LocalContext.current
if (it) Color(context.getThemeColor(R.attr.colorTintedBackground)) else MaterialTheme.colors.primary
}
val textColor = selectedState.animateColor {
if (it) MaterialTheme.colors.primary else colorResource(R.color.text_ternary)
if (it) MaterialTheme.colors.primary else Color(context.getThemeColor(R.attr.textColorTintedSecondary))
}
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp), modifier = modifier) {
Box(

View file

@ -40,14 +40,14 @@ class TaskSerializer : JsonSerializer<Task>, JsonDeserializer<Task> {
return intList
}
private fun getMonthlyDays(e: JsonElement, task: Task) {
val weeksOfMonth = e.asJsonObject.getAsJsonArray("weeksOfMonth")
private fun getMonthlyDays(e: JsonObject, task: Task) {
val weeksOfMonth = e.getAsJsonArray("weeksOfMonth")
if (weeksOfMonth != null && weeksOfMonth.size() > 0) {
task.setWeeksOfMonth(getIntListFromJsonArray(weeksOfMonth))
}
val daysOfMonth = e.asJsonObject.getAsJsonArray("daysOfMonth")
if (weeksOfMonth != null && weeksOfMonth.size() > 0) {
val daysOfMonth = e.getAsJsonArray("daysOfMonth")
if (daysOfMonth != null && daysOfMonth.size() > 0) {
task.setDaysOfMonth(getIntListFromJsonArray(daysOfMonth))
}
}
@ -134,7 +134,7 @@ class TaskSerializer : JsonSerializer<Task>, JsonDeserializer<Task> {
task.group = group
}
// Work around since Realm does not support Arrays of ints
getMonthlyDays(json, task)
getMonthlyDays(obj, task)
// Workaround, since gson doesn't call setter methods
task.id = obj.getAsString("_id")

View file

@ -23,7 +23,7 @@ buildscript {
navigation_version = '2.5.3'
okhttp_version = '4.10.0'
play_wearables_version = '18.0.0'
play_auth_version = '20.3.0'
play_auth_version = '20.4.0'
preferences_version = '1.2.0'
realm_version = '1.0.2'
retrofit_version = '2.9.0'

View file

@ -9,4 +9,12 @@ val TaskDifficulty.nameRes: Int
TaskDifficulty.EASY -> R.string.easy
TaskDifficulty.MEDIUM -> R.string.medium
TaskDifficulty.HARD -> R.string.hard
}
val TaskDifficulty.nameSentenceRes: Int
get() = when (this) {
TaskDifficulty.TRIVIAL -> R.string.trivial_sentence
TaskDifficulty.EASY -> R.string.easy_sentence
TaskDifficulty.MEDIUM -> R.string.medium_sentence
TaskDifficulty.HARD -> R.string.hard_sentence
}

View file

@ -12,7 +12,7 @@ object NumberAbbreviator {
}
fun abbreviate(context: Context?, number: Double, numberOfDecimals: Int = 2, minForAbbrevation: Int = 0): String {
val decimalCount = if (number != 0.0 && number > -1 && number < 1 && numberOfDecimals == 0) 1 else numberOfDecimals
val decimalCount = if (number != 0.0 && number > -1 && number < 1 && numberOfDecimals == 0) 2 else numberOfDecimals
var usedNumber = number
var counter = 0
while (usedNumber >= 1000 && number >= minForAbbrevation) {
@ -21,7 +21,7 @@ object NumberAbbreviator {
}
var pattern = "###"
if (decimalCount > 0) {
pattern = ("$pattern.").padEnd(4 + numberOfDecimals, '#')
pattern = ("$pattern.").padEnd(4 + decimalCount, '#')
}
val formatter = DecimalFormat(pattern + abbreviationForCounter(context, counter).replace(".", ""))
formatter.roundingMode = RoundingMode.FLOOR

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="brand_sub_text">@color/brand_500</color>
</resources>

View file

@ -12,13 +12,13 @@
<color name="brand_400">#925CF3</color>
<color name="brand_500">#bda8ff</color>
<color name="brand_600">#D5C8FF</color>
<color name="brand_700">#F4F0FC</color>
<color name="brand_700">#EEEBF8</color>
<color name="brand_800">#FAF7FF</color>
<color name="brand">@color/brand_100</color>
<!-- HabitRPG task color -->
<color name="maroon_700">#FFF5F5</color>
<color name="maroon_600">#FCEDED</color>
<color name="maroon_600">#F7E9E9</color>
<color name="maroon_500">#f19595</color>
<color name="maroon_100">#DE3F3F</color>
<color name="maroon_50">#C92B2B</color>
@ -26,14 +26,15 @@
<color name="maroon_5">#7D0C0C</color>
<color name="red_700">#FFF6F7</color>
<color name="red_600">#FCEEEF</color>
<color name="red_600">#F7E9E9</color>
<color name="red_500">#ffb6b8</color>
<color name="red_100">#FF6165</color>
<color name="red_50">#F74E52</color>
<color name="red_10">#F23035</color>
<color name="red_5">#BF262B</color>
<color name="orange_700">#FFF9F5</color>
<color name="orange_600">#FCF3ED</color>
<color name="orange_600">#F7EDED</color>
<color name="orange_500">#ffc8a7</color>
<color name="orange_100">#FF944C</color>
<color name="orange_50">#FA8537</color>
@ -49,7 +50,7 @@
<color name="yellow_5">#EE9109</color>
<color name="green_700">#F7FFFC</color>
<color name="green_600">#EDFAF6</color>
<color name="green_600">#EBF5F5</color>
<color name="green_500">#77f4c7</color>
<color name="green_100">#24CC8F</color>
<color name="green_50">#20B780</color>
@ -57,7 +58,7 @@
<color name="green_5">#168059</color>
<color name="teal_700">#F5FEFF</color>
<color name="teal_600">#EDFBFC</color>
<color name="teal_600">#E5F5F5</color>
<color name="teal_500">#8eedf6</color>
<color name="teal_100">#3BCAD7</color>
<color name="teal_50">#34B5C1</color>
@ -95,7 +96,7 @@
<color name="yellow_1">#794b00</color>
<color name="orange_1">#7f3300</color>
<color name="brand_0">#30283D</color>
<color name="brand_0">#2F283F</color>
<color name="maroon_0">#3D2828</color>
<color name="red_0">#3D2828</color>
<color name="blue_0">#28373D</color>
@ -104,7 +105,7 @@
<color name="yellow_0">#3D3528</color>
<color name="orange_0">#3D3028</color>
<color name="brand_00">#201C26</color>
<color name="brand_00">#1A181D</color>
<color name="maroon_00">#261C1C</color>
<color name="red_00">#261C1C</color>
<color name="blue_00">#191D21</color>
@ -113,6 +114,15 @@
<color name="yellow_00">#26221C</color>
<color name="orange_00">#26201C</color>
<color name="brand_sub_text">#7C65AB</color>
<color name="maroon_sub_text">#AB6565</color>
<color name="red_sub_text">#AB6570</color>
<color name="orange_sub_text">#AB8165</color>
<color name="yellow_sub_text">#AB9065</color>
<color name="green_sub_text">#65AB94</color>
<color name="teal_sub_text">#65A7AB</color>
<color name="blue_sub_text">#6594AB</color>
<color name="hpColor">@color/red_100</color>
<color name="xpColor">@color/yellow_100</color>
<color name="mpColor">@color/blue_100</color>

View file

@ -16,6 +16,10 @@
<string name="easy">Easy</string>
<string name="medium">Medium</string>
<string name="hard">Hard</string>
<string name="trivial_sentence">trivial</string>
<string name="easy_sentence">easy</string>
<string name="medium_sentence">medium</string>
<string name="hard_sentence">hard</string>
<string name="habits">Habits</string>
<string name="dailies">Dailies</string>
<string name="todos">To Do\'s</string>

View file

@ -1,2 +1,2 @@
NAME=4.1
CODE=4901
CODE=4951

View file

@ -125,6 +125,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.preference:preference-ktx:$preferences_version"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "com.google.android.gms:play-services-auth:$play_auth_version"

View file

@ -2,6 +2,7 @@ package com.habitrpg.wearos.habitica.ui.activities
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
@ -19,6 +20,10 @@ import kotlin.time.toDuration
class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
override val viewModel: SplashViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
}
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivitySplashBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
@ -52,7 +57,6 @@ class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
}
}
override fun onStop() {
messageClient.removeListener(viewModel)
super.onStop()