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