diff --git a/uhabits-android/src/main/AndroidManifest.xml b/uhabits-android/src/main/AndroidManifest.xml
index 8c7758439..56529c4bd 100644
--- a/uhabits-android/src/main/AndroidManifest.xml
+++ b/uhabits-android/src/main/AndroidManifest.xml
@@ -42,6 +42,14 @@
android:value=".activities.habits.list.ListHabitsActivity" />
+
+
+
+
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt
index 06412103b..b9c6ffaa4 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt
@@ -38,7 +38,7 @@ import javax.inject.Inject
* Provides data that backs a [HabitCardListView].
*
*
- * The data if fetched and cached by a [HabitCardListCache]. This adapter
+ * The data is fetched and cached by a [HabitCardListCache]. This adapter
* also holds a list of items that have been selected.
*/
@ActivityScope
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt
new file mode 100644
index 000000000..12832b4e4
--- /dev/null
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardView.kt
@@ -0,0 +1,160 @@
+package org.isoron.uhabits.activities.habits.list.views
+
+import android.content.Context
+import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Handler
+import android.os.Looper
+import android.text.TextUtils
+import android.view.Gravity
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.TextView
+import org.isoron.platform.gui.toInt
+import org.isoron.uhabits.R
+import org.isoron.uhabits.activities.common.views.RingView
+import org.isoron.uhabits.core.models.HabitGroup
+import org.isoron.uhabits.core.models.ModelObservable
+import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
+import org.isoron.uhabits.inject.ActivityContext
+import org.isoron.uhabits.utils.currentTheme
+import org.isoron.uhabits.utils.dp
+import org.isoron.uhabits.utils.sres
+import javax.inject.Inject
+
+class HabitGroupCardViewFactory
+@Inject constructor(
+ @ActivityContext val context: Context,
+ private val behavior: ListHabitsBehavior
+) {
+ fun create() = HabitGroupCardView(context, behavior)
+}
+
+class HabitGroupCardView(
+ @ActivityContext context: Context,
+ private val behavior: ListHabitsBehavior
+) : FrameLayout(context),
+ ModelObservable.Listener {
+
+ var habitGroup: HabitGroup? = null
+ set(newHabitGroup) {
+ if (isAttachedToWindow) {
+ field?.observable?.removeListener(this)
+ newHabitGroup?.observable?.addListener(this)
+ }
+ field = newHabitGroup
+ if (newHabitGroup != null) copyAttributesFrom(newHabitGroup)
+ }
+
+ var score
+ get() = scoreRing.getPercentage().toDouble()
+ set(value) {
+ scoreRing.setPercentage(value.toFloat())
+ scoreRing.setPrecision(1.0f / 16)
+ }
+
+ private var innerFrame: LinearLayout
+ private var label: TextView
+ private var scoreRing: RingView
+
+ private var currentToggleTaskId = 0
+
+ init {
+ scoreRing = RingView(context).apply {
+ val thickness = dp(3f)
+ val margin = dp(8f).toInt()
+ val ringSize = dp(15f).toInt()
+ layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply {
+ setMargins(margin, 0, margin, 0)
+ gravity = Gravity.CENTER
+ }
+ setThickness(thickness)
+ }
+
+ label = TextView(context).apply {
+ maxLines = 2
+ ellipsize = TextUtils.TruncateAt.END
+ layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
+ if (SDK_INT >= Build.VERSION_CODES.Q) {
+ breakStrategy = BREAK_STRATEGY_BALANCED
+ }
+ }
+
+ innerFrame = LinearLayout(context).apply {
+ gravity = Gravity.CENTER_VERTICAL
+ orientation = LinearLayout.HORIZONTAL
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ elevation = dp(1f)
+
+ addView(scoreRing)
+ addView(label)
+
+ setOnTouchListener { v, event ->
+ v.background.setHotspot(event.x, event.y)
+ false
+ }
+ }
+
+ clipToPadding = false
+ layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ val margin = dp(3f).toInt()
+ setPadding(margin, 0, margin, margin)
+ addView(innerFrame)
+ }
+
+ override fun onModelChange() {
+ Handler(Looper.getMainLooper()).post {
+ habitGroup?.let { copyAttributesFrom(it) }
+ }
+ }
+
+ override fun setSelected(isSelected: Boolean) {
+ super.setSelected(isSelected)
+ updateBackground(isSelected)
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ habitGroup?.observable?.addListener(this)
+ }
+
+ override fun onDetachedFromWindow() {
+ habitGroup?.observable?.removeListener(this)
+ super.onDetachedFromWindow()
+ }
+
+ private fun copyAttributesFrom(hgr: HabitGroup) {
+ fun getActiveColor(hgr: HabitGroup): Int {
+ return when (hgr.isArchived) {
+ true -> sres.getColor(R.attr.contrast60)
+ false -> currentTheme().color(hgr.color).toInt()
+ }
+ }
+
+ val c = getActiveColor(hgr)
+ label.apply {
+ text = hgr.name
+ setTextColor(c)
+ }
+ scoreRing.apply {
+ setColor(c)
+ }
+ }
+
+ private fun updateBackground(isSelected: Boolean) {
+ val background = when (isSelected) {
+ true -> R.drawable.selected_box
+ false -> R.drawable.ripple
+ }
+ innerFrame.setBackgroundResource(background)
+ }
+
+ companion object {
+ fun (() -> Unit).delay(delayInMillis: Long) {
+ Handler(Looper.getMainLooper()).postDelayed(this, delayInMillis)
+ }
+ }
+}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt
new file mode 100644
index 000000000..740d85009
--- /dev/null
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitGroupCardViewHolder.kt
@@ -0,0 +1,5 @@
+package org.isoron.uhabits.activities.habits.list.views
+
+import androidx.recyclerview.widget.RecyclerView
+
+class HabitGroupCardViewHolder(itemView: HabitGroupCardView) : RecyclerView.ViewHolder(itemView)
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt
new file mode 100644
index 000000000..61705f31e
--- /dev/null
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ArchiveHabitGroupsCommand.kt
@@ -0,0 +1,19 @@
+package org.isoron.uhabits.core.commands
+
+import org.isoron.uhabits.core.models.HabitGroup
+import org.isoron.uhabits.core.models.HabitGroupList
+
+data class ArchiveHabitGroupsCommand(
+ val habitGroupList: HabitGroupList,
+ val selected: List
+) : Command {
+ override fun run() {
+ for (hgr in selected) {
+ hgr.isArchived = true
+ for (h in hgr.habitList) {
+ h.isArchived = true
+ }
+ }
+ habitGroupList.update(selected)
+ }
+}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt
new file mode 100644
index 000000000..a7655507f
--- /dev/null
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/ChangeHabitGroupColorCommand.kt
@@ -0,0 +1,16 @@
+package org.isoron.uhabits.core.commands
+
+import org.isoron.uhabits.core.models.HabitGroup
+import org.isoron.uhabits.core.models.HabitGroupList
+import org.isoron.uhabits.core.models.PaletteColor
+
+data class ChangeHabitGroupColorCommand(
+ val habitGroupList: HabitGroupList,
+ val selected: List,
+ val newColor: PaletteColor
+) : Command {
+ override fun run() {
+ for (hgr in selected) hgr.color = newColor
+ habitGroupList.update(selected)
+ }
+}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt
new file mode 100644
index 000000000..7e37b39c5
--- /dev/null
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/DeleteHabitGroupsCommand.kt
@@ -0,0 +1,13 @@
+package org.isoron.uhabits.core.commands
+
+import org.isoron.uhabits.core.models.HabitGroup
+import org.isoron.uhabits.core.models.HabitGroupList
+
+data class DeleteHabitGroupsCommand(
+ val habitGroupList: HabitGroupList,
+ val selected: List
+) : Command {
+ override fun run() {
+ for (hgr in selected) habitGroupList.remove(hgr)
+ }
+}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt
new file mode 100644
index 000000000..166edd339
--- /dev/null
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/UnarchiveHabitGroupsCommand.kt
@@ -0,0 +1,19 @@
+package org.isoron.uhabits.core.commands
+
+import org.isoron.uhabits.core.models.HabitGroup
+import org.isoron.uhabits.core.models.HabitGroupList
+
+data class UnarchiveHabitGroupsCommand(
+ val habitGroupList: HabitGroupList,
+ val selected: List
+) : Command {
+ override fun run() {
+ for (hgr in selected) {
+ hgr.isArchived = false
+ for (h in hgr.habitList) {
+ h.isArchived = false
+ }
+ }
+ habitGroupList.update(selected)
+ }
+}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt
index ff57e823e..776b02be7 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt
@@ -111,12 +111,8 @@ data class Habit(
return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset()
}
- fun hierarchyLevel(): Int {
- return if (parentID == null) {
- 0
- } else {
- 1 + parent!!.hierarchyLevel()
- }
+ fun isInGroup(): Boolean {
+ return (parentID != null)
}
fun copyFrom(other: Habit) {
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt
index 4526e1872..1bf42a89e 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroup.kt
@@ -15,15 +15,12 @@ data class HabitGroup(
var uuid: String? = null,
var habitList: HabitList,
val scores: ScoreList,
- val streaks: StreakList,
- var parentID: Long? = null,
- var parentUUID: String? = null
+ val streaks: StreakList
) {
init {
if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "")
}
- var parent: HabitGroup? = null
var observable = ModelObservable()
val uriString: String
@@ -80,8 +77,6 @@ data class HabitGroup(
this.question = other.question
this.reminder = other.reminder
this.uuid = other.uuid
- this.parentID = other.parentID
- this.parentUUID = other.parentUUID
}
override fun equals(other: Any?): Boolean {
@@ -97,8 +92,6 @@ data class HabitGroup(
if (question != other.question) return false
if (reminder != other.reminder) return false
if (uuid != other.uuid) return false
- if (parentID != other.parentID) return false
- if (parentUUID != other.parentUUID) return false
return true
}
@@ -113,19 +106,9 @@ data class HabitGroup(
result = 31 * result + question.hashCode()
result = 31 * result + (reminder?.hashCode() ?: 0)
result = 31 * result + (uuid?.hashCode() ?: 0)
- result = 31 * result + (parentID?.hashCode() ?: 0)
- result = 31 * result + (parentUUID?.hashCode() ?: 0)
return result
}
- fun hierarchyLevel(): Int {
- return if (parentID == null) {
- 0
- } else {
- 1 + parent!!.hierarchyLevel()
- }
- }
-
- fun getHabitByUUIDDeep(uuid: String?): Habit? =
+ fun getHabitByUUID(uuid: String?): Habit? =
habitList.getByUUID(uuid)
}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt
index f597eb1f7..471e0b62f 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/HabitGroupList.kt
@@ -18,10 +18,10 @@ abstract class HabitGroupList : Iterable {
protected val filter: HabitMatcher
/**
- * Creates a new HabitList.
+ * Creates a new HabitGroupList.
*
* Depending on the implementation, this list can either be empty or be
- * populated by some pre-existing habits, for example, from a certain
+ * populated by some pre-existing habitgroups, for example, from a certain
* database.
*/
constructor() {
@@ -35,12 +35,12 @@ abstract class HabitGroupList : Iterable {
}
/**
- * Inserts a new habit in the list.
+ * Inserts a new habit group in the list.
*
- * If the id of the habit is null, the list will assign it a new id, which
+ * If the id of the habit group is null, the list will assign it a new id, which
* is guaranteed to be unique in the scope of the list. If id is not null,
* the caller should make sure that the list does not already contain
- * another habit with same id, otherwise a RuntimeException will be thrown.
+ * another habit group with same id, otherwise a RuntimeException will be thrown.
*
* @param habitGroup the habit to be inserted
* @throws IllegalArgumentException if the habit is already on the list.
@@ -49,28 +49,28 @@ abstract class HabitGroupList : Iterable {
abstract fun add(habitGroup: HabitGroup)
/**
- * Returns the habit with specified id.
+ * Returns the habit group with specified id.
*
- * @param id the id of the habit
- * @return the habit, or null if none exist
+ * @param id the id of the habit group
+ * @return the habit group, or null if none exist
*/
abstract fun getById(id: Long): HabitGroup?
/**
- * Returns the habit with specified UUID.
+ * Returns the habit group with specified UUID.
*
- * @param uuid the UUID of the habit
- * @return the habit, or null if none exist
+ * @param uuid the UUID of the habit group
+ * @return the habit group, or null if none exist
*/
abstract fun getByUUID(uuid: String?): HabitGroup?
/**
* Returns the habit with the specified UUID which is
- * present at any hierarchy within this list.
+ * present in any of the habit groups within this habit group list.
*/
- fun getHabitByUUIDDeep(uuid: String?): Habit? {
+ fun getHabitByUUID(uuid: String?): Habit? {
for (hgr in this) {
- val habit = hgr.getHabitByUUIDDeep(uuid)
+ val habit = hgr.getHabitByUUID(uuid)
if (habit != null) {
return habit
}
@@ -79,46 +79,46 @@ abstract class HabitGroupList : Iterable {
}
/**
- * Returns the habit that occupies a certain position.
+ * Returns the habit group that occupies a certain position.
*
- * @param position the position of the desired habit
- * @return the habit at that position
+ * @param position the position of the desired habit group
+ * @return the habit group at that position
* @throws IndexOutOfBoundsException when the position is invalid
*/
abstract fun getByPosition(position: Int): HabitGroup
/**
- * Returns the list of habits that match a given condition.
+ * Returns the list of habit groups that match a given condition.
*
* @param matcher the matcher that checks the condition
- * @return the list of matching habits
+ * @return the list of matching habit groups
*/
abstract fun getFiltered(matcher: HabitMatcher?): HabitGroupList
abstract var primaryOrder: Order
abstract var secondaryOrder: Order
/**
- * Returns the index of the given habit in the list, or -1 if the list does
- * not contain the habit.
+ * Returns the index of the given habit group in the list, or -1 if the list does
+ * not contain the habit group.
*
- * @param h the habit
- * @return the index of the habit, or -1 if not in the list
+ * @param h the habit group
+ * @return the index of the habit group, or -1 if not in the list
*/
abstract fun indexOf(h: HabitGroup): Int
val isEmpty: Boolean
get() = size() == 0
/**
- * Removes the given habit from the list.
+ * Removes the given habit group from the list.
*
- * If the given habit is not in the list, does nothing.
+ * If the given habit group is not in the list, does nothing.
*
- * @param h the habit to be removed.
+ * @param h the habit group to be removed.
*/
abstract fun remove(h: HabitGroup)
/**
- * Removes all the habits from the list.
+ * Removes all the habit groups from the list.
*/
open fun removeAll() {
val copy: MutableList = LinkedList()
@@ -128,43 +128,49 @@ abstract class HabitGroupList : Iterable {
}
/**
- * Changes the position of a habit in the list.
+ * Changes the position of a habit group in the list.
*
- * @param from the habit that should be moved
- * @param to the habit that currently occupies the desired position
+ * @param from the habit group that should be moved
+ * @param to the habit group that currently occupies the desired position
*/
abstract fun reorder(from: HabitGroup, to: HabitGroup)
open fun repair() {}
/**
- * Returns the number of habits in this list.
+ * Returns the number of habit groups in this list.
*
- * @return number of habits
+ * @return number of habit groups
*/
abstract fun size(): Int
/**
- * Notifies the list that a certain list of habits has been modified.
+ * Notifies the list that a certain list of habit groups has been modified.
*
* Depending on the implementation, this operation might trigger a write to
- * disk, or do nothing at all. To make sure that the habits get persisted,
+ * disk, or do nothing at all. To make sure that the habit groups get persisted,
* this operation must be called.
*
- * @param habitGroups the list of habits that have been modified.
+ * @param habitGroups the list of habit groups that have been modified.
*/
abstract fun update(habitGroups: List)
/**
- * Notifies the list that a certain habit has been modified.
+ * Notifies the list that a certain habit group has been modified.
*
* See [.update] for more details.
*
- * @param habitGroup the habit that has been modified.
+ * @param habitGroup the habit groups that has been modified.
*/
fun update(habitGroup: HabitGroup) {
update(listOf(habitGroup))
}
+ /**
+ * For an empty Habit group list, and a given list of habits,
+ * populate the habit groups with their appropriate habits
+ *
+ * @param habitList list of habits to add to the groups
+ * */
fun populateGroupsWith(habitList: HabitList) {
val toRemove = mutableListOf()
for (habit in habitList) {
@@ -187,10 +193,9 @@ abstract class HabitGroupList : Iterable {
}
/**
- * Writes the list of habits to the given writer, in CSV format. There is
- * one line for each habit, containing the fields name, description,
- * frequency numerator, frequency denominator and color. The color is
- * written in HTML format (#000000).
+ * Writes the list of habit groups to the given writer, in CSV format. There is
+ * one line for each habit group, containing the fields name, description,
+ * , and color. The color is written in HTML format (#000000).
*
* @param out the writer that will receive the result
* @throws IOException if write operations fail
@@ -208,13 +213,13 @@ abstract class HabitGroupList : Iterable {
)
val csv = CSVWriter(out)
csv.writeNext(header, false)
- for (habit in this) {
+ for (hgr in this) {
val cols = arrayOf(
- String.format("%03d", indexOf(habit) + 1),
- habit.name,
- habit.question,
- habit.description,
- habit.color.toCsvColor()
+ String.format("%03d", indexOf(hgr) + 1),
+ hgr.name,
+ hgr.question,
+ hgr.description,
+ hgr.color.toCsvColor()
)
csv.writeNext(cols, false)
}