mirror of https://github.com/iSoron/uhabits.git
Merge bad674dd36
into 22331ed364
commit
ff7ad445ab
@ -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("habitGroupId", -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.uhabits.activities.habits.list
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.TextView
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
|
import org.isoron.uhabits.core.commands.AddToGroupCommand
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
|
|
||||||
|
class HabitGroupPickerDialog : Activity() {
|
||||||
|
|
||||||
|
private lateinit var habitGroupList: HabitGroupList
|
||||||
|
private lateinit var habitList: HabitList
|
||||||
|
private lateinit var selected: List<Habit>
|
||||||
|
private lateinit var component: HabitsApplicationComponent
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
component = (applicationContext as HabitsApplication).component
|
||||||
|
AndroidThemeSwitcher(this, component.preferences).apply()
|
||||||
|
habitList = component.habitList
|
||||||
|
habitGroupList = component.habitGroupList
|
||||||
|
|
||||||
|
val selectedIds = intent.getLongArrayExtra("selected")!!
|
||||||
|
selected = selectedIds.map { id ->
|
||||||
|
habitList.getById(id) ?: habitGroupList.getHabitByID(id)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupIds = ArrayList<Long>()
|
||||||
|
val groupNames = ArrayList<String>()
|
||||||
|
|
||||||
|
for (hgr in habitGroupList) {
|
||||||
|
if (hgr.isArchived) continue
|
||||||
|
groupIds.add(hgr.id!!)
|
||||||
|
groupNames.add(hgr.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupNames.isEmpty()) {
|
||||||
|
setContentView(R.layout.widget_empty_activity)
|
||||||
|
findViewById<TextView>(R.id.message).setText(R.string.no_habit_groups)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.widget_configure_activity)
|
||||||
|
val listView = findViewById<ListView>(R.id.listView)
|
||||||
|
|
||||||
|
with(listView) {
|
||||||
|
adapter = ArrayAdapter(
|
||||||
|
context,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
groupNames
|
||||||
|
)
|
||||||
|
setOnItemClickListener { parent, view, position, id ->
|
||||||
|
performTransfer(groupIds[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun performTransfer(toGroupId: Long) {
|
||||||
|
val hgr = habitGroupList.getById(toGroupId)!!
|
||||||
|
selected = selected.filter { it.groupId != hgr.id }
|
||||||
|
component.commandRunner.run(
|
||||||
|
AddToGroupCommand(habitList, hgr, selected)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.MeasureSpec.EXACTLY
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.utils.getFontAwesome
|
||||||
|
import org.isoron.uhabits.utils.sp
|
||||||
|
import org.isoron.uhabits.utils.sres
|
||||||
|
import org.isoron.uhabits.utils.toMeasureSpec
|
||||||
|
|
||||||
|
class AddButtonView(
|
||||||
|
context: Context,
|
||||||
|
var habitGroup: HabitGroup?
|
||||||
|
) : View(context),
|
||||||
|
View.OnClickListener {
|
||||||
|
|
||||||
|
private var drawer = Drawer()
|
||||||
|
|
||||||
|
init {
|
||||||
|
setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
(context as ListHabitsActivity).component.listHabitsMenu.behavior.onCreateHabit(habitGroup!!.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
drawer.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
|
||||||
|
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
|
||||||
|
super.onMeasure(
|
||||||
|
width.toMeasureSpec(EXACTLY),
|
||||||
|
height.toMeasureSpec(EXACTLY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Drawer {
|
||||||
|
private val rect = RectF()
|
||||||
|
private val highContrastColor = sres.getColor(R.attr.contrast80)
|
||||||
|
|
||||||
|
private val paint = TextPaint().apply {
|
||||||
|
typeface = getFontAwesome()
|
||||||
|
isAntiAlias = true
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
fun draw(canvas: Canvas) {
|
||||||
|
paint.color = highContrastColor
|
||||||
|
val id = R.string.fa_plus
|
||||||
|
paint.textSize = sp(12.0f)
|
||||||
|
paint.strokeWidth = 0f
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
|
||||||
|
val label = resources.getString(id)
|
||||||
|
val em = paint.measureText("m")
|
||||||
|
|
||||||
|
rect.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||||
|
rect.offset(0f, 0.4f * em)
|
||||||
|
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.view.View
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.ModelObservable
|
||||||
|
import org.isoron.uhabits.utils.getFontAwesome
|
||||||
|
import org.isoron.uhabits.utils.sp
|
||||||
|
import org.isoron.uhabits.utils.sres
|
||||||
|
import org.isoron.uhabits.utils.toMeasureSpec
|
||||||
|
|
||||||
|
class CollapseButtonView(
|
||||||
|
context: Context,
|
||||||
|
var habitGroup: HabitGroup?
|
||||||
|
) : View(context),
|
||||||
|
View.OnClickListener,
|
||||||
|
ModelObservable.Listener {
|
||||||
|
|
||||||
|
private var drawer = Drawer()
|
||||||
|
|
||||||
|
var collapsed = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
drawer.rotate()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
collapsed = !collapsed
|
||||||
|
habitGroup!!.collapsed = collapsed
|
||||||
|
(context as ListHabitsActivity).component.listHabitsMenu.behavior.onPreferencesChanged()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
drawer.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
|
||||||
|
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
|
||||||
|
super.onMeasure(
|
||||||
|
width.toMeasureSpec(MeasureSpec.EXACTLY),
|
||||||
|
height.toMeasureSpec(MeasureSpec.EXACTLY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Drawer {
|
||||||
|
private val rect = RectF()
|
||||||
|
private val highContrastColor = sres.getColor(R.attr.contrast100)
|
||||||
|
|
||||||
|
private var rotationAngle = 0f
|
||||||
|
private var offset_y = 0.4f
|
||||||
|
private var offset_x = 0f
|
||||||
|
private val paint = TextPaint().apply {
|
||||||
|
typeface = getFontAwesome()
|
||||||
|
isAntiAlias = true
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rotate() {
|
||||||
|
if (collapsed) {
|
||||||
|
rotationAngle = 90f
|
||||||
|
offset_y = 0f
|
||||||
|
offset_x = -0.4f
|
||||||
|
} else {
|
||||||
|
rotationAngle = 0f
|
||||||
|
offset_y = 0.4f
|
||||||
|
offset_x = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun draw(canvas: Canvas) {
|
||||||
|
paint.color = highContrastColor
|
||||||
|
val id = R.string.fa_angle_down
|
||||||
|
paint.textSize = sp(12.0f)
|
||||||
|
paint.strokeWidth = 0f
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
|
||||||
|
val label = resources.getString(id)
|
||||||
|
val em = paint.measureText("m")
|
||||||
|
|
||||||
|
rect.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||||
|
rect.offset(offset_x * em, offset_y * em)
|
||||||
|
|
||||||
|
canvas.save() // Save the current state of the canvas
|
||||||
|
canvas.rotate(rotationAngle, rect.centerX(), rect.centerY()) // Rotate the canvas
|
||||||
|
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onModelChange() {}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
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
|
||||||
|
|
||||||
|
class HabitGroupCardView(
|
||||||
|
@ActivityContext context: Context,
|
||||||
|
private val behavior: ListHabitsBehavior
|
||||||
|
) : FrameLayout(context),
|
||||||
|
ModelObservable.Listener {
|
||||||
|
|
||||||
|
var dataOffset = 0
|
||||||
|
|
||||||
|
var habitGroup: HabitGroup? = null
|
||||||
|
set(newHabitGroup) {
|
||||||
|
if (isAttachedToWindow) {
|
||||||
|
field?.observable?.removeListener(this)
|
||||||
|
newHabitGroup?.observable?.addListener(this)
|
||||||
|
}
|
||||||
|
field = newHabitGroup
|
||||||
|
if (newHabitGroup != null) copyAttributesFrom(newHabitGroup)
|
||||||
|
addButtonView.habitGroup = newHabitGroup
|
||||||
|
collapseButtonView.habitGroup = newHabitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
var score
|
||||||
|
get() = scoreRing.getPercentage().toDouble()
|
||||||
|
set(value) {
|
||||||
|
scoreRing.setPercentage(value.toFloat())
|
||||||
|
scoreRing.setPrecision(1.0f / 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
var addButtonView: AddButtonView
|
||||||
|
var collapseButtonView: CollapseButtonView
|
||||||
|
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
|
||||||
|
}
|
||||||
|
setTypeface(typeface, Typeface.BOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
addButtonView = AddButtonView(context, habitGroup)
|
||||||
|
collapseButtonView = CollapseButtonView(context, habitGroup)
|
||||||
|
|
||||||
|
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)
|
||||||
|
addView(addButtonView)
|
||||||
|
addView(collapseButtonView)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseButtonView.collapsed = hgr.collapsed
|
||||||
|
|
||||||
|
if (collapseButtonView.collapsed) {
|
||||||
|
addButtonView.visibility = GONE
|
||||||
|
} else {
|
||||||
|
addButtonView.visibility = VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,148 @@
|
|||||||
|
package org.isoron.uhabits.activities.habits.show
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
|
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||||
|
import org.isoron.uhabits.core.commands.Command
|
||||||
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupMenuPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupPresenter
|
||||||
|
import org.isoron.uhabits.intents.IntentFactory
|
||||||
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
|
import org.isoron.uhabits.utils.dismissCurrentDialog
|
||||||
|
import org.isoron.uhabits.utils.showMessage
|
||||||
|
import org.isoron.uhabits.utils.showSendFileScreen
|
||||||
|
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||||
|
|
||||||
|
class ShowHabitGroupActivity : AppCompatActivity(), CommandRunner.Listener {
|
||||||
|
|
||||||
|
private lateinit var commandRunner: CommandRunner
|
||||||
|
private lateinit var menu: ShowHabitGroupMenu
|
||||||
|
private lateinit var view: ShowHabitGroupView
|
||||||
|
private lateinit var habitGroup: HabitGroup
|
||||||
|
private lateinit var preferences: Preferences
|
||||||
|
private lateinit var themeSwitcher: AndroidThemeSwitcher
|
||||||
|
private lateinit var widgetUpdater: WidgetUpdater
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
private lateinit var presenter: ShowHabitGroupPresenter
|
||||||
|
private val screen = Screen()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val appComponent = (applicationContext as HabitsApplication).component
|
||||||
|
val habitGroupList = appComponent.habitGroupList
|
||||||
|
habitGroup = habitGroupList.getById(ContentUris.parseId(intent.data!!))!!
|
||||||
|
preferences = appComponent.preferences
|
||||||
|
commandRunner = appComponent.commandRunner
|
||||||
|
widgetUpdater = appComponent.widgetUpdater
|
||||||
|
|
||||||
|
themeSwitcher = AndroidThemeSwitcher(this, preferences)
|
||||||
|
themeSwitcher.apply()
|
||||||
|
|
||||||
|
presenter = ShowHabitGroupPresenter(
|
||||||
|
commandRunner = commandRunner,
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
preferences = preferences,
|
||||||
|
screen = screen
|
||||||
|
)
|
||||||
|
|
||||||
|
view = ShowHabitGroupView(this)
|
||||||
|
|
||||||
|
val menuPresenter = ShowHabitGroupMenuPresenter(
|
||||||
|
commandRunner = commandRunner,
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
habitGroupList = habitGroupList,
|
||||||
|
screen = screen
|
||||||
|
)
|
||||||
|
|
||||||
|
menu = ShowHabitGroupMenu(
|
||||||
|
activity = this,
|
||||||
|
presenter = menuPresenter,
|
||||||
|
preferences = preferences
|
||||||
|
)
|
||||||
|
|
||||||
|
view.setListener(presenter)
|
||||||
|
setContentView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(m: Menu): Boolean {
|
||||||
|
return menu.onCreateOptionsMenu(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return menu.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
commandRunner.addListener(this)
|
||||||
|
screen.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
dismissCurrentDialog()
|
||||||
|
commandRunner.removeListener(this)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCommandFinished(command: Command) {
|
||||||
|
screen.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Screen : ShowHabitGroupMenuPresenter.Screen, ShowHabitGroupPresenter.Screen {
|
||||||
|
override fun updateWidgets() {
|
||||||
|
widgetUpdater.updateWidgets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refresh() {
|
||||||
|
scope.launch {
|
||||||
|
view.setState(
|
||||||
|
ShowHabitGroupPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
preferences = preferences,
|
||||||
|
theme = themeSwitcher.currentTheme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showEditHabitGroupScreen(habitGroup: HabitGroup) {
|
||||||
|
startActivity(IntentFactory().startEditGroupActivity(this@ShowHabitGroupActivity, habitGroup))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showMessage(m: ShowHabitGroupMenuPresenter.Message?) {
|
||||||
|
when (m) {
|
||||||
|
ShowHabitGroupMenuPresenter.Message.COULD_NOT_EXPORT -> {
|
||||||
|
showMessage(resources.getString(R.string.could_not_export))
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showSendFileScreen(filename: String) {
|
||||||
|
this@ShowHabitGroupActivity.showSendFileScreen(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
|
||||||
|
ConfirmDeleteDialog(this@ShowHabitGroupActivity, callback, 1).dismissCurrentAndShow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
this@ShowHabitGroupActivity.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.isoron.uhabits.activities.habits.show
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupMenuPresenter
|
||||||
|
|
||||||
|
class ShowHabitGroupMenu(
|
||||||
|
val activity: ShowHabitGroupActivity,
|
||||||
|
val presenter: ShowHabitGroupMenuPresenter,
|
||||||
|
val preferences: Preferences
|
||||||
|
) {
|
||||||
|
fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
activity.menuInflater.inflate(R.menu.show_habit_group, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_edit_habit_group -> {
|
||||||
|
presenter.onEditHabitGroup()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_delete -> {
|
||||||
|
presenter.onDeleteHabitGroup()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package org.isoron.uhabits.activities.habits.show
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitGroupState
|
||||||
|
import org.isoron.uhabits.databinding.ShowHabitGroupBinding
|
||||||
|
import org.isoron.uhabits.utils.setupToolbar
|
||||||
|
|
||||||
|
class ShowHabitGroupView(context: Context) : FrameLayout(context) {
|
||||||
|
private val binding = ShowHabitGroupBinding.inflate(LayoutInflater.from(context))
|
||||||
|
|
||||||
|
init {
|
||||||
|
addView(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setState(data: ShowHabitGroupState) {
|
||||||
|
setupToolbar(
|
||||||
|
binding.toolbar,
|
||||||
|
title = data.title,
|
||||||
|
color = data.color,
|
||||||
|
theme = data.theme
|
||||||
|
)
|
||||||
|
binding.subtitleCard.setState(data.subtitle)
|
||||||
|
binding.overviewCard.setState(data.overview)
|
||||||
|
binding.notesCard.setState(data.notes)
|
||||||
|
binding.targetCard.setState(data.target)
|
||||||
|
binding.streakCard.setState(data.streaks)
|
||||||
|
binding.scoreCard.setState(data.scores)
|
||||||
|
binding.barCard.setState(data.bar)
|
||||||
|
binding.frequencyCard.setState(data.frequency)
|
||||||
|
if (!data.isBoolean) {
|
||||||
|
binding.streakCard.visibility = GONE
|
||||||
|
if (!data.isNumerical) {
|
||||||
|
binding.barCard.visibility = GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data.isNumerical) {
|
||||||
|
binding.targetCard.visibility = GONE
|
||||||
|
}
|
||||||
|
if (data.isEmpty) {
|
||||||
|
binding.targetCard.visibility = GONE
|
||||||
|
binding.barCard.visibility = GONE
|
||||||
|
binding.streakCard.visibility = GONE
|
||||||
|
binding.overviewCard.visibility = GONE
|
||||||
|
binding.scoreCard.visibility = GONE
|
||||||
|
binding.frequencyCard.visibility = GONE
|
||||||
|
binding.noSubHabitsCard.visibility = VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setListener(presenter: ShowHabitGroupPresenter) {
|
||||||
|
binding.scoreCard.setListener(presenter.scoreCardPresenter)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/contrast0"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".activities.habits.edit.EditHabitGroupActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||||
|
app:title="@string/create_habit_group"
|
||||||
|
app:titleTextColor="@color/white">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/buttonSave"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/save"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:rippleColor="@color/white"
|
||||||
|
app:strokeColor="@color/white" />
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingLeft="4dp"
|
||||||
|
android:paddingRight="4dp">
|
||||||
|
|
||||||
|
<!-- Title and color -->
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
|
<!-- Habit Title -->
|
||||||
|
<FrameLayout
|
||||||
|
style="@style/FormOuterBox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/name" />
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/nameInput"
|
||||||
|
style="@style/FormInput"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:inputType="textCapSentences"
|
||||||
|
android:hint="@string/yes_or_no_short_example"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- Habit Color -->
|
||||||
|
<FrameLayout
|
||||||
|
style="@style/FormOuterBox"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/color" />
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/colorButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:backgroundTint="#E23673" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Habit Question -->
|
||||||
|
<FrameLayout style="@style/FormOuterBox">
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/question" />
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/questionInput"
|
||||||
|
style="@style/FormInput"
|
||||||
|
android:inputType="textCapSentences|textMultiLine"
|
||||||
|
android:hint="@string/example_question_boolean"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- Reminder -->
|
||||||
|
<FrameLayout style="@style/FormOuterBox">
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/reminder" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/FormDropdown"
|
||||||
|
android:id="@+id/reminderTimePicker"
|
||||||
|
android:text="@string/reminder_off" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
style="@style/FormDivider"
|
||||||
|
android:id="@+id/reminderDivider"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/FormDropdown"
|
||||||
|
android:id="@+id/reminderDatePicker"
|
||||||
|
android:text="" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<FrameLayout style="@style/FormOuterBox">
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/notes" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/notesInput"
|
||||||
|
style="@style/FormInput"
|
||||||
|
android:inputType="textCapSentences|textMultiLine"
|
||||||
|
android:hint="@string/example_notes" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -0,0 +1,101 @@
|
|||||||
|
<!--
|
||||||
|
~ 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"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/noSubHabitsCard"
|
||||||
|
style="@style/Card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/no_sub_habits" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<org.isoron.uhabits.activities.habits.show.views.TargetCardView
|
||||||
|
android:id="@+id/targetCard"
|
||||||
|
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.BarCardView
|
||||||
|
android:id="@+id/barCard"
|
||||||
|
style="@style/Card"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
<org.isoron.uhabits.activities.habits.show.views.StreakCardView
|
||||||
|
android:id="@+id/streakCard"
|
||||||
|
style="@style/Card"/>
|
||||||
|
|
||||||
|
<org.isoron.uhabits.activities.habits.show.views.FrequencyCardView
|
||||||
|
android:id="@+id/frequencyCard"
|
||||||
|
style="@style/Card"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_delete"
|
||||||
|
android:title="@string/delete"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_edit_habit_group"
|
||||||
|
android:icon="?iconEdit"
|
||||||
|
android:title="@string/edit"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
|
</menu>
|
@ -0,0 +1,2 @@
|
|||||||
|
alter table Habits add column skip_days integer not null default 0
|
||||||
|
alter table Habits add column skip_days_list integer not null default 0
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.commands
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
|
||||||
|
data class AddToGroupCommand(
|
||||||
|
val habitList: HabitList,
|
||||||
|
val hgr: HabitGroup,
|
||||||
|
val selected: List<Habit>
|
||||||
|
) : Command {
|
||||||
|
override fun run() {
|
||||||
|
for (habit in selected) {
|
||||||
|
val entries = habit.originalEntries.getKnown()
|
||||||
|
val oldGroup = habit.group
|
||||||
|
(oldGroup?.habitList ?: habitList).remove(habit)
|
||||||
|
habit.groupId = hgr.id
|
||||||
|
habit.groupUUID = hgr.uuid
|
||||||
|
habit.group = hgr
|
||||||
|
hgr.habitList.add(habit)
|
||||||
|
entries.forEach { habit.originalEntries.add(it) }
|
||||||
|
habit.observable.notifyListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,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,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,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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.commands
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
|
||||||
|
data class RefreshParentGroupCommand(
|
||||||
|
val habit: Habit,
|
||||||
|
val habitGroupList: HabitGroupList
|
||||||
|
) : Command {
|
||||||
|
override fun run() {
|
||||||
|
if (!habit.isSubHabit()) return
|
||||||
|
val hgr = habit.group
|
||||||
|
hgr!!.recompute()
|
||||||
|
habitGroupList.resort()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.commands
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
|
||||||
|
data class RemoveFromGroupCommand(
|
||||||
|
val habitList: HabitList,
|
||||||
|
val selected: List<Habit>
|
||||||
|
) : Command {
|
||||||
|
override fun run() {
|
||||||
|
for (habit in selected) {
|
||||||
|
val hgr = habit.group!!
|
||||||
|
val entries = habit.originalEntries.getKnown()
|
||||||
|
hgr.habitList.remove(habit)
|
||||||
|
habit.groupId = null
|
||||||
|
habit.group = null
|
||||||
|
habit.groupUUID = null
|
||||||
|
habitList.add(habit)
|
||||||
|
entries.forEach { habit.originalEntries.add(it) }
|
||||||
|
habit.observable.notifyListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,148 @@
|
|||||||
|
package org.isoron.uhabits.core.models
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class HabitGroup(
|
||||||
|
var color: PaletteColor = PaletteColor(8),
|
||||||
|
var description: String = "",
|
||||||
|
var id: Long? = null,
|
||||||
|
var isArchived: Boolean = false,
|
||||||
|
var name: String = "",
|
||||||
|
var position: Int = 0,
|
||||||
|
var question: String = "",
|
||||||
|
var reminder: Reminder? = null,
|
||||||
|
var uuid: String? = null,
|
||||||
|
var habitList: HabitList,
|
||||||
|
val scores: ScoreList,
|
||||||
|
val streaks: StreakList
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
parent: HabitGroup,
|
||||||
|
matcher: HabitMatcher
|
||||||
|
) : this(
|
||||||
|
parent.color,
|
||||||
|
parent.description,
|
||||||
|
parent.id,
|
||||||
|
parent.isArchived,
|
||||||
|
parent.name,
|
||||||
|
parent.position,
|
||||||
|
parent.question,
|
||||||
|
parent.reminder,
|
||||||
|
parent.uuid,
|
||||||
|
parent.habitList.getFiltered(matcher),
|
||||||
|
parent.scores,
|
||||||
|
parent.streaks
|
||||||
|
) {
|
||||||
|
this.collapsed = parent.collapsed
|
||||||
|
this.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var observable = ModelObservable()
|
||||||
|
|
||||||
|
var parent: HabitGroup? = null
|
||||||
|
|
||||||
|
val uriString: String
|
||||||
|
get() = "content://org.isoron.uhabits/habitgroup/$id"
|
||||||
|
|
||||||
|
var collapsed = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
habitList.collapsed = value
|
||||||
|
if (parent != null) parent!!.collapsed = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasReminder(): Boolean = reminder != null
|
||||||
|
|
||||||
|
fun isCompletedToday(): Boolean {
|
||||||
|
if (habitList.isEmpty) return false
|
||||||
|
return habitList.all { it.isCompletedToday() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEnteredToday(): Boolean {
|
||||||
|
if (habitList.isEmpty) return false
|
||||||
|
return habitList.all { it.isEnteredToday() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstEntryDate(): Timestamp {
|
||||||
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
var earliest = today
|
||||||
|
for (h in habitList) {
|
||||||
|
val first = h.firstEntryDate()
|
||||||
|
if (earliest.isNewerThan(first)) earliest = first
|
||||||
|
}
|
||||||
|
return earliest
|
||||||
|
}
|
||||||
|
|
||||||
|
fun recompute() {
|
||||||
|
for (h in habitList) h.recompute()
|
||||||
|
|
||||||
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
val to = today.plus(30)
|
||||||
|
var from = firstEntryDate()
|
||||||
|
if (from.isNewerThan(to)) from = to
|
||||||
|
|
||||||
|
scores.combineFrom(
|
||||||
|
habitList = habitList,
|
||||||
|
from = from,
|
||||||
|
to = to
|
||||||
|
)
|
||||||
|
|
||||||
|
streaks.combineFrom(
|
||||||
|
habitList = habitList,
|
||||||
|
from = from,
|
||||||
|
to = to
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyFrom(other: HabitGroup) {
|
||||||
|
this.color = other.color
|
||||||
|
this.description = other.description
|
||||||
|
// this.id should not be copied
|
||||||
|
this.isArchived = other.isArchived
|
||||||
|
this.name = other.name
|
||||||
|
this.position = other.position
|
||||||
|
this.question = other.question
|
||||||
|
this.reminder = other.reminder
|
||||||
|
this.uuid = other.uuid
|
||||||
|
this.habitList.groupId = this.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is HabitGroup) return false
|
||||||
|
|
||||||
|
if (color != other.color) return false
|
||||||
|
if (description != other.description) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (isArchived != other.isArchived) return false
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (position != other.position) return false
|
||||||
|
if (question != other.question) return false
|
||||||
|
if (reminder != other.reminder) return false
|
||||||
|
if (uuid != other.uuid) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = color.hashCode()
|
||||||
|
result = 31 * result + description.hashCode()
|
||||||
|
result = 31 * result + (id?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + isArchived.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + position
|
||||||
|
result = 31 * result + question.hashCode()
|
||||||
|
result = 31 * result + (reminder?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (uuid?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHabitByUUID(uuid: String?): Habit? =
|
||||||
|
habitList.getByUUID(uuid)
|
||||||
|
}
|
@ -0,0 +1,221 @@
|
|||||||
|
package org.isoron.uhabits.core.models
|
||||||
|
|
||||||
|
import com.opencsv.CSVWriter
|
||||||
|
import org.isoron.uhabits.core.models.HabitList.Order
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.Writer
|
||||||
|
import java.util.LinkedList
|
||||||
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ordered collection of [HabitGroup]s.
|
||||||
|
*/
|
||||||
|
@ThreadSafe
|
||||||
|
abstract class HabitGroupList : Iterable<HabitGroup> {
|
||||||
|
val observable: ModelObservable
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val filter: HabitMatcher
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new HabitGroupList.
|
||||||
|
*
|
||||||
|
* Depending on the implementation, this list can either be empty or be
|
||||||
|
* populated by some pre-existing habitgroups, for example, from a certain
|
||||||
|
* database.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
observable = ModelObservable()
|
||||||
|
filter = HabitMatcher(isArchivedAllowed = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected constructor(filter: HabitMatcher) {
|
||||||
|
observable = ModelObservable()
|
||||||
|
this.filter = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new habit group in the list.
|
||||||
|
*
|
||||||
|
* 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 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.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
abstract fun add(habitGroup: HabitGroup)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the habit group with specified id.
|
||||||
|
*
|
||||||
|
* @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 group with specified UUID.
|
||||||
|
*
|
||||||
|
* @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 in any of the habit groups within this habit group list.
|
||||||
|
*/
|
||||||
|
fun getHabitByUUID(uuid: String?): Habit? {
|
||||||
|
for (hgr in this) {
|
||||||
|
val habit = hgr.getHabitByUUID(uuid)
|
||||||
|
if (habit != null) {
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the habit with the specified UUID which is
|
||||||
|
* present in any of the habit groups within this habit group list.
|
||||||
|
*/
|
||||||
|
fun getHabitByID(id: Long): Habit? {
|
||||||
|
for (hgr in this) {
|
||||||
|
val habit = hgr.habitList.getById(id)
|
||||||
|
if (habit != null) {
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the habit group that occupies a certain 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 habit groups that match a given condition.
|
||||||
|
*
|
||||||
|
* @param matcher the matcher that checks the condition
|
||||||
|
* @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 group in the list, or -1 if the list does
|
||||||
|
* not contain the habit group.
|
||||||
|
*
|
||||||
|
* @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 group from the list.
|
||||||
|
*
|
||||||
|
* If the given habit group is not in the list, does nothing.
|
||||||
|
*
|
||||||
|
* @param h the habit group to be removed.
|
||||||
|
*/
|
||||||
|
abstract fun remove(h: HabitGroup)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all the habit groups from the list.
|
||||||
|
*/
|
||||||
|
open fun removeAll() {
|
||||||
|
val copy: MutableList<HabitGroup> = LinkedList()
|
||||||
|
for (h in this) copy.add(h)
|
||||||
|
for (h in copy) remove(h)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the position of a habit group in the list.
|
||||||
|
*
|
||||||
|
* @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 habit groups in this list.
|
||||||
|
*
|
||||||
|
* @return number of habit groups
|
||||||
|
*/
|
||||||
|
abstract fun size(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 habit groups get persisted,
|
||||||
|
* this operation must be called.
|
||||||
|
*
|
||||||
|
* @param habitGroups the list of habit groups that have been modified.
|
||||||
|
*/
|
||||||
|
abstract fun update(habitGroups: List<HabitGroup>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the list that a certain habit group has been modified.
|
||||||
|
*
|
||||||
|
* See [.update] for more details.
|
||||||
|
*
|
||||||
|
* @param habitGroup the habit groups that has been modified.
|
||||||
|
*/
|
||||||
|
fun update(habitGroup: HabitGroup) {
|
||||||
|
update(listOf(habitGroup))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each habit group, point all the habits in it
|
||||||
|
* to the group it is contained in
|
||||||
|
* */
|
||||||
|
abstract fun attachHabitsToGroups()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun writeCSV(out: Writer) {
|
||||||
|
val header = arrayOf(
|
||||||
|
"Position",
|
||||||
|
"Name",
|
||||||
|
"Question",
|
||||||
|
"Description",
|
||||||
|
"Color"
|
||||||
|
)
|
||||||
|
val csv = CSVWriter(out)
|
||||||
|
csv.writeNext(header, false)
|
||||||
|
for (hgr in this) {
|
||||||
|
val cols = arrayOf(
|
||||||
|
String.format("%03d", indexOf(hgr) + 1),
|
||||||
|
hgr.name,
|
||||||
|
hgr.question,
|
||||||
|
hgr.description,
|
||||||
|
hgr.color.toCsvColor()
|
||||||
|
)
|
||||||
|
csv.writeNext(cols, false)
|
||||||
|
}
|
||||||
|
csv.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun resort()
|
||||||
|
}
|
@ -0,0 +1,225 @@
|
|||||||
|
package org.isoron.uhabits.core.models.memory
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.HabitList.Order
|
||||||
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory implementation of [HabitGroupList].
|
||||||
|
*/
|
||||||
|
class MemoryHabitGroupList : HabitGroupList {
|
||||||
|
private val list = LinkedList<HabitGroup>()
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
override var primaryOrder = Order.BY_POSITION
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
override var secondaryOrder = Order.BY_NAME_ASC
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var comparator: Comparator<HabitGroup>? =
|
||||||
|
getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
private var parent: MemoryHabitGroupList? = null
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
constructor(
|
||||||
|
matcher: HabitMatcher,
|
||||||
|
comparator: Comparator<HabitGroup>?,
|
||||||
|
parent: MemoryHabitGroupList
|
||||||
|
) : super(matcher) {
|
||||||
|
this.parent = parent
|
||||||
|
this.comparator = comparator
|
||||||
|
primaryOrder = parent.primaryOrder
|
||||||
|
secondaryOrder = parent.secondaryOrder
|
||||||
|
parent.observable.addListener { loadFromParent() }
|
||||||
|
loadFromParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
override fun add(habitGroup: HabitGroup) {
|
||||||
|
throwIfHasParent()
|
||||||
|
require(!list.contains(habitGroup)) { "habit already added" }
|
||||||
|
val id = habitGroup.id
|
||||||
|
if (id != null && getById(id) != null) throw RuntimeException("duplicate id")
|
||||||
|
if (id == null) habitGroup.id = list.size.toLong()
|
||||||
|
list.addLast(habitGroup)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getById(id: Long): HabitGroup? {
|
||||||
|
for (h in list) {
|
||||||
|
checkNotNull(h.id)
|
||||||
|
if (h.id == id) return h
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByUUID(uuid: String?): HabitGroup? {
|
||||||
|
for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByPosition(position: Int): HabitGroup {
|
||||||
|
return list[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getFiltered(matcher: HabitMatcher?): HabitGroupList {
|
||||||
|
return MemoryHabitGroupList(matcher!!, comparator, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComposedComparatorByOrder(
|
||||||
|
firstOrder: Order,
|
||||||
|
secondOrder: Order?
|
||||||
|
): Comparator<HabitGroup> {
|
||||||
|
return Comparator { h1: HabitGroup, h2: HabitGroup ->
|
||||||
|
val firstResult = getComparatorByOrder(firstOrder).compare(h1, h2)
|
||||||
|
if (firstResult != 0 || secondOrder == null) {
|
||||||
|
return@Comparator firstResult
|
||||||
|
}
|
||||||
|
getComparatorByOrder(secondOrder).compare(h1, h2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComparatorByOrder(order: Order): Comparator<HabitGroup> {
|
||||||
|
val nameComparatorAsc = Comparator<HabitGroup> { habit1, habit2 ->
|
||||||
|
habit1.name.compareTo(habit2.name)
|
||||||
|
}
|
||||||
|
val nameComparatorDesc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> nameComparatorAsc.compare(h2, h1) }
|
||||||
|
val colorComparatorAsc = Comparator<HabitGroup> { (color1), (color2) ->
|
||||||
|
color1.compareTo(color2)
|
||||||
|
}
|
||||||
|
val colorComparatorDesc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> colorComparatorAsc.compare(h2, h1) }
|
||||||
|
val scoreComparatorDesc =
|
||||||
|
Comparator<HabitGroup> { habit1, habit2 ->
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
habit1.scores[today].value.compareTo(habit2.scores[today].value)
|
||||||
|
}
|
||||||
|
val scoreComparatorAsc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> scoreComparatorDesc.compare(h2, h1) }
|
||||||
|
val positionComparator =
|
||||||
|
Comparator<HabitGroup> { habit1, habit2 -> habit1.position.compareTo(habit2.position) }
|
||||||
|
val statusComparatorDesc = Comparator { h1: HabitGroup, h2: HabitGroup ->
|
||||||
|
if (h1.isCompletedToday() != h2.isCompletedToday()) {
|
||||||
|
return@Comparator if (h1.isCompletedToday()) -1 else 1
|
||||||
|
}
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
val v1 = h1.scores[today].value
|
||||||
|
val v2 = h2.scores[today].value
|
||||||
|
v2.compareTo(v1)
|
||||||
|
}
|
||||||
|
val statusComparatorAsc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> statusComparatorDesc.compare(h2, h1) }
|
||||||
|
return when {
|
||||||
|
order === Order.BY_POSITION -> positionComparator
|
||||||
|
order === Order.BY_NAME_ASC -> nameComparatorAsc
|
||||||
|
order === Order.BY_NAME_DESC -> nameComparatorDesc
|
||||||
|
order === Order.BY_COLOR_ASC -> colorComparatorAsc
|
||||||
|
order === Order.BY_COLOR_DESC -> colorComparatorDesc
|
||||||
|
order === Order.BY_SCORE_DESC -> scoreComparatorDesc
|
||||||
|
order === Order.BY_SCORE_ASC -> scoreComparatorAsc
|
||||||
|
order === Order.BY_STATUS_DESC -> statusComparatorDesc
|
||||||
|
order === Order.BY_STATUS_ASC -> statusComparatorAsc
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun indexOf(h: HabitGroup): Int {
|
||||||
|
return list.indexOf(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun iterator(): Iterator<HabitGroup> {
|
||||||
|
return ArrayList(list).iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(h: HabitGroup) {
|
||||||
|
throwIfHasParent()
|
||||||
|
list.remove(h)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reorder(from: HabitGroup, to: HabitGroup) {
|
||||||
|
throwIfHasParent()
|
||||||
|
check(!(primaryOrder !== Order.BY_POSITION)) { "cannot reorder automatically sorted list" }
|
||||||
|
require(indexOf(from) >= 0) { "list does not contain (from) habit" }
|
||||||
|
val toPos = indexOf(to)
|
||||||
|
require(toPos >= 0) { "list does not contain (to) habit" }
|
||||||
|
list.remove(from)
|
||||||
|
list.add(toPos, from)
|
||||||
|
var position = 0
|
||||||
|
for (h in list) h.position = position++
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
return list.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(habitGroups: List<HabitGroup>) {
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachHabitsToGroups() {
|
||||||
|
for (hgr in list) {
|
||||||
|
for (h in hgr.habitList) {
|
||||||
|
h.group = hgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwIfHasParent() {
|
||||||
|
check(parent == null) {
|
||||||
|
"Filtered lists cannot be modified directly. " +
|
||||||
|
"You should modify the parent list instead."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun loadFromParent() {
|
||||||
|
checkNotNull(parent)
|
||||||
|
list.clear()
|
||||||
|
for (hgr in parent!!) {
|
||||||
|
if (filter.matches(hgr)) {
|
||||||
|
val filteredHgr = HabitGroup(hgr, filter)
|
||||||
|
list.add(filteredHgr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primaryOrder = parent!!.primaryOrder
|
||||||
|
secondaryOrder = parent!!.secondaryOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun resort() {
|
||||||
|
for (hgr in list) {
|
||||||
|
hgr.habitList.primaryOrder = primaryOrder
|
||||||
|
hgr.habitList.secondaryOrder = secondaryOrder
|
||||||
|
}
|
||||||
|
if (comparator != null) list.sortWith(comparator!!)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
package org.isoron.uhabits.core.models.sqlite
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.database.Repository
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.HabitList.Order
|
||||||
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.memory.MemoryHabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.sqlite.records.HabitGroupRecord
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a [HabitGroupList] that is backed by SQLite.
|
||||||
|
*/
|
||||||
|
class SQLiteHabitGroupList @Inject constructor(private val modelFactory: ModelFactory) : HabitGroupList() {
|
||||||
|
private val repository: Repository<HabitGroupRecord> = modelFactory.buildHabitGroupListRepository()
|
||||||
|
private val list: MemoryHabitGroupList = MemoryHabitGroupList()
|
||||||
|
private var loaded = false
|
||||||
|
private fun loadRecords() {
|
||||||
|
if (loaded) return
|
||||||
|
loaded = true
|
||||||
|
list.removeAll()
|
||||||
|
val records = repository.findAll("order by position")
|
||||||
|
var shouldRebuildOrder = false
|
||||||
|
for ((expectedPosition, rec) in records.withIndex()) {
|
||||||
|
if (rec.position != expectedPosition) shouldRebuildOrder = true
|
||||||
|
val h = modelFactory.buildHabitGroup()
|
||||||
|
rec.copyTo(h)
|
||||||
|
list.add(h)
|
||||||
|
}
|
||||||
|
if (shouldRebuildOrder) rebuildOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun add(habitGroup: HabitGroup) {
|
||||||
|
loadRecords()
|
||||||
|
habitGroup.position = size()
|
||||||
|
habitGroup.id = repository.getNextAvailableId("habitandgroup")
|
||||||
|
val record = HabitGroupRecord()
|
||||||
|
record.copyFrom(habitGroup)
|
||||||
|
repository.save(record)
|
||||||
|
habitGroup.habitList.groupId = record.id
|
||||||
|
list.add(habitGroup)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getById(id: Long): HabitGroup? {
|
||||||
|
loadRecords()
|
||||||
|
return list.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByUUID(uuid: String?): HabitGroup? {
|
||||||
|
loadRecords()
|
||||||
|
return list.getByUUID(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByPosition(position: Int): HabitGroup {
|
||||||
|
loadRecords()
|
||||||
|
return list.getByPosition(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getFiltered(matcher: HabitMatcher?): HabitGroupList {
|
||||||
|
loadRecords()
|
||||||
|
return list.getFiltered(matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@set:Synchronized
|
||||||
|
override var primaryOrder: Order
|
||||||
|
get() = list.primaryOrder
|
||||||
|
set(order) {
|
||||||
|
list.primaryOrder = order
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@set:Synchronized
|
||||||
|
override var secondaryOrder: Order
|
||||||
|
get() = list.secondaryOrder
|
||||||
|
set(order) {
|
||||||
|
list.secondaryOrder = order
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun indexOf(h: HabitGroup): Int {
|
||||||
|
loadRecords()
|
||||||
|
return list.indexOf(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun iterator(): Iterator<HabitGroup> {
|
||||||
|
loadRecords()
|
||||||
|
return list.iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun rebuildOrder() {
|
||||||
|
val records = repository.findAll("order by position")
|
||||||
|
repository.executeAsTransaction {
|
||||||
|
for ((pos, r) in records.withIndex()) {
|
||||||
|
if (r.position != pos) {
|
||||||
|
r.position = pos
|
||||||
|
repository.save(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(h: HabitGroup) {
|
||||||
|
loadRecords()
|
||||||
|
list.remove(h)
|
||||||
|
val record = repository.find(
|
||||||
|
h.id!!
|
||||||
|
) ?: throw RuntimeException("habit not in database")
|
||||||
|
repository.executeAsTransaction {
|
||||||
|
repository.remove(record)
|
||||||
|
}
|
||||||
|
rebuildOrder()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun removeAll() {
|
||||||
|
list.removeAll()
|
||||||
|
repository.execSQL("delete from habits")
|
||||||
|
repository.execSQL("delete from repetitions")
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reorder(from: HabitGroup, to: HabitGroup) {
|
||||||
|
loadRecords()
|
||||||
|
list.reorder(from, to)
|
||||||
|
val fromRecord = repository.find(
|
||||||
|
from.id!!
|
||||||
|
)
|
||||||
|
val toRecord = repository.find(
|
||||||
|
to.id!!
|
||||||
|
)
|
||||||
|
if (fromRecord == null) throw RuntimeException("habit not in database")
|
||||||
|
if (toRecord == null) throw RuntimeException("habit not in database")
|
||||||
|
if (toRecord.position!! < fromRecord.position!!) {
|
||||||
|
repository.execSQL(
|
||||||
|
"update habitgroups set position = position + 1 " +
|
||||||
|
"where position >= ? and position < ?",
|
||||||
|
toRecord.position!!,
|
||||||
|
fromRecord.position!!
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
repository.execSQL(
|
||||||
|
"update habitgroups set position = position - 1 " +
|
||||||
|
"where position > ? and position <= ?",
|
||||||
|
fromRecord.position!!,
|
||||||
|
toRecord.position!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fromRecord.position = toRecord.position
|
||||||
|
repository.save(fromRecord)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun repair() {
|
||||||
|
loadRecords()
|
||||||
|
rebuildOrder()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
loadRecords()
|
||||||
|
return list.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(habitGroups: List<HabitGroup>) {
|
||||||
|
loadRecords()
|
||||||
|
list.update(habitGroups)
|
||||||
|
for (h in habitGroups) {
|
||||||
|
val record = repository.find(h.id!!) ?: continue
|
||||||
|
record.copyFrom(h)
|
||||||
|
repository.save(record)
|
||||||
|
}
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachHabitsToGroups() {
|
||||||
|
list.attachHabitsToGroups()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resort() {
|
||||||
|
list.resort()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun reload() {
|
||||||
|
loaded = false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package org.isoron.uhabits.core.models.sqlite.records
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.database.Column
|
||||||
|
import org.isoron.uhabits.core.database.Table
|
||||||
|
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 java.util.Objects.requireNonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SQLite database record corresponding to a [HabitGroup].
|
||||||
|
*/
|
||||||
|
@Table(name = "habitgroups")
|
||||||
|
class HabitGroupRecord {
|
||||||
|
@field:Column
|
||||||
|
var description: String? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var question: String? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var name: String? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var color: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var position: Int? = null
|
||||||
|
|
||||||
|
@field:Column(name = "reminder_hour")
|
||||||
|
var reminderHour: Int? = null
|
||||||
|
|
||||||
|
@field:Column(name = "reminder_min")
|
||||||
|
var reminderMin: Int? = null
|
||||||
|
|
||||||
|
@field:Column(name = "reminder_days")
|
||||||
|
var reminderDays: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var highlight: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var archived: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var uuid: String? = null
|
||||||
|
|
||||||
|
fun copyFrom(model: HabitGroup) {
|
||||||
|
id = model.id
|
||||||
|
name = model.name
|
||||||
|
description = model.description
|
||||||
|
highlight = 0
|
||||||
|
color = model.color.paletteIndex
|
||||||
|
archived = if (model.isArchived) 1 else 0
|
||||||
|
position = model.position
|
||||||
|
question = model.question
|
||||||
|
uuid = model.uuid
|
||||||
|
reminderDays = 0
|
||||||
|
reminderMin = null
|
||||||
|
reminderHour = null
|
||||||
|
if (model.hasReminder()) {
|
||||||
|
val reminder = model.reminder
|
||||||
|
reminderHour = requireNonNull(reminder)!!.hour
|
||||||
|
reminderMin = reminder!!.minute
|
||||||
|
reminderDays = reminder.days.toInteger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyTo(habitGroup: HabitGroup) {
|
||||||
|
habitGroup.id = id
|
||||||
|
habitGroup.name = name!!
|
||||||
|
habitGroup.description = description!!
|
||||||
|
habitGroup.question = question!!
|
||||||
|
habitGroup.color = PaletteColor(color!!)
|
||||||
|
habitGroup.isArchived = archived != 0
|
||||||
|
habitGroup.position = position!!
|
||||||
|
habitGroup.uuid = uuid
|
||||||
|
habitGroup.habitList.groupId = id
|
||||||
|
if (reminderHour != null && reminderMin != null) {
|
||||||
|
habitGroup.reminder = Reminder(
|
||||||
|
reminderHour!!,
|
||||||
|
reminderMin!!,
|
||||||
|
WeekdayList(reminderDays!!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.test
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList
|
||||||
|
|
||||||
|
class HabitGroupFixtures(private val modelFactory: ModelFactory, private val habitList: HabitList, private val habitGroupList: HabitGroupList) {
|
||||||
|
private val habitFixtures = HabitFixtures(modelFactory, habitList)
|
||||||
|
|
||||||
|
fun createEmptyHabitGroup(
|
||||||
|
name: String = "Exercise",
|
||||||
|
color: PaletteColor = PaletteColor(3),
|
||||||
|
position: Int = 0,
|
||||||
|
id: Long = 0L
|
||||||
|
): HabitGroup {
|
||||||
|
val hgr = modelFactory.buildHabitGroup()
|
||||||
|
hgr.name = name
|
||||||
|
hgr.id = id
|
||||||
|
hgr.question = "Did you exercise today?"
|
||||||
|
hgr.color = color
|
||||||
|
hgr.position = position
|
||||||
|
saveIfSQLite(hgr)
|
||||||
|
return hgr
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroupWithEmptyHabits(
|
||||||
|
name: String = "Exercise",
|
||||||
|
color: PaletteColor = PaletteColor(3),
|
||||||
|
position: Int = 0,
|
||||||
|
numHabits: Int = 1,
|
||||||
|
id: Long = 0L
|
||||||
|
): HabitGroup {
|
||||||
|
val hgr = createEmptyHabitGroup(name, color, position, id)
|
||||||
|
for (i in 1..numHabits) {
|
||||||
|
val h = habitFixtures.createEmptyHabit()
|
||||||
|
h.id = id + i
|
||||||
|
addGroupId(h, hgr)
|
||||||
|
hgr.habitList.add(h)
|
||||||
|
}
|
||||||
|
saveIfSQLite(hgr)
|
||||||
|
return hgr
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroupWithEmptyNumericalHabits(
|
||||||
|
name: String = "Exercise",
|
||||||
|
color: PaletteColor = PaletteColor(3),
|
||||||
|
position: Int = 0,
|
||||||
|
targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
|
||||||
|
numHabits: Int = 1,
|
||||||
|
id: Long = 0L
|
||||||
|
): HabitGroup {
|
||||||
|
val hgr = createEmptyHabitGroup(name, color, position, id)
|
||||||
|
for (i in 1..numHabits) {
|
||||||
|
val h = habitFixtures.createEmptyNumericalHabit(targetType)
|
||||||
|
h.id = id + i
|
||||||
|
addGroupId(h, hgr)
|
||||||
|
hgr.habitList.add(h)
|
||||||
|
}
|
||||||
|
saveIfSQLite(hgr)
|
||||||
|
return hgr
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroupWithNumericalHabits(
|
||||||
|
name: String = "Exercise",
|
||||||
|
color: PaletteColor = PaletteColor(3),
|
||||||
|
position: Int = 0,
|
||||||
|
numHabits: Int = 1,
|
||||||
|
id: Long = 0L
|
||||||
|
): HabitGroup {
|
||||||
|
val hgr = createEmptyHabitGroup(name, color, position, id)
|
||||||
|
for (i in 1..numHabits) {
|
||||||
|
val h = habitFixtures.createNumericalHabit()
|
||||||
|
h.id = id + i
|
||||||
|
addGroupId(h, hgr)
|
||||||
|
hgr.habitList.add(h)
|
||||||
|
}
|
||||||
|
saveIfSQLite(hgr)
|
||||||
|
return hgr
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroupWithLongHabits(
|
||||||
|
name: String = "Exercise",
|
||||||
|
color: PaletteColor = PaletteColor(3),
|
||||||
|
position: Int = 0,
|
||||||
|
numHabits: Int = 1,
|
||||||
|
id: Long = 0L
|
||||||
|
): HabitGroup {
|
||||||
|
val hgr = createEmptyHabitGroup(name, color, position, id)
|
||||||
|
for (i in 1..numHabits) {
|
||||||
|
val h = habitFixtures.createLongHabit()
|
||||||
|
h.id = id + i
|
||||||
|
addGroupId(h, hgr)
|
||||||
|
hgr.habitList.add(h)
|
||||||
|
}
|
||||||
|
saveIfSQLite(hgr)
|
||||||
|
return hgr
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroupWithShortHabits(
|
||||||
|
name: String = "Exercise",
|
||||||
|
color: PaletteColor = PaletteColor(3),
|
||||||
|
position: Int = 0,
|
||||||
|
numHabits: Int = 1,
|
||||||
|
id: Long = 0L
|
||||||
|
): HabitGroup {
|
||||||
|
val hgr = createEmptyHabitGroup(name, color, position, id)
|
||||||
|
for (i in 1..numHabits) {
|
||||||
|
val h = habitFixtures.createShortHabit()
|
||||||
|
h.id = id + i
|
||||||
|
addGroupId(h, hgr)
|
||||||
|
hgr.habitList.add(h)
|
||||||
|
}
|
||||||
|
saveIfSQLite(hgr)
|
||||||
|
return hgr
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveIfSQLite(hgr: HabitGroup) {
|
||||||
|
if (hgr.habitList !is SQLiteHabitList) return
|
||||||
|
habitGroupList.add(hgr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addGroupId(h: Habit, hgr: HabitGroup) {
|
||||||
|
h.groupId = hgr.id
|
||||||
|
h.group = hgr
|
||||||
|
h.groupUUID = hgr.uuid
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package org.isoron.uhabits.core.ui.screens.habits.show
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCartPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardState
|
||||||
|
import org.isoron.uhabits.core.ui.views.Theme
|
||||||
|
|
||||||
|
data class ShowHabitGroupState(
|
||||||
|
val title: String = "",
|
||||||
|
val isEmpty: Boolean = false,
|
||||||
|
val isNumerical: Boolean = false,
|
||||||
|
val isBoolean: Boolean = false,
|
||||||
|
val color: PaletteColor = PaletteColor(1),
|
||||||
|
val subtitle: SubtitleCardState,
|
||||||
|
val overview: OverviewCardState,
|
||||||
|
val notes: NotesCardState,
|
||||||
|
val target: TargetCardState,
|
||||||
|
val streaks: StreakCardState,
|
||||||
|
val scores: ScoreCardState,
|
||||||
|
val frequency: FrequencyCardState,
|
||||||
|
val bar: BarCardState,
|
||||||
|
val theme: Theme
|
||||||
|
)
|
||||||
|
|
||||||
|
class ShowHabitGroupPresenter(
|
||||||
|
val habitGroup: HabitGroup,
|
||||||
|
val preferences: Preferences,
|
||||||
|
val screen: Screen,
|
||||||
|
val commandRunner: CommandRunner
|
||||||
|
) {
|
||||||
|
|
||||||
|
val barCardPresenter = BarCardPresenter(
|
||||||
|
preferences = preferences,
|
||||||
|
screen = screen
|
||||||
|
)
|
||||||
|
|
||||||
|
val scoreCardPresenter = ScoreCardPresenter(
|
||||||
|
preferences = preferences,
|
||||||
|
screen = screen
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildState(
|
||||||
|
habitGroup: HabitGroup,
|
||||||
|
preferences: Preferences,
|
||||||
|
theme: Theme
|
||||||
|
): ShowHabitGroupState {
|
||||||
|
return ShowHabitGroupState(
|
||||||
|
title = habitGroup.name,
|
||||||
|
isEmpty = habitGroup.habitList.isEmpty,
|
||||||
|
isNumerical = habitGroup.habitList.all { it.isNumerical },
|
||||||
|
isBoolean = habitGroup.habitList.all { !it.isNumerical },
|
||||||
|
color = habitGroup.color,
|
||||||
|
theme = theme,
|
||||||
|
subtitle = SubtitleCardPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
theme = theme
|
||||||
|
),
|
||||||
|
overview = OverviewCardPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
theme = theme
|
||||||
|
),
|
||||||
|
notes = NotesCardPresenter.buildState(
|
||||||
|
habitGroup = habitGroup
|
||||||
|
),
|
||||||
|
target = TargetCardPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
firstWeekday = preferences.firstWeekdayInt,
|
||||||
|
theme = theme
|
||||||
|
),
|
||||||
|
streaks = StreakCartPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
theme = theme
|
||||||
|
),
|
||||||
|
scores = ScoreCardPresenter.buildState(
|
||||||
|
spinnerPosition = preferences.scoreCardSpinnerPosition,
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
firstWeekday = preferences.firstWeekdayInt,
|
||||||
|
theme = theme
|
||||||
|
),
|
||||||
|
frequency = FrequencyCardPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
firstWeekday = preferences.firstWeekdayInt,
|
||||||
|
theme = theme
|
||||||
|
),
|
||||||
|
bar = BarCardPresenter.buildState(
|
||||||
|
habitGroup = habitGroup,
|
||||||
|
firstWeekday = preferences.firstWeekdayInt,
|
||||||
|
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
|
||||||
|
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
|
||||||
|
theme = theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Screen :
|
||||||
|
BarCardPresenter.Screen,
|
||||||
|
ScoreCardPresenter.Screen
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.isoron.uhabits.core.ui.screens.habits.show
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
|
import org.isoron.uhabits.core.commands.DeleteHabitGroupsCommand
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ShowHabitGroupMenuPresenter(
|
||||||
|
private val commandRunner: CommandRunner,
|
||||||
|
private val habitGroup: HabitGroup,
|
||||||
|
private val habitGroupList: HabitGroupList,
|
||||||
|
private val screen: Screen
|
||||||
|
) {
|
||||||
|
fun onEditHabitGroup() {
|
||||||
|
screen.showEditHabitGroupScreen(habitGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeleteHabitGroup() {
|
||||||
|
screen.showDeleteConfirmationScreen {
|
||||||
|
commandRunner.run(DeleteHabitGroupsCommand(habitGroupList, listOf(habitGroup)))
|
||||||
|
screen.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Message {
|
||||||
|
COULD_NOT_EXPORT
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Screen {
|
||||||
|
fun showEditHabitGroupScreen(habitGroup: HabitGroup)
|
||||||
|
fun showMessage(m: Message?)
|
||||||
|
fun showSendFileScreen(filename: String)
|
||||||
|
fun showDeleteConfirmationScreen(callback: OnConfirmedCallback)
|
||||||
|
fun close()
|
||||||
|
fun refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface System {
|
||||||
|
fun getCSVOutputDir(): File
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue