Merge branch 'version/3.6' into Fixes#1523

This commit is contained in:
Phillip Thelen 2022-04-28 12:01:54 +02:00 committed by GitHub
commit 327d36e58d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 2210 additions and 920 deletions

View file

@ -30,6 +30,9 @@
<meta-data
android:name="firebase_performance_logcat_enabled"
android:value="true" />
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
<activity
android:name=".ui.activities.MainActivity"
android:label="@string/app_name"
@ -67,6 +70,13 @@
android:pathPattern="/settings/.*"/>
</intent-filter>
</activity>
<activity
android:name=".ui.activities.ArmoireActivity"
android:parentActivityName=".ui.activities.MainActivity"
android:label="@string/armoire"
android:screenOrientation="unspecified"
tools:ignore="UnusedAttribute">
</activity>
<activity
android:name=".ui.activities.NotificationsActivity"
android:parentActivityName=".ui.activities.MainActivity"

View file

@ -15,7 +15,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'net.sourceforge.pmd:pmd-java:5.5.3'
}
}
@ -121,6 +121,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:20.6.0'
implementation 'com.google.android.gms:play-services-auth:20.1.0'
implementation 'com.nex3z:flow-layout:1.2.2'
@ -128,8 +129,8 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.1"
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
@ -144,7 +145,7 @@ dependencies {
attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
}
}
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.6.10"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.6.20"
}
android {
@ -165,8 +166,8 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\""
resConfigs 'en', 'bg', 'de', 'en-rGB', 'es', 'fr', 'hr-rHR', 'in', 'it', 'iw', 'ja', 'ko', 'lt', 'nl', 'pl', 'pt-rBR', 'pt-rPT', 'ru', 'tr', 'zh', 'zh-rTW'
versionCode 3274
versionName "3.5.1.3"
versionCode 3320
versionName "3.6"
targetSdkVersion 32
@ -178,7 +179,6 @@ android {
viewBinding true
}
signingConfigs {
release
}
@ -386,14 +386,6 @@ jacoco {
toolVersion = "0.8.7"
}
// packages to exclude for example generated classes, R class and models package, add all packages that you wish to exclude from test coverage
def fileFilter = [
'**/*$ViewInjector*.*','**/*$ViewBinder*.*', '**/HabiticaIcons*.*', '**/DeviceName.*', '**/databinding/*Binding.*',
'**/R.class', '**/R.styleable', '**/R$*.class', '**/BuildConfig.*', '**/EmojiMap.*',
'**/Manifest*.*', 'android/**/*.*', '**/*RealmProxy*.*', '**/io/realm/*']
def debugTree = fileTree(dir: "${buildDir}/intermediates/asm_instrumented_project_classes/prodDebug", excludes: fileFilter)
def mainSrc = "${project.projectDir}/src/main/java"
task ktlint(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
classpath = configurations.ktlint

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:state_focused="true"/>
<item android:color="?attr/colorPrimary" android:state_hovered="true"/>
<item android:color="?attr/colorPrimary"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#4DFEDEAD" />
<corners android:radius="20dip"/>
<padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<!-- create gradient you want to use with the angle you want to use -->
<shape android:shape="rectangle" >
<gradient
android:angle="0"
android:startColor="@color/green_100"
android:endColor="@color/green_500" />
<corners android:radius="8dp" />
</shape>
</item>
<item
android:bottom="3dp"
android:left="3dp"
android:right="3dp"
android:top="3dp">
<shape
android:shape="rectangle"
android:layout_width="match_parent"
android:layout_height="match_parent">
<solid android:color="@color/brand_400" />
<corners android:radius="6dp" />
</shape>
<ripple android:color="@color/white" />
</item>
</layer-list>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:color="@color/brand_500" android:width="3dp" />
<corners android:radius="8dp" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#66FEDEAD"/>
<corners android:radius="20dip"/>
<padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
</shape>

View file

@ -1,10 +1,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<corners android:radius="@dimen/rounded_button_radius" />
<stroke
android:width="0.5dip"
android:color="#1f000000"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/content_background" />
<corners android:topLeftRadius="@dimen/bottom_sheet_radius" android:topRightRadius="@dimen/bottom_sheet_radius" />
</shape>

View file

@ -2,4 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/window_background" />
<corners android:topLeftRadius="@dimen/bottom_sheet_radius" android:topRightRadius="@dimen/bottom_sheet_radius" />
</shape>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorAccent"/>
<corners android:radius="20dip"/>
<corners android:radius="8dip"/>
<padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
</shape>

View file

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/content_background"/>
<corners android:radius="20dip"/>
<stroke android:color="@color/separator" android:width="1dp" />
<padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
<solid android:color="@color/offset_background"/>
<corners android:radius="8dip"/>
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/yellow_500"/>
<corners android:radius="20dip"/>
<padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
</shape>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/white"/>
<item android:color="@color/days_gray"/>
<item android:color="@color/text_dimmed"/>
</selector>

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/content_background">
<FrameLayout
android:id="@+id/confetti_anchor"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingTop="19dp">
<com.habitrpg.android.habitica.ui.views.CurrencyView
android:id="@+id/gold_view"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:background="@drawable/armoire_gold_background"
android:gravity="center"
android:paddingStart="6dp"
android:paddingEnd="8dp"
android:textStyle="bold"
android:textSize="20sp"
tools:text="118"/>
<FrameLayout
android:layout_width="165dp"
android:layout_height="158dp"
android:background="@drawable/armoire_circle"
android:layout_marginTop="23dp"
android:layout_marginBottom="23dp">
<ImageView
android:id="@+id/icon_view"
android:layout_width="136dp"
android:layout_height="136dp"
android:layout_gravity="center"/>
</FrameLayout>
<TextView
android:id="@+id/title_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="+21 Experience"
android:textStyle="bold"
android:textSize="28sp"
android:textColor="@color/text_primary"/>
<TextView
android:id="@+id/subtitle_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="You wrestle with the Armoire and gain Experience. Take that!"
android:layout_marginHorizontal="50dp"
android:gravity="center_horizontal"
android:layout_marginTop="8dp"
android:textSize="20sp"
/>
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/armoire_background"
android:orientation="vertical"
android:gravity="center"
android:paddingHorizontal="12dp"
android:paddingTop="28dp">
<TextView
android:id="@+id/equipment_count_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/equipment_remaining"
android:textColor="@color/white"
style="@style/Headline"
android:textStyle="bold"/>
<TextView
android:id="@+id/no_equipment_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_pieces_added_every_month"
style="@style/Body2"
android:textColor="@color/white"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<Button
android:id="@+id/equip_button"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="60dp"
android:text="@string/equip"
android:textStyle="bold"
style="@style/HabiticaButton.White"
android:layout_marginEnd="12dp"/>
<Button
android:id="@+id/close_button"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="60dp"
android:text="@string/close"
android:textStyle="bold"
style="@style/HabiticaButton.White"/>
</LinearLayout>
<com.habitrpg.android.habitica.ui.views.ads.AdButton
android:id="@+id/ad_button"
android:layout_width="match_parent"
android:layout_height="60dp"
app:text="@string/watch_ad_to_open"
android:layout_marginTop="4dp"
app:currency="gold" />
<TextView
android:id="@+id/drop_rate_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/armoire_drop_rates"
android:textColor="@color/brand_500"
android:layout_marginTop="22dp"
style="@style/Body2"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View file

@ -74,7 +74,7 @@
android:layout_toStartOf="@id/toolbar_accessory_container"
android:layout_alignParentStart="true"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
tools:text="Habitica"/>
tools:text="Habitica" />
<FrameLayout
android:id="@+id/toolbar_accessory_container"
@ -103,17 +103,29 @@
app:tabGravity="fill"
app:tabIndicatorColor="?colorPrimary"
app:tabMode="fixed" />
<TextView
android:id="@+id/connection_issue_textview"
<FrameLayout
android:id="@+id/connection_issue_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:background="@color/text_primary"
android:paddingVertical="2dp"
android:paddingHorizontal="@dimen/spacing_medium"
android:background="@color/error_banner_background"
tools:visibility="visible"
android:visibility="gone"
>
<TextView
android:id="@+id/connection_issue_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/maroon_500"
android:textSize="14sp"
android:gravity="center_horizontal"
android:padding="@dimen/spacing_small"
android:layout_gravity="center"
android:gravity="center_vertical"
android:text="@string/internal_error_api"
android:visibility="gone"/>
app:drawableStartCompat="@drawable/ic_warning_black"
app:drawableTint="@color/maroon_500"
android:drawablePadding="4dp"/>
</FrameLayout>
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
@ -127,6 +139,7 @@
android:layout_height="400dp"
android:layout_alignParentBottom="true"
android:paddingBottom="50dp"/>
<com.habitrpg.android.habitica.ui.views.navigation.HabiticaBottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"

View file

@ -45,7 +45,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TaskFormTextInputLayoutAppearance"
app:boxStrokeColor="?attr/colorPrimary"
app:boxStrokeWidth="3dp"
app:boxStrokeWidthFocused="1dp"
app:hintTextColor="?colorPrimaryText"
android:backgroundTint="?attr/colorPrimaryText"
android:hint="@string/task_title"
@ -59,10 +60,11 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/notes_input_layout"
style="@style/TaskFormTextInputLayoutAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxStrokeColor="?attr/colorPrimary"
style="@style/TaskFormTextInputLayoutAppearance"
app:boxStrokeWidth="3dp"
app:boxStrokeWidthFocused="2dp"
android:backgroundTint="?attr/colorPrimaryText"
app:hintTextColor="?colorPrimaryText"
android:hint="@string/notes"

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="android.widget.LinearLayout">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp" />
<com.habitrpg.android.habitica.ui.views.CurrencyView
android:id="@+id/currency_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:currency="gold"
android:layout_marginStart="24dp" />
<ProgressBar
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateBehavior="cycle"
android:indeterminateTint="@color/white"/>
</merge>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginHorizontal="@dimen/spacing_xlarge"
android:layout_marginVertical="@dimen/spacing_large">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enchanted_armoire_drop_rates"
style="@style/Title1"
android:textAlignment="center"
android:textColor="@color/text_primary"
android:layout_marginBottom="18dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:text="@string/armoire_rate_equipment_title"
style="@style/Subheader1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:text="@string/armoire_rate_equipment_description"
style="@style/Caption2"
android:layout_marginBottom="12dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:text="@string/armoire_rate_food_title"
style="@style/Subheader1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:text="@string/armoire_rate_food_description"
style="@style/Caption2"
android:layout_marginBottom="12dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:text="@string/armoire_rate_experience_title"
style="@style/Subheader1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:text="@string/armoire_rate_experience_description"
style="@style/Caption2"
/>
</LinearLayout>

View file

@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
android:paddingStart="@dimen/bottom_sheet_inset"
android:paddingEnd="@dimen/bottom_sheet_inset">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_large">
<TextView
style="@style/Headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/background_filters"
android:textColor="@color/text_ternary"
android:layout_gravity="center_vertical"/>
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />
<Button
android:id="@+id/clear_button"
style="@style/Body1_Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/clear"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:gravity="end|center_vertical"
android:paddingEnd="0dp"
android:paddingStart="0dp"/>
</LinearLayout>
<TextView
android:id="@+id/task_type_title"
style="@style/Caption3"
android:textColor="@color/text_quad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/show_me"
android:textAllCaps="true"/>
<RadioGroup
android:id="@+id/show_me_wrapper"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="12dp"
android:layout_marginBottom="28dp">
<RadioButton
android:id="@+id/show_all_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all"
style="@style/TaskFilterRadioButton"
android:checked="true"
/>
<RadioButton
android:id="@+id/show_purchased_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/purchased"
style="@style/TaskFilterRadioButton"
android:layout_marginStart="8dp" />
</RadioGroup>
<TextView
android:id="@+id/sort_by_title"
style="@style/Caption3"
android:textColor="@color/text_quad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sort_by"
android:textAllCaps="true"/>
<RadioGroup
android:id="@+id/sort_by_wrapper"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginTop="12dp"
android:layout_marginBottom="28dp">
<RadioButton
android:id="@+id/newest_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/newest"
style="@style/TaskFilterRadioButton"
android:checked="true"
/>
<RadioButton
android:id="@+id/oldest_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/oldest"
style="@style/TaskFilterRadioButton"
android:layout_marginStart="8dp" />
</RadioGroup>
<TextView
android:id="@+id/month_released_title"
style="@style/Caption3"
android:textColor="@color/text_quad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sort_by"
android:textAllCaps="true"/>
<LinearLayout
android:id="@+id/month_released_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="12dp"
android:layout_marginBottom="24dp">
<CheckBox
android:id="@+id/january_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/january"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/febuary_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/febuary"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/march_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/march"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/april_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/april"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/may_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/may"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/june_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/june"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/july_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/july"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/august_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/august"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/september_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/september"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/october_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/october"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/november_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/november"
android:layout_marginStart="-6dp"
/>
<CheckBox
android:id="@+id/december_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/december"
android:layout_marginStart="-6dp"
/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/grabber"
android:layout_width="40dp"
android:layout_height="4dp"
android:background="@drawable/layout_rounded_bg_gray"
android:layout_margin="@dimen/spacing_large"
android:layout_gravity="center_horizontal"
/>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -1,28 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<com.habitrpg.android.habitica.ui.MaxHeightLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:paddingStart="@dimen/bottom_sheet_inset"
android:paddingEnd="@dimen/bottom_sheet_inset"
android:orientation="vertical"
app:maxHeightMultiplier="0.7">
<!-- margins can't be at the first LinearLayout, it has to be the inner one -->
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/dialog_marginLeftRight"
android:layout_marginStart="@dimen/dialog_marginLeftRight"
android:layout_marginTop="16dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_large">
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#1f000000" />
<TextView
style="@style/Headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filters"
android:textColor="@color/text_primary"
android:layout_gravity="center_vertical"/>
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />
<Button
android:id="@+id/clear_button"
style="@style/Body1_Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/clear"
android:textColor="?colorPrimary"
android:textSize="16sp"
android:gravity="end|center_vertical"
android:paddingEnd="0dp"
android:paddingStart="0dp"
android:visibility="invisible"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
@ -84,7 +97,8 @@
android:checked="true"
android:text="@string/owned"
android:textColor="@color/text_primary"
android:paddingStart="8dp" />
android:paddingStart="8dp"
android:paddingEnd="0dp"/>
<CheckBox
style="@style/Subheader2"
@ -94,6 +108,6 @@
android:checked="true"
android:text="@string/not_owned"
android:textColor="@color/text_primary"
android:paddingStart="8dp"/>
android:paddingStart="8dp"
android:paddingEnd="0dp"/>
</LinearLayout>
</com.habitrpg.android.habitica.ui.MaxHeightLinearLayout>

View file

@ -4,7 +4,38 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp">
android:layout_marginBottom="16dp"
android:paddingStart="@dimen/bottom_sheet_inset"
android:paddingEnd="@dimen/bottom_sheet_inset">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_large">
<TextView
style="@style/Headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filters"
android:textColor="@color/text_primary"
android:layout_gravity="center_vertical"/>
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />
<Button
android:id="@+id/clear_button"
style="@style/Body1_Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/clear"
android:textColor="?colorPrimary"
android:textSize="16sp"
android:gravity="end|center_vertical"
android:paddingEnd="0dp"
android:paddingStart="0dp"/>
</LinearLayout>
<TextView
android:id="@+id/task_type_title"
style="@style/Caption3"
@ -42,10 +73,6 @@
android:text="@string/strong"
style="@style/TaskFilterRadioButton"/>
</RadioGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/separator" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -6,7 +6,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/RowWrapper"
android:id="@+id/gear_container">
android:id="@+id/gear_container"
android:gravity="center_vertical">
<FrameLayout
android:id="@+id/gear_icon_background_view"
@ -16,14 +17,14 @@
<ImageView
android:layout_width="@dimen/gear_image_size"
android:layout_height="@dimen/gear_image_size"
android:layout_gravity="center"
android:id="@+id/gear_image"/>
</FrameLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"

View file

@ -5,8 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/BottomMenu"
android:clickable="false"
android:background="@drawable/rounded_avatar_bg">
android:clickable="false">
<TextView
android:id="@+id/title_view"
android:layout_width="match_parent"

View file

@ -10,8 +10,7 @@
android:id="@+id/imageView"
android:layout_width="@dimen/shopitem_image_size"
android:layout_height="@dimen/shopitem_image_size"
android:layout_marginEnd="8dp"
android:scaleType="center"/>
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/titleTextView"

View file

@ -0,0 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.habitrpg.android.habitica.ui.activities.MainActivity">
<item android:id="@+id/action_filter"
android:icon="@drawable/ic_action_filter_list"
android:title="@string/filter"
app:showAsAction="ifRoom" />
</menu>

View file

@ -35,6 +35,11 @@
app:argType="string"
app:nullable="true"
android:defaultValue=""/>
<argument
android:name="ownerID"
app:argType="string"
app:nullable="true"
android:defaultValue=""/>
<deepLink app:uri="habitica.com/user/tasks/{taskType}" />
<deepLink app:uri="habitica.com/tasks" />
<deepLink app:uri="habitica.com" />
@ -142,6 +147,20 @@
android:id="@+id/openMountDetail"
app:destination="@id/mountDetailRecyclerFragment" />
</fragment>
<activity
android:id="@+id/armoireActivity"
android:name="com.habitrpg.android.habitica.ui.activities.ArmoireActivity"
android:label="@string/armoire" >
<argument
android:name="type"
app:argType="string" />
<argument
android:name="text"
app:argType="string" />
<argument
android:name="key"
app:argType="string" />
</activity>
<activity
android:id="@+id/subscriptionPurchaseActivity"
android:name="com.habitrpg.android.habitica.ui.activities.GemPurchaseActivity"
@ -399,6 +418,9 @@
<action
android:id="@+id/openProfileActivity"
app:destination="@id/fullProfileActivity" />
<action
android:id="@+id/openArmoireActivity"
app:destination="@id/armoireActivity" />
<fragment
android:id="@+id/achievementsFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.AchievementsFragment"
@ -466,12 +488,4 @@
app:argType="string" />
<deepLink app:uri="habitica.com/promo/web?url={url}" />
</fragment>
<fragment
android:id="@+id/teamBoardFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.tasks.TeamBoardFragment"
android:label="TeamBoardFragment" >
<argument
android:name="teamID"
app:argType="string" />
</fragment>
</navigation>

View file

@ -27,6 +27,8 @@
<attr name="headerOffsetColor" format="color" />
<attr name="headerTextColor" format="color" />
<attr name="widgetBackgroundRadius" format="dimension" />
<attr name="currency" format="string" />
<declare-styleable name="AvatarView">
<attr name="showBackground" format="boolean" />
<attr name="showMount" format="boolean" />
@ -77,7 +79,7 @@
<attr name="barBackgroundColor" />
</declare-styleable>
<declare-styleable name="CurrencyView">
<attr name="currency" format="string" />
<attr name="currency" />
<attr name="hasLightBackground" />
</declare-styleable>
<declare-styleable name="CurrencyViews">
@ -153,4 +155,8 @@
<attr name="android:inputType" />
<attr name="android:maxLines" />
</declare-styleable>
<declare-styleable name="AdButton">
<attr name="text" />
<attr name="currency" />
</declare-styleable>
</resources>

View file

@ -212,4 +212,5 @@
<color name="text_brand_white">@color/brand_300</color>
<color name="lightly_tinted_background">@color/brand_700</color>
<color name="dialog_background">@color/white</color>
<color name="error_banner_background">@color/maroon_5</color>
</resources>

View file

@ -118,4 +118,6 @@
<dimen name="widget_rounding">5dp</dimen>
<dimen name="task_spacing_vertical">4dp</dimen>
<dimen name="task_spacing_horizontal">10dp</dimen>
<dimen name="bottom_sheet_radius">20dp</dimen>
<dimen name="bottom_sheet_inset">16dp</dimen>
</resources>

View file

@ -276,8 +276,8 @@
<string name="mounts">Mounts</string>
<string name="quest_items_found">You\'ve found %d quest items</string>
<string name="armoireEquipment">You found %s in the Armoire</string>
<string name="armoireFood">You rummage in the Armoire and find%1$s %2$s. What\'s that doing in here?</string>
<string name="armoireEquipment_new">You find a piece of rare Equipment in the Armoire!</string>
<string name="armoireFood_new">You rummage in the Armoire and find food. What\'s that doing in here?</string>
<string name="armoireExp">You wrestle with the Armoire and gain Experience. Take that!</string>
<string name="sell">Sell (%d Gold)</string>
<string name="hatch_with_potion">Hatch with potion</string>
@ -1230,4 +1230,39 @@
<string name="compact">Compact</string>
<string name="minimal">Minimal</string>
<string name="are_you_sure_you_want_to_delete">Are you sure you want to delete?</string>
<string name="armoire_drop_rates">Armoire drop rates</string>
<string name="armoire">Armoire</string>
<string name="equipment_remaining">Equipment Remaining: %d</string>
<string name="new_pieces_added_every_month">New pieces added every month</string>
<string name="watch_ad">Watch Ad</string>
<string name="available_in">Available in %s</string>
<string name="watch_ad_to_open">Watch ad to open again</string>
<string name="watch_ad_to_revive">Watch Ad to revive</string>
<string name="enchanted_armoire_drop_rates">Enchanted Armoire Drop Rates</string>
<string name="armoire_rate_equipment_description">New Equipment pieces are added every month. If you own all pieces of equipment, then you\'ll get Food or Ezperience, 50/50 odds.</string>
<string name="armoire_rate_equipment_title">60% Piece of Equipment</string>
<string name="armoire_rate_food_title">20% Piece of Food</string>
<string name="armoire_rate_food_description">During special events, normal food items will change to their cake or candy counterparts.</string>
<string name="armoire_rate_experience_title">20% Experience points</string>
<string name="armoire_rate_experience_description">The amount gained varies randomly from 10 to 50</string>
<string name="day_start_adjustment">Day Start Adjustment</string>
<string name="purchased">Purchased</string>
<string name="show_me">Show Me</string>
<string name="background_filters">Background Filters</string>
<string name="newest">Newest</string>
<string name="oldest">Oldest</string>
<string name="sort_by">Sort By</string>
<string name="january">January</string>
<string name="febuary">Febuary</string>
<string name="march">March</string>
<string name="april">April</string>
<string name="may">May</string>
<string name="june">June</string>
<string name="july">July</string>
<string name="august">August</string>
<string name="september">September</string>
<string name="october">October</string>
<string name="november">November</string>
<string name="december">December</string>
<string name="cds_subtitle">Adjust when your day switches over past the default time of midnight.</string>
</resources>

View file

@ -714,6 +714,11 @@
<item name="android:backgroundTint">@color/yellow_100</item>
</style>
<style name="HabiticaButton.White" parent="HabiticaButton">
<item name="android:backgroundTint">@color/white</item>
<item name="android:textColor">@color/brand_400</item>
</style>
<style name="CurrencyTextView">
<item name="android:textSize">12sp</item>
<item name="android:layout_marginLeft">4dp</item>
@ -755,6 +760,7 @@
<style name="TaskFormTextInputLayoutAppearance" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<!-- reference our hint & error styles -->
<item name="boxBackgroundColor">@color/white</item>
<item name="boxStrokeColor">@color/text_input_box_stroke</item>
<item name="android:textColor">?attr/colorPrimaryText</item>
<item name="android:textColorHint">?colorPrimaryText</item>
<item name="colorControlNormal">?attr/colorPrimary</item>
@ -861,5 +867,40 @@
<item name="android:textStyle">bold</item>
</style>
<style name="SheetDialog" parent="Theme.Design.Light.BottomSheetDialog">
<!--<item name="android:windowCloseOnTouchOutside">false</item>-->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:colorBackground">@color/content_background</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.3</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="shapeAppearanceOverlay">@style/CustomShapeAppearanceBottomSheetDialog</item>
<item name="bottomSheetStyle">@style/Widget.App.BottomSheet.Modal</item>
<item name="colorPrimary">@color/brand</item>
<item name="colorPrimaryDark">@color/brand_50</item>
<item name="colorAccent">@color/color_accent</item>
<item name="android:colorPrimary">@color/brand</item>
<item name="android:colorPrimaryDark">@color/brand_50</item>
<item name="android:colorAccent">@color/brand_400</item>
</style>
<style name="Widget.App.BottomSheet.Modal" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="shapeAppearanceOverlay">@style/CustomShapeAppearanceBottomSheetDialog</item>
<item name="android:layout_marginStart">4dp</item>
<item name="android:layout_marginEnd">4dp</item>
</style>
<style name="CustomShapeAppearanceBottomSheetDialog" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopRight">20dp</item>
<item name="cornerSizeTopLeft">20dp</item>
<item name="cornerSizeBottomRight">0dp</item>
<item name="cornerSizeBottomLeft">0dp</item>
</style>
<dimen name="my_background_radius_dimen">12dp</dimen>
</resources>

View file

@ -28,6 +28,39 @@
<item>1</item>
</string-array>
<string-array name="cds_labels">
<item>Default (12:00 AM)</item>
<item>+1 Hour (01:00 AM)</item>
<item>+2 Hours (02:00 AM)</item>
<item>+3 Hours (03:00 AM)</item>
<item>+4 Hours (04:00 AM)</item>
<item>+5 Hours (05:00 AM)</item>
<item>+6 Hours (06:00 AM)</item>
<item>+7 Hours (07:00 AM)</item>
<item>+8 Hours (08:00 AM)</item>
<item>+9 Hours (09:00 AM)</item>
<item>+10 Hours (10:00 AM)</item>
<item>+11 Hours (11:00 AM)</item>
<item>+12 Hours (12:00 PM)</item>
</string-array>
<string-array name="cds_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
</string-array>
<string-array name="avatar_sizes">
<item>@string/avatar_size_slim</item>
<item>@string/avatar_size_broad</item>

View file

@ -182,6 +182,13 @@
android:layout="@layout/preference_child_summary"
android:summary="@string/pref_first_day_of_the_week_summary"
android:title="@string/pref_first_day_of_the_week_title" />
<com.habitrpg.android.habitica.ui.views.HabiticaListPreference
android:key="cds_time"
android:entries="@array/cds_labels"
android:entryValues="@array/cds_values"
android:layout="@layout/preference_child_summary"
android:title="@string/day_start_adjustment"/>
</PreferenceCategory>
<PreferenceCategory
@ -202,17 +209,6 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_cds_header"
android:layout="@layout/preference_category">
<com.habitrpg.android.habitica.prefs.TimePreference
android:key="cds_time"
android:defaultValue="00:00"
android:layout="@layout/preference_child_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/push_notifications"
android:layout="@layout/preference_category">

View file

@ -104,5 +104,26 @@
<key>enableTeamBoards</key>
<value>false</value>
</entry>
<entry>
<key>hideFacebook</key>
<value>false</value>
</entry>
<entry>
<key>enableNewArmoire</key>
<value>true</value>
</entry>
<entry>
<key>enableArmoireAds</key>
<value>true</value>
</entry>
<entry>
<key>enableFaintAds</key>
<value>false</value>
</entry>
<entry>
<key>enableSpellAds</key>
<value>false</value>
</entry>
</defaultsMap>
<!-- END xml_defaults -->

View file

@ -34,6 +34,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.LanguageHelper
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
@ -73,6 +74,7 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
setLocale()
setupRemoteConfig()
setupNotifications()
setupAdHandler()
HabiticaIconsHelper.init(this)
MarkdownParser.setup(this)
@ -110,6 +112,10 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
checkIfNewVersion()
}
private fun setupAdHandler() {
AdHandler.setup(sharedPrefs, analyticsManager)
}
private fun setLocale() {
val resources = resources
val configuration: Configuration = resources.configuration

View file

@ -13,6 +13,7 @@ import com.habitrpg.android.habitica.receivers.NotificationPublisher;
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.ChallengeFormActivity;
import com.habitrpg.android.habitica.ui.activities.ClassSelectionActivity;
import com.habitrpg.android.habitica.ui.activities.FixCharacterValuesActivity;
@ -98,13 +99,13 @@ import com.habitrpg.android.habitica.ui.fragments.support.FAQOverviewFragment;
import com.habitrpg.android.habitica.ui.fragments.support.SupportMainFragment;
import com.habitrpg.android.habitica.ui.fragments.tasks.TaskRecyclerViewFragment;
import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment;
import com.habitrpg.android.habitica.ui.fragments.tasks.TeamBoardFragment;
import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.InboxViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.MainActivityViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel;
import com.habitrpg.android.habitica.ui.viewmodels.inventory.equipment.EquipmentOverviewViewModel;
import com.habitrpg.android.habitica.ui.views.dialogs.PetSuggestHatchDialog;
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog;
@ -337,8 +338,6 @@ public interface UserComponent {
void inject(PromoInfoFragment promoInfoFragment);
void inject(@NotNull TeamBoardFragment teamBoardFragment);
void inject(@NotNull GuildOverviewFragment guildOverviewFragment);
void inject(@NotNull PromoWebFragment promoWebFragment);
@ -358,4 +357,8 @@ public interface UserComponent {
void inject(@NotNull MainUserViewModel mainUserViewModel);
void inject(@NotNull PetSuggestHatchDialog petSuggestHatchDialog);
void inject(@NotNull ArmoireActivity armoireActivity);
void inject(@NotNull TasksViewModel tasksViewModel);
}

View file

@ -58,13 +58,6 @@ import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.FlowableTransformer
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.schedulers.Schedulers
import java.io.IOException
import java.net.SocketException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.util.GregorianCalendar
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLException
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.Request
@ -73,6 +66,13 @@ import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import java.net.SocketException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.util.GregorianCalendar
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLException
class ApiClientImpl(
private val gsonConverter: GsonConverterFactory,
@ -94,6 +94,9 @@ class ApiClientImpl(
habitResponse.notifications?.let {
notificationsManager.setNotifications(it)
}
if (hadError) {
hideConnectionProblemDialog()
}
habitResponse.data
}
.subscribeOn(Schedulers.io())
@ -102,6 +105,7 @@ class ApiClientImpl(
}
private var languageCode: String? = null
private var lastAPICallURL: String? = null
private var hadError = false
init {
HabiticaBaseApplication.userComponent?.inject(this)
@ -306,12 +310,21 @@ class ApiClientImpl(
resourceTitleString: String?,
resourceMessageString: String
) {
hadError = true
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication)
application?.currentActivity?.get()
?.showConnectionProblem(resourceTitleString, resourceMessageString)
}
private fun hideConnectionProblemDialog() {
hadError = false
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication)
application?.currentActivity?.get()
?.hideConnectionProblem()
}
/*
This function is used with Observer.compose to reuse transformers across the application.
See here for more info: http://blog.danlew.net/2015/03/02/dont-break-the-chain/

View file

@ -4,16 +4,12 @@ import android.content.res.Resources
import com.habitrpg.android.habitica.R
import java.util.Calendar
import java.util.Date
import kotlin.math.round
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.milliseconds
import kotlin.time.toDuration
class DateUtils {
companion object {
fun createDate(year: Int, month: Int, day: Int): Date {
val cal = Calendar.getInstance()
cal.set(Calendar.YEAR, year)
@ -33,11 +29,11 @@ fun Date.getAgoString(res: Resources): String {
}
fun Long.getAgoString(res: Resources): String {
val diff = Date().time - this
val diff = (Date().time - this).toDuration(DurationUnit.MILLISECONDS)
val diffMinutes = diff / (60 * 1000) % 60
val diffHours = diff / (60 * 60 * 1000) % 24
val diffDays = diff / (24 * 60 * 60 * 1000)
val diffMinutes = diff.inWholeMinutes
val diffHours = diff.inWholeHours
val diffDays = diff.inWholeDays
val diffWeeks = diffDays / 7
val diffMonths = diffDays / 30
@ -63,30 +59,29 @@ fun Date.getRemainingString(res: Resources): String {
return this.time.getRemainingString(res)
}
@OptIn(ExperimentalTime::class)
fun Long.getRemainingString(res: Resources): String {
val diff = (this - Date().time).milliseconds
val diff = (this - Date().time).toDuration(DurationUnit.MILLISECONDS)
val diffMinutes = diff.inMinutes
val diffHours = diff.inHours
val diffDays = diff.inDays
val diffWeeks = diffDays / 7f
val diffMonths = diffDays / 30f
val diffMinutes = diff.inWholeMinutes
val diffHours = diff.inWholeHours
val diffDays = diff.inWholeDays
val diffWeeks = diffDays / 7
val diffMonths = diffDays / 30
return when {
diffMonths != 0.0 -> if (round(diffMonths) == 1.0) {
diffMonths != 0L -> if (diffMonths == 1L) {
res.getString(R.string.remaining_1month)
} else res.getString(R.string.remaining_months, round(diffMonths).toInt())
diffWeeks != 0.0 -> if (round(diffWeeks) == 1.0) {
} else res.getString(R.string.remaining_months, diffMonths)
diffWeeks != 0L -> if (diffWeeks == 1L) {
res.getString(R.string.remaining_1week)
} else res.getString(R.string.remaining_weeks, round(diffWeeks).toInt())
diffDays != 0.0 -> if (diffDays == 1.0) {
} else res.getString(R.string.remaining_weeks, diffWeeks)
diffDays != 0L -> if (diffDays == 1L) {
res.getString(R.string.remaining_1day)
} else res.getString(R.string.remaining_days, diffDays)
diffHours != 0.0 -> if (diffHours == 1.0) {
diffHours != 0L -> if (diffHours == 1L) {
res.getString(R.string.remaining_1hour)
} else res.getString(R.string.remaining_hours, diffHours)
diffMinutes == 1.0 -> res.getString(R.string.remaining_1Minute)
diffMinutes == 1L -> res.getString(R.string.remaining_1Minute)
else -> res.getString(R.string.remaining_minutes, diffMinutes)
}
}
@ -95,16 +90,15 @@ fun Date.getShortRemainingString(): String {
return time.getShortRemainingString()
}
@OptIn(ExperimentalTime::class)
fun Long.getShortRemainingString(): String {
var diff = Duration.milliseconds((this - Date().time))
var diff = (this - Date().time).toDuration(DurationUnit.MILLISECONDS)
val diffDays = diff.toInt(DurationUnit.DAYS)
diff -= Duration.days(diffDays)
diff -= diffDays.toDuration(DurationUnit.DAYS)
val diffHours = diff.toInt(DurationUnit.HOURS)
diff -= Duration.hours(diffHours)
diff -= diffDays.toDuration(DurationUnit.HOURS)
val diffMinutes = diff.toInt(DurationUnit.MINUTES)
diff -= Duration.minutes(diffMinutes)
diff -= diffMinutes.toDuration(DurationUnit.MINUTES)
val diffSeconds = diff.toInt(DurationUnit.SECONDS)
var str = "${diffMinutes}m"
@ -119,3 +113,7 @@ fun Long.getShortRemainingString(): String {
}
return str
}
fun Duration.getMinuteOrSeconds(): DurationUnit {
return if (this.inWholeHours < 1) DurationUnit.SECONDS else DurationUnit.MINUTES
}

View file

@ -0,0 +1,224 @@
package com.habitrpg.android.habitica.helpers
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.provider.Settings
import android.util.Log
import androidx.core.content.edit
import androidx.core.os.bundleOf
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.FullScreenContentCallback
import com.google.android.gms.ads.LoadAdError
import com.google.android.gms.ads.MobileAds
import com.google.android.gms.ads.OnUserEarnedRewardListener
import com.google.android.gms.ads.RequestConfiguration
import com.google.android.gms.ads.rewarded.RewardItem
import com.google.android.gms.ads.rewarded.RewardedAd
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.util.Date
import kotlin.time.DurationUnit
import kotlin.time.toDuration
enum class AdType {
ARMOIRE,
SPELL,
FAINT;
val adUnitID: String
get() {
if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
return "ca-app-pub-3940256099942544/5224354917"
}
return when (this) {
ARMOIRE -> "ca-app-pub-5911973472413421/9392092486"
SPELL -> "ca-app-pub-5911973472413421/1738504765"
FAINT -> "ca-app-pub-5911973472413421/1738504765"
}
}
}
fun String.md5(): String? {
try {
val md = MessageDigest.getInstance("MD5")
val array = md.digest(this.toByteArray())
val sb = StringBuffer()
for (i in array.indices) {
sb.append(Integer.toHexString(array[i].toInt() and 0xFF or 0x100).substring(1, 3))
}
return sb.toString()
} catch (e: java.security.NoSuchAlgorithmException) {
} catch (ex: UnsupportedEncodingException) {
}
return null
}
class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boolean) -> Unit): OnUserEarnedRewardListener {
private var rewardedAd: RewardedAd? = null
companion object {
private enum class AdStatus {
UNINITIALIZED,
INITIALIZING,
READY,
DISABLED
}
private lateinit var analyticsManager: AnalyticsManager
private lateinit var sharedPreferences: SharedPreferences
const val TAG = "AdHandler"
private var currentAdStatus = AdStatus.UNINITIALIZED
private var nextAdAllowed: MutableMap<AdType, Date> = mutableMapOf()
fun nextAdAllowedDate(type: AdType): Date? {
return nextAdAllowed[type]
}
fun isAllowed(type: AdType): Boolean {
return nextAdAllowedDate(type)?.after(Date()) == true
}
fun setNextAllowedDate(type: AdType, date: Date) {
nextAdAllowed[type] = date
sharedPreferences.edit {
putLong("nextAd${type.name}", date.time)
}
}
fun initialize(context: Context, onComplete: () -> Unit) {
if (currentAdStatus != AdStatus.UNINITIALIZED) return
if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
val android_id: String =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
val deviceId: String = android_id.md5()?.uppercase() ?: ""
val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(deviceId)).build()
MobileAds.setRequestConfiguration(configuration)
}
currentAdStatus = AdStatus.INITIALIZING
MobileAds.initialize(context) {
currentAdStatus = AdStatus.READY
onComplete()
FirebaseCrashlytics.getInstance().recordException(Throwable("Ads Initialized"))
}
}
fun whenAdsInitialized(context: Context, onComplete: () -> Unit) {
when (currentAdStatus) {
AdStatus.READY -> {
onComplete()
}
AdStatus.DISABLED -> {
return
}
AdStatus.UNINITIALIZED -> {
initialize(context) {
onComplete()
}
}
AdStatus.INITIALIZING -> {
return
}
}
}
fun setup(sharedPrefs: SharedPreferences, analyticsManager: AnalyticsManager) {
this.sharedPreferences = sharedPrefs
this.analyticsManager = analyticsManager
for (type in AdType.values()) {
val time = sharedPrefs.getLong("nextAd${type.name}", 0)
if (time > 0) {
nextAdAllowed[type] = Date(time)
}
}
}
}
fun prepare(onComplete: ((Boolean) -> Unit)? = null) {
whenAdsInitialized(activity) {
val adRequest = AdRequest.Builder()
.build()
if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
if (!adRequest.isTestDevice(activity)) {
// users in this group need to be configured as Test device. better to fail if they aren't
// currentAdStatus = AdStatus.DISABLED
FirebaseCrashlytics.getInstance().recordException(Throwable("Device not test device"))
}
}
RewardedAd.load(activity, type.adUnitID, adRequest, object : RewardedAdLoadCallback() {
override fun onAdFailedToLoad(adError: LoadAdError) {
FirebaseCrashlytics.getInstance().recordException(Throwable(adError.message))
rewardAction(false)
onComplete?.invoke(false)
}
override fun onAdLoaded(rewardedAd: RewardedAd) {
this@AdHandler.rewardedAd = rewardedAd
configureReward()
onComplete?.invoke(true)
}
})
}
}
fun show() {
when (currentAdStatus) {
AdStatus.READY -> {
showRewardedAd()
}
AdStatus.DISABLED -> {
rewardAction(false)
return
}
AdStatus.UNINITIALIZED -> {
initialize(activity) {
showRewardedAd()
}
}
AdStatus.INITIALIZING -> {
return
}
}
}
private fun configureReward() {
rewardedAd?.run {
fullScreenContentCallback = object : FullScreenContentCallback() {
}
}
}
private fun showRewardedAd() {
if (nextAdAllowedDate(type)?.after(Date()) == true) {
return
}
if (rewardedAd != null) {
rewardedAd?.show(activity, this)
setNextAllowedDate(type, Date(Date().time + 1.toDuration(DurationUnit.HOURS).inWholeMilliseconds))
} else {
Log.d(TAG, "The rewarded ad wasn't ready yet.")
}
}
override fun onUserEarnedReward(rewardItem: RewardItem) {
analyticsManager.logEvent("adRewardEarned", bundleOf(
Pair("type", type.name)
))
FirebaseAnalytics.getInstance(activity).logEvent("adRewardEarned", bundleOf(
Pair("type", type.name)
))
rewardAction(true)
}
}

View file

@ -136,6 +136,29 @@ class AppConfigManager(contentRepository: ContentRepository?) {
}
fun enableTeamBoards(): Boolean {
if (BuildConfig.DEBUG) {
return true
}
return remoteConfig.getBoolean("enableTeamBoards")
}
fun enableArmoireAds(): Boolean {
return remoteConfig.getBoolean("enableArmoireAds")
}
fun enableFaintAds(): Boolean {
return remoteConfig.getBoolean("enableFaintAds")
}
fun enableSpellAds(): Boolean {
return remoteConfig.getBoolean("enableSpellAds")
}
fun enableNewArmoire(): Boolean {
return remoteConfig.getBoolean("enableNewArmoire")
}
fun hideFacebook(): Boolean {
return remoteConfig.getBoolean("hideFacebook")
}
}

View file

@ -27,6 +27,10 @@ class LanguageHelper(languageSharedPref: String?) {
locale = Locale("pt", "PT")
languageCode = "pt"
}
"uk" -> {
locale = Locale("uk", "UA")
languageCode = "uk"
}
else -> {
locale = if (pref.contains("_")) {
val languageCodeParts = pref.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()

View file

@ -7,10 +7,10 @@ import java.text.DecimalFormat
object NumberAbbreviator {
fun abbreviate(context: Context, number: Double, numberOfDecimals: Int = 2): String {
fun abbreviate(context: Context, number: Double, numberOfDecimals: Int = 2, minForAbbrevation: Int = 0): String {
var usedNumber = number
var counter = 0
while (usedNumber >= 1000) {
while (usedNumber >= 1000 && number >= minForAbbrevation) {
counter++
usedNumber /= 1000
}

View file

@ -1,125 +1,5 @@
package com.habitrpg.android.habitica.helpers
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskType
import io.realm.Case
import io.realm.OrderedRealmCollection
import io.realm.RealmQuery
import io.realm.Sort
class TaskFilterHelper {
var searchQuery: String? = null
private val activeFilters = HashMap<TaskType, String>()
var tags: MutableList<String> = mutableListOf()
fun howMany(type: TaskType?): Int {
return this.tags.size + if (isTaskFilterActive(type)) 1 else 0
}
private fun isTaskFilterActive(type: TaskType?): Boolean {
if (activeFilters[type] == null) {
return false
}
return if (TaskType.TODO == type) {
Task.FILTER_ACTIVE != activeFilters[type]
} else {
Task.FILTER_ALL != activeFilters[type]
}
}
fun filter(tasks: List<Task>): List<Task> {
if (tasks.isEmpty()) {
return tasks
}
val filtered = ArrayList<Task>()
var activeFilter: String? = null
if (activeFilters.size > 0) {
activeFilter = activeFilters[tasks[0].type]
}
for (task in tasks) {
if (isFiltered(task, activeFilter)) {
filtered.add(task)
}
}
return filtered
}
private fun isFiltered(task: Task, activeFilter: String?): Boolean {
if (!task.containsAllTagIds(tags)) {
return false
}
return if (activeFilter != null && activeFilter != Task.FILTER_ALL) {
when (activeFilter) {
Task.FILTER_ACTIVE -> if (task.type == TaskType.DAILY) {
task.isDisplayedActive
} else {
!task.completed
}
Task.FILTER_GRAY -> task.completed || !task.isDisplayedActive
Task.FILTER_WEAK -> task.value < 1
Task.FILTER_STRONG -> task.value >= 1
Task.FILTER_DATED -> task.dueDate != null
Task.FILTER_COMPLETED -> task.completed
else -> true
}
} else {
true
}
}
fun setActiveFilter(type: TaskType, activeFilter: String) {
activeFilters[type] = activeFilter
}
fun getActiveFilter(type: TaskType?): String? {
return if (activeFilters.containsKey(type)) {
activeFilters[type]
} else {
null
}
}
fun createQuery(unfilteredData: OrderedRealmCollection<Task>): RealmQuery<Task>? {
if (!unfilteredData.isValid) {
return null
}
var query = unfilteredData.where()
if (unfilteredData.size != 0) {
val taskType = unfilteredData[0].type
val activeFilter = getActiveFilter(taskType)
if (tags.size > 0) {
query = query.`in`("tags.id", tags.toTypedArray())
}
if (searchQuery?.isNotEmpty() == true) {
query = query
.beginGroup()
.contains("text", searchQuery ?: "", Case.INSENSITIVE)
.or()
.contains("notes", searchQuery ?: "", Case.INSENSITIVE)
.endGroup()
}
if (activeFilter != null && activeFilter != Task.FILTER_ALL) {
when (activeFilter) {
Task.FILTER_ACTIVE -> query = if (TaskType.DAILY == taskType) {
query.equalTo("completed", false).equalTo("isDue", true)
} else {
query.equalTo("completed", false)
}
Task.FILTER_GRAY -> query = query.equalTo("completed", true).or().equalTo("isDue", false)
Task.FILTER_WEAK -> query = query.lessThan("value", 1.0)
Task.FILTER_STRONG -> query = query.greaterThanOrEqualTo("value", 1.0)
Task.FILTER_DATED -> query = query.isNotNull("dueDate").equalTo("completed", false).sort("dueDate")
Task.FILTER_COMPLETED -> query = query.equalTo("completed", true)
}
}
if (activeFilter != Task.FILTER_DATED) {
query = query.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
}
}
return query
}
}

View file

@ -0,0 +1,12 @@
package com.habitrpg.android.habitica.models
data class CustomizationFilter(
var onlyPurchased: Boolean = false,
var ascending: Boolean = false,
var months: MutableList<String> = mutableListOf()
) {
val isFiltering: Boolean
get() {
return onlyPurchased || months.isNotEmpty()
}
}

View file

@ -0,0 +1,184 @@
package com.habitrpg.android.habitica.ui.activities
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
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.ActivityArmoireBinding
import com.habitrpg.android.habitica.extensions.observeOnce
import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.AdType
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.ui.helpers.loadImage
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.ads.AdButton
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
import com.plattysoft.leonids.ParticleSystem
import javax.inject.Inject
class ArmoireActivity: BaseActivity() {
private var equipmentKey: String? = null
private var gold: Double? = null
private var hasAnimatedChanges: Boolean = false
private lateinit var binding: ActivityArmoireBinding
@Inject
internal lateinit var inventoryRepository: InventoryRepository
@Inject
internal lateinit var appConfigManager: AppConfigManager
@Inject
lateinit var userViewModel: MainUserViewModel
override fun getLayoutResId(): Int = R.layout.activity_armoire
override fun injectActivity(component: UserComponent?) {
component?.inject(this)
}
override fun getContentView(): View {
binding = ActivityArmoireBinding.inflate(layoutInflater)
return binding.root
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.goldView.currency = "gold"
binding.goldView.animationDuration = 1000
binding.goldView.animationDelay = 500
binding.goldView.minForAbbrevation = 1000000
binding.goldView.decimals = 0
userViewModel.user.observeOnce(this) { user ->
gold = user?.stats?.gp
val remaining = inventoryRepository.getArmoireRemainingCount()
binding.equipmentCountView.text = getString(R.string.equipment_remaining, remaining)
binding.noEquipmentView.visibility = if (remaining > 0) View.GONE else View.VISIBLE
}
if (appConfigManager.enableArmoireAds()) {
val handler = AdHandler(this, AdType.ARMOIRE) {
if (!it) {
return@AdHandler
}
Log.d("AdHandler", "Giving Armoire")
val user = userViewModel.user.value ?: return@AdHandler
val currentGold = user.stats?.gp ?: return@AdHandler
compositeSubscription.add(userRepository.updateUser("stats.gp", currentGold + 100)
.flatMap { inventoryRepository.buyItem(user, "armoire", 100.0, 1) }
.subscribe({
configure(it.armoire["type"] ?: "",
it.armoire["dropKey"] ?: "",
it.armoire["dropText"] ?: "")
binding.adButton.updateForAdType(AdType.ARMOIRE, lifecycleScope)
hasAnimatedChanges = false
gold = null
}, RxErrorHandler.handleEmptyError()))
}
handler.prepare {
if (it && binding.adButton.state == AdButton.State.EMPTY) {
binding.adButton.state = AdButton.State.READY
} else if (!it) {
binding.adButton.visibility = View.INVISIBLE
}
}
binding.adButton.updateForAdType(AdType.ARMOIRE, lifecycleScope)
binding.adButton.setOnClickListener {
binding.adButton.state = AdButton.State.LOADING
handler.show()
}
} else {
binding.adButton.visibility = View.GONE
}
binding.closeButton.setOnClickListener {
finish()
}
binding.equipButton.setOnClickListener {
equipmentKey?.let { it1 -> inventoryRepository.equip("gear", it1).subscribe() }
finish()
}
binding.dropRateButton.setOnClickListener {
showDropRateDialog()
}
intent.extras?.let {
val args = ArmoireActivityArgs.fromBundle(it)
equipmentKey = args.key
configure(args.type, args.key, args.text)
}
}
override fun onResume() {
super.onResume()
startAnimation()
}
private fun startAnimation() {
val gold = gold?.toInt()
if (hasAnimatedChanges) return
if (gold != null) {
binding.goldView.value = (gold).toDouble()
binding.goldView.value = (gold - 100).toDouble()
}
val container = binding.confettiAnchor
container.postDelayed(
{
createParticles(container, R.drawable.confetti_blue)
createParticles(container, R.drawable.confetti_red)
createParticles(container, R.drawable.confetti_yellow)
createParticles(container, R.drawable.confetti_purple)
},
500
)
hasAnimatedChanges = true
}
private fun createParticles(container: FrameLayout, resource: Int) {
ParticleSystem(
container,
30,
ContextCompat.getDrawable(this, resource),
6000
)
.setRotationSpeed(144f)
.setScaleRange(1.0f, 1.6f)
.setSpeedByComponentsRange(-0.15f, 0.15f, 0.15f, 0.45f)
.setFadeOut(200, AccelerateInterpolator())
.emitWithGravity(binding.confettiAnchor, Gravity.TOP, 15, 2000)
}
fun configure(type: String, key: String, text: String) {
binding.titleView.text = text
binding.equipButton.visibility = if (type == "gear") View.VISIBLE else View.GONE
when (type) {
"gear" -> {
binding.subtitleView.text = getString(R.string.armoireEquipment_new)
binding.iconView.loadImage("shop_$key")
}
"food" -> {
binding.subtitleView.text = getString(R.string.armoireFood_new)
binding.iconView.loadImage("Pet_Food_$key")
}
else -> {
binding.subtitleView.text = getString(R.string.armoireExp)
binding.iconView.setImageResource(R.drawable.armoire_experience)
}
}
}
fun showDropRateDialog() {
val dialog = HabiticaBottomSheetDialog(this)
dialog.setContentView(R.layout.armoire_drop_rate_dialog)
dialog.show()
}
}

View file

@ -236,6 +236,10 @@ abstract class BaseActivity : AppCompatActivity() {
alert.enqueue()
}
open fun hideConnectionProblem() {
}
fun shareContent(identifier: String, message: String, image: Bitmap? = null) {
analyticsManager.logEvent("shared", bundleOf(Pair("identifier", identifier)))
val sharingIntent = Intent(Intent.ACTION_SEND)

View file

@ -36,16 +36,17 @@ import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.adapter.social.challenges.ChallengeTasksRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.fragments.social.challenges.ChallengesOverviewFragmentDirections
import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaProgressDialog
import io.reactivex.rxjava3.core.Flowable
import java.util.UUID
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
import javax.inject.Named
class ChallengeFormActivity : BaseActivity() {
@ -223,7 +224,7 @@ class ChallengeFormActivity : BaseActivity() {
val bundle = intent.extras
ChallengeTasksRecyclerViewAdapter(
null, 0, this, "",
TasksViewModel(), 0, this, "",
openTaskDisabled = false,
taskActionsDisabled = true
).also { challengeTasks = it }

View file

@ -224,7 +224,7 @@ class LoginActivity : BaseActivity() {
}
binding.password.imeOptions = EditorInfo.IME_ACTION_DONE
binding.fbLoginButton.setText(R.string.login_btn_fb)
binding.fbLoginButton.visibility = View.VISIBLE
binding.fbLoginButton.visibility = if (configManager.hideFacebook()) View.GONE else View.VISIBLE
binding.googleLoginButton.setText(R.string.login_btn_google)
}
this.resetLayout()

View file

@ -9,6 +9,7 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
@ -17,6 +18,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.children
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
@ -35,6 +37,8 @@ import com.habitrpg.android.habitica.extensions.hideKeyboard
import com.habitrpg.android.habitica.extensions.isUsingNightModeResources
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.extensions.updateStatusBarColor
import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.AdType
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
@ -63,10 +67,13 @@ import com.habitrpg.android.habitica.widget.AvatarStatsWidgetProvider
import com.habitrpg.android.habitica.widget.DailiesWidgetProvider
import com.habitrpg.android.habitica.widget.HabitButtonWidgetProvider
import com.habitrpg.android.habitica.widget.TodoListWidgetProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
open class MainActivity : BaseActivity(), SnackbarActivity {
private var launchScreen: String? = null
@ -445,6 +452,7 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
}
if (this.faintDialog == null && !this.isFinishing) {
val binding = DialogFaintBinding.inflate(this.layoutInflater)
binding.hpBar.setLightBackground(true)
binding.hpBar.setIcon(HabiticaIconsHelper.imageOfHeartLightBg())
@ -457,6 +465,20 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
faintDialog = null
userRepository.revive().subscribe({ }, RxErrorHandler.handleEmptyError())
}
if (AdHandler.isAllowed(AdType.FAINT)) {
val handler = AdHandler(this, AdType.FAINT) {
Log.d("AdHandler", "Reviving user")
compositeSubscription.add(
userRepository.updateUser("stats.hp", 50)
.subscribe({}, RxErrorHandler.handleEmptyError())
)
}
handler.prepare()
faintDialog?.addButton(R.string.watch_ad_to_revive, true) { _, _ ->
faintDialog = null
handler.show()
}
}
soundManager.loadAndPlayAudio(SoundManager.SoundDeath)
this.faintDialog?.enqueue()
}
@ -528,22 +550,33 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
return snackbarContainer
}
private var errorJob: Job? = null
override fun showConnectionProblem(title: String?, message: String) {
if (title != null) {
super.showConnectionProblem(title, message)
} else {
binding.connectionIssueTextview.visibility = View.VISIBLE
if (errorJob?.isCancelled == false) {
// a new error resets the timer to hide the error message
errorJob?.cancel()
}
binding.connectionIssueView.visibility = View.VISIBLE
binding.connectionIssueTextview.text = message
compositeSubscription.add(
Observable.just("")
.delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe(
{
binding.connectionIssueTextview.visibility = View.GONE
},
{}
)
)
errorJob = lifecycleScope.launch(Dispatchers.Main) {
delay(1.toDuration(DurationUnit.MINUTES))
binding.connectionIssueView.visibility = View.GONE
}
}
}
override fun hideConnectionProblem() {
if (errorJob?.isCancelled == false) {
errorJob?.cancel()
}
lifecycleScope.launch(Dispatchers.Main) {
if (binding.connectionIssueView.visibility == View.VISIBLE) {
binding.connectionIssueView.visibility = View.GONE
}
}
}
}

View file

@ -94,8 +94,8 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
notifyItemRemoved(x)
}
for ((index, team) in teams.withIndex()) {
val item = HabiticaDrawerItem(R.id.teamBoardFragment, team.id, team.summary)
item.bundle = bundleOf(Pair("teamID", team.id))
val item = HabiticaDrawerItem(R.id.tasksFragment, team.id, team.summary)
item.bundle = bundleOf(Pair("ownerID", team.id))
val newIndex = teamHeaderIndex + index + 1
items.add(newIndex, item)
notifyItemInserted(newIndex)

View file

@ -7,7 +7,6 @@ import android.widget.Button
import android.widget.TextView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskType
import com.habitrpg.android.habitica.ui.adapter.tasks.BaseTasksRecyclerViewAdapter
@ -17,18 +16,19 @@ import com.habitrpg.android.habitica.ui.viewHolders.tasks.DailyViewHolder
import com.habitrpg.android.habitica.ui.viewHolders.tasks.HabitViewHolder
import com.habitrpg.android.habitica.ui.viewHolders.tasks.RewardViewHolder
import com.habitrpg.android.habitica.ui.viewHolders.tasks.TodoViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.subjects.PublishSubject
class ChallengeTasksRecyclerViewAdapter(
taskFilterHelper: TaskFilterHelper?,
viewModel: TasksViewModel,
layoutResource: Int,
newContext: Context,
userID: String,
private val openTaskDisabled: Boolean,
private val taskActionsDisabled: Boolean
) : BaseTasksRecyclerViewAdapter<BindableViewHolder<Task>>(TaskType.HABIT, taskFilterHelper, layoutResource, newContext, userID) {
) : BaseTasksRecyclerViewAdapter<BindableViewHolder<Task>>(TaskType.HABIT, viewModel, layoutResource, newContext, userID) {
private val addItemSubject = PublishSubject.create<Task>()

View file

@ -7,17 +7,17 @@ import android.view.ViewGroup
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskType
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.android.habitica.ui.adapter.BaseRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.viewHolders.BindableViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import javax.inject.Inject
abstract class BaseTasksRecyclerViewAdapter<VH : BindableViewHolder<Task>>(
var taskType: TaskType,
private val taskFilterHelper: TaskFilterHelper?,
private val viewModel: TasksViewModel,
private val layoutResource: Int,
newContext: Context,
private val userID: String?
@ -73,12 +73,12 @@ abstract class BaseTasksRecyclerViewAdapter<VH : BindableViewHolder<Task>>(
}
fun filter() {
if (this.taskFilterHelper == null || this.taskFilterHelper.howMany(taskType) == 0) {
if (this.viewModel.howMany(taskType) == 0) {
filteredContent = content
} else {
filteredContent = ArrayList()
content?.let {
filteredContent?.addAll(this.taskFilterHelper.filter(it))
filteredContent?.addAll(this.viewModel.filter(it))
}
}

View file

@ -2,10 +2,10 @@ package com.habitrpg.android.habitica.ui.adapter.tasks
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.ui.viewHolders.tasks.DailyViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
class DailiesRecyclerViewHolder(layoutResource: Int, taskFilterHelper: TaskFilterHelper) : RealmBaseTasksRecyclerViewAdapter(layoutResource, taskFilterHelper) {
class DailiesRecyclerViewHolder(layoutResource: Int, viewModel: TasksViewModel) : RealmBaseTasksRecyclerViewAdapter(layoutResource, viewModel) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 0) {

View file

@ -2,10 +2,10 @@ package com.habitrpg.android.habitica.ui.adapter.tasks
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.ui.viewHolders.tasks.HabitViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
class HabitsRecyclerViewAdapter(layoutResource: Int, taskFilterHelper: TaskFilterHelper) : RealmBaseTasksRecyclerViewAdapter(layoutResource, taskFilterHelper) {
class HabitsRecyclerViewAdapter(layoutResource: Int, viewModel: TasksViewModel) : RealmBaseTasksRecyclerViewAdapter(layoutResource, viewModel) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 0) {

View file

@ -11,13 +11,13 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.AdventureGuideMenuBannerBinding
import com.habitrpg.android.habitica.extensions.dpToPx
import com.habitrpg.android.habitica.extensions.layoutInflater
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.responses.TaskDirection
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.BaseRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
@ -27,7 +27,7 @@ import io.realm.OrderedRealmCollection
abstract class RealmBaseTasksRecyclerViewAdapter(
private val layoutResource: Int,
private val taskFilterHelper: TaskFilterHelper?
private val viewModel: TasksViewModel
) : BaseRecyclerViewAdapter<Task, RecyclerView.ViewHolder>(), TaskRecyclerViewAdapter {
override var canScoreTasks = true
private var unfilteredData: List<Task>? = null
@ -120,8 +120,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter(
final override fun filter() {
val unfilteredData = this.unfilteredData ?: return
if (taskFilterHelper != null && unfilteredData is OrderedRealmCollection) {
val query = taskFilterHelper.createQuery(unfilteredData)
if (unfilteredData is OrderedRealmCollection) {
val query = viewModel.createQuery(unfilteredData)
if (query != null) {
data = query.findAll()
}

View file

@ -2,10 +2,10 @@ package com.habitrpg.android.habitica.ui.adapter.tasks
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.ui.viewHolders.tasks.TodoViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
class TodosRecyclerViewAdapter(layoutResource: Int, taskFilterHelper: TaskFilterHelper) : RealmBaseTasksRecyclerViewAdapter(layoutResource, taskFilterHelper) {
class TodosRecyclerViewAdapter(layoutResource: Int, viewModel: TasksViewModel) : RealmBaseTasksRecyclerViewAdapter(layoutResource, viewModel) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 0) {

View file

@ -4,8 +4,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.TutorialRepository
@ -18,7 +18,7 @@ import io.reactivex.rxjava3.functions.Consumer
import java.util.concurrent.TimeUnit
import javax.inject.Inject
abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment() {
abstract class BaseDialogFragment<VB : ViewBinding> : BottomSheetDialogFragment() {
var isModal: Boolean = false
abstract var binding: VB?

View file

@ -24,6 +24,7 @@ import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.DrawerMainBinding
import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds
import com.habitrpg.android.habitica.extensions.getRemainingString
import com.habitrpg.android.habitica.extensions.getShortRemainingString
import com.habitrpg.android.habitica.extensions.getThemeColor
@ -49,16 +50,18 @@ import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.toDuration
class NavigationDrawerFragment : DialogFragment() {
@ -227,11 +230,9 @@ class NavigationDrawerFragment : DialogFragment() {
subscriptions?.add(
Flowable.combineLatest(
contentRepository.getWorldState(), inventoryRepository.getAvailableLimitedItems(),
{ state, items ->
return@combineLatest Pair(state, items)
}
).subscribe(
contentRepository.getWorldState(), inventoryRepository.getAvailableLimitedItems()) { state, items ->
return@combineLatest Pair(state, items)
}.subscribe(
{ pair ->
val gearEvent = pair.first.events.firstOrNull { it.gear }
createUpdatingJob("seasonal", {
@ -332,6 +333,10 @@ class NavigationDrawerFragment : DialogFragment() {
}
private fun updateUser(user: User) {
binding?.avatarView?.setOnClickListener {
MainNavigationController.navigate(R.id.openProfileActivity, bundleOf(Pair("userID", user.id)))
}
setMessagesCount(user.inbox)
setSettingsCount(if (user.flags?.verifiedUsername != true) 1 else 0)
setDisplayName(user.profile?.name)
@ -675,8 +680,8 @@ class NavigationDrawerFragment : DialogFragment() {
createUpdatingJob(activePromo.promoType.name, {
activePromo.isActive
}, {
val diff = activePromo.endDate.time - Date().time
if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1)
val diff = (activePromo.endDate.time - Date().time).toDuration(DurationUnit.SECONDS)
1.toDuration(diff.getMinuteOrSeconds())
}) {
if (activePromo.isActive) {
promotedItem.subtitle = context?.getString(R.string.sale_ends_in, activePromo.endDate.getShortRemainingString())

View file

@ -1,29 +1,47 @@
package com.habitrpg.android.habitica.ui.fragments.inventory.customization
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import androidx.core.content.ContextCompat
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.CustomizationRepository
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.databinding.BottomSheetBackgroundsFilterBinding
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
import com.habitrpg.android.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.setTintWith
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.CustomizationFilter
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.user.OwnedCustomization
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.CustomizationRecyclerViewAdapter
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 com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.kotlin.combineLatest
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import javax.inject.Inject
class AvatarCustomizationFragment :
BaseMainFragment<FragmentRefreshRecyclerviewBinding>(),
SwipeRefreshLayout.OnRefreshListener {
private var filterMenuItem: MenuItem? = null
override var binding: FragmentRefreshRecyclerviewBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRefreshRecyclerviewBinding {
@ -44,6 +62,9 @@ class AvatarCustomizationFragment :
internal var adapter: CustomizationRecyclerViewAdapter = CustomizationRecyclerViewAdapter()
internal var layoutManager: GridLayoutManager = GridLayoutManager(activity, 2)
private val currentFilter = BehaviorSubject.create<CustomizationFilter>()
private val ownedCustomizations = PublishSubject.create<List<OwnedCustomization>>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -115,6 +136,7 @@ class AvatarCustomizationFragment :
this.loadCustomizations()
userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
currentFilter.onNext(CustomizationFilter())
}
override fun onDestroy() {
@ -122,6 +144,42 @@ class AvatarCustomizationFragment :
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_list_customizations, menu)
filterMenuItem = menu.findItem(R.id.action_filter)
updateFilterIcon()
}
private fun updateFilterIcon() {
if (currentFilter.value?.isFiltering != true) {
filterMenuItem?.setIcon(R.drawable.ic_action_filter_list)
context?.let {
val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_action_filter_list)
filterIcon?.setTintWith(it.getThemeColor(R.attr.headerTextColor), PorterDuff.Mode.MULTIPLY)
filterMenuItem?.setIcon(filterIcon)
}
} else {
context?.let {
val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_filters_active)
filterIcon?.setTintWith(it.getThemeColor(R.attr.textColorPrimaryDark), PorterDuff.Mode.MULTIPLY)
filterMenuItem?.setIcon(filterIcon)
}
}
}
@Suppress("ReturnCount")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_filter -> {
showFilterDialog()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun injectFragment(component: UserComponent) {
component.inject(this)
}
@ -129,10 +187,44 @@ class AvatarCustomizationFragment :
private fun loadCustomizations() {
val type = this.type ?: return
compositeSubscription.add(
customizationRepository.getCustomizations(type, category, false).subscribe(
{
adapter.setCustomizations(if (type == "background") { it.reversed() } else { it })
},
customizationRepository.getCustomizations(type, category, false)
.combineLatest(currentFilter.toFlowable(BackpressureStrategy.DROP),
ownedCustomizations.toFlowable(BackpressureStrategy.DROP))
.subscribe(
{ (customizations, filter, ownedCustomizations) ->
if (filter.isFiltering) {
val displayedCustomizations = mutableListOf<Customization>()
for (customization in customizations) {
if (filter.onlyPurchased) {
if (ownedCustomizations.find { it.key == customization.identifier } == null) {
continue
}
}
if (filter.months.isNotEmpty()) {
if (!filter.months.contains(customization.customizationSetName?.substringAfter('.'))) {
continue
}
}
displayedCustomizations.add(customization)
}
adapter.setCustomizations(
if (!filter.ascending) {
displayedCustomizations.reversed()
} else {
displayedCustomizations
}
)
} else {
adapter.setCustomizations(
if (!filter.ascending) {
customizations.reversed()
} else {
customizations
}
)
}
adapter.ownedCustomizations = ownedCustomizations.map { it.key + "_" + it.type + "_" + it.category }
},
RxErrorHandler.handleEmptyError()
)
)
@ -154,9 +246,7 @@ class AvatarCustomizationFragment :
fun updateUser(user: User?) {
if (user == null) return
this.updateActiveCustomization(user)
val ownedCustomizations = ArrayList<String>()
user.purchased?.customizations?.filter { it.type == this.type && it.purchased }?.mapTo(ownedCustomizations) { it.key + "_" + it.type + "_" + it.category }
adapter.updateOwnership(ownedCustomizations)
ownedCustomizations.onNext(user.purchased?.customizations?.filter { it.type == this.type && it.purchased })
this.adapter.userSize = user.preferences?.size
this.adapter.hairColor = user.preferences?.hair?.color
this.adapter.gemBalance = user.gemCount
@ -192,7 +282,7 @@ class AvatarCustomizationFragment :
override fun onRefresh() {
compositeSubscription.add(
userRepository.retrieveUser(false, true).subscribe(
userRepository.retrieveUser(withTasks = false, forced = true).subscribe(
{
binding?.refreshLayout?.isRefreshing = false
},
@ -200,4 +290,60 @@ class AvatarCustomizationFragment :
)
)
}
fun showFilterDialog() {
val filter = currentFilter.value ?: CustomizationFilter()
val context = context ?: return
val dialog = HabiticaBottomSheetDialog(context)
val binding = BottomSheetBackgroundsFilterBinding.inflate(layoutInflater)
binding.showMeWrapper.check(if (filter.onlyPurchased) R.id.show_purchased_button else R.id.show_all_button)
binding.showMeWrapper.setOnCheckedChangeListener { _, checkedId ->
filter.onlyPurchased = checkedId == R.id.show_purchased_button
currentFilter.onNext(filter)
}
binding.clearButton.setOnClickListener {
currentFilter.onNext(CustomizationFilter())
dialog.dismiss()
}
if (type == "background") {
binding.sortByWrapper.check(if (filter.ascending) R.id.oldest_button else R.id.newest_button)
binding.sortByWrapper.setOnCheckedChangeListener { _, checkedId ->
filter.ascending = checkedId == R.id.oldest_button
currentFilter.onNext(filter)
}
configureMonthFilterButton(binding.januaryButton, 1, filter)
configureMonthFilterButton(binding.febuaryButton, 2, filter)
configureMonthFilterButton(binding.marchButton, 3, filter)
configureMonthFilterButton(binding.aprilButton, 4, filter)
configureMonthFilterButton(binding.mayButton, 5, filter)
configureMonthFilterButton(binding.juneButton, 6, filter)
configureMonthFilterButton(binding.julyButton, 7, filter)
configureMonthFilterButton(binding.augustButton, 8, filter)
configureMonthFilterButton(binding.septemberButton, 9, filter)
configureMonthFilterButton(binding.octoberButton, 10, filter)
configureMonthFilterButton(binding.novemberButton, 11, filter)
configureMonthFilterButton(binding.decemberButton, 12, filter)
} else {
binding.sortByTitle.visibility = View.GONE
binding.sortByWrapper.visibility = View.GONE
binding.monthReleasedTitle.visibility = View.GONE
binding.monthReleasedWrapper.visibility = View.GONE
}
dialog.setContentView(binding.root)
dialog.show()
}
private fun configureMonthFilterButton(button: CheckBox, value: Int, filter: CustomizationFilter) {
val identifier = value.toString().padStart(2, '0')
button.isChecked = filter.months.contains(identifier)
button.text
button.setOnCheckedChangeListener { _, isChecked ->
if (!isChecked && filter.months.contains(identifier)) {
filter.months.remove(identifier)
} else if (isChecked && !filter.months.contains(identifier)) {
filter.months.add(identifier)
}
currentFilter.onNext(filter)
}
}
}

View file

@ -70,14 +70,6 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
val useReminder = preferenceManager.sharedPreferences?.getBoolean("use_reminder", false)
timePreference?.isEnabled = useReminder ?: false
pushNotificationsPreference = findPreference("pushNotifications") as? PreferenceScreen
val usePushNotifications = preferenceManager.sharedPreferences?.getBoolean("usePushNotifications", true)
pushNotificationsPreference?.isEnabled = usePushNotifications ?: false
emailNotificationsPreference = findPreference("emailNotifications") as? PreferenceScreen
val useEmailNotifications = preferenceManager.sharedPreferences?.getBoolean("useEmailNotifications", true)
emailNotificationsPreference?.isEnabled = useEmailNotifications ?: false
classSelectionPreference = findPreference("choose_class")
val weekdayPreference = findPreference("FirstDayOfTheWeek") as? ListPreference
@ -193,23 +185,24 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
"usePushNotifications" -> {
val userPushNotifications = sharedPreferences.getBoolean(key, false)
pushNotificationsPreference?.isEnabled = userPushNotifications
userRepository.updateUser("preferences.pushNotifications.unsubscribeFromAll", userPushNotifications).subscribe()
if (userPushNotifications) {
pushNotificationManager.addPushDeviceUsingStoredToken()
} else {
pushNotificationManager.removePushDeviceUsingStoredToken()
}
}
"useEmailNotifications" -> {
"useEmails" -> {
val useEmailNotifications = sharedPreferences.getBoolean(key, false)
emailNotificationsPreference?.isEnabled = useEmailNotifications
userRepository.updateUser("preferences.emailNotifications.unsubscribeFromAll", useEmailNotifications).subscribe()
}
"cds_time" -> {
val timeval = sharedPreferences.getString("cds_time", "00:00")
val pieces = timeval?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
if (pieces != null) {
val hour = Integer.parseInt(pieces[0])
userRepository.changeCustomDayStart(hour).subscribe({ }, RxErrorHandler.handleEmptyError())
}
val timeval = sharedPreferences.getString("cds_time", "0") ?: "0"
val hour = Integer.parseInt(timeval)
userRepository.changeCustomDayStart(hour).subscribe({ }, RxErrorHandler.handleEmptyError())
val preference = findPreference<ListPreference>(key)
preference?.summary = preference?.entry
}
"language" -> {
val languageHelper = LanguageHelper(sharedPreferences.getString(key, "en"))
@ -316,8 +309,9 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
} else {
classSelectionPreference?.isVisible = false
}
val cdsTimePreference = findPreference("cds_time") as? TimePreference
cdsTimePreference?.text = user?.preferences?.dayStart.toString() + ":00"
val cdsTimePreference = findPreference("cds_time") as? ListPreference
cdsTimePreference?.value = user?.preferences?.dayStart.toString()
cdsTimePreference?.summary = cdsTimePreference?.entry
findPreference<Preference>("dailyDueDefaultView")?.setDefaultValue(user?.preferences?.dailyDueDefaultView)
val languagePreference = findPreference("language") as? ListPreference
languagePreference?.value = user?.preferences?.language
@ -345,6 +339,18 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
val inbox = user?.inbox
disablePMsPreference?.isChecked = inbox?.optOut ?: true
val usePushPreference = findPreference("usePushNotifications") as? CheckBoxPreference
pushNotificationsPreference = findPreference("pushNotifications") as? PreferenceScreen
val usePushNotifications = user?.preferences?.pushNotifications?.unsubscribeFromAll ?: false
pushNotificationsPreference?.isEnabled = usePushNotifications
usePushPreference?.isChecked = usePushNotifications
val useEmailPreference = findPreference("useEmails") as? CheckBoxPreference
emailNotificationsPreference = findPreference("emailNotifications") as? PreferenceScreen
val useEmailNotifications = user?.preferences?.emailNotifications?.unsubscribeFromAll ?: false
emailNotificationsPreference?.isEnabled = useEmailNotifications
useEmailPreference?.isChecked = useEmailNotifications
if (configManager.testingLevel() == AppTestingLevel.STAFF || BuildConfig.DEBUG) {
serverUrlPreference?.isVisible = true
taskListPreference?.isVisible = true

View file

@ -8,6 +8,7 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.DialogChallengeFilterBinding
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.ui.adapter.social.challenges.ChallengesFilterRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
import com.habitrpg.android.habitica.utils.Action1
internal class ChallengeFilterDialogHolder private constructor(
@ -28,13 +29,10 @@ internal class ChallengeFilterDialogHolder private constructor(
}
fun bind(
builder: AlertDialog.Builder,
filterGroups: List<Group>,
currentFilter: ChallengeFilterOptions?,
selectedGroupsCallback: Action1<ChallengeFilterOptions>
) {
builder.setPositiveButton(context.getString(R.string.done)) { _, _ -> doneClicked() }
.show()
this.filterGroups = filterGroups
this.currentFilter = currentFilter
this.selectedGroupsCallback = selectedGroupsCallback
@ -86,11 +84,11 @@ internal class ChallengeFilterDialogHolder private constructor(
val challengeFilterDialogHolder = ChallengeFilterDialogHolder(dialogLayout, activity)
val builder = AlertDialog.Builder(activity)
.setTitle(R.string.filter)
.setView(dialogLayout)
val sheet = HabiticaBottomSheetDialog(activity)
sheet.setContentView(dialogLayout)
challengeFilterDialogHolder.bind(builder, filterGroups, currentFilter, selectedGroupsCallback)
challengeFilterDialogHolder.bind(filterGroups, currentFilter, selectedGroupsCallback)
sheet.show()
}
}
}

View file

@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.shops.ShopItem
@ -22,6 +23,7 @@ import com.habitrpg.android.habitica.ui.adapter.tasks.RewardsRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import io.reactivex.rxjava3.functions.Consumer
import javax.inject.Inject
class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
@ -87,6 +89,11 @@ class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
)
}
override fun onDestroy() {
inventoryRepository.close()
super.onDestroy()
}
override fun getLayoutManager(context: Context?): LinearLayoutManager {
return GridLayoutManager(context, 4)
}

View file

@ -5,18 +5,15 @@ import android.content.Intent
import android.content.SharedPreferences
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.UserRepository
@ -24,13 +21,11 @@ import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBind
import com.habitrpg.android.habitica.extensions.observeOnce
import com.habitrpg.android.habitica.extensions.setScaledPadding
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.HapticFeedbackManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.SoundManager
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.responses.TaskDirection
import com.habitrpg.android.habitica.models.responses.TaskScoringResult
import com.habitrpg.android.habitica.models.tasks.Task
@ -47,16 +42,21 @@ import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.helpers.EmptyItem
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject
open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBinding>(), androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener {
var viewModel: TasksViewModel? = null
private var taskSubscription: Disposable? = null
internal var canEditTasks: Boolean = true
internal var canScoreTaks: Boolean = true
override var binding: FragmentRefreshRecyclerviewBinding? = null
@ -69,10 +69,6 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
var recyclerAdapter: TaskRecyclerViewAdapter? = null
var itemAnimator = SafeDefaultItemAnimator()
@Inject
lateinit var apiClient: ApiClient
@Inject
lateinit var taskFilterHelper: TaskFilterHelper
@Inject
lateinit var userRepository: UserRepository
@Inject
lateinit var inventoryRepository: InventoryRepository
@ -90,8 +86,6 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
internal var taskType: TaskType = TaskType.HABIT
private var itemTouchCallback: ItemTouchHelper.Callback? = null
var refreshAction: ((() -> Unit) -> Unit)? = null
internal val className: TaskType
get() = this.taskType
@ -103,18 +97,19 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
recyclerSubscription.dispose()
}
recyclerSubscription = CompositeDisposable()
val adapter: BaseRecyclerViewAdapter<*, *>? = when (this.taskType) {
TaskType.HABIT -> HabitsRecyclerViewAdapter(R.layout.habit_item_card, taskFilterHelper)
TaskType.DAILY -> DailiesRecyclerViewHolder(R.layout.daily_item_card, taskFilterHelper)
TaskType.TODO -> TodosRecyclerViewAdapter(R.layout.todo_item_card, taskFilterHelper)
TaskType.REWARD -> RewardsRecyclerViewAdapter(null, R.layout.reward_item_card)
else -> null
viewModel?.let { viewModel ->
val adapter: BaseRecyclerViewAdapter<*, *>? = when (this.taskType) {
TaskType.HABIT -> HabitsRecyclerViewAdapter(R.layout.habit_item_card, viewModel)
TaskType.DAILY -> DailiesRecyclerViewHolder(R.layout.daily_item_card, viewModel)
TaskType.TODO -> TodosRecyclerViewAdapter(R.layout.todo_item_card, viewModel)
TaskType.REWARD -> RewardsRecyclerViewAdapter(null, R.layout.reward_item_card)
else -> null
}
recyclerAdapter = adapter as? TaskRecyclerViewAdapter
recyclerAdapter?.canScoreTasks = canScoreTaks
binding?.recyclerView?.adapter = adapter
}
recyclerAdapter = adapter as? TaskRecyclerViewAdapter
recyclerAdapter?.canScoreTasks = canScoreTaks
binding?.recyclerView?.adapter = adapter
context?.let { recyclerAdapter?.taskDisplayMode = configManager.taskDisplayMode(it) }
recyclerAdapter?.errorButtonEvents?.subscribe(
@ -136,14 +131,11 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
recyclerAdapter?.brokenTaskEvents?.subscribeWithErrorHandler { showBrokenChallengeDialog(it) }?.let { recyclerSubscription.add(it) }
recyclerAdapter?.adventureGuideOpenEvents?.subscribeWithErrorHandler { MainNavigationController.navigate(R.id.adventureGuideActivity) }?.let { recyclerSubscription.add(it) }
recyclerSubscription.add(
taskRepository.getTasks(this.taskType).subscribe(
{
this.recyclerAdapter?.updateUnfilteredData(it)
},
RxErrorHandler.handleEmptyError()
)
)
viewModel?.ownerID?.observe(viewLifecycleOwner) {
canEditTasks = viewModel?.isPersonalBoard ?: true
canScoreTaks = viewModel?.isPersonalBoard ?: true
updateTaskSubscription(it)
}
}
private fun handleTaskResult(result: TaskScoringResult, value: Int) {
@ -190,7 +182,6 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
override fun onDestroy() {
userRepository.close()
inventoryRepository.close()
super.onDestroy()
}
@ -265,7 +256,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
) {
if (validTaskId != null) {
var newPosition = viewHolder.absoluteAdapterPosition
if (taskFilterHelper.howMany(taskType) > 0) {
if ((viewModel?.howMany(taskType) ?: 0) > 0) {
newPosition = if ((newPosition + 1) == recyclerAdapter?.data?.size) {
recyclerAdapter?.data?.get(newPosition - 1)?.position ?: newPosition
} else {
@ -318,6 +309,16 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
)
}
private fun updateTaskSubscription(ownerID: String?) {
taskSubscription = taskRepository.getTasks(this.taskType, ownerID).subscribe(
{
this.recyclerAdapter?.updateUnfilteredData(it)
},
RxErrorHandler.handleEmptyError()
)
}
protected fun showBrokenChallengeDialog(task: Task) {
context?.let {
if (!task.isValid) {
@ -354,7 +355,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
private fun setEmptyLabels() {
binding?.recyclerView?.emptyItem = if (taskFilterHelper.howMany(taskType) > 0) {
binding?.recyclerView?.emptyItem = if ((viewModel?.howMany(taskType) ?: 0) > 0) {
when (this.taskType) {
TaskType.HABIT -> {
EmptyItem(
@ -422,21 +423,9 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
private fun scoreTask(task: Task, direction: TaskDirection) {
compositeSubscription.add(
taskRepository.taskChecked(null, task.id ?: "", direction == TaskDirection.UP, false) { result ->
handleTaskResult(result, task.value.toInt())
if (!DateUtils.isToday(sharedPreferences.getLong("last_task_reporting", 0))) {
AmplitudeManager.sendEvent(
"task scored",
AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR,
AmplitudeManager.EVENT_HITTYPE_EVENT
)
sharedPreferences.edit {
putLong("last_task_reporting", Date().time)
}
}
}.subscribe()
)
viewModel?.scoreTask(task, direction) { result, value ->
handleTaskResult(result, value)
}
}
override fun onSaveInstanceState(outState: Bundle) {
@ -449,7 +438,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
override fun onRefresh() {
binding?.refreshLayout?.isRefreshing = true
refreshAction?.invoke {
viewModel?.refreshData {
binding?.refreshLayout?.isRefreshing = false
}
}
@ -459,13 +448,13 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
(activity as? MainActivity)?.viewModel?.user?.observeOnce(this) {
if (it != null) {
when (taskType) {
TaskType.TODO -> taskFilterHelper.setActiveFilter(
TaskType.TODO -> viewModel?.setActiveFilter(
TaskType.TODO,
Task.FILTER_ACTIVE
)
TaskType.DAILY -> {
if (it.isValid && it.preferences?.dailyDueDefaultView == true) {
taskFilterHelper.setActiveFilter(TaskType.DAILY, Task.FILTER_ACTIVE)
viewModel?.setActiveFilter(TaskType.DAILY, Task.FILTER_ACTIVE)
}
}
}
@ -481,7 +470,7 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
}
fun setActiveFilter(activeFilter: String) {
taskFilterHelper.setActiveFilter(taskType, activeFilter)
viewModel?.setActiveFilter(taskType, activeFilter)
recyclerAdapter?.filter()
setEmptyLabels()
@ -512,9 +501,10 @@ open class TaskRecyclerViewFragment : BaseFragment<FragmentRefreshRecyclerviewBi
companion object {
private const val CLASS_TYPE_KEY = "CLASS_TYPE_KEY"
fun newInstance(context: Context?, classType: TaskType): TaskRecyclerViewFragment {
fun newInstance(context: Context?, classType: TaskType, viewModel: TasksViewModel): TaskRecyclerViewFragment {
val fragment = TaskRecyclerViewFragment()
fragment.taskType = classType
fragment.viewModel = viewModel
var tutorialTexts: List<String>? = null
if (context != null) {
when (fragment.taskType) {

View file

@ -2,7 +2,6 @@ package com.habitrpg.android.habitica.ui.fragments.tasks
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.PorterDuff
import android.os.Bundle
import android.text.format.DateUtils
@ -16,51 +15,38 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
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.TagRepository
import com.habitrpg.android.habitica.databinding.FragmentViewpagerBinding
import com.habitrpg.android.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.setTintWith
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.tasks.TaskType
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.activities.TaskFormActivity
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.viewmodels.TasksViewModel
import com.habitrpg.android.habitica.ui.views.navigation.HabiticaBottomNavigationViewListener
import com.habitrpg.android.habitica.ui.views.tasks.TaskFilterDialog
import io.reactivex.rxjava3.disposables.Disposable
import java.util.Date
import java.util.WeakHashMap
import javax.inject.Inject
import javax.inject.Named
class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.OnQueryTextListener, HabiticaBottomNavigationViewListener {
internal val viewModel: TasksViewModel by viewModels()
override var binding: FragmentViewpagerBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentViewpagerBinding {
return FragmentViewpagerBinding.inflate(inflater, container, false)
}
@field:[Inject Named(AppModule.NAMED_USER_ID)]
lateinit var userID: String
@Inject
lateinit var taskFilterHelper: TaskFilterHelper
@Inject
lateinit var tagRepository: TagRepository
@Inject
lateinit var appConfigManager: AppConfigManager
@Inject
lateinit var sharedPreferences: SharedPreferences
private var refreshItem: MenuItem? = null
internal var viewFragmentsDictionary: MutableMap<Int, TaskRecyclerViewFragment>? = WeakHashMap()
@ -93,11 +79,16 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
arguments?.let {
val args = TasksFragmentArgs.fromBundle(it)
val taskTypeValue = args.taskType
if (args.ownerID?.isNotBlank() == true) {
viewModel.ownerID.value = args.ownerID ?: viewModel.userID
} else {
viewModel.ownerID.value = viewModel.userID
}
if (taskTypeValue?.isNotBlank() == true) {
val taskType = TaskType.from(taskTypeValue)
switchToTaskTab(taskType)
} else {
when (sharedPreferences.getString("launch_screen", "")) {
when (viewModel.sharedPreferences.getString("launch_screen", "")) {
"/user/tasks/habits" -> onTabSelected(TaskType.HABIT, false)
"/user/tasks/dailies" -> onTabSelected(TaskType.DAILY, false)
"/user/tasks/todos" -> onTabSelected(TaskType.TODO, false)
@ -105,6 +96,10 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
}
}
viewModel.ownerID.observe(viewLifecycleOwner) {
updateBoardDisplay()
}
}
override fun onResume() {
@ -118,7 +113,11 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
binding?.viewPager?.currentItem = binding?.viewPager?.currentItem ?: 0
bottomNavigation?.listener = this
bottomNavigation?.canAddTasks = true
bottomNavigation?.canAddTasks = viewModel.isPersonalBoard
activity?.binding?.toolbarTitle?.setOnClickListener {
viewModel.cycleOwnerIDs()
}
}
override fun onPause() {
@ -128,17 +127,16 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
super.onPause()
}
override fun onDestroy() {
tagRepository.close()
super.onDestroy()
}
override fun injectFragment(component: UserComponent) {
component.inject(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_main_activity, menu)
if (viewModel.isPersonalBoard) {
inflater.inflate(R.menu.menu_main_activity, menu)
} else {
inflater.inflate(R.menu.menu_team_board, menu)
}
filterMenuItem = menu.findItem(R.id.action_filter)
updateFilterIcon()
@ -167,7 +165,7 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
override fun onQueryTextChange(newText: String?): Boolean {
taskFilterHelper.searchQuery = newText
viewModel.searchQuery = newText
viewFragmentsDictionary?.values?.forEach { values -> values.recyclerAdapter?.filter() }
return true
}
@ -180,7 +178,11 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
R.id.action_reload -> {
refreshItem = item
refresh()
viewModel.refreshData { }
true
}
R.id.action_team_info -> {
MainNavigationController.navigate(R.id.guildFragment, bundleOf(Pair("groupID", viewModel.ownerID)))
true
}
else -> super.onOptionsItemSelected(item)
@ -191,15 +193,15 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
context?.let {
val disposable: Disposable
val dialog = TaskFilterDialog(it, HabiticaBaseApplication.userComponent)
disposable = tagRepository.getTags().subscribe({ tagsList -> dialog.setTags(tagsList) }, RxErrorHandler.handleEmptyError())
dialog.setActiveTags(taskFilterHelper.tags)
disposable = viewModel.tagRepository.getTags().subscribe({ tagsList -> dialog.setTags(tagsList) }, RxErrorHandler.handleEmptyError())
dialog.setActiveTags(viewModel.tags)
// There are some cases where these things might not be correctly set after the app resumes. This is just to catch that as best as possible
val navigation = bottomNavigation ?: activity?.binding?.bottomNavigation
val taskType = navigation?.activeTaskType ?: activeFragment?.taskType
if (taskType != null) {
dialog.setTaskType(taskType, taskFilterHelper.getActiveFilter(taskType))
dialog.setTaskType(taskType, viewModel.getActiveFilter(taskType))
}
dialog.setListener(object : TaskFilterDialog.OnFilterCompletedListener {
override fun onFilterCompleted(
@ -209,7 +211,7 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
if (viewFragmentsDictionary == null) {
return
}
taskFilterHelper.tags = activeTags
viewModel.tags = activeTags
if (activeTaskFilter != null) {
activeFragment?.setActiveFilter(activeTaskFilter)
}
@ -226,34 +228,18 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
}
private fun refresh() {
activeFragment?.onRefresh()
}
private fun loadTaskLists() {
val fragmentManager = childFragmentManager
binding?.viewPager?.adapter = object : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun createFragment(position: Int): Fragment {
val fragment: TaskRecyclerViewFragment = when (position) {
0 -> TaskRecyclerViewFragment.newInstance(context, TaskType.HABIT)
1 -> TaskRecyclerViewFragment.newInstance(context, TaskType.DAILY)
0 -> TaskRecyclerViewFragment.newInstance(context, TaskType.HABIT, viewModel)
1 -> TaskRecyclerViewFragment.newInstance(context, TaskType.DAILY, viewModel)
3 -> RewardsRecyclerviewFragment.newInstance(context, TaskType.REWARD, true)
else -> TaskRecyclerViewFragment.newInstance(context, TaskType.TODO)
}
fragment.refreshAction = {
compositeSubscription.add(
userRepository.retrieveUser(
withTasks = true,
forced = true
).doOnTerminate {
it()
}.subscribe({ }, RxErrorHandler.handleEmptyError())
)
else -> TaskRecyclerViewFragment.newInstance(context, TaskType.TODO, viewModel)
}
viewFragmentsDictionary?.put(position, fragment)
return fragment
}
@ -271,7 +257,7 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
private fun updateFilterIcon() {
val filterCount = taskFilterHelper.howMany(activeFragment?.taskType)
val filterCount = viewModel.howMany(activeFragment?.taskType)
filterMenuItem?.isVisible = activeFragment?.taskType != TaskType.REWARD
if (filterCount == 0) {
@ -361,7 +347,7 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
val bundle = Bundle()
bundle.putString(TaskFormActivity.TASK_TYPE_KEY, type.value)
bundle.putStringArrayList(TaskFormActivity.SELECTED_TAGS_KEY, ArrayList(taskFilterHelper.tags))
bundle.putStringArrayList(TaskFormActivity.SELECTED_TAGS_KEY, ArrayList(viewModel.tags))
val intent = Intent(activity, TaskFormActivity::class.java)
intent.putExtras(bundle)
@ -391,13 +377,13 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
}
}
if (!DateUtils.isToday(sharedPreferences.getLong("last_creation_reporting", 0))) {
if (!DateUtils.isToday(viewModel.sharedPreferences.getLong("last_creation_reporting", 0))) {
AmplitudeManager.sendEvent(
"task created",
AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR,
AmplitudeManager.EVENT_HITTYPE_EVENT
)
sharedPreferences.edit {
viewModel.sharedPreferences.edit {
putLong("last_creation_reporting", Date().time)
}
}
@ -448,4 +434,10 @@ class TasksFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.O
override fun onAdd(taskType: TaskType) {
openNewTaskActivity(taskType)
}
private fun updateBoardDisplay() {
activity?.title = viewModel.ownerTitle
val isPersonalBoard = viewModel.isPersonalBoard
bottomNavigation?.canAddTasks = isPersonalBoard
}
}

View file

@ -1,391 +0,0 @@
package com.habitrpg.android.habitica.ui.fragments.tasks
import android.app.Activity
import android.content.Intent
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
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.TagRepository
import com.habitrpg.android.habitica.databinding.FragmentViewpagerBinding
import com.habitrpg.android.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.setTintWith
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.tasks.TaskType
import com.habitrpg.android.habitica.ui.activities.TaskFormActivity
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.views.navigation.HabiticaBottomNavigationViewListener
import com.habitrpg.android.habitica.ui.views.tasks.TaskFilterDialog
import io.reactivex.rxjava3.disposables.Disposable
import java.util.Date
import java.util.WeakHashMap
import javax.inject.Inject
class TeamBoardFragment : BaseMainFragment<FragmentViewpagerBinding>(), SearchView.OnQueryTextListener, HabiticaBottomNavigationViewListener {
override var binding: FragmentViewpagerBinding? = null
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentViewpagerBinding {
return FragmentViewpagerBinding.inflate(inflater, container, false)
}
var teamID: String = ""
@Inject
lateinit var taskFilterHelper: TaskFilterHelper
@Inject
lateinit var tagRepository: TagRepository
@Inject
lateinit var appConfigManager: AppConfigManager
private var refreshItem: MenuItem? = null
internal var viewFragmentsDictionary: MutableMap<Int, TaskRecyclerViewFragment>? = WeakHashMap()
private var filterMenuItem: MenuItem? = null
private val activeFragment: TaskRecyclerViewFragment?
get() {
var fragment = viewFragmentsDictionary?.get(binding?.viewPager?.currentItem)
if (fragment == null) {
if (isAdded) {
fragment = (childFragmentManager.findFragmentByTag("android:switcher:" + R.id.viewPager + ":" + binding?.viewPager?.currentItem) as? TaskRecyclerViewFragment)
}
}
return fragment
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
this.usesTabLayout = false
this.hidesToolbar = true
this.usesBottomNavigation = true
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
val args = TeamBoardFragmentArgs.fromBundle(it)
teamID = args.teamID
}
compositeSubscription.add(
userRepository.getTeamPlan(teamID)
.subscribe(
{
activity?.title = it.name
},
RxErrorHandler.handleEmptyError()
)
)
compositeSubscription.add(userRepository.retrieveTeamPlan(teamID).subscribe({ }, RxErrorHandler.handleEmptyError()))
loadTaskLists()
}
override fun onResume() {
super.onResume()
bottomNavigation?.activeTaskType = when (binding?.viewPager?.currentItem) {
0 -> TaskType.HABIT
1 -> TaskType.DAILY
2 -> TaskType.TODO
3 -> TaskType.REWARD
else -> TaskType.HABIT
}
bottomNavigation?.listener = this
bottomNavigation?.canAddTasks = false
}
override fun onPause() {
if (bottomNavigation?.listener == this) {
bottomNavigation?.listener = null
}
super.onPause()
}
override fun onDestroy() {
tagRepository.close()
super.onDestroy()
}
override fun injectFragment(component: UserComponent) {
component.inject(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_team_board, menu)
filterMenuItem = menu.findItem(R.id.action_filter)
updateFilterIcon()
val item = menu.findItem(R.id.action_search)
tintMenuIcon(item)
val sv = item.actionView as? SearchView
sv?.setOnQueryTextListener(this)
sv?.setIconifiedByDefault(false)
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
filterMenuItem?.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
return true
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
// Do something when expanded
filterMenuItem?.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
return true
}
})
}
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
taskFilterHelper.searchQuery = newText
viewFragmentsDictionary?.values?.forEach { values -> values.recyclerAdapter?.filter() }
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_filter -> {
showFilterDialog()
true
}
R.id.action_reload -> {
refreshItem = item
refresh()
true
}
R.id.action_team_info -> {
MainNavigationController.navigate(R.id.guildFragment, bundleOf(Pair("groupID", teamID)))
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun showFilterDialog() {
context?.let {
val disposable: Disposable
val dialog = TaskFilterDialog(it, HabiticaBaseApplication.userComponent)
disposable = tagRepository.getTags().subscribe({ tagsList -> dialog.setTags(tagsList) }, RxErrorHandler.handleEmptyError())
dialog.setActiveTags(taskFilterHelper.tags)
if (activeFragment != null) {
val taskType = activeFragment?.taskType
if (taskType != null) {
dialog.setTaskType(taskType, taskFilterHelper.getActiveFilter(taskType))
}
}
dialog.setListener(object : TaskFilterDialog.OnFilterCompletedListener {
override fun onFilterCompleted(
activeTaskFilter: String?,
activeTags: MutableList<String>
) {
if (viewFragmentsDictionary == null) {
return
}
taskFilterHelper.tags = activeTags
if (activeTaskFilter != null) {
activeFragment?.setActiveFilter(activeTaskFilter)
}
viewFragmentsDictionary?.values?.forEach { values -> values.recyclerAdapter?.filter() }
updateFilterIcon()
}
})
dialog.setOnDismissListener {
if (!disposable.isDisposed) {
disposable.dispose()
}
}
dialog.show()
}
}
private fun refresh() {
activeFragment?.onRefresh()
}
private fun loadTaskLists() {
val fragmentManager = childFragmentManager
binding?.viewPager?.adapter = object : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun createFragment(position: Int): androidx.fragment.app.Fragment {
val fragment: TaskRecyclerViewFragment = when (position) {
0 -> TaskRecyclerViewFragment.newInstance(context, TaskType.HABIT)
1 -> TaskRecyclerViewFragment.newInstance(context, TaskType.DAILY)
3 -> RewardsRecyclerviewFragment.newInstance(context, TaskType.REWARD, false)
else -> TaskRecyclerViewFragment.newInstance(context, TaskType.TODO)
}
fragment.canEditTasks = false
fragment.canScoreTaks = false
fragment.refreshAction = {
compositeSubscription.add(
userRepository.retrieveTeamPlan(teamID)
.doOnTerminate {
it()
}.subscribe({ }, RxErrorHandler.handleEmptyError())
)
}
viewFragmentsDictionary?.put(position, fragment)
return fragment
}
override fun getItemCount(): Int = 4
}
binding?.viewPager?.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
bottomNavigation?.selectedPosition = position
updateFilterIcon()
}
})
}
private fun updateFilterIcon() {
if (filterMenuItem == null) {
return
}
var filterCount = 0
if (activeFragment != null) {
filterCount = taskFilterHelper.howMany(activeFragment?.taskType)
}
if (filterCount == 0) {
filterMenuItem?.setIcon(R.drawable.ic_action_filter_list)
context?.let {
val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_action_filter_list)
filterIcon?.setTintWith(it.getThemeColor(R.attr.headerTextColor), PorterDuff.Mode.MULTIPLY)
filterMenuItem?.setIcon(filterIcon)
}
} else {
context?.let {
val filterIcon = ContextCompat.getDrawable(it, R.drawable.ic_filters_active)
filterIcon?.setTintWith(it.getThemeColor(R.attr.textColorPrimaryDark), PorterDuff.Mode.MULTIPLY)
filterMenuItem?.setIcon(filterIcon)
}
}
}
// endregion
private fun openNewTaskActivity(type: TaskType) {
if (Date().time - (lastTaskFormOpen?.time ?: 0) < 2000) {
return
}
val additionalData = HashMap<String, Any>()
additionalData["created task type"] = type
additionalData["viewed task type"] = when (binding?.viewPager?.currentItem) {
0 -> TaskType.HABIT
1 -> TaskType.DAILY
2 -> TaskType.TODO
3 -> TaskType.REWARD
else -> ""
}
AmplitudeManager.sendEvent("open create task form", AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR, AmplitudeManager.EVENT_HITTYPE_EVENT, additionalData)
val bundle = Bundle()
bundle.putString(TaskFormActivity.TASK_TYPE_KEY, type.value)
bundle.putStringArrayList(TaskFormActivity.SELECTED_TAGS_KEY, ArrayList(taskFilterHelper.tags))
val intent = Intent(activity, TaskFormActivity::class.java)
intent.putExtras(bundle)
intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
if (this.isAdded) {
lastTaskFormOpen = Date()
taskCreatedResult.launch(intent)
}
}
//endregion Events
private val taskCreatedResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
onTaskCreatedResult(it.resultCode, it.data)
}
private fun onTaskCreatedResult(resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
val taskTypeValue = data?.getStringExtra(TaskFormActivity.TASK_TYPE_KEY)
if (taskTypeValue != null) {
val taskType = TaskType.from(taskTypeValue)
switchToTaskTab(taskType)
val index = indexForTaskType(taskType)
if (index != -1) {
val fragment = viewFragmentsDictionary?.get(index)
fragment?.binding?.recyclerView?.scrollToPosition(0)
}
}
}
}
private fun switchToTaskTab(taskType: TaskType?) {
val index = indexForTaskType(taskType)
if (binding?.viewPager != null && index != -1) {
binding?.viewPager?.currentItem = index
}
}
private fun indexForTaskType(taskType: TaskType?): Int {
if (taskType != null) {
for (index in 0 until (viewFragmentsDictionary?.size ?: 0)) {
val fragment = viewFragmentsDictionary?.get(index)
if (fragment != null && taskType == fragment.className) {
return index
}
}
}
return -1
}
override val displayedClassName: String?
get() = null
override fun addToBackStack(): Boolean = false
companion object {
var lastTaskFormOpen: Date? = null
}
override fun onTabSelected(taskType: TaskType, smooth: Boolean) {
val newItem = when (taskType) {
TaskType.HABIT -> 0
TaskType.DAILY -> 1
TaskType.TODO -> 2
TaskType.REWARD -> 3
else -> 0
}
binding?.viewPager?.setCurrentItem(newItem, smooth)
}
override fun onAdd(taskType: TaskType) {
openNewTaskActivity(taskType)
}
}

View file

@ -2,21 +2,16 @@ package com.habitrpg.android.habitica.ui.menu
import android.content.Context
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.habitrpg.android.habitica.databinding.MenuBottomSheetBinding
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
class BottomSheetMenu(context: Context) : BottomSheetDialog(context), View.OnClickListener {
class BottomSheetMenu(context: Context) : HabiticaBottomSheetDialog(context), View.OnClickListener {
private var binding = MenuBottomSheetBinding.inflate(layoutInflater)
private var runnable: ((Int) -> Unit)? = null
init {
setContentView(binding.root)
binding.titleView.visibility = View.GONE
val behavior = BottomSheetBehavior.from(binding.root.parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = 0
}
fun setSelectionRunnable(runnable: (Int) -> Unit) {
@ -26,12 +21,14 @@ class BottomSheetMenu(context: Context) : BottomSheetDialog(context), View.OnCli
override fun setTitle(title: CharSequence?) {
binding.titleView.text = title
binding.titleView.visibility = View.VISIBLE
grabberVisibility = View.GONE
}
fun addMenuItem(menuItem: BottomSheetMenuItem) {
val item = menuItem.inflate(this.context, layoutInflater, this.binding.menuItems)
item.setOnClickListener(this)
this.binding.menuItems.addView(item)
binding.root.requestLayout()
}
override fun onClick(v: View) {

View file

@ -20,6 +20,8 @@ class MainUserViewModel(val userRepository: UserRepository) {
get() = user.value?.id
val username: CharSequence
get() = user.value?.username ?: ""
val displayName: CharSequence
get() = user.value?.profile?.name ?: ""
val partyID: String?
get() = user.value?.party?.id
val isUserFainted: Boolean

View file

@ -0,0 +1,232 @@
package com.habitrpg.android.habitica.ui.viewmodels
import android.content.SharedPreferences
import android.text.format.DateUtils
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.TagRepository
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.responses.TaskDirection
import com.habitrpg.android.habitica.models.responses.TaskScoringResult
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskType
import com.habitrpg.android.habitica.modules.AppModule
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.realm.Case
import io.realm.OrderedRealmCollection
import io.realm.RealmQuery
import io.realm.Sort
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
class TasksViewModel: BaseViewModel() {
private var compositeSubscription: CompositeDisposable = CompositeDisposable()
override fun inject(component: UserComponent) {
component.inject(this)
}
@field:[Inject Named(AppModule.NAMED_USER_ID)]
lateinit var userID: String
@Inject
lateinit var taskRepository: TaskRepository
@Inject
lateinit var taskFilterHelper: TaskFilterHelper
@Inject
lateinit var tagRepository: TagRepository
@Inject
lateinit var appConfigManager: AppConfigManager
@Inject
lateinit var sharedPreferences: SharedPreferences
private var owners: List<Pair<String, CharSequence>> = listOf()
val ownerID: MutableLiveData<String?> by lazy {
MutableLiveData()
}
val isPersonalBoard: Boolean
get() {
return ownerID.value == userID
}
val ownerTitle: CharSequence
get() {
return owners.firstOrNull { it.first == ownerID.value }?.second ?: ""
}
init {
compositeSubscription.add(userRepository.getTeamPlans()
.subscribe({
owners = listOf(Pair(userID ?: "", userViewModel.displayName)) + it.map { Pair(it.id, it.summary) }
}, RxErrorHandler.handleEmptyError()))
compositeSubscription.add(userRepository.retrieveTeamPlans().subscribe({}, RxErrorHandler.handleEmptyError()))
}
internal fun refreshData(onComplete: () -> Unit) {
if (isPersonalBoard) {
compositeSubscription.add(
userRepository.retrieveUser(
withTasks = true,
forced = true
).doOnTerminate {
onComplete()
}.subscribe({ }, RxErrorHandler.handleEmptyError())
)
} else {
compositeSubscription.add(
userRepository.retrieveTeamPlan(ownerID.value ?: "")
.doOnTerminate {
onComplete()
}.subscribe({ }, RxErrorHandler.handleEmptyError())
)
}
}
fun cycleOwnerIDs() {
val nextIndex = owners.indexOfFirst { it.first == ownerID.value } + 1
if (nextIndex < owners.size) {
ownerID.value = owners[nextIndex].first
} else {
ownerID.value = owners[0].first
}
}
fun scoreTask(task: Task, direction: TaskDirection, onResult: (TaskScoringResult, Int) -> Unit) {
compositeSubscription.add(
taskRepository.taskChecked(null, task.id ?: "", direction == TaskDirection.UP, false) { result ->
onResult(result, task.value.toInt())
if (!DateUtils.isToday(sharedPreferences.getLong("last_task_reporting", 0))) {
AmplitudeManager.sendEvent(
"task scored",
AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR,
AmplitudeManager.EVENT_HITTYPE_EVENT
)
sharedPreferences.edit {
putLong("last_task_reporting", Date().time)
}
}
}.subscribe({}, RxErrorHandler.handleEmptyError())
)
}
var searchQuery: String? = null
private val activeFilters = HashMap<TaskType, String>()
var tags: MutableList<String> = mutableListOf()
fun howMany(type: TaskType?): Int {
return this.tags.size + if (isTaskFilterActive(type)) 1 else 0
}
private fun isTaskFilterActive(type: TaskType?): Boolean {
if (activeFilters[type] == null) {
return false
}
return if (TaskType.TODO == type) {
Task.FILTER_ACTIVE != activeFilters[type]
} else {
Task.FILTER_ALL != activeFilters[type]
}
}
fun filter(tasks: List<Task>): List<Task> {
if (tasks.isEmpty()) {
return tasks
}
val filtered = ArrayList<Task>()
var activeFilter: String? = null
if (activeFilters.size > 0) {
activeFilter = activeFilters[tasks[0].type]
}
for (task in tasks) {
if (isFiltered(task, activeFilter)) {
filtered.add(task)
}
}
return filtered
}
private fun isFiltered(task: Task, activeFilter: String?): Boolean {
if (!task.containsAllTagIds(tags)) {
return false
}
return if (activeFilter != null && activeFilter != Task.FILTER_ALL) {
when (activeFilter) {
Task.FILTER_ACTIVE -> if (task.type == TaskType.DAILY) {
task.isDisplayedActive
} else {
!task.completed
}
Task.FILTER_GRAY -> task.completed || !task.isDisplayedActive
Task.FILTER_WEAK -> task.value < 1
Task.FILTER_STRONG -> task.value >= 1
Task.FILTER_DATED -> task.dueDate != null
Task.FILTER_COMPLETED -> task.completed
else -> true
}
} else {
true
}
}
fun setActiveFilter(type: TaskType, activeFilter: String) {
activeFilters[type] = activeFilter
}
fun getActiveFilter(type: TaskType?): String? {
return if (activeFilters.containsKey(type)) {
activeFilters[type]
} else {
null
}
}
fun createQuery(unfilteredData: OrderedRealmCollection<Task>): RealmQuery<Task>? {
if (!unfilteredData.isValid) {
return null
}
var query = unfilteredData.where()
if (unfilteredData.size != 0) {
val taskType = unfilteredData[0].type
val activeFilter = getActiveFilter(taskType)
if (tags.size > 0) {
query = query.`in`("tags.id", tags.toTypedArray())
}
if (searchQuery?.isNotEmpty() == true) {
query = query
.beginGroup()
.contains("text", searchQuery ?: "", Case.INSENSITIVE)
.or()
.contains("notes", searchQuery ?: "", Case.INSENSITIVE)
.endGroup()
}
if (activeFilter != null && activeFilter != Task.FILTER_ALL) {
when (activeFilter) {
Task.FILTER_ACTIVE -> query = if (TaskType.DAILY == taskType) {
query.equalTo("completed", false).equalTo("isDue", true)
} else {
query.equalTo("completed", false)
}
Task.FILTER_GRAY -> query = query.equalTo("completed", true).or().equalTo("isDue", false)
Task.FILTER_WEAK -> query = query.lessThan("value", 1.0)
Task.FILTER_STRONG -> query = query.greaterThanOrEqualTo("value", 1.0)
Task.FILTER_DATED -> query = query.isNotNull("dueDate").equalTo("completed", false).sort("dueDate")
Task.FILTER_COMPLETED -> query = query.equalTo("completed", true)
}
}
if (activeFilter != Task.FILTER_DATED) {
query = query.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
}
}
return query
}
}

View file

@ -1,5 +1,6 @@
package com.habitrpg.android.habitica.ui.views
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
@ -7,6 +8,7 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.isUsingNightModeResources
@ -40,6 +42,7 @@ class CurrencyView : androidx.appcompat.widget.AppCompatTextView {
} catch (_: ArrayIndexOutOfBoundsException) {
!context.isUsingNightModeResources()
}
currency = attributes?.getString(R.styleable.CurrencyView_currency)
visibility = GONE
}
@ -105,13 +108,38 @@ class CurrencyView : androidx.appcompat.widget.AppCompatTextView {
}
}
var minForAbbrevation = 0
var decimals = 2
var animationDuration = 500L
var animationDelay = 0L
private fun update(value: Double) {
text = NumberAbbreviator.abbreviate(context, value, decimals, minForAbbrevation = minForAbbrevation)
}
private fun endUpdate() {
contentDescription = "$text $currencyContentDescription"
updateVisibility()
}
var value = 0.0
set(value) {
if (text.isEmpty() || animationDuration == 0L) {
update(value)
endUpdate()
} else {
val animator = ValueAnimator.ofFloat(field.toFloat(), value.toFloat())
animator.duration = animationDuration
animator.startDelay = animationDelay
animator.addUpdateListener {
update((it.animatedValue as Float).toDouble())
}
animator.doOnEnd {
endUpdate()
}
animator.start()
}
field = value
val abbreviatedValue = NumberAbbreviator.abbreviate(context, value)
text = abbreviatedValue
contentDescription = "$abbreviatedValue $currencyContentDescription"
updateVisibility()
}
var isLocked = false

View file

@ -0,0 +1,43 @@
package com.habitrpg.android.habitica.ui.views
import android.app.AlertDialog
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.preference.ListPreference
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.setScaledPadding
class HabiticaListPreference: ListPreference {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) :
super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
super(context,attrs,defStyleAttr)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onClick() {
val subtitleText = TextView(context)
subtitleText.setText(R.string.cds_subtitle)
val builder = AlertDialog.Builder(context).setSingleChoiceItems(entries,getValueIndex())
{ dialog, index ->
if (callChangeListener(entryValues[index].toString())) {
setValueIndex(index)
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
.setTitle(title)
val dialog = builder.create()
subtitleText.setScaledPadding(context, 24, 0, 24, 8)
dialog.listView.addHeaderView(subtitleText)
dialog.window?.decorView?.setBackgroundResource(R.color.window_background)
dialog.show()
}
private fun getValueIndex() = entryValues.indexOf(value)
}

View file

@ -1,5 +1,6 @@
package com.habitrpg.android.habitica.ui.views
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
@ -164,9 +165,23 @@ class ValueBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, at
binding.descriptionTextView.setTextColor(textColor)
}
var animationDuration = 500L
var animationDelay = 0L
fun set(value: Double, valueMax: Double) {
if (currentValue != value || maxValue != valueMax) {
currentValue = value
if (animationDuration == 0L || binding.valueTextView.text.isEmpty()) {
currentValue = value
} else {
val animator = ValueAnimator.ofInt(currentValue.toInt(), value.toInt())
animator.duration = animationDuration
animator.startDelay = animationDelay
animator.addUpdateListener {
currentValue = (it.animatedValue as Int).toDouble()
updateBar()
}
animator.start()
}
maxValue = valueMax
updateBar()
}

View file

@ -0,0 +1,116 @@
package com.habitrpg.android.habitica.ui.views.ads
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.AdButtonBinding
import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds
import com.habitrpg.android.habitica.extensions.getShortRemainingString
import com.habitrpg.android.habitica.extensions.layoutInflater
import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.AdType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class AdButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
var state: State = State.EMPTY
set(value) {
field = value
updateViews()
}
enum class State {
EMPTY,
READY,
LOADING,
UNAVAILABLE
}
private var updateJob: Job? = null
private var nextAdDate: Date? = null
private val binding = AdButtonBinding.inflate(context.layoutInflater, this)
var text: String = ""
set(value) {
field = value
updateViews()
}
init {
context.theme?.obtainStyledAttributes(
attrs,
R.styleable.AdButton,
0, 0
)?.let { attributes ->
text = attributes.getString(R.styleable.AdButton_text) ?: ""
binding.currencyView.currency = attributes.getString(R.styleable.AdButton_currency)
}
binding.textView.setTextColor(ContextCompat.getColor(context, R.color.white))
binding.currencyView.setTextColor(ContextCompat.getColor(context, R.color.white))
binding.currencyView.value = 0.0
gravity = Gravity.CENTER
state = State.EMPTY
}
private fun updateViews() {
when (state) {
State.READY -> {
binding.loadingIndicator.visibility = GONE
binding.textView.text = text
binding.textView.alpha = 1.0f
binding.textView.visibility = VISIBLE
binding.currencyView.visibility = VISIBLE
setBackgroundResource(R.drawable.ad_button_background)
}
State.UNAVAILABLE -> {
binding.loadingIndicator.visibility = GONE
binding.textView.text = context.getString(R.string.available_in, nextAdDate?.getShortRemainingString() ?: "")
binding.textView.alpha = 0.75f
binding.textView.visibility = VISIBLE
binding.currencyView.visibility = GONE
setBackgroundResource(R.drawable.ad_button_background_disabled)
}
State.EMPTY -> {
binding.loadingIndicator.visibility = GONE
binding.textView.visibility = GONE
binding.currencyView.visibility = GONE
}
State.LOADING -> {
binding.loadingIndicator.visibility = VISIBLE
binding.textView.visibility = GONE
binding.currencyView.visibility = GONE
}
}
isEnabled = state == State.READY
}
fun updateForAdType(type: AdType, lifecycleScope: LifecycleCoroutineScope) {
if (updateJob?.isActive == true) {
updateJob?.cancel()
}
nextAdDate = AdHandler.nextAdAllowedDate(type)
if (nextAdDate?.after(Date()) == true) {
updateJob = lifecycleScope.launch(Dispatchers.Main) {
while (nextAdDate?.after(Date()) == true) {
val remaining = ((nextAdDate?.time ?: 0L) - Date().time).toDuration(DurationUnit.MILLISECONDS)
state = if (remaining.isNegative()) State.READY else State.UNAVAILABLE
updateViews()
delay(1.toDuration(remaining.getMinuteOrSeconds()))
}
state = State.READY
}
}
}
}

View file

@ -0,0 +1,31 @@
package com.habitrpg.android.habitica.ui.views.dialogs
import android.content.Context
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.BottomSheetWrapperBinding
open class HabiticaBottomSheetDialog(context: Context) : BottomSheetDialog(context, R.style.SheetDialog) {
private val wrapperBinding = BottomSheetWrapperBinding.inflate(layoutInflater)
init {
behavior.peekHeight = context.resources.displayMetrics.heightPixels / 2
}
var grabberVisibility: Int
get() = wrapperBinding.grabber.visibility
set(value) {
wrapperBinding.grabber.visibility = value
}
override fun setContentView(view: View) {
wrapperBinding.container.addView(view)
super.setContentView(wrapperBinding.root)
}
override fun setContentView(layoutResId: Int) {
layoutInflater.inflate(layoutResId, wrapperBinding.container)
super.setContentView(wrapperBinding.root)
}
}

View file

@ -27,6 +27,8 @@ import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.activities.ArmoireActivityArgs
import com.habitrpg.android.habitica.ui.activities.ArmoireActivityDirections
import com.habitrpg.android.habitica.ui.views.CurrencyView
import com.habitrpg.android.habitica.ui.views.CurrencyViews
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
@ -357,12 +359,10 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
return
} else if ("gold" == shopItem.currency && "gem" != shopItem.key) {
observable = inventoryRepository.buyItem(user, shopItem.key, shopItem.value.toDouble(), quantity).map { buyResponse ->
if (shopItem.key == "armoire") {
snackbarText[0] = when {
buyResponse.armoire["type"] == "gear" -> context.getString(R.string.armoireEquipment, buyResponse.armoire["dropText"])
buyResponse.armoire["type"] == "food" -> context.getString(R.string.armoireFood, buyResponse.armoire["dropArticle"] ?: "", buyResponse.armoire["dropText"])
else -> context.getString(R.string.armoireExp)
}
if (shopItem.key == "armoire" && configManager.enableNewArmoire()) {
MainNavigationController.navigate(R.id.armoireActivity, ArmoireActivityDirections.openArmoireActivity(buyResponse.armoire["type"] ?: "",
buyResponse.armoire["dropText"] ?: "",
buyResponse.armoire["dropKey"] ?: "").arguments)
}
buyResponse
}

View file

@ -12,9 +12,7 @@ import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.content.ContextCompat
@ -22,32 +20,24 @@ import androidx.core.widget.CompoundButtonCompat
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.TagRepository
import com.habitrpg.android.habitica.databinding.DialogTaskFilterBinding
import com.habitrpg.android.habitica.extensions.OnChangeTextWatcher
import com.habitrpg.android.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskType
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
import io.reactivex.rxjava3.core.Observable
import java.util.UUID
import javax.inject.Inject
class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAlertDialog(context), RadioGroup.OnCheckedChangeListener {
private var clearButton: Button
class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaBottomSheetDialog(context), RadioGroup.OnCheckedChangeListener {
private val binding = DialogTaskFilterBinding.inflate(layoutInflater)
@Inject
lateinit var repository: TagRepository
private var taskTypeTitle: TextView
private var taskFilters: RadioGroup
private var allTaskFilter: RadioButton
private var secondTaskFilter: RadioButton
private var thirdTaskFilter: RadioButton
private var tagsEditButton: Button
private var tagsList: LinearLayout
private var taskType: TaskType? = null
private var listener: OnFilterCompletedListener? = null
@ -65,22 +55,12 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
component?.inject(this)
addIcon = ContextCompat.getDrawable(context, R.drawable.ic_add_purple_300_36dp)
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.dialog_task_filter, null)
setTitle(R.string.filters)
this.setAdditionalContentView(view)
this.setContentView(binding.root)
taskTypeTitle = view.findViewById(R.id.task_type_title)
taskFilters = view.findViewById(R.id.task_filter_wrapper)
allTaskFilter = view.findViewById(R.id.all_task_filter)
secondTaskFilter = view.findViewById(R.id.second_task_filter)
thirdTaskFilter = view.findViewById(R.id.third_task_filter)
tagsEditButton = view.findViewById(R.id.tag_edit_button)
tagsList = view.findViewById(R.id.tags_list)
binding.taskFilterWrapper.setOnCheckedChangeListener(this)
taskFilters.setOnCheckedChangeListener(this)
clearButton = addButton(R.string.clear, false, false, false) { _, _ ->
binding.clearButton.setOnClickListener {
if (isEditing) {
stopEditing()
}
@ -88,16 +68,12 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
setActiveTags(null)
}
addButton(R.string.done, false) { _, _ ->
if (isEditing) {
stopEditing()
}
listener?.onFilterCompleted(filterType, activeTags)
this.dismiss()
}
buttonAxis = LinearLayout.HORIZONTAL
binding.tagEditButton.setOnClickListener { editButtonClicked() }
}
tagsEditButton.setOnClickListener { editButtonClicked() }
override fun dismiss() {
listener?.onFilterCompleted(filterType, activeTags)
super.dismiss()
}
override fun show() {
@ -111,7 +87,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
private fun createTagViews() {
tagsList.removeAllViews()
binding.tagsList.removeAllViews()
val colorStateList = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_checked), // disabled
@ -149,7 +125,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
filtersChanged()
}
tagsList.addView(tagCheckbox)
binding.tagsList.addView(tagCheckbox)
}
createAddTagButton()
}
@ -164,7 +140,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
button.setBackgroundResource(R.drawable.layout_rounded_bg_lighter_gray)
button.setTextColor(ContextCompat.getColor(context, R.color.text_secondary))
tagsList.addView(button)
binding.tagsList.addView(button)
}
private fun createTag() {
@ -177,17 +153,17 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
private fun startEditing() {
isEditing = true
tagsList.removeAllViews()
binding.tagsList.removeAllViews()
createTagEditViews()
tagsEditButton.setText(R.string.done)
binding.tagEditButton.setText(R.string.done)
this.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}
private fun stopEditing() {
isEditing = false
tagsList.removeAllViews()
binding.tagsList.removeAllViews()
createTagViews()
tagsEditButton.setText(R.string.edit_tag_btn_edit)
binding.tagEditButton.setText(R.string.edit_tag_btn_edit)
this.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
repository.updateTags(editedTags.values).toObservable().flatMap { tags -> Observable.fromIterable(tags) }.subscribe({ tag -> editedTags.remove(tag.id) }, RxErrorHandler.handleEmptyError())
repository.createTags(createdTags.values).toObservable().flatMap { tags -> Observable.fromIterable(tags) }.subscribe({ tag -> createdTags.remove(tag.id) }, RxErrorHandler.handleEmptyError())
@ -204,7 +180,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
private fun createTagEditView(inflater: LayoutInflater, index: Int, tag: Tag) {
val wrapper = inflater.inflate(R.layout.edit_tag_item, tagsList, false) as? LinearLayout
val wrapper = inflater.inflate(R.layout.edit_tag_item, binding.tagsList, false) as? LinearLayout
val tagEditText = wrapper?.findViewById<View>(R.id.edit_text) as? EditText
tagEditText?.setText(tag.name)
tagEditText?.addTextChangedListener(
@ -233,9 +209,9 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
activeTags.remove(tag.id)
tags.remove(tag)
tagsList.removeView(wrapper)
binding.tagsList.removeView(wrapper)
}
tagsList.addView(wrapper)
binding.tagsList.addView(wrapper)
}
fun setActiveTags(tagIds: MutableList<String>?) {
@ -244,13 +220,13 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
} else {
this.activeTags = tagIds
}
for (index in 0 until tagsList.childCount - 1) {
(tagsList.getChildAt(index) as? AppCompatCheckBox)?.isChecked = false
for (index in 0 until binding.tagsList.childCount - 1) {
(binding.tagsList.getChildAt(index) as? AppCompatCheckBox)?.isChecked = false
}
for (tagId in this.activeTags) {
val index = indexForId(tagId)
if (index >= 0) {
(tagsList.getChildAt(index) as? AppCompatCheckBox)?.isChecked = true
(binding.tagsList.getChildAt(index) as? AppCompatCheckBox)?.isChecked = true
}
}
filtersChanged()
@ -269,22 +245,22 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
this.taskType = taskType
when (taskType) {
TaskType.HABIT -> {
taskTypeTitle.setText(R.string.habits)
allTaskFilter.setText(R.string.all)
secondTaskFilter.setText(R.string.weak)
thirdTaskFilter.setText(R.string.strong)
binding.taskTypeTitle.setText(R.string.habits)
binding.allTaskFilter.setText(R.string.all)
binding.secondTaskFilter.setText(R.string.weak)
binding.thirdTaskFilter.setText(R.string.strong)
}
TaskType.DAILY -> {
taskTypeTitle.setText(R.string.dailies)
allTaskFilter.setText(R.string.all)
secondTaskFilter.setText(R.string.due)
thirdTaskFilter.setText(R.string.gray)
binding.taskTypeTitle.setText(R.string.dailies)
binding.allTaskFilter.setText(R.string.all)
binding.secondTaskFilter.setText(R.string.due)
binding.thirdTaskFilter.setText(R.string.gray)
}
TaskType.TODO -> {
taskTypeTitle.setText(R.string.todos)
allTaskFilter.setText(R.string.active)
secondTaskFilter.setText(R.string.dated)
thirdTaskFilter.setText(R.string.completed)
binding.taskTypeTitle.setText(R.string.todos)
binding.allTaskFilter.setText(R.string.active)
binding.secondTaskFilter.setText(R.string.dated)
binding.thirdTaskFilter.setText(R.string.completed)
}
}
setActiveFilter(activeFilter)
@ -307,7 +283,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
}
}
taskFilters.check(checkedId)
binding.taskFilterWrapper.check(checkedId)
filtersChanged()
}
@ -345,9 +321,9 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
private fun filtersChanged() {
clearButton.isEnabled = hasActiveFilters()
clearButton.setTextColor(
if (clearButton.isEnabled) {
binding.clearButton.isEnabled = hasActiveFilters()
binding.clearButton.setTextColor(
if (binding.clearButton.isEnabled) {
context.getThemeColor(R.attr.colorAccent)
} else {
ContextCompat.getColor(context, R.color.text_dimmed)
@ -356,7 +332,7 @@ class TaskFilterDialog(context: Context, component: UserComponent?) : HabiticaAl
}
private fun hasActiveFilters(): Boolean {
return taskFilters.checkedRadioButtonId != R.id.all_task_filter || activeTags.size > 0
return binding.taskFilterWrapper.checkedRadioButtonId != R.id.all_task_filter || activeTags.size > 0
}
fun setListener(listener: OnFilterCompletedListener) {

View file

@ -8,7 +8,7 @@ buildscript {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'

View file

@ -1,6 +1,6 @@
#Sun Apr 17 15:16:40 EDT 2022
#Fri Apr 22 14:22:42 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME