Make HabitGroups simpler

No subgroups
pull/2020/head
Dharanish 1 year ago
parent 262b49d025
commit 832d51a055

@ -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 = "<font color=#FFFFFF>${getString(resId)}</font>"
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())
}
}
}

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

@ -0,0 +1,72 @@
<!--
~ Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout
android:id="@+id/container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
style="@style/Toolbar"
app:popupTheme="?toolbarPopupTheme"
android:layout_alignParentTop="true"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?windowBackgroundColor"
android:clipToPadding="false">
<LinearLayout
style="@style/CardList"
android:clipToPadding="false">
<org.isoron.uhabits.activities.habits.show.views.SubtitleCardView
android:id="@+id/subtitleCard"
style="@style/ShowHabit.Subtitle"/>
<org.isoron.uhabits.activities.habits.show.views.NotesCardView
android:id="@+id/notesCard"
style="@style/Card"
android:gravity="center" />
<org.isoron.uhabits.activities.habits.show.views.OverviewCardView
android:id="@+id/overviewCard"
style="@style/Card"
android:paddingTop="12dp"/>
<org.isoron.uhabits.activities.habits.show.views.ScoreCardView
android:id="@+id/scoreCard"
style="@style/Card"
android:gravity="center"/>
<org.isoron.uhabits.activities.habits.show.views.StreakCardView
android:id="@+id/streakCard"
style="@style/Card"/>
</LinearLayout>
</ScrollView>
</RelativeLayout>

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

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

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

@ -78,16 +78,6 @@ abstract class HabitGroupList : Iterable<HabitGroup> {
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<HabitGroup> {
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()
}

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

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

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

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

@ -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!!,

@ -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
);
Loading…
Cancel
Save