diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt
new file mode 100644
index 000000000..50fc5ef78
--- /dev/null
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitGroupActivity.kt
@@ -0,0 +1,232 @@
+package org.isoron.uhabits.activities.habits.edit
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Bundle
+import android.text.Html
+import android.text.Spanned
+import android.text.format.DateFormat
+import android.view.View
+import androidx.annotation.StringRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.DialogFragment
+import com.android.datetimepicker.time.RadialPickerLayout
+import com.android.datetimepicker.time.TimePickerDialog
+import org.isoron.platform.gui.toInt
+import org.isoron.uhabits.HabitsApplication
+import org.isoron.uhabits.R
+import org.isoron.uhabits.activities.AndroidThemeSwitcher
+import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
+import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
+import org.isoron.uhabits.core.commands.CommandRunner
+import org.isoron.uhabits.core.commands.CreateHabitGroupCommand
+import org.isoron.uhabits.core.commands.EditHabitGroupCommand
+import org.isoron.uhabits.core.models.HabitGroup
+import org.isoron.uhabits.core.models.PaletteColor
+import org.isoron.uhabits.core.models.Reminder
+import org.isoron.uhabits.core.models.WeekdayList
+import org.isoron.uhabits.databinding.ActivityEditHabitGroupBinding
+import org.isoron.uhabits.utils.ColorUtils
+import org.isoron.uhabits.utils.dismissCurrentAndShow
+import org.isoron.uhabits.utils.formatTime
+import org.isoron.uhabits.utils.toFormattedString
+
+class EditHabitGroupActivity : AppCompatActivity() {
+
+ private lateinit var themeSwitcher: AndroidThemeSwitcher
+ private lateinit var binding: ActivityEditHabitGroupBinding
+ private lateinit var commandRunner: CommandRunner
+
+ var habitGroupId = -1L
+ var color = PaletteColor(11)
+ var androidColor = 0
+ var reminderHour = -1
+ var reminderMin = -1
+ var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
+
+ override fun onCreate(state: Bundle?) {
+ super.onCreate(state)
+
+ val component = (application as HabitsApplication).component
+ themeSwitcher = AndroidThemeSwitcher(this, component.preferences)
+ themeSwitcher.apply()
+
+ binding = ActivityEditHabitGroupBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ if (intent.hasExtra("habitGroupId")) {
+ binding.toolbar.title = getString(R.string.edit_habit_group)
+ habitGroupId = intent.getLongExtra("habitId", -1)
+ val hgr = component.habitGroupList.getById(habitGroupId)!!
+ color = hgr.color
+ hgr.reminder?.let {
+ reminderHour = it.hour
+ reminderMin = it.minute
+ reminderDays = it.days
+ }
+ binding.nameInput.setText(hgr.name)
+ binding.questionInput.setText(hgr.question)
+ binding.notesInput.setText(hgr.description)
+ }
+
+ if (state != null) {
+ habitGroupId = state.getLong("habitGroupId")
+ color = PaletteColor(state.getInt("paletteColor"))
+ reminderHour = state.getInt("reminderHour")
+ reminderMin = state.getInt("reminderMin")
+ reminderDays = WeekdayList(state.getInt("reminderDays"))
+ }
+
+ updateColors()
+
+ setSupportActionBar(binding.toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowHomeEnabled(true)
+ supportActionBar?.elevation = 10.0f
+
+ val colorPickerDialogFactory = ColorPickerDialogFactory(this)
+ binding.colorButton.setOnClickListener {
+ val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
+ picker.setListener { paletteColor ->
+ this.color = paletteColor
+ updateColors()
+ }
+ picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker")
+ }
+
+ populateReminder()
+ binding.reminderTimePicker.setOnClickListener {
+ val currentHour = if (reminderHour >= 0) reminderHour else 8
+ val currentMin = if (reminderMin >= 0) reminderMin else 0
+ val is24HourMode = DateFormat.is24HourFormat(this)
+ val dialog = TimePickerDialog.newInstance(
+ object : TimePickerDialog.OnTimeSetListener {
+ override fun onTimeSet(view: RadialPickerLayout?, hourOfDay: Int, minute: Int) {
+ reminderHour = hourOfDay
+ reminderMin = minute
+ populateReminder()
+ }
+
+ override fun onTimeCleared(view: RadialPickerLayout?) {
+ reminderHour = -1
+ reminderMin = -1
+ reminderDays = WeekdayList.EVERY_DAY
+ populateReminder()
+ }
+ },
+ currentHour,
+ currentMin,
+ is24HourMode,
+ androidColor
+ )
+ dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker")
+ }
+
+ binding.reminderDatePicker.setOnClickListener {
+ val dialog = WeekdayPickerDialog()
+
+ dialog.setListener { days: WeekdayList ->
+ reminderDays = days
+ if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY
+ populateReminder()
+ }
+ dialog.setSelectedDays(reminderDays)
+ dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker")
+ }
+
+ binding.buttonSave.setOnClickListener {
+ if (validate()) save()
+ }
+
+ for (fragment in supportFragmentManager.fragments) {
+ (fragment as DialogFragment).dismiss()
+ }
+ }
+
+ private fun save() {
+ val component = (application as HabitsApplication).component
+ val hgr = component.modelFactory.buildHabitGroup()
+
+ var original: HabitGroup? = null
+ if (habitGroupId >= 0) {
+ original = component.habitGroupList.getById(habitGroupId)!!
+ hgr.copyFrom(original)
+ }
+
+ hgr.name = binding.nameInput.text.trim().toString()
+ hgr.question = binding.questionInput.text.trim().toString()
+ hgr.description = binding.notesInput.text.trim().toString()
+ hgr.color = color
+ if (reminderHour >= 0) {
+ hgr.reminder = Reminder(reminderHour, reminderMin, reminderDays)
+ } else {
+ hgr.reminder = null
+ }
+
+ val command = if (habitGroupId >= 0) {
+ EditHabitGroupCommand(
+ component.habitGroupList,
+ habitGroupId,
+ hgr
+ )
+ } else {
+ CreateHabitGroupCommand(
+ component.modelFactory,
+ component.habitGroupList,
+ hgr
+ )
+ }
+ component.commandRunner.run(command)
+ finish()
+ }
+
+ private fun validate(): Boolean {
+ var isValid = true
+ if (binding.nameInput.text.isEmpty()) {
+ binding.nameInput.error = getFormattedValidationError(R.string.validation_cannot_be_blank)
+ isValid = false
+ }
+ return isValid
+ }
+
+ private fun populateReminder() {
+ if (reminderHour < 0) {
+ binding.reminderTimePicker.text = getString(R.string.reminder_off)
+ binding.reminderDatePicker.visibility = View.GONE
+ binding.reminderDivider.visibility = View.GONE
+ } else {
+ val time = formatTime(this, reminderHour, reminderMin)
+ binding.reminderTimePicker.text = time
+ binding.reminderDatePicker.visibility = View.VISIBLE
+ binding.reminderDivider.visibility = View.VISIBLE
+ binding.reminderDatePicker.text = reminderDays.toFormattedString(this)
+ }
+ }
+
+ private fun updateColors() {
+ androidColor = themeSwitcher.currentTheme.color(color).toInt()
+ binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
+ if (!themeSwitcher.isNightMode) {
+ val darkerAndroidColor = ColorUtils.mixColors(Color.BLACK, androidColor, 0.15f)
+ window.statusBarColor = darkerAndroidColor
+ binding.toolbar.setBackgroundColor(androidColor)
+ }
+ }
+
+ private fun getFormattedValidationError(@StringRes resId: Int): Spanned {
+ val html = "${getString(resId)}"
+ return Html.fromHtml(html)
+ }
+
+ override fun onSaveInstanceState(state: Bundle) {
+ super.onSaveInstanceState(state)
+ with(state) {
+ putLong("habitGroupId", habitGroupId)
+ putInt("paletteColor", color.paletteIndex)
+ putInt("androidColor", androidColor)
+ putInt("reminderHour", reminderHour)
+ putInt("reminderMin", reminderMin)
+ putInt("reminderDays", reminderDays.toInteger())
+ }
+ }
+}
diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt
index 71b2938e7..33fa31aa3 100644
--- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt
+++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentFactory.kt
@@ -25,10 +25,12 @@ import android.net.Uri
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.about.AboutActivity
import org.isoron.uhabits.activities.habits.edit.EditHabitActivity
+import org.isoron.uhabits.activities.habits.edit.EditHabitGroupActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.activities.intro.IntroActivity
import org.isoron.uhabits.activities.settings.SettingsActivity
import org.isoron.uhabits.core.models.Habit
+import org.isoron.uhabits.core.models.HabitGroup
import javax.inject.Inject
class IntentFactory
@@ -100,4 +102,14 @@ class IntentFactory
intent.putExtra("habitType", habitType)
return intent
}
+
+ fun startEditGroupActivity(context: Context): Intent {
+ return Intent(context, EditHabitGroupActivity::class.java)
+ }
+
+ fun startEditGroupActivity(context: Context, habitGroup: HabitGroup): Intent {
+ val intent = startEditGroupActivity(context)
+ intent.putExtra("habitGroupId", habitGroup.id)
+ return intent
+ }
}
diff --git a/uhabits-android/src/main/res/layout/show_habit_group.xml b/uhabits-android/src/main/res/layout/show_habit_group.xml
new file mode 100644
index 000000000..b8b433577
--- /dev/null
+++ b/uhabits-android/src/main/res/layout/show_habit_group.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt
new file mode 100644
index 000000000..29675ca5c
--- /dev/null
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateHabitGroupCommand.kt
@@ -0,0 +1,18 @@
+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.ModelFactory
+
+data class CreateHabitGroupCommand(
+ val modelFactory: ModelFactory,
+ val habitGroupList: HabitGroupList,
+ val model: HabitGroup
+) : Command {
+ override fun run() {
+ val habitGroup = modelFactory.buildHabitGroup()
+ habitGroup.copyFrom(model)
+ habitGroupList.add(habitGroup)
+ habitGroup.recompute()
+ }
+}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt
new file mode 100644
index 000000000..aa7ab6243
--- /dev/null
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/EditHabitGroupCommand.kt
@@ -0,0 +1,20 @@
+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.HabitNotFoundException
+
+data class EditHabitGroupCommand(
+ val habitGroupList: HabitGroupList,
+ val habitGroupId: Long,
+ val modified: HabitGroup
+) : Command {
+ override fun run() {
+ val habitGroup = habitGroupList.getById(habitGroupId) ?: throw HabitNotFoundException()
+ habitGroup.copyFrom(modified)
+ habitGroupList.update(habitGroup)
+ habitGroup.observable.notifyListeners()
+ habitGroup.recompute()
+ habitGroupList.resort()
+ }
+}
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 5b80f1bc7..4526e1872 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
@@ -14,7 +14,6 @@ data class HabitGroup(
var reminder: Reminder? = null,
var uuid: String? = null,
var habitList: HabitList,
- var habitGroupList: HabitGroupList,
val scores: ScoreList,
val streaks: StreakList,
var parentID: Long? = null,
@@ -33,11 +32,11 @@ data class HabitGroup(
fun hasReminder(): Boolean = reminder != null
fun isCompletedToday(): Boolean {
- return habitList.all { it.isCompletedToday() } && habitGroupList.all { it.isCompletedToday() }
+ return habitList.all { it.isCompletedToday() }
}
fun isEnteredToday(): Boolean {
- return habitList.all { it.isEnteredToday() } && habitGroupList.all { it.isEnteredToday() }
+ return habitList.all { it.isEnteredToday() }
}
fun firstEntryDate(): Timestamp {
@@ -47,16 +46,11 @@ data class HabitGroup(
val first = h.firstEntryDate()
if (earliest.isNewerThan(first)) earliest = first
}
- for (hgr in habitGroupList) {
- val first = hgr.firstEntryDate()
- if (earliest.isNewerThan(first)) earliest = first
- }
return earliest
}
fun recompute() {
for (h in habitList) h.recompute()
- for (hgr in habitGroupList) hgr.recompute()
val today = DateUtils.getTodayWithOffset()
val to = today.plus(30)
@@ -65,14 +59,12 @@ data class HabitGroup(
scores.combineFrom(
habitList = habitList,
- habitGroupList = habitGroupList,
from = from,
to = to
)
streaks.combineFrom(
habitList = habitList,
- habitGroupList = habitGroupList,
from = from,
to = to
)
@@ -134,23 +126,6 @@ data class HabitGroup(
}
}
- fun getHabitByUUIDDeep(uuid: String?): Habit? {
- val habit = habitList.getByUUID(uuid)
- if (habit != null) return habit
- for (hgr in habitGroupList) {
- val found = hgr.getHabitByUUIDDeep(uuid)
- if (found != null) return found
- }
- return null
- }
-
- fun getHabitGroupByUUIDDeep(uuid: String?): HabitGroup? {
- val habitGroup = habitGroupList.getByUUID(uuid)
- if (habitGroup != null) return habitGroup
- for (hgr in habitGroupList) {
- val found = hgr.getHabitGroupByUUIDDeep(uuid)
- if (found != null) return found
- }
- return null
- }
+ fun getHabitByUUIDDeep(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 944adfbf3..f597eb1f7 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
@@ -78,16 +78,6 @@ abstract class HabitGroupList : Iterable {
return null
}
- fun getHabitGroupByUUIDDeep(uuid: String?): HabitGroup? {
- for (hgr in this) {
- val habit = hgr.getHabitGroupByUUIDDeep(uuid)
- if (habit != null) {
- return habit
- }
- }
- return null
- }
-
/**
* Returns the habit that occupies a certain position.
*
@@ -191,21 +181,6 @@ abstract class HabitGroupList : Iterable {
habitList.remove(h)
}
}
- toRemove.clear()
- for (hgr1 in this) {
- val hgr2 = getByUUID(hgr1.parentUUID)
- if (hgr2 != null) {
- hgr2.habitGroupList.add(hgr1)
- toRemove.add(hgr1.uuid)
- hgr1.parent = hgr2
- }
- }
- for (uuid in toRemove) {
- val h = getByUUID(uuid)
- if (h != null) {
- remove(h)
- }
- }
for (hgr in this) {
hgr.recompute()
}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt
index b150aa03a..6287f6602 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelFactory.kt
@@ -41,12 +41,10 @@ interface ModelFactory {
}
fun buildHabitGroup(): HabitGroup {
val habits = buildHabitList()
- val groups = buildHabitGroupList()
val scores = buildScoreList()
val streaks = buildStreakList()
return HabitGroup(
habitList = habits,
- habitGroupList = groups,
scores = scores,
streaks = streaks
)
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt
index b608334b0..dc99c0677 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt
@@ -141,15 +141,13 @@ class ScoreList {
@Synchronized
fun combineFrom(
habitList: HabitList,
- habitGroupList: HabitGroupList,
from: Timestamp,
to: Timestamp
) {
var current = to
while (current >= from) {
val habitScores = habitList.map { it.scores[current].value }
- val groupScores = habitGroupList.map { it.scores[current].value }
- val averageScore = (habitScores + groupScores).average()
+ val averageScore = habitScores.average()
map[current] = Score(current, averageScore)
current = current.minus(1)
}
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt
index 466e7b596..494209fce 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt
@@ -71,7 +71,6 @@ class StreakList {
@Synchronized
fun combineFrom(
habitList: HabitList,
- habitGroupList: HabitGroupList,
from: Timestamp,
to: Timestamp
) {
@@ -79,9 +78,7 @@ class StreakList {
var streakRunning = false
var streakStart = from
while (current <= to) {
- if (habitList.all { it.streaks.isInStreaks(current) } &&
- habitGroupList.all { it.streaks.isInStreaks(current) } &&
- !streakRunning
+ if (habitList.all { it.streaks.isInStreaks(current) } && !streakRunning
) {
streakStart = current
streakRunning = true
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt
index 9502fdb28..bc80617ae 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitGroupList.kt
@@ -205,10 +205,6 @@ class MemoryHabitGroupList : HabitGroupList {
hgr.habitList.primaryOrder = primaryOrder
hgr.habitList.secondaryOrder = secondaryOrder
hgr.habitList.resort()
-
- hgr.habitGroupList.primaryOrder = primaryOrder
- hgr.habitGroupList.secondaryOrder = secondaryOrder
- hgr.habitGroupList.resort()
}
if (comparator != null) list.sortWith(comparator!!)
observable.notifyListeners()
diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt
index 0e5a90699..0b34be917 100644
--- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt
+++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitGroupRecord.kt
@@ -49,12 +49,6 @@ class HabitGroupRecord {
@field:Column
var uuid: String? = null
- @field:Column(name = "parent_id")
- var parentID: Long? = null
-
- @field:Column(name = "parent_uuid")
- var parentUUID: String? = null
-
fun copyFrom(model: HabitGroup) {
id = model.id
name = model.name
@@ -68,8 +62,6 @@ class HabitGroupRecord {
reminderDays = 0
reminderMin = null
reminderHour = null
- parentID = model.parentID
- parentUUID = model.parentUUID
if (model.hasReminder()) {
val reminder = model.reminder
reminderHour = requireNonNull(reminder)!!.hour
@@ -87,8 +79,6 @@ class HabitGroupRecord {
habitGroup.isArchived = archived != 0
habitGroup.position = position!!
habitGroup.uuid = uuid
- habitGroup.parentID = parentID
- habitGroup.parentUUID = parentUUID
if (reminderHour != null && reminderMin != null) {
habitGroup.reminder = Reminder(
reminderHour!!,
diff --git a/uhabits-core/src/jvmMain/resources/migrations/26.sql b/uhabits-core/src/jvmMain/resources/migrations/26.sql
index d934bd4b8..067a6bb1f 100644
--- a/uhabits-core/src/jvmMain/resources/migrations/26.sql
+++ b/uhabits-core/src/jvmMain/resources/migrations/26.sql
@@ -15,7 +15,5 @@ create table HabitGroups (
reminder_hour integer,
reminder_min integer,
question text not null default "",
- uuid text,
- parent_id integer,
- parent_uuid integer
+ uuid text
);
\ No newline at end of file