diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 76fbff872..6368c6a3a 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -150,7 +150,7 @@ android { buildConfigField "String", "STORE", "\"google\"" multiDexEnabled true - versionCode 2080 + versionCode 2081 versionName "1.8.1" } diff --git a/Habitica/res/anim/rotate_45_degrees.xml b/Habitica/res/anim/rotate_45_degrees.xml new file mode 100644 index 000000000..b2cf69b8f --- /dev/null +++ b/Habitica/res/anim/rotate_45_degrees.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/Habitica/res/drawable/plus_taskform.xml b/Habitica/res/drawable/plus_taskform.xml new file mode 100644 index 000000000..e3979cd7f --- /dev/null +++ b/Habitica/res/drawable/plus_taskform.xml @@ -0,0 +1,5 @@ + + + diff --git a/Habitica/res/drawable/task_form_control_bg.xml b/Habitica/res/drawable/task_form_control_bg.xml new file mode 100644 index 000000000..64cd42452 --- /dev/null +++ b/Habitica/res/drawable/task_form_control_bg.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/activity_task_form.xml b/Habitica/res/layout/activity_task_form.xml index 2cfebee45..bbb97492e 100644 --- a/Habitica/res/layout/activity_task_form.xml +++ b/Habitica/res/layout/activity_task_form.xml @@ -75,6 +75,17 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/task_form_reminder_item.xml b/Habitica/res/layout/task_form_reminder_item.xml new file mode 100644 index 000000000..fbfa7e8ca --- /dev/null +++ b/Habitica/res/layout/task_form_reminder_item.xml @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/task_form_task_scheduling.xml b/Habitica/res/layout/task_form_task_scheduling.xml new file mode 100644 index 000000000..31d20cf5d --- /dev/null +++ b/Habitica/res/layout/task_form_task_scheduling.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml index 587eeac9e..447d9eca8 100644 --- a/Habitica/res/values/strings.xml +++ b/Habitica/res/values/strings.xml @@ -152,7 +152,7 @@ %d MP You used %1$s for %2$d mana. You used %1$s. - new checklist item + New checklist entry Add Remember to check off your tasks @@ -505,7 +505,7 @@ Monthly gem cap Inactive 1 Month - %d Months + %d Months month 3 months 6 months @@ -876,4 +876,14 @@ Assigned Stat Monthly Weekly + Repeats + Every + Scheduling + Days + Weeks + Months + Years + Checklist Text + New reminder + Streak diff --git a/Habitica/res/values/styles.xml b/Habitica/res/values/styles.xml index 83422e781..d6f363211 100644 --- a/Habitica/res/values/styles.xml +++ b/Habitica/res/values/styles.xml @@ -518,5 +518,11 @@ 28dp + + #99edecee \ No newline at end of file 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 77653bbbd..1764f7fbb 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 @@ -81,10 +81,10 @@ open class Task : RealmObject, Parcelable { private var weeksOfMonthString: String? = null @Ignore - private var daysOfMonth: MutableList? = null + private var daysOfMonth: List? = null @Ignore - private var weeksOfMonth: MutableList? = null + private var weeksOfMonth: List? = null val completedChecklistCount: Int get() = checklist?.count { it.completed } ?: 0 @@ -306,56 +306,54 @@ open class Task : RealmObject, Parcelable { } - fun setWeeksOfMonth(weeksOfMonth: MutableList) { + fun setWeeksOfMonth(weeksOfMonth: List?) { this.weeksOfMonth = weeksOfMonth this.weeksOfMonthString = this.weeksOfMonth?.toString() } fun getWeeksOfMonth(): List? { if (weeksOfMonth == null) { - weeksOfMonth = ArrayList() + val weeksOfMonth = mutableListOf() if (weeksOfMonthString != null) { try { val obj = JSONArray(weeksOfMonthString) var i = 0 while (i < obj.length()) { - weeksOfMonth?.add(obj.getInt(i)) + weeksOfMonth.add(obj.getInt(i)) i += 1 } } catch (e: JSONException) { e.printStackTrace() } - } else { - weeksOfMonth = ArrayList() } + this.weeksOfMonth = weeksOfMonth.toList() } return weeksOfMonth } - fun setDaysOfMonth(daysOfMonth: MutableList) { + fun setDaysOfMonth(daysOfMonth: List?) { this.daysOfMonth = daysOfMonth this.daysOfMonthString = daysOfMonth.toString() } fun getDaysOfMonth(): List? { if (daysOfMonth == null) { - daysOfMonth = ArrayList() + val daysOfMonth = mutableListOf() if (daysOfMonthString != null) { try { val obj = JSONArray(daysOfMonthString) var i = 0 while (i < obj.length()) { - daysOfMonth?.add(obj.getInt(i)) + daysOfMonth.add(obj.getInt(i)) i += 1 } } catch (e: JSONException) { e.printStackTrace() } - } else { - daysOfMonth = ArrayList() } + this.daysOfMonth = daysOfMonth } return daysOfMonth @@ -381,6 +379,7 @@ open class Task : RealmObject, Parcelable { const val FREQUENCY_WEEKLY = "weekly" const val FREQUENCY_DAILY = "daily" const val FREQUENCY_MONTHLY = "monthly" + const val FREQUENCY_YEARLY = "yearly" @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/OldTaskFormActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/OldTaskFormActivity.kt index 0bd7d880e..24aaa0de3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/OldTaskFormActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/OldTaskFormActivity.kt @@ -964,13 +964,8 @@ class OldTaskFormActivity : BaseActivity() { val dayOfTheWeek = sharedPreferences.getString("FirstDayOfTheWeek", Integer.toString(Calendar.getInstance().firstDayOfWeek)) val firstDayOfTheWeekHelper = FirstDayOfTheWeekHelper.newInstance(Integer.parseInt(dayOfTheWeek ?: "0")) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) { - @Suppress("DEPRECATION") - datePickerDialog.datePicker.calendarView.firstDayOfWeek = firstDayOfTheWeekHelper.firstDayOfTheWeek - } else { - datePickerDialog.datePicker.firstDayOfWeek = firstDayOfTheWeekHelper + datePickerDialog.datePicker.firstDayOfWeek = firstDayOfTheWeekHelper .firstDayOfTheWeek - } this.datePickerDialog.setButton(DialogInterface.BUTTON_NEUTRAL, resources.getString(R.string.today)) { _, _ -> setCalendar(Calendar.getInstance().time) } updateDateText() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt index 295016c78..d4736a938 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt @@ -23,9 +23,6 @@ import com.habitrpg.android.habitica.models.tasks.HabitResetOption import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.models.user.Stats import com.habitrpg.android.habitica.ui.helpers.bindView -import com.habitrpg.android.habitica.ui.views.tasks.form.HabitResetStreakButtons -import com.habitrpg.android.habitica.ui.views.tasks.form.HabitScoringButtonsView -import com.habitrpg.android.habitica.ui.views.tasks.form.TaskDifficultyButtons import io.reactivex.functions.Consumer import javax.inject.Inject import android.content.res.ColorStateList @@ -37,7 +34,9 @@ import androidx.core.view.children import androidx.core.view.forEach import androidx.core.view.forEachIndexed import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.ui.views.tasks.form.* import io.realm.RealmList +import java.util.* class TaskFormActivity : BaseActivity() { @@ -54,13 +53,20 @@ class TaskFormActivity : BaseActivity() { private val textEditText: EditText by bindView(R.id.text_edit_text) private val notesEditText: EditText by bindView(R.id.notes_edit_text) private val habitScoringButtons: HabitScoringButtonsView by bindView(R.id.habit_scoring_buttons) + private val checklistTitleView: TextView by bindView(R.id.checklist_title) + private val checklistContainer: ChecklistContainer by bindView(R.id.checklist_container) private val habitResetStreakTitleView: TextView by bindView(R.id.habit_reset_streak_title) private val habitResetStreakButtons: HabitResetStreakButtons by bindView(R.id.habit_reset_streak_buttons) + private val taskSchedulingTitleView: TextView by bindView(R.id.scheduling_title) + private val taskSchedulingControls: TaskSchedulingControls by bindView(R.id.scheduling_controls) private val adjustStreakWrapper: ViewGroup by bindView(R.id.adjust_streak_wrapper) private val adjustStreakTitleView: TextView by bindView(R.id.adjust_streak_title) private val habitAdjustPositiveStreakView: EditText by bindView(R.id.habit_adjust_positive_streak) private val habitAdjustNegativeStreakView: EditText by bindView(R.id.habit_adjust_negative_streak) + private val remindersTitleView: TextView by bindView(R.id.reminders_title) + private val remindersContainer: ReminderContainer by bindView(R.id.reminders_container) + private val taskDifficultyTitleView: TextView by bindView(R.id.task_difficulty_title) private val taskDifficultyButtons: TaskDifficultyButtons by bindView(R.id.task_difficulty_buttons) private val statWrapper: ViewGroup by bindView(R.id.stat_wrapper) @@ -115,7 +121,7 @@ class TaskFormActivity : BaseActivity() { val taskId = bundle.getString(OldTaskFormActivity.TASK_ID_KEY) if (taskId != null) { isCreating = false - compositeSubscription.add(taskRepository.getUnmanagedTask(taskId).subscribe(Consumer { + compositeSubscription.add(taskRepository.getUnmanagedTask(taskId).firstElement().subscribe(Consumer { task = it //tintColor = ContextCompat.getColor(this, it.mediumTaskColor) fillForm(it) @@ -164,11 +170,32 @@ class TaskFormActivity : BaseActivity() { habitScoringButtons.visibility = habitViewsVisibility habitResetStreakTitleView.visibility = habitViewsVisibility habitResetStreakButtons.visibility = habitViewsVisibility - habitAdjustPositiveStreakView.visibility = habitViewsVisibility habitAdjustNegativeStreakView.visibility = habitViewsVisibility - val dailyViewsVisibility = if (taskType == Task.TYPE_DAILY) View.VISIBLE else View.GONE - val todoViewsVisibility = if (taskType == Task.TYPE_TODO) View.VISIBLE else View.GONE + val habitDailyVisibility = if (taskType == Task.TYPE_DAILY || taskType == Task.TYPE_HABIT) View.VISIBLE else View.GONE + adjustStreakTitleView.visibility = habitDailyVisibility + adjustStreakWrapper.visibility = habitDailyVisibility + if (taskType == Task.TYPE_HABIT) { + habitAdjustPositiveStreakView.hint = getString(R.string.positive_habit_form) + } else { + habitAdjustPositiveStreakView.hint = getString(R.string.streak) + } + + val todoDailyViewsVisibility = if (taskType == Task.TYPE_DAILY || taskType == Task.TYPE_TODO) View.VISIBLE else View.GONE + + checklistTitleView.visibility = todoDailyViewsVisibility + checklistContainer.visibility = todoDailyViewsVisibility + + remindersTitleView.visibility = todoDailyViewsVisibility + remindersContainer.visibility = todoDailyViewsVisibility + + val rewardHideViews = if (taskType == Task.TYPE_REWARD) View.GONE else View.VISIBLE + taskDifficultyTitleView.visibility = rewardHideViews + taskDifficultyButtons.visibility = rewardHideViews + + taskSchedulingTitleView.visibility = todoDailyViewsVisibility + taskSchedulingControls.visibility = todoDailyViewsVisibility + taskSchedulingControls.taskType = taskType statWrapper.visibility = if (usesTaskAttributeStats) View.VISIBLE else View.GONE if (isCreating) { @@ -201,12 +228,28 @@ class TaskFormActivity : BaseActivity() { textEditText.setText(task.text) notesEditText.setText(task.notes) taskDifficultyButtons.selectedDifficulty = task.priority - if (taskType == Task.TYPE_HABIT) { - habitScoringButtons.isPositive = task.up ?: false - habitScoringButtons.isNegative = task.down ?: false - task.frequency?.let { habitResetStreakButtons.selectedResetOption = HabitResetOption.valueOf(it.toUpperCase()) } - habitAdjustPositiveStreakView.setText((task.counterUp ?: 0).toString()) - habitAdjustNegativeStreakView.setText((task.counterDown ?: 0).toString()) + when (taskType) { + Task.TYPE_HABIT -> { + habitScoringButtons.isPositive = task.up ?: false + habitScoringButtons.isNegative = task.down ?: false + task.frequency?.let { habitResetStreakButtons.selectedResetOption = HabitResetOption.valueOf(it.toUpperCase()) } + habitAdjustPositiveStreakView.setText((task.counterUp ?: 0).toString()) + habitAdjustNegativeStreakView.setText((task.counterDown ?: 0).toString()) + } + Task.TYPE_DAILY -> { + taskSchedulingControls.startDate = task.startDate ?: Date() + taskSchedulingControls.frequency = task.frequency ?: Task.FREQUENCY_DAILY + taskSchedulingControls.everyX = task.everyX ?: 1 + task.repeat?.let { taskSchedulingControls.weeklyRepeat = it } + taskSchedulingControls.daysOfMonth = task.getDaysOfMonth() + taskSchedulingControls.weeksOfMonth = task.getWeeksOfMonth() + habitAdjustPositiveStreakView.setText((task.streak ?: 0).toString()) + } + Task.TYPE_TODO -> taskSchedulingControls.dueDate = task.dueDate + } + if (taskType == Task.TYPE_DAILY || taskType == Task.TYPE_TODO) { + task.checklist?.let { checklistContainer.checklistItems = it } + task.reminders?.let { remindersContainer.reminders = it } } task.attribute?.let { setSelectedAttribute(it) } setAllTagSelections() @@ -254,8 +297,22 @@ class TaskFormActivity : BaseActivity() { thisTask.frequency = habitResetStreakButtons.selectedResetOption.value if (habitAdjustPositiveStreakView.text.isNotEmpty()) thisTask.counterUp = habitAdjustPositiveStreakView.text.toString().toInt() if (habitAdjustNegativeStreakView.text.isNotEmpty()) thisTask.counterDown = habitAdjustNegativeStreakView.text.toString().toInt() + } else if (taskType == Task.TYPE_DAILY) { + thisTask.startDate = taskSchedulingControls.startDate + thisTask.everyX = taskSchedulingControls.everyX + thisTask.frequency = taskSchedulingControls.frequency + thisTask.repeat = taskSchedulingControls.weeklyRepeat + thisTask.setDaysOfMonth(taskSchedulingControls.daysOfMonth) + thisTask.setWeeksOfMonth(taskSchedulingControls.weeksOfMonth) + if (habitAdjustPositiveStreakView.text.isNotEmpty()) thisTask.streak = habitAdjustPositiveStreakView.text.toString().toInt() + } else if (taskType == Task.TYPE_TODO) { + thisTask.dueDate = taskSchedulingControls.dueDate } + if (taskType == Task.TYPE_DAILY || taskType == Task.TYPE_TODO) { + thisTask.checklist = checklistContainer.checklistItems + thisTask.reminders = remindersContainer.reminders + } thisTask.tags = RealmList() tagsWrapper.forEachIndexed { index, view -> val tagView = view as? CheckBox diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt index ab63696b5..255e3f0b7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TasksFragment.kt @@ -286,12 +286,8 @@ class TasksFragment : BaseMainFragment() { return } - val allocationMode = user?.preferences?.hasTaskBasedAllocation() ?: false - val bundle = Bundle() bundle.putString(OldTaskFormActivity.TASK_TYPE_KEY, type) - bundle.putString(OldTaskFormActivity.USER_ID_KEY, if (this.user != null) this.user?.id else null) - bundle.putBoolean(OldTaskFormActivity.ALLOCATION_MODE_KEY, allocationMode) bundle.putBoolean(OldTaskFormActivity.SAVE_TO_DB, true) val intent = Intent(activity, TaskFormActivity::class.java) @@ -309,14 +305,9 @@ class TasksFragment : BaseMainFragment() { return } - val allocationMode = user?.preferences?.hasTaskBasedAllocation() ?: false - val bundle = Bundle() bundle.putString(OldTaskFormActivity.TASK_TYPE_KEY, event.Task.type) bundle.putString(OldTaskFormActivity.TASK_ID_KEY, event.Task.id) - bundle.putString(OldTaskFormActivity.USER_ID_KEY, if (this.user != null) this.user?.id else null) - bundle.putBoolean(OldTaskFormActivity.ALLOCATION_MODE_KEY, allocationMode) - bundle.putBoolean(OldTaskFormActivity.SAVE_TO_DB, true) val intent = Intent(activity, TaskFormActivity::class.java) intent.putExtras(bundle) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ChecklistContainer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ChecklistContainer.kt new file mode 100644 index 000000000..6085058e5 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ChecklistContainer.kt @@ -0,0 +1,98 @@ +package com.habitrpg.android.habitica.ui.views.tasks.form + +import android.animation.LayoutTransition +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.children +import androidx.core.view.updateMargins +import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.models.tasks.ChecklistItem +import io.realm.RealmList + +class ChecklistContainer @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + var checklistItems: RealmList + get() { + val list = RealmList() + for (child in children) { + val view = child as? ChecklistItemFormView ?: continue + if (view.item.text?.isNotEmpty() == true) { + list.add(view.item) + } + } + return list + } + set(value) { + val unAnimatedTransitions = LayoutTransition() + unAnimatedTransitions.disableTransitionType(LayoutTransition.APPEARING) + unAnimatedTransitions.disableTransitionType(LayoutTransition.CHANGING) + unAnimatedTransitions.disableTransitionType(LayoutTransition.DISAPPEARING) + layoutTransition = unAnimatedTransitions + if (childCount > 1) { + for (child in children.take(childCount - 1)) { + removeView(child) + } + } + for (item in value) { + addChecklistViewAt(childCount-1, item) + } + val animatedTransitions = LayoutTransition() + layoutTransition = animatedTransitions + } + + init { + orientation = LinearLayout.VERTICAL + + addChecklistViewAt(0) + } + + private fun addChecklistViewAt(index: Int, item: ChecklistItem? = null) { + val view = ChecklistItemFormView(context) + item?.let { + view.item = it + view.isAddButton = false + } + view.textChangedListener = { + if (isLastChild(view)) { + addChecklistViewAt(-1) + view.animDuration = 300 + view.isAddButton = false + } else if (shouldBecomeNewAddButton(view)) { + removeViewAt(childCount-1) + view.animDuration = 300 + view.isAddButton = true + } + } + val indexToUse = if (index < 0) { + childCount - index + } else { + index + } + if (childCount <= indexToUse) { + addView(view) + view.isAddButton = true + } else { + addView(view, indexToUse) + } + val layoutParams = view.layoutParams as? LinearLayout.LayoutParams + layoutParams?.updateMargins(bottom = 8.dpToPx(context)) + view.layoutParams = layoutParams + } + + private fun shouldBecomeNewAddButton(view: ChecklistItemFormView): Boolean { + if (childCount > 2 && view.item.text?.isEmpty() != false && children.indexOf(view) == childCount-2) { + val lastView = (getChildAt(childCount-1) as? ChecklistItemFormView) + if (lastView != null && lastView.item.text?.isEmpty() != false) { + return true + } + } + return false + } + + private fun isLastChild(view: View): Boolean { + return children.lastOrNull() == view + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ChecklistItemFormView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ChecklistItemFormView.kt new file mode 100644 index 000000000..1956be9d3 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ChecklistItemFormView.kt @@ -0,0 +1,90 @@ +package com.habitrpg.android.habitica.ui.views.tasks.form + +import android.animation.ObjectAnimator +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.view.Gravity +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageButton +import android.widget.LinearLayout +import androidx.core.content.ContextCompat +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.extensions.inflate +import com.habitrpg.android.habitica.models.tasks.ChecklistItem +import com.habitrpg.android.habitica.ui.helpers.bindView +import android.view.animation.LinearInterpolator +import android.view.animation.Animation +import android.view.animation.RotateAnimation + + + + +class ChecklistItemFormView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr), TextWatcher { + + + private val button: ImageButton by bindView(R.id.button) + private val editText: EditText by bindView(R.id.edit_text) + + var item: ChecklistItem = ChecklistItem() + set(value) { + field = value + editText.setText(item.text) + } + + var tintColor: Int = ContextCompat.getColor(context, R.color.brand_300) + var textChangedListener: ((String) -> Unit)? = null + var animDuration = 0L + var isAddButton: Boolean = true + set(value) { + field = value + editText.hint = context.getString(if (value) R.string.new_checklist_item else R.string.checklist_text) + if (value) { + val rotate = RotateAnimation(135f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + rotate.duration = animDuration + rotate.interpolator = LinearInterpolator() + rotate.fillAfter = true + button.startAnimation(rotate) + } else { + val rotate = RotateAnimation(0f, 135f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + rotate.duration = animDuration + rotate.interpolator = LinearInterpolator() + rotate.fillAfter = true + button.startAnimation(rotate) + } + } + + init { + minimumHeight = 38.dpToPx(context) + inflate(R.layout.task_form_checklist_item, true) + background = context.getDrawable(R.drawable.layout_rounded_bg_task_form) + background.mutate().setTint(ContextCompat.getColor(context, R.color.taskform_gray)) + gravity = Gravity.CENTER_VERTICAL + + button.setOnClickListener { + if (!isAddButton) { + (parent as? ViewGroup)?.removeView(this) + } + } + button.drawable.mutate().setTint(tintColor) + + editText.addTextChangedListener(this) + } + + override fun afterTextChanged(s: Editable?) { + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + item.text = s.toString() + textChangedListener?.let { it(s.toString()) } + } + +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/HabitScoringButtonsView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/HabitScoringButtonsView.kt index 74249bf85..13dbc567b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/HabitScoringButtonsView.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/HabitScoringButtonsView.kt @@ -64,6 +64,4 @@ class HabitScoringButtonsView @JvmOverloads constructor( isPositive = true isNegative = true } - - } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt new file mode 100644 index 000000000..91215f4af --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderContainer.kt @@ -0,0 +1,85 @@ +package com.habitrpg.android.habitica.ui.views.tasks.form + +import android.animation.LayoutTransition +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.children +import androidx.core.view.updateMargins +import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.models.tasks.ChecklistItem +import com.habitrpg.android.habitica.models.tasks.RemindersItem +import io.realm.RealmList + +class ReminderContainer @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + var reminders: RealmList + get() { + val list = RealmList() + for (child in children) { + val view = child as? ReminderItemFormView ?: continue + if (view.item.time != null) { + list.add(view.item) + } + } + return list + } + set(value) { + val unAnimatedTransitions = LayoutTransition() + unAnimatedTransitions.disableTransitionType(LayoutTransition.APPEARING) + unAnimatedTransitions.disableTransitionType(LayoutTransition.CHANGING) + unAnimatedTransitions.disableTransitionType(LayoutTransition.DISAPPEARING) + layoutTransition = unAnimatedTransitions + if (childCount > 1) { + for (child in children.take(childCount - 1)) { + removeView(child) + } + } + for (item in value) { + addReminderViewAt(childCount-1, item) + } + val animatedTransitions = LayoutTransition() + layoutTransition = animatedTransitions + } + + init { + orientation = LinearLayout.VERTICAL + + addReminderViewAt(0) + } + + private fun addReminderViewAt(index: Int, item: RemindersItem? = null) { + val view = ReminderItemFormView(context) + item?.let { + view.item = it + view.isAddButton = false + } + view.valueChangedListener = { + if (isLastChild(view)) { + addReminderViewAt(-1) + view.animDuration = 300 + view.isAddButton = false + } + } + val indexToUse = if (index < 0) { + childCount - index + } else { + index + } + if (childCount <= indexToUse) { + addView(view) + view.isAddButton = true + } else { + addView(view, indexToUse) + } + val layoutParams = view.layoutParams as? LinearLayout.LayoutParams + layoutParams?.updateMargins(bottom = 8.dpToPx(context)) + view.layoutParams = layoutParams + } + + private fun isLastChild(view: View): Boolean { + return children.lastOrNull() == view + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt new file mode 100644 index 000000000..684f0039c --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/ReminderItemFormView.kt @@ -0,0 +1,99 @@ +package com.habitrpg.android.habitica.ui.views.tasks.form + +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Context +import android.content.DialogInterface +import android.util.AttributeSet +import android.view.Gravity +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.TimePicker +import androidx.core.content.ContextCompat +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.extensions.inflate +import com.habitrpg.android.habitica.models.tasks.RemindersItem +import com.habitrpg.android.habitica.ui.helpers.bindView +import java.text.DateFormat +import java.util.* + + +class ReminderItemFormView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr), TimePickerDialog.OnTimeSetListener { + + + private val button: ImageButton by bindView(R.id.button) + private val textView: TextView by bindView(R.id.text_view) + + private val formatter = DateFormat.getTimeInstance(DateFormat.SHORT) + + var item: RemindersItem = RemindersItem() + set(value) { + field = value + textView.text = formatter.format(item.time) + } + + var tintColor: Int = ContextCompat.getColor(context, R.color.brand_300) + var valueChangedListener: ((Date) -> Unit)? = null + var animDuration = 0L + var isAddButton: Boolean = true + set(value) { + field = value + textView.text = if (value) context.getString(R.string.new_reminder) else formatter.format(item.time) + if (value) { + val rotate = RotateAnimation(135f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + rotate.duration = animDuration + rotate.interpolator = LinearInterpolator() + rotate.fillAfter = true + button.startAnimation(rotate) + } else { + val rotate = RotateAnimation(0f, 135f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + rotate.duration = animDuration + rotate.interpolator = LinearInterpolator() + rotate.fillAfter = true + button.startAnimation(rotate) + } + } + + init { + minimumHeight = 38.dpToPx(context) + inflate(R.layout.task_form_reminder_item, true) + background = context.getDrawable(R.drawable.layout_rounded_bg_task_form) + background.mutate().setTint(ContextCompat.getColor(context, R.color.taskform_gray)) + gravity = Gravity.CENTER_VERTICAL + + button.setOnClickListener { + if (!isAddButton) { + (parent as? ViewGroup)?.removeView(this) + } + } + button.drawable.mutate().setTint(tintColor) + + textView.setOnClickListener { + val calendar = Calendar.getInstance() + item.time?.let { calendar.time = it } + val timePickerDialog = TimePickerDialog(context, this, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + android.text.format.DateFormat.is24HourFormat(context)) + timePickerDialog.show() + } + } + override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { + valueChangedListener?.let { + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) + item.time = calendar.time + textView.text = formatter.format(item.time) + item.time?.let { date -> it(date) } + } + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskDifficultyButtons.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskDifficultyButtons.kt index 31f2980d5..855e66c0f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskDifficultyButtons.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskDifficultyButtons.kt @@ -52,10 +52,10 @@ class TaskDifficultyButtons @JvmOverloads constructor( val isActive = selectedDifficulty == difficulty.value var difficultyColor = ContextCompat.getColor(context, R.color.white) if (isActive) { - view.findViewById(R.id.image_view).background.setTint(tintColor) + view.findViewById(R.id.image_view).background.mutate().setTint(tintColor) view.findViewById(R.id.text_view).setTextColor(tintColor) } else { - view.findViewById(R.id.image_view).background.setTint(ContextCompat.getColor(context, R.color.taskform_gray)) + view.findViewById(R.id.image_view).background.mutate().setTint(ContextCompat.getColor(context, R.color.taskform_gray)) view.findViewById(R.id.text_view).setTextColor(ContextCompat.getColor(context, R.color.gray_100)) difficultyColor = ContextCompat.getColor(context, R.color.gray_400) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskSchedulingControls.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskSchedulingControls.kt new file mode 100644 index 000000000..508d048c9 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/tasks/form/TaskSchedulingControls.kt @@ -0,0 +1,269 @@ +package com.habitrpg.android.habitica.ui.views.tasks.form + +import android.app.DatePickerDialog +import android.content.Context +import android.content.DialogInterface +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.dpToPx +import com.habitrpg.android.habitica.extensions.inflate +import com.habitrpg.android.habitica.helpers.FirstDayOfTheWeekHelper +import com.habitrpg.android.habitica.models.tasks.Days +import com.habitrpg.android.habitica.models.tasks.Task +import com.habitrpg.android.habitica.ui.helpers.bindView +import java.text.DateFormat +import java.text.DateFormatSymbols +import java.util.* + +class TaskSchedulingControls @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr), DatePickerDialog.OnDateSetListener { + + var tintColor: Int = ContextCompat.getColor(context, R.color.brand_300) + + private val startDateWrapper: ViewGroup by bindView(R.id.start_date_wrapper) + private val startDateTitleView: TextView by bindView(R.id.start_date_title) + private val startDateTextView: TextView by bindView(R.id.start_date_textview) + private val repeatsEveryWrapper: ViewGroup by bindView(R.id.repeats_every_wrapper) + private val repeatsEverySpinner: Spinner by bindView(R.id.repeats_every_spinner) + private val repeatsEveryEdittext: AppCompatEditText by bindView(R.id.repeats_every_edittext) + private val repeatsEveryTitleView: TextView by bindView(R.id.repeats_every_title) + private val weeklyRepeatWrapper: ViewGroup by bindView(R.id.weekly_repeat_wrapper) + private val monthlyRepeatWrapper: ViewGroup by bindView(R.id.monthly_repeat_wrapper) + private val monthlyRepeatDaysButton: TextView by bindView(R.id.monthly_repeat_days) + private val monthlyRepeatWeeksButton: TextView by bindView(R.id.monthly_repeat_weeks) + private val summaryTextView: TextView by bindView(R.id.summary_textview) + + private val dateFormatter = DateFormat.getDateInstance(DateFormat.MEDIUM) + private val frequencyAdapter = ArrayAdapter.createFromResource(context, + R.array.repeatables_frequencies, android.R.layout.simple_spinner_item) + + var taskType = Task.TYPE_DAILY + set(value) { + field = value + configureViewsForType() + } + var startDate = Date() + set(value) { + field = value + startDateTextView.text = dateFormatter.format(value) + startDateCalendar.time = value + } + private var startDateCalendar = Calendar.getInstance() + var dueDate: Date? = null + set(value) { + field = value + value?.let { startDateTextView.text = dateFormatter.format(it) } + } + var frequency = Task.FREQUENCY_DAILY + set(value) { + field = value + repeatsEverySpinner.setSelection(when (value) { + Task.FREQUENCY_WEEKLY -> 1 + Task.FREQUENCY_MONTHLY -> 2 + Task.FREQUENCY_YEARLY -> 3 + else -> 0 + }) + configureViewsForFrequency() + } + var everyX + get() = (repeatsEveryEdittext.text ?: "1").toString().toInt() + set(value) { + repeatsEveryEdittext.setText(value.toString()) + } + var weeklyRepeat: Days = Days() + set(value) { + field = value + createWeeklyRepeatViews() + } + + var daysOfMonth: List? = null + set(value) { + field = value + configureMonthlyRepeatViews() + } + var weeksOfMonth: List? = null + set(value) { + field = value + configureMonthlyRepeatViews() + } + + private val weekdays: Array by lazy { + DateFormatSymbols().weekdays + } + private val weekdayOrder: List by lazy { + val codes = (1..7).toList() + Collections.rotate(codes, -startDateCalendar.firstDayOfWeek) + codes + } + + init { + inflate(R.layout.task_form_task_scheduling, true) + repeatsEverySpinner.adapter = frequencyAdapter + + repeatsEverySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(parent: AdapterView<*>?) { + frequency = frequency + } + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + frequency = when (position) { + 1 -> Task.FREQUENCY_WEEKLY + 2 -> Task.FREQUENCY_MONTHLY + 3 -> Task.FREQUENCY_YEARLY + else -> Task.FREQUENCY_DAILY + } + } + } + + startDateWrapper.setOnClickListener { + val datePickerDialog = DatePickerDialog(context, this, + startDateCalendar.get(Calendar.YEAR), + startDateCalendar.get(Calendar.MONTH), + startDateCalendar.get(Calendar.DAY_OF_MONTH)) + datePickerDialog.setButton(DialogInterface.BUTTON_NEUTRAL, resources.getString(R.string.today)) { _, _ -> + if (taskType == Task.TYPE_TODO) { + dueDate = Date() + } else { + startDate = Date() + } + } + if (taskType == Task.TYPE_TODO) { + datePickerDialog.setButton(DialogInterface.BUTTON_NEUTRAL, resources.getString(R.string.clear)) { _, _ -> + dueDate = null + } + } + datePickerDialog.show() + } + + monthlyRepeatDaysButton.setOnClickListener { + daysOfMonth = mutableListOf(startDateCalendar.get(Calendar.DATE)) + weeksOfMonth = null + } + monthlyRepeatWeeksButton.setOnClickListener { + weeksOfMonth = mutableListOf(startDateCalendar.get(Calendar.WEEK_OF_MONTH)) + daysOfMonth = null + } + + orientation = LinearLayout.VERTICAL + configureViewsForType() + configureViewsForFrequency() + } + + private fun configureViewsForType() { + startDateTitleView.text = context.getString(if (taskType == Task.TYPE_DAILY) R.string.start_date else R.string.due_date) + repeatsEveryWrapper.visibility = if (taskType == Task.TYPE_DAILY) View.VISIBLE else View.GONE + } + + override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) { + startDateCalendar.set(year, month, dayOfMonth) + if (taskType == Task.TYPE_TODO) { + dueDate = startDateCalendar.time + } else { + startDate = startDateCalendar.time + } + } + + private fun configureViewsForFrequency() { + repeatsEveryTitleView.text = context.getText(when (frequency) { + Task.FREQUENCY_WEEKLY -> R.string.weeks + Task.FREQUENCY_MONTHLY -> R.string.months + Task.FREQUENCY_YEARLY -> R.string.years + else -> R.string.days + }) + weeklyRepeatWrapper.visibility = if (frequency == Task.FREQUENCY_WEEKLY && taskType == Task.TYPE_DAILY) View.VISIBLE else View.GONE + monthlyRepeatWrapper.visibility = if (frequency == Task.FREQUENCY_MONTHLY && taskType == Task.TYPE_DAILY) View.VISIBLE else View.GONE + if (frequency == Task.FREQUENCY_WEEKLY) { + createWeeklyRepeatViews() + } else if (frequency == Task.FREQUENCY_MONTHLY) { + if (weeksOfMonth?.isNotEmpty() != true && daysOfMonth?.isNotEmpty() != true) { + daysOfMonth = listOf(startDateCalendar.get(Calendar.DATE)) + } + } + } + + private fun setWeekdayActive(weekday: Int, isActive: Boolean) { + when (weekday) { + 2 -> weeklyRepeat.m = isActive + 3 -> weeklyRepeat.t = isActive + 4 -> weeklyRepeat.w = isActive + 5 -> weeklyRepeat.th = isActive + 6 -> weeklyRepeat.f = isActive + 7 -> weeklyRepeat.s = isActive + 1 -> weeklyRepeat.su = isActive + } + createWeeklyRepeatViews() + } + + private fun isWeekdayActive(weekday: Int): Boolean { + return when (weekday) { + 2 -> weeklyRepeat.m + 3 -> weeklyRepeat.t + 4 -> weeklyRepeat.w + 5 -> weeklyRepeat.th + 6 -> weeklyRepeat.f + 7 -> weeklyRepeat.s + 1 -> weeklyRepeat.su + else -> false + } + } + + private fun createWeeklyRepeatViews() { + weeklyRepeatWrapper.removeAllViews() + val size = 32.dpToPx(context) + val lastWeekday = weekdayOrder.last() + for (weekdayCode in weekdayOrder) { + val button = TextView(context, null, 0, R.style.TaskFormWeekdayButton) + val layoutParams = LinearLayout.LayoutParams(size, size) + button.layoutParams = layoutParams + button.text = weekdays[weekdayCode].first().toUpperCase().toString() + val isActive = isWeekdayActive(weekdayCode) + if (isActive) { + button.background = context.getDrawable(R.drawable.habit_scoring_circle_selected) + button.background.mutate().setTint(tintColor) + button.setTextColor(ContextCompat.getColor(context, R.color.white)) + } else { + button.background = context.getDrawable(R.drawable.habit_scoring_circle) + button.setTextColor(ContextCompat.getColor(context, R.color.gray_100)) + } + button.setOnClickListener { + setWeekdayActive(weekdayCode, !isActive) + } + weeklyRepeatWrapper.addView(button) + if (weekdayCode != lastWeekday) { + val space = Space(context) + val spaceLayoutParams = LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT) + spaceLayoutParams.weight = 1f + space.layoutParams = spaceLayoutParams + weeklyRepeatWrapper.addView(space) + } + } + } + + private fun configureMonthlyRepeatViews() { + val white = ContextCompat.getColor(context, R.color.white) + val unselectedText = ContextCompat.getColor(context, R.color.gray_100) + val unselectedBackground = ContextCompat.getColor(context, R.color.taskform_gray) + if (daysOfMonth != null && daysOfMonth?.isEmpty() != true) { + monthlyRepeatDaysButton.setTextColor(white) + monthlyRepeatDaysButton.background.mutate().setTint(tintColor) + } else { + monthlyRepeatDaysButton.setTextColor(unselectedText) + monthlyRepeatDaysButton.background.mutate().setTint(unselectedBackground) + } + if (weeksOfMonth != null && weeksOfMonth?.isEmpty() != true) { + monthlyRepeatWeeksButton.setTextColor(white) + monthlyRepeatWeeksButton.background.mutate().setTint(tintColor) + } else { + monthlyRepeatWeeksButton.setTextColor(unselectedText) + monthlyRepeatWeeksButton.background.mutate().setTint(unselectedBackground) + } + } +} \ No newline at end of file