Can create habit groups now

pull/2020/head
Dharanish 1 year ago
parent 08113a57ac
commit af3283e52f

@ -42,6 +42,14 @@
android:value=".activities.habits.list.ListHabitsActivity" /> android:value=".activities.habits.list.ListHabitsActivity" />
</activity> </activity>
<activity
android:name=".activities.habits.edit.EditHabitGroupActivity"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.habits.list.ListHabitsActivity" />
</activity>
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" /> android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw" />

@ -38,7 +38,7 @@ import javax.inject.Inject
* Provides data that backs a [HabitCardListView]. * 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. * also holds a list of items that have been selected.
*/ */
@ActivityScope @ActivityScope

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

@ -0,0 +1,5 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.recyclerview.widget.RecyclerView
class HabitGroupCardViewHolder(itemView: HabitGroupCardView) : RecyclerView.ViewHolder(itemView)

@ -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<HabitGroup>
) : Command {
override fun run() {
for (hgr in selected) {
hgr.isArchived = true
for (h in hgr.habitList) {
h.isArchived = true
}
}
habitGroupList.update(selected)
}
}

@ -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<HabitGroup>,
val newColor: PaletteColor
) : Command {
override fun run() {
for (hgr in selected) hgr.color = newColor
habitGroupList.update(selected)
}
}

@ -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<HabitGroup>
) : Command {
override fun run() {
for (hgr in selected) habitGroupList.remove(hgr)
}
}

@ -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<HabitGroup>
) : Command {
override fun run() {
for (hgr in selected) {
hgr.isArchived = false
for (h in hgr.habitList) {
h.isArchived = false
}
}
habitGroupList.update(selected)
}
}

@ -111,12 +111,8 @@ data class Habit(
return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset() return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset()
} }
fun hierarchyLevel(): Int { fun isInGroup(): Boolean {
return if (parentID == null) { return (parentID != null)
0
} else {
1 + parent!!.hierarchyLevel()
}
} }
fun copyFrom(other: Habit) { fun copyFrom(other: Habit) {

@ -15,15 +15,12 @@ data class HabitGroup(
var uuid: String? = null, var uuid: String? = null,
var habitList: HabitList, var habitList: HabitList,
val scores: ScoreList, val scores: ScoreList,
val streaks: StreakList, val streaks: StreakList
var parentID: Long? = null,
var parentUUID: String? = null
) { ) {
init { init {
if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "") if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "")
} }
var parent: HabitGroup? = null
var observable = ModelObservable() var observable = ModelObservable()
val uriString: String val uriString: String
@ -80,8 +77,6 @@ data class HabitGroup(
this.question = other.question this.question = other.question
this.reminder = other.reminder this.reminder = other.reminder
this.uuid = other.uuid this.uuid = other.uuid
this.parentID = other.parentID
this.parentUUID = other.parentUUID
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -97,8 +92,6 @@ data class HabitGroup(
if (question != other.question) return false if (question != other.question) return false
if (reminder != other.reminder) return false if (reminder != other.reminder) return false
if (uuid != other.uuid) return false if (uuid != other.uuid) return false
if (parentID != other.parentID) return false
if (parentUUID != other.parentUUID) return false
return true return true
} }
@ -113,19 +106,9 @@ data class HabitGroup(
result = 31 * result + question.hashCode() result = 31 * result + question.hashCode()
result = 31 * result + (reminder?.hashCode() ?: 0) result = 31 * result + (reminder?.hashCode() ?: 0)
result = 31 * result + (uuid?.hashCode() ?: 0) result = 31 * result + (uuid?.hashCode() ?: 0)
result = 31 * result + (parentID?.hashCode() ?: 0)
result = 31 * result + (parentUUID?.hashCode() ?: 0)
return result return result
} }
fun hierarchyLevel(): Int { fun getHabitByUUID(uuid: String?): Habit? =
return if (parentID == null) {
0
} else {
1 + parent!!.hierarchyLevel()
}
}
fun getHabitByUUIDDeep(uuid: String?): Habit? =
habitList.getByUUID(uuid) habitList.getByUUID(uuid)
} }

@ -18,10 +18,10 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
protected val filter: HabitMatcher protected val filter: HabitMatcher
/** /**
* Creates a new HabitList. * Creates a new HabitGroupList.
* *
* Depending on the implementation, this list can either be empty or be * 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. * database.
*/ */
constructor() { constructor() {
@ -35,12 +35,12 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
} }
/** /**
* 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, * 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 * 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 * @param habitGroup the habit to be inserted
* @throws IllegalArgumentException if the habit is already on the list. * @throws IllegalArgumentException if the habit is already on the list.
@ -49,28 +49,28 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
abstract fun add(habitGroup: HabitGroup) 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 * @param id the id of the habit group
* @return the habit, or null if none exist * @return the habit group, or null if none exist
*/ */
abstract fun getById(id: Long): HabitGroup? 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 * @param uuid the UUID of the habit group
* @return the habit, or null if none exist * @return the habit group, or null if none exist
*/ */
abstract fun getByUUID(uuid: String?): HabitGroup? abstract fun getByUUID(uuid: String?): HabitGroup?
/** /**
* Returns the habit with the specified UUID which is * 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) { for (hgr in this) {
val habit = hgr.getHabitByUUIDDeep(uuid) val habit = hgr.getHabitByUUID(uuid)
if (habit != null) { if (habit != null) {
return habit return habit
} }
@ -79,46 +79,46 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
} }
/** /**
* 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 * @param position the position of the desired habit group
* @return the habit at that position * @return the habit group at that position
* @throws IndexOutOfBoundsException when the position is invalid * @throws IndexOutOfBoundsException when the position is invalid
*/ */
abstract fun getByPosition(position: Int): HabitGroup 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 * @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 fun getFiltered(matcher: HabitMatcher?): HabitGroupList
abstract var primaryOrder: Order abstract var primaryOrder: Order
abstract var secondaryOrder: Order abstract var secondaryOrder: Order
/** /**
* Returns the index of the given habit in the list, or -1 if the list does * Returns the index of the given habit group in the list, or -1 if the list does
* not contain the habit. * not contain the habit group.
* *
* @param h the habit * @param h the habit group
* @return the index of the habit, or -1 if not in the list * @return the index of the habit group, or -1 if not in the list
*/ */
abstract fun indexOf(h: HabitGroup): Int abstract fun indexOf(h: HabitGroup): Int
val isEmpty: Boolean val isEmpty: Boolean
get() = size() == 0 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) abstract fun remove(h: HabitGroup)
/** /**
* Removes all the habits from the list. * Removes all the habit groups from the list.
*/ */
open fun removeAll() { open fun removeAll() {
val copy: MutableList<HabitGroup> = LinkedList() val copy: MutableList<HabitGroup> = LinkedList()
@ -128,43 +128,49 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
} }
/** /**
* 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 from the habit group that should be moved
* @param to the habit that currently occupies the desired position * @param to the habit group that currently occupies the desired position
*/ */
abstract fun reorder(from: HabitGroup, to: HabitGroup) abstract fun reorder(from: HabitGroup, to: HabitGroup)
open fun repair() {} 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 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 * 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. * 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<HabitGroup>) abstract fun update(habitGroups: List<HabitGroup>)
/** /**
* 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. * 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) { fun update(habitGroup: HabitGroup) {
update(listOf(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) { fun populateGroupsWith(habitList: HabitList) {
val toRemove = mutableListOf<String?>() val toRemove = mutableListOf<String?>()
for (habit in habitList) { for (habit in habitList) {
@ -187,10 +193,9 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
} }
/** /**
* Writes the list of habits to the given writer, in CSV format. There is * Writes the list of habit groups to the given writer, in CSV format. There is
* one line for each habit, containing the fields name, description, * one line for each habit group, containing the fields name, description,
* frequency numerator, frequency denominator and color. The color is * , and color. The color is written in HTML format (#000000).
* written in HTML format (#000000).
* *
* @param out the writer that will receive the result * @param out the writer that will receive the result
* @throws IOException if write operations fail * @throws IOException if write operations fail
@ -208,13 +213,13 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
) )
val csv = CSVWriter(out) val csv = CSVWriter(out)
csv.writeNext(header, false) csv.writeNext(header, false)
for (habit in this) { for (hgr in this) {
val cols = arrayOf( val cols = arrayOf(
String.format("%03d", indexOf(habit) + 1), String.format("%03d", indexOf(hgr) + 1),
habit.name, hgr.name,
habit.question, hgr.question,
habit.description, hgr.description,
habit.color.toCsvColor() hgr.color.toCsvColor()
) )
csv.writeNext(cols, false) csv.writeNext(cols, false)
} }

Loading…
Cancel
Save