Can show habit group without interaction / scrolling

pull/2020/head
Dharanish 1 year ago
parent af3283e52f
commit 506086f003

@ -53,7 +53,7 @@ class HabitCardViewTest : BaseViewTest() {
.getByInterval(today.minus(300), today) .getByInterval(today.minus(300), today)
.map { it.value }.toIntArray() .map { it.value }.toIntArray()
view = component.getHabitCardViewFactory().create().apply { view = component.getHabitCardViewFactory().createHabitCard().apply {
habit = habit1 habit = habit1
values = entries values = entries
score = habit1.scores[today].value score = habit1.scores[today].value

@ -148,7 +148,7 @@ class ListHabitsRootView @Inject constructor(
private fun updateEmptyView() { private fun updateEmptyView() {
if (listAdapter.itemCount == 0) { if (listAdapter.itemCount == 0) {
if (listAdapter.hasNoHabit()) { if (listAdapter.hasNoHabit() && listAdapter.hasNoHabitGroup()) {
llEmpty.showEmpty() llEmpty.showEmpty()
} else { } else {
llEmpty.showDone() llEmpty.showDone()

@ -82,7 +82,7 @@ class ListHabitsSelectionMenu @Inject constructor(
itemArchive.isVisible = behavior.canArchive() itemArchive.isVisible = behavior.canArchive()
itemUnarchive.isVisible = behavior.canUnarchive() itemUnarchive.isVisible = behavior.canUnarchive()
itemNotify.isVisible = prefs.isDeveloper itemNotify.isVisible = prefs.isDeveloper
activeActionMode?.title = listAdapter.selected.size.toString() activeActionMode?.title = listAdapter.selectedHabits.size.toString()
return true return true
} }
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
@ -117,7 +117,7 @@ class ListHabitsSelectionMenu @Inject constructor(
} }
R.id.action_notify -> { R.id.action_notify -> {
for (h in listAdapter.selected) for (h in listAdapter.selectedHabits)
notificationTray.show(h, DateUtils.getToday(), 0) notificationTray.show(h, DateUtils.getToday(), 0)
return true return true
} }

@ -0,0 +1,72 @@
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.utils.getFontAwesome
import org.isoron.uhabits.utils.sp
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec
class AddButtonView(
context: Context
) : View(context),
View.OnClickListener {
var onEdit: () -> Unit = { }
private var drawer = Drawer()
init {
setOnClickListener(this)
}
override fun onClick(v: View) {
onEdit()
}
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.contrast100)
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)
}
}
}

@ -19,9 +19,10 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter
import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.models.ModelObservable import org.isoron.uhabits.core.models.ModelObservable
@ -32,6 +33,7 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBeh
import org.isoron.uhabits.core.utils.MidnightTimer import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.inject.ActivityScope import org.isoron.uhabits.inject.ActivityScope
import java.util.LinkedList import java.util.LinkedList
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -46,14 +48,16 @@ class HabitCardListAdapter @Inject constructor(
private val cache: HabitCardListCache, private val cache: HabitCardListCache,
private val preferences: Preferences, private val preferences: Preferences,
private val midnightTimer: MidnightTimer private val midnightTimer: MidnightTimer
) : RecyclerView.Adapter<HabitCardViewHolder?>(), ) : Adapter<HabitCardViewHolder?>(),
HabitCardListCache.Listener, HabitCardListCache.Listener,
MidnightTimer.MidnightListener, MidnightTimer.MidnightListener,
ListHabitsMenuBehavior.Adapter, ListHabitsMenuBehavior.Adapter,
ListHabitsSelectionMenuBehavior.Adapter { ListHabitsSelectionMenuBehavior.Adapter {
val observable: ModelObservable = ModelObservable() val observable: ModelObservable = ModelObservable()
private var listView: HabitCardListView? = null private var listView: HabitCardListView? = null
val selected: LinkedList<Habit> = LinkedList() val selectedHabits: LinkedList<Habit> = LinkedList()
val selectedHabitGroups: LinkedList<HabitGroup> = LinkedList()
override fun atMidnight() { override fun atMidnight() {
cache.refreshAllHabits() cache.refreshAllHabits()
} }
@ -66,17 +70,25 @@ class HabitCardListAdapter @Inject constructor(
return cache.hasNoHabit() return cache.hasNoHabit()
} }
fun hasNoHabitGroup(): Boolean {
return cache.hasNoHabitGroup()
}
/** /**
* Sets all items as not selected. * Sets all items as not selected.
*/ */
override fun clearSelection() { override fun clearSelection() {
selected.clear() selectedHabits.clear()
notifyDataSetChanged() notifyDataSetChanged()
observable.notifyListeners() observable.notifyListeners()
} }
override fun getSelected(): List<Habit> { override fun getSelected(): List<Habit> {
return ArrayList(selected) return ArrayList(selectedHabits)
}
override fun getSelectedHabitGroups(): List<HabitGroup> {
return ArrayList(selectedHabitGroups)
} }
/** /**
@ -91,11 +103,38 @@ class HabitCardListAdapter @Inject constructor(
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return cache.habitCount return cache.itemCount
} }
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return getItem(position)!!.id!! val uuidString = getItemUUID(position)
return if (uuidString != null) {
val formattedUUIDString = formatUUID(uuidString)
val uuid = UUID.fromString(formattedUUIDString)
uuid.mostSignificantBits and Long.MAX_VALUE
} else {
-1
}
}
fun getItemUUID(position: Int): String? {
val h = cache.getHabitByPosition(position)
val hgr = cache.getHabitGroupByPosition(position)
return if (h != null) {
h.uuid!!
} else if (hgr != null) {
hgr.uuid!!
} else {
null
}
}
private fun formatUUID(uuidString: String): String {
return uuidString.substring(0, 8) + "-" +
uuidString.substring(8, 12) + "-" +
uuidString.substring(12, 16) + "-" +
uuidString.substring(16, 20) + "-" +
uuidString.substring(20, 32)
} }
/** /**
@ -104,7 +143,7 @@ class HabitCardListAdapter @Inject constructor(
* @return true if selection is empty, false otherwise * @return true if selection is empty, false otherwise
*/ */
val isSelectionEmpty: Boolean val isSelectionEmpty: Boolean
get() = selected.isEmpty() get() = selectedHabits.isEmpty() && selectedHabitGroups.isEmpty()
val isSortable: Boolean val isSortable: Boolean
get() = cache.primaryOrder == HabitList.Order.BY_POSITION get() = cache.primaryOrder == HabitList.Order.BY_POSITION
@ -122,11 +161,18 @@ class HabitCardListAdapter @Inject constructor(
) { ) {
if (listView == null) return if (listView == null) return
val habit = cache.getHabitByPosition(position) val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!) if (habit != null) {
val checkmarks = cache.getCheckmarks(habit.id!!) val score = cache.getScore(habit.uuid!!)
val notes = cache.getNotes(habit.id!!) val checkmarks = cache.getCheckmarks(habit.uuid!!)
val selected = selected.contains(habit) val notes = cache.getNotes(habit.uuid!!)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected) val selected = selectedHabits.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, notes, selected)
} else {
val habitGroup = cache.getHabitGroupByPosition(position)
val score = cache.getScore(habitGroup!!.uuid!!)
val selected = selectedHabitGroups.contains(habitGroup)
listView!!.bindGroupCardView(holder, habitGroup, score, selected)
}
} }
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
@ -141,8 +187,22 @@ class HabitCardListAdapter @Inject constructor(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): HabitCardViewHolder { ): HabitCardViewHolder {
val view = listView!!.createHabitCardView() if (viewType == 0) {
return HabitCardViewHolder(view) val view = listView!!.createHabitCardView()
return HabitCardViewHolder(view, null)
} else {
val view = listView!!.createHabitGroupCardView()
return HabitCardViewHolder(null, view)
}
}
// function to override getItemViewType and return the type of the view. The view can either be a HabitCardView or a HabitGroupCardView
override fun getItemViewType(position: Int): Int {
return if (position < cache.habitCount) {
0
} else {
1
}
} }
/** /**
@ -190,7 +250,11 @@ class HabitCardListAdapter @Inject constructor(
* @param selected list of habits to be removed * @param selected list of habits to be removed
*/ */
override fun performRemove(selected: List<Habit>) { override fun performRemove(selected: List<Habit>) {
for (habit in selected) cache.remove(habit.id!!) for (habit in selected) cache.remove(habit.uuid!!)
}
override fun performRemoveHabitGroup(selected: List<HabitGroup>) {
for (hgr in selected) cache.remove(hgr.uuid!!)
} }
/** /**
@ -250,10 +314,17 @@ class HabitCardListAdapter @Inject constructor(
* @param position position of the item to be toggled * @param position position of the item to be toggled
*/ */
fun toggleSelection(position: Int) { fun toggleSelection(position: Int) {
val h = getItem(position) ?: return val h = cache.getHabitByPosition(position)
val k = selected.indexOf(h) val hgr = cache.getHabitGroupByPosition(position)
if (k < 0) selected.add(h) else selected.remove(h) if (h != null) {
notifyDataSetChanged() val k = selectedHabits.indexOf(h)
if (k < 0) selectedHabits.add(h) else selectedHabits.remove(h)
notifyDataSetChanged()
} else if (hgr != null) {
val k = selectedHabitGroups.indexOf(hgr)
if (k < 0) selectedHabitGroups.add(hgr) else selectedHabitGroups.remove(hgr)
notifyDataSetChanged()
}
} }
init { init {

@ -36,6 +36,7 @@ import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.BundleSavedState import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject import javax.inject.Inject
@ -79,7 +80,11 @@ class HabitCardListView(
} }
fun createHabitCardView(): HabitCardView { fun createHabitCardView(): HabitCardView {
return cardViewFactory.create() return cardViewFactory.createHabitCard()
}
fun createHabitGroupCardView(): HabitGroupCardView {
return cardViewFactory.createHabitGroupCard()
} }
fun bindCardView( fun bindCardView(
@ -110,8 +115,28 @@ class HabitCardListView(
return cardView return cardView
} }
fun bindGroupCardView(
holder: HabitCardViewHolder,
habitGroup: HabitGroup,
score: Double,
selected: Boolean
): View {
val cardView = holder.itemView as HabitGroupCardView
cardView.habitGroup = habitGroup
cardView.isSelected = selected
cardView.score = score
val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev ->
detector.onTouchEvent(ev)
true
}
return cardView
}
fun attachCardView(holder: HabitCardViewHolder) { fun attachCardView(holder: HabitCardViewHolder) {
(holder.itemView as HabitCardView).dataOffset = dataOffset (holder.itemView as? HabitCardView)?.dataOffset = dataOffset
attachedHolders.add(holder) attachedHolders.add(holder)
} }

@ -38,6 +38,7 @@ import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.models.ModelObservable import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ -55,7 +56,8 @@ class HabitCardViewFactory
private val numberPanelFactory: NumberPanelViewFactory, private val numberPanelFactory: NumberPanelViewFactory,
private val behavior: ListHabitsBehavior private val behavior: ListHabitsBehavior
) { ) {
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior) fun createHabitCard() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
fun createHabitGroupCard() = HabitGroupCardView(context, behavior)
} }
class HabitCardView( class HabitCardView(
@ -285,6 +287,32 @@ class HabitCardView(
} }
} }
private fun copyAttributesFrom(hgr: HabitGroup) {
fun getActiveColor(habitGroup: HabitGroup): Int {
return when (habitGroup.isArchived) {
true -> sres.getColor(R.attr.contrast60)
false -> currentTheme().color(habitGroup.color).toInt()
}
}
val c = getActiveColor(hgr)
label.apply {
text = hgr.name
setTextColor(c)
}
scoreRing.apply {
setColor(c)
}
checkmarkPanel.apply {
color = c
visibility = View.GONE
}
numberPanel.apply {
color = c
visibility = View.GONE
}
}
private fun triggerRipple(x: Float, y: Float) { private fun triggerRipple(x: Float, y: Float) {
val background = innerFrame.background val background = innerFrame.background
background.setHotspot(x, y) background.setHotspot(x, y)

@ -21,4 +21,4 @@ package org.isoron.uhabits.activities.habits.list.views
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class HabitCardViewHolder(itemView: HabitCardView) : RecyclerView.ViewHolder(itemView) class HabitCardViewHolder(itemView1: HabitCardView?, itemView2: HabitGroupCardView?) : RecyclerView.ViewHolder(itemView1 ?: itemView2!!)

@ -23,15 +23,6 @@ import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dp import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import javax.inject.Inject
class HabitGroupCardViewFactory
@Inject constructor(
@ActivityContext val context: Context,
private val behavior: ListHabitsBehavior
) {
fun create() = HabitGroupCardView(context, behavior)
}
class HabitGroupCardView( class HabitGroupCardView(
@ActivityContext context: Context, @ActivityContext context: Context,
@ -56,6 +47,7 @@ class HabitGroupCardView(
scoreRing.setPrecision(1.0f / 16) scoreRing.setPrecision(1.0f / 16)
} }
var addButtonView: AddButtonView
private var innerFrame: LinearLayout private var innerFrame: LinearLayout
private var label: TextView private var label: TextView
private var scoreRing: RingView private var scoreRing: RingView
@ -83,6 +75,8 @@ class HabitGroupCardView(
} }
} }
addButtonView = AddButtonView(context)
innerFrame = LinearLayout(context).apply { innerFrame = LinearLayout(context).apply {
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
@ -91,6 +85,7 @@ class HabitGroupCardView(
addView(scoreRing) addView(scoreRing)
addView(label) addView(label)
addView(addButtonView)
setOnTouchListener { v, event -> setOnTouchListener { v, event ->
v.background.setHotspot(event.x, event.y) v.background.setHotspot(event.x, event.y)

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

@ -24,6 +24,7 @@
<string translatable="false" name="fa_arrow_circle_down">&#xf0ab;</string> <string translatable="false" name="fa_arrow_circle_down">&#xf0ab;</string>
<string translatable="false" name="fa_check">&#xf00c;</string> <string translatable="false" name="fa_check">&#xf00c;</string>
<string translatable="false" name="fa_times">&#xf00d;</string> <string translatable="false" name="fa_times">&#xf00d;</string>
<string translatable="false" name="fa_plus">&#xf067;</string>
<string translatable="false" name="fa_skipped">&#xf068;</string> <string translatable="false" name="fa_skipped">&#xf068;</string>
<string translatable="false" name="fa_bell_o">&#xf0f3;</string> <string translatable="false" name="fa_bell_o">&#xf0f3;</string>
<string translatable="false" name="fa_calendar">&#xf073;</string> <string translatable="false" name="fa_calendar">&#xf073;</string>

@ -146,7 +146,7 @@ class ScoreList {
var current = to var current = to
while (current >= from) { while (current >= from) {
val habitScores = habitList.map { it.scores[current].value } val habitScores = habitList.map { it.scores[current].value }
val averageScore = habitScores.average() val averageScore = if (habitScores.isNotEmpty()) habitScores.average() else 0.0
map[current] = Score(current, averageScore) map[current] = Score(current, averageScore)
current = current.minus(1) current = current.minus(1)
} }

@ -25,15 +25,15 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.io.Logging import org.isoron.uhabits.core.io.Logging
import org.isoron.uhabits.core.models.Habit 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.HabitList
import org.isoron.uhabits.core.models.HabitList.Order import org.isoron.uhabits.core.models.HabitList.Order
import org.isoron.uhabits.core.models.HabitMatcher import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.tasks.Task import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import java.util.ArrayList
import java.util.Arrays import java.util.Arrays
import java.util.HashMap
import java.util.LinkedList import java.util.LinkedList
import java.util.TreeSet import java.util.TreeSet
import javax.inject.Inject import javax.inject.Inject
@ -54,6 +54,7 @@ import javax.inject.Inject
@AppScope @AppScope
class HabitCardListCache @Inject constructor( class HabitCardListCache @Inject constructor(
private val allHabits: HabitList, private val allHabits: HabitList,
private val allHabitGroups: HabitGroupList,
private val commandRunner: CommandRunner, private val commandRunner: CommandRunner,
taskRunner: TaskRunner, taskRunner: TaskRunner,
logging: Logging logging: Logging
@ -66,6 +67,7 @@ class HabitCardListCache @Inject constructor(
private var listener: Listener private var listener: Listener
private val data: CacheData private val data: CacheData
private var filteredHabits: HabitList private var filteredHabits: HabitList
private var filteredHabitGroups: HabitGroupList
private val taskRunner: TaskRunner private val taskRunner: TaskRunner
@Synchronized @Synchronized
@ -74,13 +76,13 @@ class HabitCardListCache @Inject constructor(
} }
@Synchronized @Synchronized
fun getCheckmarks(habitId: Long): IntArray { fun getCheckmarks(habitUUID: String): IntArray {
return data.checkmarks[habitId]!! return data.checkmarks[habitUUID]!!
} }
@Synchronized @Synchronized
fun getNotes(habitId: Long): Array<String> { fun getNotes(habitUUID: String): Array<String> {
return data.notes[habitId]!! return data.notes[habitUUID]!!
} }
@Synchronized @Synchronized
@ -88,21 +90,53 @@ class HabitCardListCache @Inject constructor(
return allHabits.isEmpty return allHabits.isEmpty
} }
@Synchronized
fun hasNoHabitGroup(): Boolean {
return allHabitGroups.isEmpty
}
/** /**
* Returns the habits that occupies a certain position on the list. * Returns the habits that occupies a certain position on the list.
* *
* @param position the position of the habit * @param position the position of the list of habits and groups
* @return the habit at given position or null if position is invalid * @return the habit at given position or null if position is invalid
*/ */
@Synchronized @Synchronized
fun getHabitByPosition(position: Int): Habit? { fun getHabitByPosition(position: Int): Habit? {
return if (position < 0 || position >= data.habits.size) null else data.habits[position] return if (position < 0 || position >= data.habits.size) {
null
} else {
data.habits[position]
}
}
/**
* Returns the habit groups that occupies a certain position on the list.
*
* @param position the position of the list of habits and groups
* @return the habit group at given position or null if position is invalid
*/
@Synchronized
fun getHabitGroupByPosition(position: Int): HabitGroup? {
return if (position < data.habits.size || position >= data.habits.size + data.habitGroups.size) {
null
} else {
data.habitGroups[position - data.habits.size]
}
} }
@get:Synchronized
val itemCount: Int
get() = habitCount + habitGroupCount
@get:Synchronized @get:Synchronized
val habitCount: Int val habitCount: Int
get() = data.habits.size get() = data.habits.size
@get:Synchronized
val habitGroupCount: Int
get() = data.habitGroups.size
@get:Synchronized @get:Synchronized
@set:Synchronized @set:Synchronized
var primaryOrder: Order var primaryOrder: Order
@ -110,6 +144,8 @@ class HabitCardListCache @Inject constructor(
set(order) { set(order) {
allHabits.primaryOrder = order allHabits.primaryOrder = order
filteredHabits.primaryOrder = order filteredHabits.primaryOrder = order
allHabitGroups.primaryOrder = order
filteredHabitGroups.primaryOrder = order
refreshAllHabits() refreshAllHabits()
} }
@ -120,12 +156,14 @@ class HabitCardListCache @Inject constructor(
set(order) { set(order) {
allHabits.secondaryOrder = order allHabits.secondaryOrder = order
filteredHabits.secondaryOrder = order filteredHabits.secondaryOrder = order
allHabitGroups.secondaryOrder = order
filteredHabitGroups.secondaryOrder = order
refreshAllHabits() refreshAllHabits()
} }
@Synchronized @Synchronized
fun getScore(habitId: Long): Double { fun getScore(habitUUID: String): Double {
return data.scores[habitId]!! return data.scores[habitUUID]!!
} }
@Synchronized @Synchronized
@ -137,7 +175,7 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
override fun onCommandFinished(command: Command) { override fun onCommandFinished(command: Command) {
if (command is CreateRepetitionCommand) { if (command is CreateRepetitionCommand) {
command.habit.id?.let { refreshHabit(it) } command.habit.uuid?.let { refreshHabit(it) }
} else { } else {
refreshAllHabits() refreshAllHabits()
} }
@ -157,27 +195,47 @@ class HabitCardListCache @Inject constructor(
} }
@Synchronized @Synchronized
fun refreshHabit(id: Long) { fun refreshHabit(uuid: String) {
taskRunner.execute(RefreshTask(id)) taskRunner.execute(RefreshTask(uuid))
} }
@Synchronized @Synchronized
fun remove(id: Long) { fun remove(uuid: String) {
val h = data.idToHabit[id] ?: return val h = data.uuidToHabit[uuid]
val position = data.habits.indexOf(h) if (h != null) {
data.habits.removeAt(position) val position = data.habits.indexOf(h)
data.idToHabit.remove(id) data.habits.removeAt(position)
data.checkmarks.remove(id) data.uuidToHabit.remove(uuid)
data.notes.remove(id) data.checkmarks.remove(uuid)
data.scores.remove(id) data.notes.remove(uuid)
listener.onItemRemoved(position) data.scores.remove(uuid)
listener.onItemRemoved(position)
} else {
val hgr = data.uuidToHabitGroup[uuid]
if (hgr != null) {
val position = data.habitGroups.indexOf(hgr)
data.habitGroups.removeAt(position)
data.uuidToHabitGroup.remove(uuid)
listener.onItemRemoved(position + data.habits.size)
}
}
} }
@Synchronized @Synchronized
fun reorder(from: Int, to: Int) { fun reorder(from: Int, to: Int) {
val fromHabit = data.habits[from] if (data.habits.size in (from + 1)..to || data.habits.size in (to + 1)..from) {
data.habits.removeAt(from) logger.error("reorder: from and to are in different sections")
data.habits.add(to, fromHabit) return
}
if (from < data.habits.size) {
val fromHabit = data.habits[from]
data.habits.removeAt(from)
data.habits.add(to, fromHabit)
} else {
val fromHabitGroup = data.habitGroups[from]
data.habitGroups.removeAt(from - data.habits.size)
data.habitGroups.add(to - data.habits.size, fromHabitGroup)
}
listener.onItemMoved(from, to) listener.onItemMoved(from, to)
} }
@ -189,6 +247,7 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun setFilter(matcher: HabitMatcher) { fun setFilter(matcher: HabitMatcher) {
filteredHabits = allHabits.getFiltered(matcher) filteredHabits = allHabits.getFiltered(matcher)
filteredHabitGroups = allHabitGroups.getFiltered(matcher)
} }
@Synchronized @Synchronized
@ -209,21 +268,23 @@ class HabitCardListCache @Inject constructor(
} }
private inner class CacheData { private inner class CacheData {
val idToHabit: HashMap<Long?, Habit> = HashMap() val uuidToHabit: HashMap<String?, Habit> = HashMap()
val uuidToHabitGroup: HashMap<String?, HabitGroup> = HashMap()
val habits: MutableList<Habit> val habits: MutableList<Habit>
val checkmarks: HashMap<Long?, IntArray> val habitGroups: MutableList<HabitGroup>
val scores: HashMap<Long?, Double> val checkmarks: HashMap<String?, IntArray>
val notes: HashMap<Long?, Array<String>> val scores: HashMap<String?, Double>
val notes: HashMap<String?, Array<String>>
@Synchronized @Synchronized
fun copyCheckmarksFrom(oldData: CacheData) { fun copyCheckmarksFrom(oldData: CacheData) {
val empty = IntArray(checkmarkCount) val empty = IntArray(checkmarkCount)
for (id in idToHabit.keys) { for (uuid in uuidToHabit.keys) {
if (oldData.checkmarks.containsKey(id)) { if (oldData.checkmarks.containsKey(uuid)) {
checkmarks[id] = checkmarks[uuid] =
oldData.checkmarks[id]!! oldData.checkmarks[uuid]!!
} else { } else {
checkmarks[id] = empty checkmarks[uuid] = empty
} }
} }
} }
@ -231,24 +292,32 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun copyNoteIndicatorsFrom(oldData: CacheData) { fun copyNoteIndicatorsFrom(oldData: CacheData) {
val empty = (0..checkmarkCount).map { "" }.toTypedArray() val empty = (0..checkmarkCount).map { "" }.toTypedArray()
for (id in idToHabit.keys) { for (uuid in uuidToHabit.keys) {
if (oldData.notes.containsKey(id)) { if (oldData.notes.containsKey(uuid)) {
notes[id] = notes[uuid] =
oldData.notes[id]!! oldData.notes[uuid]!!
} else { } else {
notes[id] = empty notes[uuid] = empty
} }
} }
} }
@Synchronized @Synchronized
fun copyScoresFrom(oldData: CacheData) { fun copyScoresFrom(oldData: CacheData) {
for (id in idToHabit.keys) { for (uuid in uuidToHabit.keys) {
if (oldData.scores.containsKey(id)) { if (oldData.scores.containsKey(uuid)) {
scores[id] = scores[uuid] =
oldData.scores[id]!! oldData.scores[uuid]!!
} else { } else {
scores[id] = 0.0 scores[uuid] = 0.0
}
}
for (uuid in uuidToHabitGroup.keys) {
if (oldData.scores.containsKey(uuid)) {
scores[uuid] =
oldData.scores[uuid]!!
} else {
scores[uuid] = 0.0
} }
} }
} }
@ -256,9 +325,15 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun fetchHabits() { fun fetchHabits() {
for (h in filteredHabits) { for (h in filteredHabits) {
if (h.id == null) continue if (h.uuid == null) continue
habits.add(h) habits.add(h)
idToHabit[h.id] = h uuidToHabit[h.uuid] = h
}
for (hgr in filteredHabitGroups) {
if (hgr.uuid == null) continue
habitGroups.add(hgr)
uuidToHabitGroup[hgr.uuid] = hgr
} }
} }
@ -267,6 +342,7 @@ class HabitCardListCache @Inject constructor(
*/ */
init { init {
habits = LinkedList() habits = LinkedList()
habitGroups = LinkedList()
checkmarks = HashMap() checkmarks = HashMap()
scores = HashMap() scores = HashMap()
notes = HashMap() notes = HashMap()
@ -275,19 +351,19 @@ class HabitCardListCache @Inject constructor(
private inner class RefreshTask : Task { private inner class RefreshTask : Task {
private val newData: CacheData private val newData: CacheData
private val targetId: Long? private val targetUUID: String?
private var isCancelled = false private var isCancelled = false
private var runner: TaskRunner? = null private var runner: TaskRunner? = null
constructor() { constructor() {
newData = CacheData() newData = CacheData()
targetId = null targetUUID = null
isCancelled = false isCancelled = false
} }
constructor(targetId: Long) { constructor(targetUUID: String) {
newData = CacheData() newData = CacheData()
this.targetId = targetId this.targetUUID = targetUUID
} }
@Synchronized @Synchronized
@ -307,8 +383,8 @@ class HabitCardListCache @Inject constructor(
for (position in newData.habits.indices) { for (position in newData.habits.indices) {
if (isCancelled) return if (isCancelled) return
val habit = newData.habits[position] val habit = newData.habits[position]
if (targetId != null && targetId != habit.id) continue if (targetUUID != null && targetUUID != habit.uuid) continue
newData.scores[habit.id] = habit.scores[today].value newData.scores[habit.uuid] = habit.scores[today].value
val list: MutableList<Int> = ArrayList() val list: MutableList<Int> = ArrayList()
val notes: MutableList<String> = ArrayList() val notes: MutableList<String> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
@ -316,10 +392,18 @@ class HabitCardListCache @Inject constructor(
notes.add(note) notes.add(note)
} }
val entries = list.toTypedArray() val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries) newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries)
newData.notes[habit.id] = notes.toTypedArray() newData.notes[habit.uuid] = notes.toTypedArray()
runner!!.publishProgress(this, position) runner!!.publishProgress(this, position)
} }
for (position in newData.habitGroups.indices) {
if (isCancelled) return
val hgr = newData.habitGroups[position]
if (targetUUID != null && targetUUID != hgr.uuid) continue
newData.scores[hgr.uuid] = hgr.scores[today].value
runner!!.publishProgress(this, position + newData.habits.size)
}
} }
@Synchronized @Synchronized
@ -340,15 +424,29 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
private fun performInsert(habit: Habit, position: Int) { private fun performInsert(habit: Habit, position: Int) {
val id = habit.id val uuid = habit.uuid
data.habits.add(position, habit) data.habits.add(position, habit)
data.idToHabit[id] = habit data.uuidToHabit[uuid] = habit
data.scores[id] = newData.scores[id]!! data.scores[uuid] = newData.scores[uuid]!!
data.checkmarks[id] = newData.checkmarks[id]!! data.checkmarks[uuid] = newData.checkmarks[uuid]!!
data.notes[id] = newData.notes[id]!! data.notes[uuid] = newData.notes[uuid]!!
listener.onItemInserted(position) listener.onItemInserted(position)
} }
@Synchronized
private fun performInsert(habitGroup: HabitGroup, position: Int) {
val newPosition = if (position < data.habits.size) {
data.habits.size
} else {
position
}
val uuid = habitGroup.uuid
data.habitGroups.add(newPosition - data.habits.size, habitGroup)
data.uuidToHabitGroup[uuid] = habitGroup
data.scores[uuid] = newData.scores[uuid]!!
listener.onItemInserted(newPosition)
}
@Synchronized @Synchronized
private fun performMove( private fun performMove(
habit: Habit, habit: Habit,
@ -359,7 +457,7 @@ class HabitCardListCache @Inject constructor(
// Workaround for https://github.com/iSoron/uhabits/issues/968 // Workaround for https://github.com/iSoron/uhabits/issues/968
val checkedToPosition = if (toPosition > data.habits.size) { val checkedToPosition = if (toPosition > data.habits.size) {
logger.error("performMove: $toPosition is strictly higher than ${data.habits.size}") logger.error("performMove: $toPosition for habit is strictly higher than ${data.habits.size}")
data.habits.size data.habits.size
} else { } else {
toPosition toPosition
@ -369,57 +467,114 @@ class HabitCardListCache @Inject constructor(
listener.onItemMoved(fromPosition, checkedToPosition) listener.onItemMoved(fromPosition, checkedToPosition)
} }
private fun performMove(
habitGroup: HabitGroup,
fromPosition: Int,
toPosition: Int
) {
data.habitGroups.removeAt(fromPosition)
// Workaround for https://github.com/iSoron/uhabits/issues/968
val checkedToPosition = if (toPosition < data.habits.size) {
logger.error("performMove: $toPosition for habit group is strictly lower than ${data.habits.size}")
data.habits.size
} else if (toPosition > data.habits.size + data.habitGroups.size) {
logger.error("performMove: $toPosition for habit group is strictly higher than ${data.habits.size + data.habitGroups.size}")
data.habits.size + data.habitGroups.size
} else {
toPosition
}
data.habitGroups.add(checkedToPosition - data.habits.size, habitGroup)
listener.onItemMoved(fromPosition, checkedToPosition)
}
@Synchronized @Synchronized
private fun performUpdate(id: Long, position: Int) { private fun performUpdate(uuid: String, position: Int) {
val oldScore = data.scores[id]!!
val oldCheckmarks = data.checkmarks[id]
val oldNoteIndicators = data.notes[id]
val newScore = newData.scores[id]!!
val newCheckmarks = newData.checkmarks[id]!!
val newNoteIndicators = newData.notes[id]!!
var unchanged = true var unchanged = true
val oldScore = data.scores[uuid]!!
val newScore = newData.scores[uuid]!!
if (oldScore != newScore) unchanged = false if (oldScore != newScore) unchanged = false
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false if (position < data.habits.size) {
val oldCheckmarks = data.checkmarks[uuid]
val newCheckmarks = newData.checkmarks[uuid]!!
val oldNoteIndicators = data.notes[uuid]
val newNoteIndicators = newData.notes[uuid]!!
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false
if (unchanged) return
data.checkmarks[uuid] = newCheckmarks
data.notes[uuid] = newNoteIndicators
}
if (unchanged) return if (unchanged) return
data.scores[id] = newScore data.scores[uuid] = newScore
data.checkmarks[id] = newCheckmarks
data.notes[id] = newNoteIndicators
listener.onItemChanged(position) listener.onItemChanged(position)
} }
@Synchronized @Synchronized
private fun processPosition(currentPosition: Int) { private fun processPosition(currentPosition: Int) {
val habit = newData.habits[currentPosition] if (currentPosition < newData.habits.size) {
val id = habit.id val habit = newData.habits[currentPosition]
val prevPosition = data.habits.indexOf(habit) val uuid = habit.uuid
if (prevPosition < 0) { val prevPosition = data.habits.indexOf(habit)
performInsert(habit, currentPosition) if (prevPosition < 0) {
performInsert(habit, currentPosition)
} else {
if (prevPosition != currentPosition) {
performMove(
habit,
prevPosition,
currentPosition
)
}
if (uuid == null) throw NullPointerException()
performUpdate(uuid, currentPosition)
}
} else { } else {
if (prevPosition != currentPosition) { val habitGroup = newData.habitGroups[currentPosition - data.habits.size]
performMove( val uuid = habitGroup.uuid
habit, val prevPosition = data.habitGroups.indexOf(habitGroup) + data.habits.size
prevPosition, if (prevPosition < 0) {
currentPosition performInsert(habitGroup, currentPosition)
) } else {
if (prevPosition != currentPosition) {
performMove(
habitGroup,
prevPosition,
currentPosition
)
}
if (uuid == null) throw NullPointerException()
performUpdate(uuid, currentPosition)
} }
if (id == null) throw NullPointerException()
performUpdate(id, currentPosition)
} }
} }
@Synchronized @Synchronized
private fun processRemovedHabits() { private fun processRemovedHabits() {
val before: Set<Long?> = data.idToHabit.keys val before: Set<String?> = data.uuidToHabit.keys
val after: Set<Long?> = newData.idToHabit.keys val after: Set<String?> = newData.uuidToHabit.keys
val removed: MutableSet<Long?> = TreeSet(before) val removed: MutableSet<String?> = TreeSet(before)
removed.removeAll(after)
for (uuid in removed) remove(uuid!!)
processRemovedHabitGroups()
}
@Synchronized
private fun processRemovedHabitGroups() {
val before: Set<String?> = data.uuidToHabitGroup.keys
val after: Set<String?> = newData.uuidToHabitGroup.keys
val removed: MutableSet<String?> = TreeSet(before)
removed.removeAll(after) removed.removeAll(after)
for (id in removed) remove(id!!) for (uuid in removed) remove(uuid!!)
} }
} }
init { init {
filteredHabits = allHabits filteredHabits = allHabits
filteredHabitGroups = allHabitGroups
this.taskRunner = taskRunner this.taskRunner = taskRunner
listener = object : Listener {} listener = object : Listener {}
data = CacheData() data = CacheData()

@ -24,6 +24,7 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitGroup
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
@ -88,7 +89,9 @@ class ListHabitsSelectionMenuBehavior @Inject constructor(
interface Adapter { interface Adapter {
fun clearSelection() fun clearSelection()
fun getSelected(): List<Habit> fun getSelected(): List<Habit>
fun getSelectedHabitGroups(): List<HabitGroup>
fun performRemove(selected: List<Habit>) fun performRemove(selected: List<Habit>)
fun performRemoveHabitGroup(selected: List<HabitGroup>)
} }
interface Screen { interface Screen {

Loading…
Cancel
Save