mirror of https://github.com/iSoron/uhabits.git
parent
340bde9f69
commit
73387e5e63
@ -0,0 +1,216 @@
|
||||
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()
|
||||
}
|
||||
|
||||
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 (h in parent!!) if (filter.matches(h)) list.add(h)
|
||||
resort()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun resort() {
|
||||
for (hgr in list) {
|
||||
hgr.habitList.primaryOrder = primaryOrder
|
||||
hgr.habitList.secondaryOrder = secondaryOrder
|
||||
hgr.habitList.resort()
|
||||
|
||||
hgr.habitGroupList.primaryOrder = primaryOrder
|
||||
hgr.habitGroupList.secondaryOrder = secondaryOrder
|
||||
hgr.habitGroupList.resort()
|
||||
}
|
||||
if (comparator != null) list.sortWith(comparator!!)
|
||||
observable.notifyListeners()
|
||||
}
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
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()
|
||||
val record = HabitGroupRecord()
|
||||
record.copyFrom(habitGroup)
|
||||
repository.save(record)
|
||||
habitGroup.id = 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 habits set position = position + 1 " +
|
||||
"where position >= ? and position < ?",
|
||||
toRecord.position!!,
|
||||
fromRecord.position!!
|
||||
)
|
||||
} else {
|
||||
repository.execSQL(
|
||||
"update habits 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 resort() {
|
||||
list.resort()
|
||||
observable.notifyListeners()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun reload() {
|
||||
loaded = false
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
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
|
||||
|
||||
@field:Column(name = "parent_id")
|
||||
var parentID: Long? = null
|
||||
|
||||
@field:Column(name = "parent_uuid")
|
||||
var parentUUID: String? = null
|
||||
|
||||
fun copyFrom(model: HabitGroup) {
|
||||
id = model.id
|
||||
name = model.name
|
||||
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
|
||||
parentID = model.parentID
|
||||
parentUUID = model.parentUUID
|
||||
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.parentID = parentID
|
||||
habitGroup.parentUUID = parentUUID
|
||||
if (reminderHour != null && reminderMin != null) {
|
||||
habitGroup.reminder = Reminder(
|
||||
reminderHour!!,
|
||||
reminderMin!!,
|
||||
WeekdayList(reminderDays!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +1,21 @@
|
||||
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;
|
||||
alter table Habits add column parent_id integer;
|
||||
alter table Habits add column parent_uuid text;
|
||||
|
||||
create table HabitGroups (
|
||||
id integer primary key autoincrement,
|
||||
archived integer,
|
||||
color integer,
|
||||
description text not null default "",
|
||||
highlight integer,
|
||||
name text,
|
||||
position integer,
|
||||
reminder_days integer not null default 127,
|
||||
reminder_hour integer,
|
||||
reminder_min integer,
|
||||
question text not null default "",
|
||||
uuid text,
|
||||
parent_id integer,
|
||||
parent_uuid integer
|
||||
);
|
Loading…
Reference in new issue