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) }