diff --git a/Habitica/res/layout/fragment_no_party.xml b/Habitica/res/layout/fragment_no_party.xml
index ff4baa7f2..8acadefce 100644
--- a/Habitica/res/layout/fragment_no_party.xml
+++ b/Habitica/res/layout/fragment_no_party.xml
@@ -1,30 +1,33 @@
+ android:id="@+id/refreshLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+
+
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ android:layout_height="wrap_content" />
+
+
+ android:layout_alignParentTop="true" />
+
+ android:background="@drawable/no_party_background_gradient" />
+
+
+
+ android:layout_marginTop="12dp"
+ android:src="@drawable/no_party_banner" />
+
+ android:layout_margin="@dimen/spacing_sides"
+ android:orientation="vertical">
+
-
+
+
+ android:layout_marginEnd="@dimen/spacing_large"
+ android:gravity="center"
+ android:text="@string/no_party_description"
+ android:textColor="@color/text_quad" />
+
+ android:layout_marginTop="@dimen/spacing_large"
+ android:text="@string/create_party" />
-
+
+
+ android:paddingTop="@dimen/spacing_sides">
+
+ android:layout_marginBottom="@dimen/spacing_large"
+ android:src="@drawable/party_seeking" />
+
+ android:text="@string/seeking_party_title"
+ android:textSize="16sp" />
+
+ android:layout_marginBottom="@dimen/spacing_large"
+ android:gravity="center"
+ android:text="@string/seeking_party_description"
+ android:textColor="@color/text_green1_green500"
+ android:textColorLink="@color/text_brand_neon" />
+ android:text="@string/look_for_party" />
+
+
+ android:textColor="@color/text_green1_green500"
+ android:textSize="16sp" />
+ android:textColor="@color/text_green1_green500" />
+ android:textColor="@color/text_red_maroon" />
-
-
+
+
+
+
diff --git a/Habitica/res/values-night/colors.xml b/Habitica/res/values-night/colors.xml
index b2e4fc7ed..b5db0e555 100644
--- a/Habitica/res/values-night/colors.xml
+++ b/Habitica/res/values-night/colors.xml
@@ -51,6 +51,7 @@
@color/white
@color/red_100
@color/brand_50
+ @color/green_500
@color/gray_10
@color/gray_400
diff --git a/Habitica/res/values/colors.xml b/Habitica/res/values/colors.xml
index afdd21852..9ce855bde 100644
--- a/Habitica/res/values/colors.xml
+++ b/Habitica/res/values/colors.xml
@@ -123,6 +123,7 @@
@color/brand_700
@color/white
@color/maroon_5
+ @color/green_1
@color/yellow_1
@color/green_10
diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml
index e9a2bbec9..b64a9e015 100644
--- a/Habitica/res/values/strings.xml
+++ b/Habitica/res/values/strings.xml
@@ -314,8 +314,9 @@
I just hatched a %1$s %2$s pet in #Habitica by completing my real-life tasks!
I just gained a %1$s mount in #Habitica by completing my real-life tasks!
Open Play Store
- Are you sure you want to change your class? This will cost 3 gems.
- Warning: You will no longer be able to buy equipment from the %s class.
+ Do you want to change your class for 3 Gems?
+ Do you want to change your class to %1$s for 3 Gems?
+ This will refund your stat points, switch which gear is unlocked in shops, and change your available skills.
Confirm
Market
Time Travelers
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDate-Extensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDate-Extensions.kt
new file mode 100644
index 000000000..ab08da6ba
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDate-Extensions.kt
@@ -0,0 +1,38 @@
+package com.habitrpg.android.habitica.extensions
+
+import java.time.DayOfWeek
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeFormatterBuilder
+import java.time.format.TextStyle
+import java.time.temporal.TemporalAccessor
+import java.util.Locale
+
+fun String.parseToZonedDateTime(): ZonedDateTime? {
+ val parsed: TemporalAccessor = formatter().parseBest(
+ this,
+ ZonedDateTime::from, LocalDateTime::from
+ )
+ return if (parsed is ZonedDateTime) {
+ parsed
+ } else {
+ val defaultZone: ZoneId = ZoneId.of("UTC")
+ (parsed as LocalDateTime).atZone(defaultZone)
+ }
+}
+
+/**
+ * Returns full display name in default Locale (Monday, Tuesday, Wednesday, etc.)
+ */
+fun ZonedDateTime.dayOfWeekString(): String {
+ return DayOfWeek.from(this).getDisplayName(TextStyle.FULL, Locale.getDefault())
+}
+
+fun formatter(): DateTimeFormatter =
+ DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE)
+ .appendPattern("['T'][' ']")
+ .append(DateTimeFormatter.ISO_LOCAL_TIME)
+ .appendPattern("[XX]")
+ .toFormatter()
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt
index e9807a5e0..dfef3b127 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt
@@ -82,10 +82,8 @@ class TaskAlarmManager(
}
private fun setTimeForDailyReminder(remindersItem: RemindersItem?, task: Task): RemindersItem? {
- val newTime = (remindersItem?.let { task.getNextReminderOccurrence(it) } ?: return null)
-
- remindersItem.time = newTime.withZoneSameLocal(ZoneId.systemDefault())
- .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+ val newTime = task.getNextReminderOccurrence(remindersItem, context)
+ remindersItem?.time = newTime?.withZoneSameLocal(ZoneId.systemDefault())?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
return remindersItem
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt
index d68f3d2d3..f9a84840b 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt
@@ -1,10 +1,13 @@
package com.habitrpg.android.habitica.models.tasks
+import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.text.Spanned
import com.google.gson.annotations.SerializedName
import com.habitrpg.android.habitica.R
+import com.habitrpg.android.habitica.extensions.dayOfWeekString
+import com.habitrpg.android.habitica.extensions.parseToZonedDateTime
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.common.habitica.helpers.ExceptionHandler
@@ -22,9 +25,6 @@ import org.json.JSONException
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
-import java.time.format.DateTimeFormatterBuilder
-import java.time.temporal.TemporalAccessor
import java.util.Date
open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
@@ -308,50 +308,53 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
fun checkIfDue(): Boolean = isDue == true
- fun getNextReminderOccurrence(remindersItem: RemindersItem): ZonedDateTime? {
- remindersItem.time?.let { oldTime ->
- val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant()
-
- // If task !isDisplayedActive or if isDisplayedActive but reminder passed,
- // set a updated reminder with nextDate
- return if (nextDue?.firstOrNull() != null && (!isDisplayedActive || remindersItem.getLocalZonedDateTimeInstant()?.isBefore(now) == true)) {
- val nextDate = LocalDateTime.ofInstant(nextDue?.firstOrNull()?.toInstant(), ZoneId.systemDefault())
+ /**
+ * When manually changing the alarm time, nextDue will provide the previously set upcoming alarm -
+ * however if the alarm was changed to a upcoming time today (and not before the current time) AND the previous alarm
+ * set for today already passed, we want to check if the alarm repeat/days includes today as well.
+ * If so - use today's date to schedule an alarm. If not, use nextDue.
+ *
+ * Alarms automatically rescheduled -> nextDue used.
+ * A alarm that already passed today and is rescheduled for a upcoming time today -> Schedule for today (instead of using nextDue)
+ */
+ fun getNextReminderOccurrence(remindersItem: RemindersItem?, context: Context): ZonedDateTime? {
+ if (remindersItem == null) {
+ return null
+ }
+ val reminderTime = remindersItem.time?.parseToZonedDateTime()
+ val zonedDateTimeNow = ZonedDateTime.now()
+ return if (nextDue?.firstOrNull() != null && (!isDisplayedActive || reminderTime?.isBefore(zonedDateTimeNow) == true)
+ ) {
+ val repeatingDays = repeat?.dayStrings(context)
+ val isScheduledForToday = repeatingDays?.find { day -> day == zonedDateTimeNow.dayOfWeekString() }
+ if (isScheduledForToday != null) {
val currentDateTime = LocalDateTime.now()
- val nextDueCalendar: LocalDateTime = LocalDateTime.of(
+ val updatedDateTime: LocalDateTime = LocalDateTime.of(
currentDateTime.year,
- currentDateTime.month, // Add one to adjust from zero-based counting.
+ currentDateTime.month,
currentDateTime.dayOfMonth,
- nextDate.hour,
- nextDate.minute
+ reminderTime?.hour ?: 0,
+ reminderTime?.minute ?: 0
)
- nextDueCalendar.atZone(ZoneId.systemDefault())
+ updatedDateTime.atZone(ZoneId.systemDefault())
} else {
- return parse(oldTime)
+ val nextDate = LocalDateTime.ofInstant(nextDue?.firstOrNull()?.toInstant(), ZoneId.systemDefault())
+ val updatedDateTime: LocalDateTime = LocalDateTime.of(
+ nextDate.year,
+ nextDate.month,
+ nextDate.dayOfMonth,
+ reminderTime?.hour ?: 0,
+ reminderTime?.minute ?: 0
+ )
+ updatedDateTime.atZone(ZoneId.systemDefault())
}
- }
- return null
- }
-
- fun formatter(): DateTimeFormatter =
- DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE)
- .appendPattern("['T'][' ']")
- .append(DateTimeFormatter.ISO_LOCAL_TIME)
- .appendPattern("[XX]")
- .toFormatter()
-
- fun parse(dateTime: String): ZonedDateTime? {
- val parsed: TemporalAccessor = formatter().parseBest(
- dateTime,
- ZonedDateTime::from, LocalDateTime::from
- )
- return if (parsed is ZonedDateTime) {
- parsed
} else {
- val defaultZone: ZoneId = ZoneId.of("UTC")
- (parsed as LocalDateTime).atZone(defaultZone)
+ return reminderTime
}
}
+
+
fun parseMarkdown() {
parsedText = MarkdownParser.parseMarkdown(text)
parsedNotes = MarkdownParser.parseMarkdown(notes)
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ClassSelectionActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ClassSelectionActivity.kt
index 89bcefb0c..ad1b65605 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ClassSelectionActivity.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ClassSelectionActivity.kt
@@ -23,6 +23,7 @@ import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaProgressDialog
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -224,28 +225,30 @@ class ClassSelectionActivity : BaseActivity() {
private fun displayConfirmationDialogForClass() {
if (!this.isInitialSelection && this.classWasUnset == false) {
val alert = HabiticaAlertDialog(this)
- alert.setTitle(getString(R.string.change_class_confirmation))
- alert.setMessage(getString(R.string.change_class_equipment_warning, currentClass))
+ alert.setTitle(getString(R.string.change_class_selected_confirmation, newClass))
+ alert.setMessage(getString(R.string.change_class_equipment_warning))
alert.addButton(R.string.choose_class, true) { _, _ ->
- selectClass(newClass)
- displayClassChanged()
+ selectClass(newClass, true)
}
alert.addButton(R.string.dialog_go_back, false)
alert.show()
} else {
val alert = HabiticaAlertDialog(this)
alert.setTitle(getString(R.string.class_confirmation, className))
- alert.addButton(R.string.choose_class, true) { _, _ -> selectClass(newClass) }
+ alert.addButton(R.string.choose_class, true) { _, _ -> selectClass(newClass, false) }
alert.addButton(R.string.dialog_go_back, false)
alert.show()
}
}
- private fun displayClassChanged() {
+ private fun displayClassChanged(selectedClass: String) {
val alert = HabiticaAlertDialog(this)
alert.setTitle(getString(R.string.class_changed, className))
- alert.setMessage(getString(R.string.class_changed_description, className))
- alert.addButton(getString(R.string.complete_tutorial), true)
+ alert.setMessage(getString(R.string.class_changed_description, selectedClass))
+ alert.addButton(getString(R.string.complete_tutorial), true){ _, _ -> dismiss() }
+ alert.setOnCancelListener {
+ dismiss()
+ }
alert.enqueue()
}
@@ -258,17 +261,17 @@ class ClassSelectionActivity : BaseActivity() {
}
}
- private fun selectClass(selectedClass: String) {
+ private fun selectClass(selectedClass: String, isChanging: Boolean) {
shouldFinish = true
this.displayProgressDialog(getString(R.string.changing_class_progress))
- lifecycleScope.launch(ExceptionHandler.coroutine()) {
+ lifecycleScope.launch(Dispatchers.Main) {
userRepository.changeClass(selectedClass)
- dismiss()
+ if (isChanging) displayClassChanged(selectedClass)
}
}
private fun displayProgressDialog(progressText: String) {
- HabiticaProgressDialog.show(this, progressText)
+ HabiticaProgressDialog.show(this, progressText, 300)
}
private fun dismiss() {
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt
index f258e9258..7947d5cdf 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/NavigationDrawerAdapter.kt
@@ -67,8 +67,7 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
fun updateItem(item: HabiticaDrawerItem) {
val position = getItemPosition(item.identifier)
items[position] = item
- val visiblePosition = getVisibleItemPosition(item.identifier)
- notifyItemChanged(visiblePosition)
+ notifyDataSetChanged()
}
fun updateItems(newItems: List) {
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
index a780f1c12..70fef8440 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
@@ -296,7 +296,7 @@ class NavigationDrawerFragment : DialogFragment() {
}
item.isVisible = true
}
- updateItem(item)
+ adapter.updateItem(item)
}
val statsItem = getItemWithIdentifier(SIDEBAR_STATS)
if (statsItem != null) {
@@ -310,7 +310,7 @@ class NavigationDrawerFragment : DialogFragment() {
} else {
statsItem.isVisible = false
}
- updateItem(statsItem)
+ adapter.updateItem(statsItem)
}
val subscriptionItem = getItemWithIdentifier(SIDEBAR_SUBSCRIPTION)
@@ -336,12 +336,12 @@ class NavigationDrawerFragment : DialogFragment() {
subscriptionItem?.subtitle = context?.getString(R.string.more_out_of_habitica)
}
- subscriptionItem?.let { updateItem(it) }
+ subscriptionItem?.let { adapter.updateItem(it) }
val promoItem = getItemWithIdentifier(SIDEBAR_SUBSCRIPTION_PROMO)
if (promoItem != null) {
promoItem.isVisible = !user.isSubscribed
- updateItem(promoItem)
+ adapter.updateItem(promoItem)
}
getItemWithIdentifier(SIDEBAR_NEWS)?.let {
it.showBubble = user.flags?.newStuff ?: false
@@ -689,10 +689,6 @@ class NavigationDrawerFragment : DialogFragment() {
private fun getItemWithIdentifier(identifier: String): HabiticaDrawerItem? =
adapter.getItemWithIdentifier(identifier)
- private fun updateItem(item: HabiticaDrawerItem) {
- adapter.updateItem(item)
- }
-
private fun setDisplayName(name: String?) {
if (name != null && name.isNotEmpty()) {
binding?.toolbarTitle?.text = name
@@ -793,17 +789,17 @@ class NavigationDrawerFragment : DialogFragment() {
R.string.sale_ends_in,
activePromo.endDate.getShortRemainingString()
)
- updateItem(promotedItem)
+ adapter.updateItem(promotedItem)
} else {
promotedItem.subtitle = null
promotedItem.pillText = null
- updateItem(promotedItem)
+ adapter.updateItem(promotedItem)
}
}
} ?: run {
promoItem.isVisible = false
}
- updateItem(promoItem)
+ adapter.updateItem(promoItem)
}
companion object {
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt
index 63c56a07f..193dd3656 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt
@@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaProgressDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaProgressDialog.kt
index 80202daee..e5cf910c5 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaProgressDialog.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/HabiticaProgressDialog.kt
@@ -11,10 +11,10 @@ class HabiticaProgressDialog(context: Context) : HabiticaAlertDialog(context) {
return show(context, context?.getString(titleID))
}
- fun show(context: Context?, title: String?): HabiticaProgressDialog? {
+ fun show(context: Context?, title: String?, dialogWidth: Int = 300): HabiticaProgressDialog? {
val dialog = context?.let { HabiticaProgressDialog(it) }
dialog?.setAdditionalContentView(R.layout.circular_progress)
- dialog?.dialogWidth = 300.dpToPx(context)
+ dialog?.dialogWidth = dialogWidth.dpToPx(context)
dialog?.setTitle(title)
dialog?.enqueue()
return dialog